import { createDecipheriv, createCipheriv, createHash, createHmac, randomBytes, } from 'node:crypto'; export type EncryptedStoreSecret = { ciphertext: Buffer; nonce: Buffer; fingerprint: string; }; /** * 密钥只在此边界短暂以明文存在。数据库保存 AES-GCM 密文和随机 nonce, * fingerprint 是不可逆 HMAC,仅用于安全地识别同一份密钥。 */ export function encryptStoreSecret( plaintext: string, keyMaterial: string, ): EncryptedStoreSecret { const key = createHash('sha256').update(keyMaterial).digest(); const nonce = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', key, nonce); const ciphertext = Buffer.concat([ cipher.update(plaintext, 'utf8'), cipher.final(), cipher.getAuthTag(), ]); return { ciphertext, nonce, fingerprint: createHmac('sha256', key) .update(plaintext) .digest('base64url'), }; } /** 仅供受信任的后台 Worker 使用;调用方不得记录返回值。 */ export function decryptStoreSecret( ciphertextWithTag: Buffer, nonce: Buffer, keyMaterial: string, ): string { const authenticationTagLength = 16; if (ciphertextWithTag.length <= authenticationTagLength) { throw new Error('Invalid encrypted store secret'); } const key = createHash('sha256').update(keyMaterial).digest(); const ciphertext = ciphertextWithTag.subarray(0, -authenticationTagLength); const tag = ciphertextWithTag.subarray(-authenticationTagLength); const decipher = createDecipheriv('aes-256-gcm', key, nonce); decipher.setAuthTag(tag); return Buffer.concat([ decipher.update(ciphertext), decipher.final(), ]).toString('utf8'); }