Files
pitchfast/v2_elemente/crypto.ts

53 lines
1.8 KiB
TypeScript

/**
* convex/lib/crypto.ts
*
* Symmetrische Ver-/Entschlüsselung der hinterlegten Kunden-Secrets
* (BYO-API-Keys, SMTP-Passwörter). Nutzt Web Crypto (AES-GCM).
*
* WICHTIG: Nur aus Convex *Actions* aufrufen — encrypt() braucht
* Zufalls-IV (nicht-deterministisch) und ist daher in Mutations/Queries
* nicht erlaubt. Der Master-Key kommt aus der Umgebung (niemals ins Repo).
*
* Env: SECRET_ENCRYPTION_KEY = base64-kodierter 32-Byte-Schlüssel.
*/
function getKeyMaterial(): Promise<CryptoKey> {
const b64 = process.env.SECRET_ENCRYPTION_KEY;
if (!b64) throw new Error("SECRET_ENCRYPTION_KEY ist nicht gesetzt");
const raw = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return crypto.subtle.importKey("raw", raw, "AES-GCM", false, [
"encrypt",
"decrypt",
]);
}
/** Klartext → "ivBase64:cipherBase64". */
export async function encryptSecret(plaintext: string): Promise<string> {
const key = await getKeyMaterial();
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = new TextEncoder().encode(plaintext);
const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc);
return `${toB64(iv)}:${toB64(new Uint8Array(cipher))}`;
}
/** "ivBase64:cipherBase64" → Klartext. */
export async function decryptSecret(stored: string): Promise<string> {
const key = await getKeyMaterial();
const [ivB64, cipherB64] = stored.split(":");
const iv = fromB64(ivB64);
const cipher = fromB64(cipherB64);
const plain = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
cipher,
);
return new TextDecoder().decode(plain);
}
function toB64(bytes: Uint8Array): string {
return btoa(String.fromCharCode(...bytes));
}
function fromB64(b64: string): Uint8Array {
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}