{"version":3,"file":"agent-keystore.mjs","names":[],"sources":["../../../src/services/agent-keystore.ts"],"sourcesContent":["/**\n * Agent Keystore — Secure storage for the agent's private key.\n *\n * The agent account is a HybridDeleGator smart account deployed by the user.\n * The agent's private key (delegate key) is what enables autonomous execution.\n * Losing this key means the agent can't execute; the user can still access\n * funds via their owner key.\n *\n * Storage hierarchy (checked in order):\n * 1. macOS Keychain — most secure, survives app reinstalls\n * 2. Encrypted file — AES-256-GCM with user passphrase\n * 3. In-memory only — lost on restart (for testing)\n *\n * The key is NEVER logged, written to unencrypted files, or sent over the network.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createCipheriv, createDecipheriv, randomBytes, createHash, scryptSync } from 'node:crypto';\nimport { execSync, execFileSync } from 'node:child_process';\n\nconst HOME = process.env.HOME ?? '/home/openclawnch';\nconst AGENT_DIR = join(HOME, '.openclawnch', 'agent');\nconst KEYSTORE_PATH = join(AGENT_DIR, 'keystore.enc');\nconst META_PATH = join(AGENT_DIR, 'meta.json');\nconst KEYCHAIN_SERVICE = 'openclawnch-agent';\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface AgentMeta {\n  /** The HybridDeleGator smart account address. */\n  smartAccountAddress: string;\n  /** The agent's EOA address (derived from the private key). */\n  agentAddress: string;\n  /** The user's EOA address (owner of the smart account). */\n  ownerAddress: string;\n  /** Chain the smart account was deployed on. */\n  chainId: number;\n  /** ISO timestamp of creation. */\n  createdAt: string;\n  /** Where the key is stored: 'keychain' | 'encrypted_file' | 'memory' */\n  storageMethod: 'keychain' | 'encrypted_file' | 'memory';\n}\n\n// ─── In-Memory Cache ────────────────────────────────────────────────────\n\nlet _cachedKey: string | null = null;\nlet _cachedMeta: AgentMeta | null = null;\n\n// ─── Directory Setup ────────────────────────────────────────────────────\n\nfunction ensureDir(): void {\n  if (!existsSync(AGENT_DIR)) {\n    mkdirSync(AGENT_DIR, { recursive: true, mode: 0o700 });\n  }\n}\n\n// ─── macOS Keychain ─────────────────────────────────────────────────────\n\nfunction isKeychainAvailable(): boolean {\n  if (process.platform !== 'darwin') return false;\n  try {\n    execSync('which security', { stdio: 'ignore' });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction saveToKeychain(agentAddress: string, privateKey: string): boolean {\n  try {\n    // Delete existing entry if present (update)\n    try {\n      execFileSync(\n        'security',\n        ['delete-generic-password', '-a', agentAddress, '-s', KEYCHAIN_SERVICE],\n        { stdio: 'ignore' },\n      );\n    } catch { /* not found — fine */ }\n\n    execFileSync(\n      'security',\n      ['add-generic-password', '-a', agentAddress, '-s', KEYCHAIN_SERVICE, '-w', privateKey, '-T', ''],\n      { stdio: 'ignore' },\n    );\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction loadFromKeychain(agentAddress: string): string | null {\n  try {\n    const result = execFileSync(\n      'security',\n      ['find-generic-password', '-a', agentAddress, '-s', KEYCHAIN_SERVICE, '-w'],\n      { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] },\n    );\n    return result.trim();\n  } catch {\n    return null;\n  }\n}\n\n// ─── Encrypted File Storage ─────────────────────────────────────────────\n\nfunction deriveFileKey(passphrase: string): Buffer {\n  // scrypt with strong params: N=2^17, r=8, p=1, maxmem=256MB\n  // Default OpenSSL maxmem is 32MB which is too low for N=2^17.\n  return scryptSync(passphrase, 'openclawnch-agent-salt-v1', 32, {\n    N: 131072, r: 8, p: 1,\n    maxmem: 256 * 1024 * 1024,\n  });\n}\n\nfunction saveToEncryptedFile(privateKey: string, passphrase: string): void {\n  ensureDir();\n  const key = deriveFileKey(passphrase);\n  const iv = randomBytes(12);\n  const cipher = createCipheriv('aes-256-gcm', key, iv);\n  const encrypted = Buffer.concat([cipher.update(privateKey, 'utf8'), cipher.final()]);\n  const tag = cipher.getAuthTag();\n\n  const data = Buffer.concat([iv, tag, encrypted]);\n  const tmpPath = KEYSTORE_PATH + '.tmp.' + Date.now();\n  writeFileSync(tmpPath, data, { mode: 0o600 });\n  renameSync(tmpPath, KEYSTORE_PATH);\n}\n\nfunction loadFromEncryptedFile(passphrase: string): string | null {\n  if (!existsSync(KEYSTORE_PATH)) return null;\n\n  try {\n    const data = readFileSync(KEYSTORE_PATH);\n    const key = deriveFileKey(passphrase);\n    const iv = data.subarray(0, 12);\n    const tag = data.subarray(12, 28);\n    const ciphertext = data.subarray(28);\n    const decipher = createDecipheriv('aes-256-gcm', key, iv);\n    decipher.setAuthTag(tag);\n    return decipher.update(ciphertext) + decipher.final('utf8');\n  } catch {\n    return null;\n  }\n}\n\n// ─── Meta Storage ───────────────────────────────────────────────────────\n\nexport function saveMeta(meta: AgentMeta): void {\n  ensureDir();\n  _cachedMeta = meta;\n  const tmpPath = META_PATH + '.tmp.' + Date.now();\n  writeFileSync(tmpPath, JSON.stringify(meta, null, 2), { mode: 0o600 });\n  renameSync(tmpPath, META_PATH);\n}\n\nexport function loadMeta(): AgentMeta | null {\n  if (_cachedMeta) return _cachedMeta;\n\n  if (!existsSync(META_PATH)) return null;\n  try {\n    _cachedMeta = JSON.parse(readFileSync(META_PATH, 'utf8')) as AgentMeta;\n    return _cachedMeta;\n  } catch {\n    return null;\n  }\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────\n\n/**\n * Store the agent's private key securely.\n * Tries: macOS Keychain → encrypted file → memory only.\n *\n * Returns the storage method used.\n */\nexport function storeAgentKey(\n  agentAddress: string,\n  privateKey: string,\n  passphrase?: string,\n): 'keychain' | 'encrypted_file' | 'memory' {\n  _cachedKey = privateKey;\n\n  // Try Keychain first\n  if (isKeychainAvailable()) {\n    if (saveToKeychain(agentAddress, privateKey)) {\n      return 'keychain';\n    }\n  }\n\n  // Fallback: encrypted file (requires passphrase)\n  if (passphrase && passphrase.length >= 8) {\n    saveToEncryptedFile(privateKey, passphrase);\n    return 'encrypted_file';\n  }\n\n  // Last resort: memory only\n  return 'memory';\n}\n\n/**\n * Load the agent's private key.\n * Tries: memory cache → macOS Keychain → encrypted file (passphrase from arg or env).\n */\nexport function loadAgentKey(passphrase?: string): string | null {\n  if (_cachedKey) return _cachedKey;\n\n  const meta = loadMeta();\n  if (!meta) return null;\n\n  // Try Keychain\n  if (meta.storageMethod === 'keychain' || isKeychainAvailable()) {\n    const key = loadFromKeychain(meta.agentAddress);\n    if (key) {\n      _cachedKey = key;\n      return key;\n    }\n  }\n\n  // Auto-unlock from DELEGATOR_PASSPHRASE env var\n  if (!passphrase) {\n    passphrase = process.env.DELEGATOR_PASSPHRASE ?? undefined;\n  }\n\n  // Try encrypted file\n  if (passphrase) {\n    const key = loadFromEncryptedFile(passphrase);\n    if (key) {\n      _cachedKey = key;\n      return key;\n    }\n  }\n\n  return null;\n}\n\n/**\n * Check if an agent account exists (meta file present).\n */\nexport function hasAgentAccount(): boolean {\n  return loadMeta() !== null;\n}\n\n/**\n * Clear all agent data (for testing).\n */\nexport function resetAgentKeystore(): void {\n  _cachedKey = null;\n  _cachedMeta = null;\n}\n"],"mappings":"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"}