import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; import { envOrNull } from '../../common/env'; /** * Encrypts the accounting-software token at rest (D27). * * The token sits in the Setting table, so a database dump or a backup would * otherwise hand out a live credential to the shop's books. AES-256-GCM with a * key derived from `ACCOUNTING_SECRET_KEY`. * * When that variable is unset the key falls back to one derived from * `JWT_SECRET`, with a loud warning at boot — the same "warn, don't refuse" * trade-off D23 made — so an admin can configure the integration before ops * add the variable. The cost: rotating JWT_SECRET then makes the stored token * unreadable and the admin has to enter it again (the screen says so). */ const PREFIX = 'enc:v1:'; export function secretKeySource(): 'ACCOUNTING_SECRET_KEY' | 'JWT_SECRET' | null { if (envOrNull('ACCOUNTING_SECRET_KEY')) return 'ACCOUNTING_SECRET_KEY'; if (envOrNull('JWT_SECRET')) return 'JWT_SECRET'; return null; } function key(): Buffer { const material = envOrNull('ACCOUNTING_SECRET_KEY') ?? envOrNull('JWT_SECRET'); if (!material) { throw new Error('ACCOUNTING_SECRET_KEY تنظیم نشده است؛ ذخیره‌ی توکن حسابداری ممکن نیست.'); } return createHash('sha256').update(`goldbank-accounting:${material}`).digest(); } export function sealSecret(plain: string): string { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', key(), iv); const body = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return PREFIX + Buffer.concat([iv, tag, body]).toString('base64'); } /** Null when the value cannot be decrypted (wrong key, tampered, malformed). */ export function openSecret(sealed: string | null | undefined): string | null { if (!sealed || !sealed.startsWith(PREFIX)) return null; try { const raw = Buffer.from(sealed.slice(PREFIX.length), 'base64'); const iv = raw.subarray(0, 12); const tag = raw.subarray(12, 28); const body = raw.subarray(28); const decipher = createDecipheriv('aes-256-gcm', key(), iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(body), decipher.final()]).toString('utf8'); } catch { return null; } }