Files
novalon-website/src/lib/crypto-server.ts
T
zhangxiang d5d04aa96d feat: implement frontend-backend encrypted communication via AES-256-GCM
参考 novavis-authority 的加解密方案,实现前后端通信的应用层加密:
- 重写 src/lib/crypto.ts 使用 Web Crypto API(浏览器兼容),PBKDF2+AES-256-GCM
- 新增 src/lib/crypto-server.ts 服务端加解密工具(Node.js crypto)
- 新增 src/lib/api-crypto.ts API 路由中间件 withCrypto(),自动解密请求体/加密响应体
- 更新 src/lib/admin-api.ts 自动加密所有请求/解密响应
- 所有 11 个 admin API 路由文件已应用 withCrypto 包装器
- 更新 .env 文件,添加 NEXT_PUBLIC_ENCRYPTION_SECRET 和 ENCRYPTION_SECRET
2026-08-02 09:11:36 +08:00

88 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 服务端 AES-256-GCM 加解密工具(Node.js 环境)
*
* 使用 PBKDF2-HMAC-SHA256 从 ENCRYPTION_SECRET 派生密钥,
* 与 crypto.ts 共享相同的算法和参数,实现前后端加解密互通。
*
* 供 Next.js API Route 在服务端解密请求体、加密响应体。
*/
import crypto from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
const PBKDF2_ITERATIONS = 100_000;
const PBKDF2_SALT = 'novalon-website-crypto-salt-v1';
const KEY_LENGTH = 32; // 256 bits
function getSecret(): string {
const secret = process.env.ENCRYPTION_SECRET;
if (!secret) {
throw new Error('ENCRYPTION_SECRET 未配置,服务端加解密无法初始化');
}
return secret;
}
let cachedKey: Buffer | null = null;
function deriveKey(): Buffer {
if (cachedKey) return cachedKey;
const secret = getSecret();
cachedKey = crypto.pbkdf2Sync(
secret,
PBKDF2_SALT,
PBKDF2_ITERATIONS,
KEY_LENGTH,
'sha256',
);
return cachedKey;
}
/**
* 加密明文,返回 Base64 编码的密文(12-byte-IV || AES-GCM-ciphertext
*/
export function encrypt(plaintext: string): string {
const key = deriveKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
// 格式: iv + authTag + encrypted
const combined = Buffer.concat([iv, authTag, encrypted]);
return combined.toString('base64');
}
/**
* 解密 Base64 编码的密文,返回明文字符串
*/
export function decrypt(encryptedBase64: string): string {
const key = deriveKey();
const combined = Buffer.from(encryptedBase64, 'base64');
const iv = combined.subarray(0, IV_LENGTH);
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
const encrypted = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString('utf8');
}
/**
* 验证加密配置是否可用
*/
export function isEncryptionAvailable(): boolean {
return !!process.env.ENCRYPTION_SECRET;
}