27 lines
1.8 KiB
TypeScript
27 lines
1.8 KiB
TypeScript
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
||
import { AiModelError } from './ai-model-access'
|
||
|
||
export interface AiModelEncryptedKey { secretCiphertext: string; secretNonce: string; secretAuthTag: string }
|
||
export function isAiModelEncryptionConfigured(): boolean {
|
||
const secret = process.env.API_KEY_ENCRYPTION_KEY?.trim()
|
||
return Boolean(secret && Buffer.byteLength(secret, 'utf8') >= 32)
|
||
}
|
||
function encryptionKey(): Buffer {
|
||
if (!isAiModelEncryptionConfigured()) throw new AiModelError('请先在服务端配置 API_KEY_ENCRYPTION_KEY(至少 32 字节)', 503, 'encryption_not_configured')
|
||
// Domain separation prevents this credential store sharing ciphertext keys with platform API keys.
|
||
return createHash('sha256').update('gongxiangp2:ai-models:v1\0').update(process.env.API_KEY_ENCRYPTION_KEY!.trim()).digest()
|
||
}
|
||
export function encryptAiModelKey(value: string): AiModelEncryptedKey {
|
||
const nonce = randomBytes(12)
|
||
const cipher = createCipheriv('aes-256-gcm', encryptionKey(), nonce)
|
||
return { secretCiphertext: Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]).toString('base64'), secretNonce: nonce.toString('base64'), secretAuthTag: cipher.getAuthTag().toString('base64') }
|
||
}
|
||
export function decryptAiModelKey(value: AiModelEncryptedKey): string {
|
||
const key = encryptionKey()
|
||
try {
|
||
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(value.secretNonce, 'base64'))
|
||
decipher.setAuthTag(Buffer.from(value.secretAuthTag, 'base64'))
|
||
return Buffer.concat([decipher.update(Buffer.from(value.secretCiphertext, 'base64')), decipher.final()]).toString('utf8')
|
||
} catch { throw new AiModelError('模型密钥无法解密,请检查服务端加密配置或替换密钥', 503, 'credential_unavailable') }
|
||
}
|