import crypto from 'crypto'; import { config } from '../config/env'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 12; const TAG_LENGTH = 16; export function encrypt(plaintext: string): string { const key = Buffer.from(config.totp.encryptionKey, 'hex'); const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); // Use PHP-compatible format: base64(nonce + ciphertext + tag) return Buffer.concat([iv, encrypted, tag]).toString('base64'); } export function decrypt(ciphertext: string): string { const key = Buffer.from(config.totp.encryptionKey, 'hex'); // Detect format: PHP uses base64(nonce+ciphertext+tag), TS uses hex:hex:hex const parts = ciphertext.split(':'); if (parts.length === 3) { // TS format: iv:encrypted:tag (hex) const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const tag = Buffer.from(parts[2], 'hex'); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // PHP format: base64(nonce + ciphertext + tag) const raw = Buffer.from(ciphertext, 'base64'); if (raw.length < IV_LENGTH + TAG_LENGTH + 1) { throw new Error('Invalid ciphertext format'); } const iv = raw.subarray(0, IV_LENGTH); const tag = raw.subarray(raw.length - TAG_LENGTH); const encrypted = raw.subarray(IV_LENGTH, raw.length - TAG_LENGTH); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); let decrypted = decipher.update(encrypted); const final = decipher.final(); return Buffer.concat([decrypted, final]).toString('utf8'); }