import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto"; const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; const SALT_LENGTH = 32; /** * Derive a 32-byte encryption key from a secret * * Uses SHA-256 to create a consistent key length from any secret. * * @param secret - Secret string (e.g., client secret) * @returns 32-byte key suitable for AES-256 */ function deriveKey(secret: string): Buffer { return createHash("sha256").update(secret).digest(); } /** * Validate encryption secret meets minimum requirements * * @param secret - Secret to validate * @throws Error if secret is invalid */ export function validateEncryptionSecret(secret: string): void { if (!secret) { throw new Error("Encryption secret is required"); } if (secret.length < 32) { throw new Error("Encryption secret must be at least 32 characters"); } } /** * Encrypt a token using AES-256-GCM * * Uses Galois/Counter Mode (GCM) which provides both confidentiality * and authenticity (prevents tampering). * * Format: iv:authTag:encryptedData (all hex-encoded) * * @param token - Plain text token to encrypt * @param secret - Encryption secret (at least 32 characters) * @returns Encrypted token with IV and auth tag */ export function encryptToken(token: string, secret: string): string { const key = deriveKey(secret); const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(token, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); // Format: iv:authTag:encryptedData return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } /** * Decrypt a token using AES-256-GCM * * Validates the auth tag to ensure the data hasn't been tampered with. * * @param encryptedToken - Encrypted token from encryptToken() * @param secret - Encryption secret used during encryption * @returns Decrypted plain text token * @throws Error if decryption fails or auth tag is invalid */ export function decryptToken(encryptedToken: string, secret: string): string { const parts = encryptedToken.split(":"); if (parts.length !== 3) { throw new Error("Invalid encrypted token format"); } const [ivHex, authTagHex, encryptedData] = parts; const key = deriveKey(secret); const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encryptedData, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; }