Files
AgentCompany/src/main/vault.ts
T

92 lines
3.2 KiB
TypeScript

import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { safeStorage } from "electron";
export class SecretVault {
private readonly keyPath: string;
constructor(private readonly profileDir: string) {
this.keyPath = join(profileDir, "vault.key");
}
getBackendLabel(): string {
if (this.canUseSafeStorage()) {
const selectedBackend = typeof safeStorage?.getSelectedStorageBackend === "function"
? safeStorage.getSelectedStorageBackend()
: "platform";
if (selectedBackend && selectedBackend !== "basic_text") {
return `safe_storage:${selectedBackend}`;
}
}
return this.allowInsecureVaultFallback() ? "vault:aes-256-gcm" : "unavailable";
}
encrypt(value: string): { backend: "safe_storage" | "vault"; payload: Buffer } {
if (this.canUseSafeStorage()) {
const selectedBackend = typeof safeStorage?.getSelectedStorageBackend === "function"
? safeStorage.getSelectedStorageBackend()
: "platform";
if (selectedBackend !== "basic_text") {
return {
backend: "safe_storage",
payload: safeStorage.encryptString(value),
};
}
}
if (!this.allowInsecureVaultFallback()) {
throw new Error("Secure secret storage is unavailable. AgentCompany requires OS-backed credential storage unless AGENTCOMPANY_ALLOW_INSECURE_VAULT=true is set for test-only environments.");
}
const iv = randomBytes(12);
const key = this.ensureKey();
const cipher = createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
backend: "vault",
payload: Buffer.concat([iv, tag, encrypted]),
};
}
decrypt(backend: "safe_storage" | "vault", payload: Buffer): string {
if (backend === "safe_storage") {
if (!this.canUseSafeStorage()) {
throw new Error("safeStorage backend is unavailable in the current runtime.");
}
return safeStorage.decryptString(payload);
}
const key = this.ensureKey();
const iv = payload.subarray(0, 12);
const tag = payload.subarray(12, 28);
const encrypted = payload.subarray(28);
const decipher = createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
}
private ensureKey(): Buffer {
mkdirSync(this.profileDir, { recursive: true });
if (!existsSync(this.keyPath)) {
const nextKey = randomBytes(32);
writeFileSync(this.keyPath, nextKey);
chmodSync(this.keyPath, 0o600);
return nextKey;
}
return readFileSync(this.keyPath);
}
private canUseSafeStorage() {
return typeof safeStorage?.isEncryptionAvailable === "function" && safeStorage.isEncryptionAvailable();
}
private allowInsecureVaultFallback() {
return (
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT === "true" ||
process.env.NODE_ENV === "test" ||
process.env.VITEST === "true"
);
}
}