92 lines
3.2 KiB
TypeScript
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"
|
|
);
|
|
}
|
|
}
|