/** * Token Encryption Utilities * * Provides AES-256-GCM encryption and decryption for OAuth tokens * stored in the database. */ import crypto from "crypto"; /** * Encryption algorithm configuration */ const ALGORITHM = "aes-256-gcm"; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits // const AUTH_TAG_LENGTH = 16; // 128 bits const SALT_LENGTH = 64; /** * Derive encryption key from secret using PBKDF2 * * @param secret - Secret string to derive key from * @param salt - Salt for key derivation * @returns Derived encryption key */ function deriveKey(secret: string, salt: Buffer): Buffer { return crypto.pbkdf2Sync(secret, salt, 100000, KEY_LENGTH, "sha256"); } /** * Encrypt a token using AES-256-GCM * * The encrypted output format is: salt:iv:authTag:encryptedData (all hex-encoded) * * @param token - Plain text token to encrypt * @param secret - Encryption secret (client secret or custom encryption key) * @returns Encrypted token string (format: salt:iv:authTag:encryptedData) * @throws Error if encryption fails */ export function encryptToken(token: string, secret: string): string { if (!token) { throw new Error("Token cannot be empty"); } if (!secret || secret.length < 32) { throw new Error("Encryption secret must be at least 32 characters"); } try { // Generate random salt for key derivation const salt = crypto.randomBytes(SALT_LENGTH); // Derive encryption key from secret const key = deriveKey(secret, salt); // Generate random IV const iv = crypto.randomBytes(IV_LENGTH); // Create cipher const cipher = crypto.createCipheriv(ALGORITHM, key, iv); // Encrypt token let encrypted = cipher.update(token, "utf8", "hex"); encrypted += cipher.final("hex"); // Get authentication tag const authTag = cipher.getAuthTag(); // Combine salt, IV, auth tag, and encrypted data const saltHex = salt.toString("hex"); const ivHex = iv.toString("hex"); const authTagHex = authTag.toString("hex"); return saltHex + ":" + ivHex + ":" + authTagHex + ":" + encrypted; } catch (error) { throw new Error("Token encryption failed: " + (error instanceof Error ? error.message : "Unknown error")); } } /** * Decrypt a token using AES-256-GCM * * @param encryptedToken - Encrypted token string (format: salt:iv:authTag:encryptedData) * @param secret - Decryption secret (must match encryption secret) * @returns Decrypted plain text token * @throws Error if decryption fails or token is tampered */ export function decryptToken(encryptedToken: string, secret: string): string { if (!encryptedToken) { throw new Error("Encrypted token cannot be empty"); } if (!secret || secret.length < 32) { throw new Error("Decryption secret must be at least 32 characters"); } try { // Parse encrypted token parts const parts = encryptedToken.split(":"); if (parts.length !== 4) { throw new Error("Invalid encrypted token format"); } const saltHex = parts[0]; const ivHex = parts[1]; const authTagHex = parts[2]; const encryptedData = parts[3]; // Convert from hex const salt = Buffer.from(saltHex, "hex"); const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); // Derive decryption key const key = deriveKey(secret, salt); // Create decipher const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); // Decrypt token let decrypted = decipher.update(encryptedData, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (error) { throw new Error("Token decryption failed: " + (error instanceof Error ? error.message : "Unknown error")); } } /** * Validate that encryption secret meets security requirements * * @param secret - Secret to validate * @returns true if valid * @throws Error if invalid */ export function validateEncryptionSecret(secret: string): boolean { if (!secret) { throw new Error("Encryption secret is required"); } if (secret.length < 32) { throw new Error("Encryption secret must be at least 32 characters for security"); } return true; }