Files
devos/apps/api/src/modules/credential/credential-vault.service.ts
T
phptits 0d6cd37579 DevOS v1 + Phase 2: unified resource model, infra ops, website deployment, AI chat ops
- 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)
2026-09-10 06:35:55 +00:00

77 lines
2.5 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 信封式加密:
* 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");
}
}
}