/** * # 凭据保险库 * AES-256-GCM 信封式加密: * Application Master Key (ENCRYPTION_KEY, 32B) → 加密凭据载荷。 * - AAD 绑定 workspaceId,防止跨租户密文移植; * - 明文只在内存中短暂存在,绝不入库、绝不入日志(审计层另有脱敏); * - API 层不提供任何“读取明文”端点。 */ import { Injectable } from "@nestjs/common"; import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; import { ConfigService } from "../../common/config/config.service"; import { ApiError } from "../../common/errors/api-error"; /** # 密文载荷结构(JSON 序列化后存储) **/ interface EncryptedPayload { v: 1; iv: string; tag: string; data: string; } @Injectable() export class CredentialVaultService { /** # 主密钥 **/ private readonly key: Buffer; constructor(config: ConfigService) { this.key = Buffer.from(config.encryptionKey, "hex"); } /** # 加密 * @param workspaceId 租户 ID(作为 AAD 绑定) * @param plaintext 明文(如私钥/密码/API Token) */ encrypt(workspaceId: string, plaintext: string): string { const iv = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", this.key, iv, { authTagLength: 16 }); cipher.setAAD(Buffer.from(workspaceId, "utf8")); const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const payload: EncryptedPayload = { v: 1, iv: iv.toString("base64"), tag: cipher.getAuthTag().toString("base64"), data: data.toString("base64"), }; return JSON.stringify(payload); } /** # 解密(仅限服务端内部使用) * @throws 完整性校验失败(密文被篡改或跨租户使用)时抛出 */ decrypt(workspaceId: string, encrypted: string): string { let payload: EncryptedPayload; try { payload = JSON.parse(encrypted) as EncryptedPayload; } catch { throw ApiError.forbidden("Credential payload corrupted"); } try { const decipher = createDecipheriv( "aes-256-gcm", this.key, Buffer.from(payload.iv, "base64"), { authTagLength: 16 }, ); decipher.setAAD(Buffer.from(workspaceId, "utf8")); decipher.setAuthTag(Buffer.from(payload.tag, "base64")); return Buffer.concat([ decipher.update(Buffer.from(payload.data, "base64")), decipher.final(), ]).toString("utf8"); } catch { throw ApiError.forbidden("Credential integrity check failed"); } } }