/** * 前后端通信 AES-256-GCM 加解密工具(前端浏览器环境) * * 使用 PBKDF2-HMAC-SHA256 从 NEXT_PUBLIC_ENCRYPTION_SECRET 派生密钥, * 与 crypto-server.ts 共享相同的算法和参数,实现前后端加解密互通。 * * 参考同级项目 novavis-authority 的加解密方案。 * * 输出格式: Base64(12-byte-IV || AES-GCM-ciphertext) */ const IV_LENGTH = 12; const ALGORITHM = 'AES-GCM'; const PBKDF2_ITERATIONS = 100_000; const PBKDF2_SALT = new TextEncoder().encode('novalon-website-crypto-salt-v1'); async function deriveKey(passphrase: string): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveBits', 'deriveKey'], ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: PBKDF2_SALT, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, keyMaterial, { name: ALGORITHM, length: 256 }, false, ['encrypt', 'decrypt'], ); } function getSecret(): string { const secret = process.env.NEXT_PUBLIC_ENCRYPTION_SECRET; if (!secret) { throw new Error('NEXT_PUBLIC_ENCRYPTION_SECRET 未配置,加解密无法初始化'); } return secret; } let cachedKey: Promise | null = null; function getKey(): Promise { if (!cachedKey) { cachedKey = deriveKey(getSecret()); } return cachedKey; } /** * 加密明文,返回 Base64 编码的密文(IV 前置) */ export async function encrypt(plaintext: string): Promise { const key = await getKey(); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const encoder = new TextEncoder(); const ciphertext = await crypto.subtle.encrypt( { name: ALGORITHM, iv, tagLength: 128 }, key, encoder.encode(plaintext), ); const combined = new Uint8Array(IV_LENGTH + ciphertext.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(ciphertext), IV_LENGTH); return btoa(Array.from(combined, (b) => String.fromCodePoint(b)).join('')); } /** * 解密 Base64 编码的密文(IV 前置),返回明文字符串 */ export async function decrypt(encryptedBase64: string): Promise { const key = await getKey(); const binaryStr = atob(encryptedBase64); const combined = new Uint8Array(binaryStr.length); for (let i = 0; i < binaryStr.length; i++) { combined[i] = binaryStr.codePointAt(i)!; } const iv = combined.slice(0, IV_LENGTH); const ciphertext = combined.slice(IV_LENGTH); const decrypted = await crypto.subtle.decrypt( { name: ALGORITHM, iv, tagLength: 128 }, key, ciphertext, ); return new TextDecoder().decode(decrypted); }