- Monorepo: NestJS API (Fastify) + React web (antd/ProComponents) + shared packages - PostgreSQL/Prisma (unified resources, relations graph, audit, events, jobs) - Auth/RBAC/workspace isolation, credential vault (AES-256-GCM) - Servers/SSH, Docker discovery & lifecycle, Job Center - Websites & deployments (git provider credential clone, env, health verification) - Domains/DNS (Cloudflare adapter), certificates (TLS probe), monitoring - Automation (cron/event/webhook HMAC + notifications), AI Gateway, MCP (29 tools) - AI chat window (LLM function-calling + rules fallback, SSE streaming, approvals) - Notes: Vditor WYSIWYG markdown editor, non-blocking autosave - Tests: 35 unit/security tests; UI audit clean (desktop+mobile, deep links)
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
/**
|
||
* # 凭据保险库
|
||
* 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");
|
||
}
|
||
}
|
||
}
|