79 lines
3.1 KiB
JavaScript
79 lines
3.1 KiB
JavaScript
/* =====================================================================
|
|
crypto.js — Verschlüsselung & Hashing für das Zugangssystem
|
|
|
|
Codes (Zugangscodes von Teammitgliedern) brauchen ZWEI Repräsentationen:
|
|
1) code_hash — SHA-256, nicht umkehrbar. Für den schnellen Login-
|
|
Vergleich ("stimmt der eingegebene Code mit dem gespeicherten
|
|
überein?"). Kann NIE zurück in den Klartext-Code verwandelt werden.
|
|
2) code_enc — AES-256-GCM, umkehrbar. Nur damit kann der
|
|
Hauptadministrator sich den Klartext-Code später wieder anzeigen
|
|
lassen ("Code anzeigen"-Funktion aus dem Anforderungsdokument).
|
|
Der Schlüssel dafür liegt NUR im ENCRYPTION_KEY-Secret (nie im
|
|
Quelltext, nie in der Datenbank).
|
|
|
|
Session-Tokens werden nur GEHASHT in der Datenbank gespeichert (wie ein
|
|
Passwort) — der Klartext-Token existiert nur kurz beim Login-Response
|
|
und im Browser des jeweiligen Geräts.
|
|
===================================================================== */
|
|
|
|
const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne I,O,0,1
|
|
|
|
export function generateFriendlyCode() {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
|
let raw = "";
|
|
for (const b of bytes) raw += CODE_ALPHABET[b % CODE_ALPHABET.length];
|
|
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
|
|
}
|
|
|
|
export function generateSessionToken() {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
export function generateId() {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
export async function sha256Hex(text) {
|
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
async function getAesKey(env) {
|
|
const raw = Uint8Array.from(atob(env.ENCRYPTION_KEY), (c) => c.charCodeAt(0));
|
|
return crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
}
|
|
|
|
function bufToBase64(buf) {
|
|
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
}
|
|
function base64ToBuf(b64) {
|
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
}
|
|
|
|
/** Verschlüsselt einen Klartext-Code reversibel. Rückgabe: "iv.ciphertext" (base64). */
|
|
export async function encryptCode(env, plaintext) {
|
|
const key = await getAesKey(env);
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const ciphertext = await crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv },
|
|
key,
|
|
new TextEncoder().encode(plaintext)
|
|
);
|
|
return `${bufToBase64(iv)}.${bufToBase64(ciphertext)}`;
|
|
}
|
|
|
|
/** Entschlüsselt einen mit encryptCode() erzeugten Wert. Nur intern für den Owner-Reveal-Flow nutzen. */
|
|
export async function decryptCode(env, encoded) {
|
|
const [ivB64, ctB64] = encoded.split(".");
|
|
const key = await getAesKey(env);
|
|
const iv = base64ToBuf(ivB64);
|
|
const ciphertext = base64ToBuf(ctB64);
|
|
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
return new TextDecoder().decode(plainBuf);
|
|
}
|
|
|
|
export function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|