import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual } from 'node:crypto'; import type { AppConfig } from './config.js'; export function randomToken(bytes = 32): string { return randomBytes(bytes).toString('base64url'); } export function hashToken(value: string): Buffer { return createHash('sha256').update(value).digest(); } export function sameTokenHash(left: Buffer, right: Buffer): boolean { return left.length === right.length && timingSafeEqual(left, right); } export function encryptText(config: AppConfig, value: string): string { return encryptBytes(config, Buffer.from(value, 'utf8')); } export function encryptBytes(config: AppConfig, value: Buffer): string { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', config.fieldEncryptionKey, iv); const ciphertext = Buffer.concat([cipher.update(value), cipher.final()]); const tag = cipher.getAuthTag(); return ['v1', iv.toString('base64url'), tag.toString('base64url'), ciphertext.toString('base64url')].join('.'); } export function decryptText(config: AppConfig, value: string | null | undefined): string { return decryptBytes(config, value).toString('utf8'); } export function decryptBytes(config: AppConfig, value: string | null | undefined): Buffer { if (!value) return Buffer.alloc(0); const [version, ivText, tagText, ciphertextText] = value.split('.'); if (version !== 'v1' || !ivText || !tagText || !ciphertextText) throw new Error('Invalid encrypted field format.'); const decipher = createDecipheriv('aes-256-gcm', config.fieldEncryptionKey, Buffer.from(ivText, 'base64url')); decipher.setAuthTag(Buffer.from(tagText, 'base64url')); return Buffer.concat([ decipher.update(Buffer.from(ciphertextText, 'base64url')), decipher.final() ]); } export function sha256Text(value: string): string { return createHash('sha256').update(value).digest('hex'); } export function sha256Bytes(value: Buffer): string { return createHash('sha256').update(value).digest('hex'); }