// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { safeStorage } from 'electron'; import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import type { WorkspaceSecretLookup, WorkspaceSecretPutRequest, WorkspaceSecretPutResult, WorkspaceSecretScope, WorkspaceSecretStatus, } from './types'; const FORMAT_VERSION = 1; const VAULT_FILE_NAME = 'workspace-secret-vault.v1.json'; const MAX_VAULT_BYTES = 8 * 1024 * 1024; export const MAX_WORKSPACE_SECRET_BYTES = 64 * 1024; interface WorkspaceSecretCrypto { isEncryptionAvailable(): boolean; encryptString(value: string): Buffer; decryptString(value: Buffer): string; getSelectedStorageBackend?(): string; } interface EncryptedWorkspaceSecretRecord extends WorkspaceSecretScope { secret_ref: string; ciphertext: string; created_at: string; updated_at: string; } interface VaultDocument { version: 1; records: Record; } interface VaultReadResult { document: VaultDocument | null; corrupted: boolean; } export interface WorkspaceSecretVaultOptions { rootDir?: string; crypto?: WorkspaceSecretCrypto; platform?: NodeJS.Platform; now?: () => Date; randomBytes?: (size: number) => Buffer; } export class WorkspaceSecretVaultError extends Error {} export class WorkspaceSecretBindingMismatchError extends WorkspaceSecretVaultError {} export class WorkspaceSecretNeedsRebindError extends WorkspaceSecretVaultError {} export class WorkspaceSecretNotFoundError extends WorkspaceSecretVaultError {} function defaultCrypto(): WorkspaceSecretCrypto { return { isEncryptionAvailable: () => safeStorage.isEncryptionAvailable(), encryptString: (value) => safeStorage.encryptString(value), decryptString: (value) => safeStorage.decryptString(value), getSelectedStorageBackend: typeof safeStorage.getSelectedStorageBackend === 'function' ? () => safeStorage.getSelectedStorageBackend() : undefined, }; } function hasOnlySafeIdentityCharacters(value: string): boolean { return ( value.length > 0 && value.length <= 256 && !/[\u0000-\u001f\u007f]/u.test(value) ); } function assertScope(scope: WorkspaceSecretScope): void { if (!/^[a-f0-9]{64}$/u.test(scope.account_scope_digest)) { throw new WorkspaceSecretVaultError( 'Workspace secret account scope digest is invalid' ); } for (const [name, value] of Object.entries({ space_id: scope.space_id, revision_id: scope.revision_id, slot_id: scope.slot_id, })) { if (!hasOnlySafeIdentityCharacters(value)) { throw new WorkspaceSecretVaultError( `Workspace secret ${name} is invalid` ); } } } function sameScope( record: WorkspaceSecretScope, expected: WorkspaceSecretScope ): boolean { return ( record.account_scope_digest === expected.account_scope_digest && record.space_id === expected.space_id && record.revision_id === expected.revision_id && record.slot_id === expected.slot_id ); } function isEncryptedRecord( value: unknown ): value is EncryptedWorkspaceSecretRecord { if (!value || typeof value !== 'object') return false; const record = value as Partial; return ( typeof record.secret_ref === 'string' && typeof record.account_scope_digest === 'string' && typeof record.space_id === 'string' && typeof record.revision_id === 'string' && typeof record.slot_id === 'string' && typeof record.ciphertext === 'string' && typeof record.created_at === 'string' && typeof record.updated_at === 'string' ); } export class WorkspaceSecretVault { readonly rootDir: string; readonly filePath: string; private readonly encryption: WorkspaceSecretCrypto; private readonly platform: NodeJS.Platform; private readonly now: () => Date; private readonly randomBytes: (size: number) => Buffer; constructor(options: WorkspaceSecretVaultOptions = {}) { this.rootDir = options.rootDir ?? path.join(os.homedir(), '.eigent', 'secure'); this.filePath = path.join(this.rootDir, VAULT_FILE_NAME); this.encryption = options.crypto ?? defaultCrypto(); this.platform = options.platform ?? process.platform; this.now = options.now ?? (() => new Date()); this.randomBytes = options.randomBytes ?? crypto.randomBytes; } put(request: WorkspaceSecretPutRequest): WorkspaceSecretPutResult { assertScope(request); this.assertEncryptionAvailable(); const valueBytes = Buffer.byteLength(request.value, 'utf8'); if (valueBytes === 0 || valueBytes > MAX_WORKSPACE_SECRET_BYTES) { throw new WorkspaceSecretVaultError( 'Workspace secret value size is invalid' ); } const read = this.readDocument(); if (read.corrupted) { throw new WorkspaceSecretNeedsRebindError( 'Workspace secret vault is corrupted and must be recovered' ); } const document = read.document ?? { version: FORMAT_VERSION, records: {} }; // References are immutable commit candidates. Reusing one would let a // second renderer overwrite ciphertext before the Brain binding CAS wins. const secretRef = this.newSecretRef(); const timestamp = this.now().toISOString(); const record: EncryptedWorkspaceSecretRecord = { secret_ref: secretRef, account_scope_digest: request.account_scope_digest, space_id: request.space_id, revision_id: request.revision_id, slot_id: request.slot_id, ciphertext: this.encryption .encryptString(request.value) .toString('base64'), created_at: timestamp, updated_at: timestamp, }; document.records[secretRef] = record; this.atomicWrite(document); return this.statusFromRecord(record, 'available'); } status(lookup: WorkspaceSecretLookup): WorkspaceSecretStatus { assertScope(lookup); this.assertSecretRef(lookup.secret_ref); const read = this.readDocument(); if (read.corrupted) { return { ...lookup, state: 'needs_rebind' }; } const record = read.document?.records[lookup.secret_ref]; if (!record) return { ...lookup, state: 'missing' }; this.assertMatchingScope(record, lookup); if (!this.canDecrypt(record)) { return this.statusFromRecord(record, 'needs_rebind'); } return this.statusFromRecord(record, 'available'); } resolve(lookup: WorkspaceSecretLookup): string { assertScope(lookup); this.assertSecretRef(lookup.secret_ref); const read = this.readDocument(); if (read.corrupted) { throw new WorkspaceSecretNeedsRebindError( 'Workspace secret vault must be rebound' ); } const record = read.document?.records[lookup.secret_ref]; if (!record) { throw new WorkspaceSecretNotFoundError('Workspace secret is missing'); } this.assertMatchingScope(record, lookup); this.assertEncryptionAvailable(); try { return this.encryption.decryptString( Buffer.from(record.ciphertext, 'base64') ); } catch { throw new WorkspaceSecretNeedsRebindError( 'Workspace secret must be rebound' ); } } delete(lookup: WorkspaceSecretLookup): WorkspaceSecretStatus { assertScope(lookup); this.assertSecretRef(lookup.secret_ref); const read = this.readDocument(); if (read.corrupted) { throw new WorkspaceSecretNeedsRebindError( 'Workspace secret vault must be recovered before deleting records' ); } const document = read.document; const record = document?.records[lookup.secret_ref]; if (!document || !record) return { ...lookup, state: 'missing' }; this.assertMatchingScope(record, lookup); delete document.records[lookup.secret_ref]; this.atomicWrite(document); return { ...lookup, state: 'missing' }; } private assertEncryptionAvailable(): void { if (!this.encryption.isEncryptionAvailable()) { throw new WorkspaceSecretVaultError( 'OS-level encryption is unavailable; refusing plaintext storage' ); } if ( this.platform === 'linux' && this.encryption.getSelectedStorageBackend?.() === 'basic_text' ) { throw new WorkspaceSecretVaultError( 'Linux safeStorage selected the insecure basic_text backend' ); } } private canDecrypt(record: EncryptedWorkspaceSecretRecord): boolean { try { this.assertEncryptionAvailable(); this.encryption.decryptString(Buffer.from(record.ciphertext, 'base64')); return true; } catch { return false; } } private assertMatchingScope( record: EncryptedWorkspaceSecretRecord, lookup: WorkspaceSecretLookup ): void { if (!sameScope(record, lookup)) { throw new WorkspaceSecretBindingMismatchError( 'Workspace secret reference does not match its binding scope' ); } } private assertSecretRef(value: string): void { if (!/^wsvault_[A-Za-z0-9_-]{32}$/u.test(value)) { throw new WorkspaceSecretVaultError( 'Workspace secret reference is invalid' ); } } private newSecretRef(): string { return `wsvault_${this.randomBytes(24).toString('base64url')}`; } private statusFromRecord( record: EncryptedWorkspaceSecretRecord, state: 'available' ): WorkspaceSecretPutResult; private statusFromRecord( record: EncryptedWorkspaceSecretRecord, state: 'needs_rebind' ): WorkspaceSecretStatus; private statusFromRecord( record: EncryptedWorkspaceSecretRecord, state: 'available' | 'needs_rebind' ): WorkspaceSecretStatus { return { secret_ref: record.secret_ref, account_scope_digest: record.account_scope_digest, space_id: record.space_id, revision_id: record.revision_id, slot_id: record.slot_id, state, created_at: record.created_at, updated_at: record.updated_at, }; } private readDocument(): VaultReadResult { if (!fs.existsSync(this.filePath)) { return { document: null, corrupted: false }; } try { const stat = fs.lstatSync(this.filePath); if ( stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_VAULT_BYTES ) { return { document: null, corrupted: true }; } const parsed = JSON.parse( fs.readFileSync(this.filePath, 'utf8') ) as Partial; if ( parsed.version !== FORMAT_VERSION || !parsed.records || typeof parsed.records !== 'object' ) { return { document: null, corrupted: true }; } for (const [secretRef, record] of Object.entries(parsed.records)) { if ( !isEncryptedRecord(record) || record.secret_ref !== secretRef || !/^wsvault_[A-Za-z0-9_-]{32}$/u.test(secretRef) ) { return { document: null, corrupted: true }; } assertScope(record); } return { document: parsed as VaultDocument, corrupted: false }; } catch { return { document: null, corrupted: true }; } } private ensureSecureDirectory(): void { if (fs.existsSync(this.rootDir)) { const stat = fs.lstatSync(this.rootDir); if (stat.isSymbolicLink() || !stat.isDirectory()) { throw new WorkspaceSecretVaultError( 'Workspace secret vault directory is unsafe' ); } } else { fs.mkdirSync(this.rootDir, { recursive: true, mode: 0o700 }); } try { fs.chmodSync(this.rootDir, 0o700); } catch { // Windows and some filesystems do not implement POSIX permissions. } } private atomicWrite(document: VaultDocument): void { this.ensureSecureDirectory(); const serialized = JSON.stringify(document); if (Buffer.byteLength(serialized, 'utf8') > MAX_VAULT_BYTES) { throw new WorkspaceSecretVaultError('Workspace secret vault is full'); } const temporaryPath = path.join( this.rootDir, `.${VAULT_FILE_NAME}.${process.pid}.${this.randomBytes(8).toString('hex')}.tmp` ); let descriptor: number | null = null; try { descriptor = fs.openSync(temporaryPath, 'wx', 0o600); fs.writeFileSync(descriptor, serialized, 'utf8'); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = null; fs.renameSync(temporaryPath, this.filePath); try { fs.chmodSync(this.filePath, 0o600); } catch { // Best effort on filesystems without POSIX permissions. } try { const directoryDescriptor = fs.openSync(this.rootDir, 'r'); try { fs.fsyncSync(directoryDescriptor); } finally { fs.closeSync(directoryDescriptor); } } catch { // Directory fsync is unavailable on Windows and some filesystems. } } finally { if (descriptor !== null) fs.closeSync(descriptor); try { fs.unlinkSync(temporaryPath); } catch { // The successful rename already removed the temporary path. } } } }