import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' let _key: Buffer | null = null function getKey() { if (_key) return _key const configured = process.env.NOVEL_SECRET_KEY || useRuntimeConfig().secretKey if (configured) { const buf = Buffer.from(configured, 'hex') if (buf.length !== 32) throw new Error('NOVEL_SECRET_KEY 必须是 64 位十六进制字符串(32 字节)') _key = buf return _key } const file = join(dataDir(), 'secret.key') if (!existsSync(file)) writeFileSync(file, randomBytes(32).toString('hex'), { mode: 0o600 }) _key = Buffer.from(readFileSync(file, 'utf8').trim(), 'hex') return _key } /** AES-256-GCM 加密,输出 iv.tag.ciphertext(base64) */ export function encryptSecret(plain: string) { if (!plain) return '' const iv = randomBytes(12) const cipher = createCipheriv('aes-256-gcm', getKey(), iv) const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]) return [iv, cipher.getAuthTag(), enc].map(b => b.toString('base64')).join('.') } export function decryptSecret(payload: string) { if (!payload) return '' const [iv, tag, enc] = payload.split('.').map(s => Buffer.from(s, 'base64')) if (!iv || !tag || !enc) throw new Error('密钥数据损坏') try { const decipher = createDecipheriv('aes-256-gcm', getKey(), iv) decipher.setAuthTag(tag) return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8') } catch { throw createError({ statusCode: 400, message: 'API Key 无法解密:加密密钥(data/secret.key 或 NOVEL_SECRET_KEY)已变更。请在「模型与 API」中重新填写该提供方的 API Key。' }) } } export function maskSecret(plain: string) { if (!plain) return '' if (plain.length <= 8) return '••••' return `${plain.slice(0, 3)}••••${plain.slice(-4)}` }