import { randomBytes, createCipheriv, createDecipheriv, scrypt } from 'crypto'; import { promisify } from 'util'; import argon2 from 'argon2'; import { config } from '../config.js'; const scryptAsync = promisify(scrypt); // Password hashing with Argon2id export async function hashPassword(password: string): Promise { return argon2.hash(password, { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 4, }); } export async function verifyPassword(hash: string, password: string): Promise { try { return await argon2.verify(hash, password); } catch { return false; } } // Generate cryptographically secure random strings export function generateSecureToken(length: number = 32): string { return randomBytes(length).toString('hex'); } export function generateSessionId(): string { return randomBytes(32).toString('base64url'); } // AES-256-GCM encryption for TOTP secrets const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; async function deriveKey(): Promise { const keyHex = config.encryptionKey; if (keyHex.length !== 64) { throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes)'); } return Buffer.from(keyHex, 'hex'); } export async function encrypt(plaintext: string): Promise { const key = await deriveKey(); const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // Format: iv:authTag:ciphertext (all hex) return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; } export async function decrypt(ciphertext: string): Promise { const key = await deriveKey(); const parts = ciphertext.split(':'); if (parts.length !== 3) { throw new Error('Invalid ciphertext format'); } const iv = Buffer.from(parts[0], 'hex'); const authTag = Buffer.from(parts[1], 'hex'); const encrypted = parts[2]; const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; }