Ctrl K
ComunidadGuías…

electron/main/workspaceSecrets/vault.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// ========= 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<string, EncryptedWorkspaceSecretRecord>;
}

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<EncryptedWorkspaceSecretRecord>;
  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<VaultDocument>;
      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.
      }
    }
  }
}