import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; const KEY_SIZE_BYTES = 32; const VERSION = 1; const NONCE_SIZE = 12; const TAG_SIZE = 16; export class TbankCrypto { constructor(private readonly key: Buffer) {} static fromBase64(key: string): TbankCrypto { const decoded = Buffer.from(key, 'base64'); if (decoded.length !== KEY_SIZE_BYTES) { throw new Error('Invalid token encryption key'); } return new TbankCrypto(decoded); } encrypt(value: string): string { const nonce = randomBytes(NONCE_SIZE); const cipher = createCipheriv('aes-256-gcm', this.key, nonce); const ciphertext = Buffer.concat([ cipher.update(value, 'utf8'), cipher.final(), ]); return Buffer.concat([ Buffer.from([VERSION]), nonce, ciphertext, cipher.getAuthTag(), ]).toString('base64'); } decrypt(payload: string): string { const bytes = Buffer.from(payload, 'base64'); if (bytes[0] !== VERSION || bytes.length < 1 + NONCE_SIZE + TAG_SIZE) { throw new Error('Invalid encrypted token'); } const nonce = bytes.subarray(1, 1 + NONCE_SIZE); const tag = bytes.subarray(-TAG_SIZE); const decipher = createDecipheriv('aes-256-gcm', this.key, nonce); decipher.setAuthTag(tag); return Buffer.concat([ decipher.update(bytes.subarray(1 + NONCE_SIZE, -TAG_SIZE)), decipher.final(), ]).toString('utf8'); } }