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)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* # 凭据保险库
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user