{"version":3,"file":"keychain-wallet.mjs","names":[],"sources":["../../../src/services/keychain-wallet.ts"],"sourcesContent":["/**\n * Keychain Wallet — local key generation with encrypted Keychain storage.\n *\n * Extends `private_key` mode with:\n * - BIP-39 mnemonic generation via viem/accounts\n * - scrypt + AES-256-GCM envelope encryption\n * - macOS Keychain storage (via `security` CLI)\n * - Encrypted file fallback for Linux/Docker\n *\n * The runtime behavior is identical to raw `private_key` mode — once we have\n * a viem Account object, the same wallet client code path is used. The only\n * difference is how the key is acquired (generated vs env var) and stored\n * (Keychain vs plaintext env).\n *\n * No new dependencies. Uses:\n * - viem/accounts: generateMnemonic, mnemonicToAccount\n * - @scure/bip39/wordlists/english: BIP-39 English wordlist (transitive via viem)\n * - @noble/hashes/scrypt: key derivation\n * - node:crypto: AES-256-GCM encryption\n */\n\nimport { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { execSync, execFileSync } from 'node:child_process';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport interface EncryptedPayload {\n  /** scrypt salt (hex) */\n  salt: string;\n  /** AES-GCM nonce (hex) */\n  nonce: string;\n  /** AES-GCM ciphertext (hex) */\n  ciphertext: string;\n  /** AES-GCM auth tag (hex) */\n  tag: string;\n  /** Encryption version for future-proofing */\n  version: 1;\n}\n\nexport interface GeneratedWallet {\n  /** 12-word BIP-39 mnemonic */\n  mnemonic: string;\n  /** Derived Ethereum address */\n  address: string;\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────\n\nconst KEYCHAIN_ACCOUNT = 'eth';\nconst KEYCHAIN_SERVICE = 'clawncher_mnemonic';\nconst FALLBACK_DIR = join(process.env.HOME ?? '/root', '.openclawnch');\nconst FALLBACK_PATH = join(FALLBACK_DIR, 'wallet.enc');\nconst BACKUP_FILENAME = 'wallet-backup.enc';\n\n/** scrypt parameters: N=2^17, r=8, p=1. ~128MB memory, ~0.5s on modern hardware. */\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_DKLEN = 32; // 256-bit key for AES-256\n\n// ─── Platform Detection ──────────────────────────────────────────────────\n\nfunction isMacOS(): boolean {\n  return process.platform === 'darwin';\n}\n\n// ─── Encryption ──────────────────────────────────────────────────────────\n\n/**\n * Derive an AES-256 key from a password using scrypt.\n * Uses @noble/hashes for the scrypt implementation (already in dep tree via viem).\n */\nasync function deriveKey(password: string, salt: Buffer): Promise<Buffer> {\n  const { scryptAsync } = await import('@noble/hashes/scrypt.js');\n  const passwordBytes = Buffer.from(password, 'utf-8');\n  const derived = await scryptAsync(passwordBytes, salt, {\n    N: SCRYPT_N,\n    r: SCRYPT_R,\n    p: SCRYPT_P,\n    dkLen: SCRYPT_DKLEN,\n  });\n  return Buffer.from(derived);\n}\n\n/**\n * Encrypt a plaintext string with AES-256-GCM using a password-derived key.\n */\nexport async function encrypt(plaintext: string, password: string): Promise<EncryptedPayload> {\n  const salt = randomBytes(32);\n  const nonce = randomBytes(12); // 96-bit nonce for GCM\n  const key = await deriveKey(password, salt);\n\n  const cipher = createCipheriv('aes-256-gcm', key, nonce);\n  const encrypted = Buffer.concat([\n    cipher.update(plaintext, 'utf-8'),\n    cipher.final(),\n  ]);\n  const tag = cipher.getAuthTag();\n\n  return {\n    salt: salt.toString('hex'),\n    nonce: nonce.toString('hex'),\n    ciphertext: encrypted.toString('hex'),\n    tag: tag.toString('hex'),\n    version: 1,\n  };\n}\n\n/**\n * Decrypt an AES-256-GCM encrypted payload using a password-derived key.\n * Throws on wrong password (GCM auth tag mismatch).\n */\nexport async function decrypt(payload: EncryptedPayload, password: string): Promise<string> {\n  if (payload.version !== 1) {\n    throw new Error(`Unsupported encryption version: ${payload.version}`);\n  }\n\n  const salt = Buffer.from(payload.salt, 'hex');\n  const nonce = Buffer.from(payload.nonce, 'hex');\n  const ciphertext = Buffer.from(payload.ciphertext, 'hex');\n  const tag = Buffer.from(payload.tag, 'hex');\n  const key = await deriveKey(password, salt);\n\n  const decipher = createDecipheriv('aes-256-gcm', key, nonce);\n  decipher.setAuthTag(tag);\n\n  try {\n    const decrypted = Buffer.concat([\n      decipher.update(ciphertext),\n      decipher.final(),\n    ]);\n    return decrypted.toString('utf-8');\n  } catch {\n    throw new Error('Decryption failed — wrong password or corrupted data.');\n  }\n}\n\n// ─── Keychain Operations (macOS) ─────────────────────────────────────────\n\n/**\n * Store an encrypted payload in macOS Keychain.\n */\nfunction keychainStore(payload: EncryptedPayload): void {\n  const json = JSON.stringify(payload);\n  // Delete existing entry first (update = delete + add)\n  try {\n    execFileSync(\n      'security',\n      ['delete-generic-password', '-a', KEYCHAIN_ACCOUNT, '-s', KEYCHAIN_SERVICE, 'login.keychain-db'],\n      { stdio: 'pipe' },\n    );\n  } catch {\n    // Not found — fine, we're creating fresh\n  }\n\n  execFileSync(\n    'security',\n    ['add-generic-password', '-a', KEYCHAIN_ACCOUNT, '-s', KEYCHAIN_SERVICE, '-w', json, 'login.keychain-db'],\n    { stdio: 'pipe' },\n  );\n}\n\n/**\n * Load an encrypted payload from macOS Keychain.\n * Returns null if not found.\n */\nfunction keychainLoad(): EncryptedPayload | null {\n  try {\n    const raw = execFileSync(\n      'security',\n      ['find-generic-password', '-a', KEYCHAIN_ACCOUNT, '-s', KEYCHAIN_SERVICE, '-w', 'login.keychain-db'],\n      { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },\n    ).trim();\n    return JSON.parse(raw) as EncryptedPayload;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Check if an encrypted wallet exists in macOS Keychain.\n */\nfunction keychainExists(): boolean {\n  try {\n    execFileSync(\n      'security',\n      ['find-generic-password', '-a', KEYCHAIN_ACCOUNT, '-s', KEYCHAIN_SERVICE, 'login.keychain-db'],\n      { stdio: 'pipe' },\n    );\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Delete the encrypted wallet from macOS Keychain.\n */\nfunction keychainDelete(): boolean {\n  try {\n    execSync(\n      `security delete-generic-password -a \"${KEYCHAIN_ACCOUNT}\" -s \"${KEYCHAIN_SERVICE}\" login.keychain-db`,\n      { stdio: 'pipe' },\n    );\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n// ─── Encrypted File Fallback (Linux/Docker) ──────────────────────────────\n\nfunction ensureFallbackDir(): void {\n  if (!existsSync(FALLBACK_DIR)) {\n    mkdirSync(FALLBACK_DIR, { recursive: true, mode: 0o700 });\n  }\n}\n\nfunction fileStore(payload: EncryptedPayload): void {\n  ensureFallbackDir();\n  writeFileSync(FALLBACK_PATH, JSON.stringify(payload), { mode: 0o600 });\n}\n\nfunction fileLoad(): EncryptedPayload | null {\n  try {\n    const raw = readFileSync(FALLBACK_PATH, 'utf-8');\n    return JSON.parse(raw) as EncryptedPayload;\n  } catch {\n    return null;\n  }\n}\n\nfunction fileExists(): boolean {\n  return existsSync(FALLBACK_PATH);\n}\n\nfunction fileDelete(): boolean {\n  try {\n    unlinkSync(FALLBACK_PATH);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n// ─── Unified Storage Interface ───────────────────────────────────────────\n\nfunction storePayload(payload: EncryptedPayload): void {\n  if (isMacOS()) {\n    keychainStore(payload);\n  } else {\n    fileStore(payload);\n  }\n}\n\nfunction loadPayload(): EncryptedPayload | null {\n  if (isMacOS()) {\n    return keychainLoad();\n  }\n  return fileLoad();\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Generate a new BIP-39 wallet. Returns the mnemonic and derived address.\n * Does NOT store anything — call encryptAndStore() after user confirms backup.\n */\nexport async function generateWallet(): Promise<GeneratedWallet> {\n  const { generateMnemonic, mnemonicToAccount } = await import('viem/accounts');\n  const { wordlist } = await import('@scure/bip39/wordlists/english.js');\n\n  const mnemonic = generateMnemonic(wordlist);\n  const account = mnemonicToAccount(mnemonic);\n\n  return {\n    mnemonic,\n    address: account.address,\n  };\n}\n\n/**\n * Encrypt a mnemonic with user password and store in Keychain (macOS) or\n * encrypted file (Linux/Docker).\n */\nexport async function encryptAndStore(mnemonic: string, password: string): Promise<void> {\n  // Validate mnemonic before storing\n  const words = mnemonic.trim().split(/\\s+/);\n  if (words.length !== 12 && words.length !== 24) {\n    throw new Error(`Invalid mnemonic: expected 12 or 24 words, got ${words.length}`);\n  }\n\n  // Enforce minimum password strength — scrypt is pointless with a weak password\n  if (!password || password.length < 8) {\n    throw new Error('Password must be at least 8 characters.');\n  }\n\n  const payload = await encrypt(mnemonic, password);\n  storePayload(payload);\n}\n\n/**\n * Load and decrypt the stored mnemonic, then derive a viem Account.\n * Returns the Account object ready for privateKeyToAccount-style usage.\n */\nexport async function loadAndDecrypt(password: string): Promise<{\n  account: any; // viem Account — using any to avoid version conflicts\n  mnemonic: string;\n  address: string;\n}> {\n  const payload = loadPayload();\n  if (!payload) {\n    throw new Error('No encrypted wallet found in storage.');\n  }\n\n  const mnemonic = await decrypt(payload, password);\n  const { mnemonicToAccount } = await import('viem/accounts');\n  const account = mnemonicToAccount(mnemonic);\n\n  return {\n    account,\n    mnemonic,\n    address: account.address,\n  };\n}\n\n/**\n * Check if an encrypted wallet exists in storage.\n */\nexport function hasKeychainWallet(): boolean {\n  if (isMacOS()) {\n    return keychainExists();\n  }\n  return fileExists();\n}\n\n/**\n * Delete the encrypted wallet from storage.\n */\nexport function deleteKeychainWallet(): boolean {\n  if (isMacOS()) {\n    return keychainDelete();\n  }\n  return fileDelete();\n}\n\n/**\n * Export encrypted backup file to a specified path (or default backup location).\n * Returns the path where the backup was written.\n */\nexport function exportBackupFile(outputDir?: string): string {\n  const payload = loadPayload();\n  if (!payload) {\n    throw new Error('No encrypted wallet found in storage.');\n  }\n\n  const dir = outputDir ?? FALLBACK_DIR;\n  if (!existsSync(dir)) {\n    mkdirSync(dir, { recursive: true, mode: 0o700 });\n  }\n\n  const backupPath = join(dir, BACKUP_FILENAME);\n  writeFileSync(backupPath, JSON.stringify(payload, null, 2), { mode: 0o600 });\n  return backupPath;\n}\n\n/**\n * Import wallet from a backup file. Requires the original password.\n * Stores into the active storage backend (Keychain or file).\n */\nexport async function importFromBackup(backupPath: string, password: string): Promise<string> {\n  const raw = readFileSync(backupPath, 'utf-8');\n  const payload = JSON.parse(raw) as EncryptedPayload;\n\n  // Verify we can decrypt it (validates password)\n  const mnemonic = await decrypt(payload, password);\n  const { mnemonicToAccount } = await import('viem/accounts');\n  const account = mnemonicToAccount(mnemonic);\n\n  // Store in active backend\n  storePayload(payload);\n\n  return account.address;\n}\n\n/**\n * Generate 3 random word indices for mnemonic confirmation.\n * Returns array of {index, word} pairs the user must confirm.\n */\nexport function getConfirmationWords(mnemonic: string, count = 3): Array<{ index: number; word: string }> {\n  const words = mnemonic.trim().split(/\\s+/);\n  const indices = new Set<number>();\n  while (indices.size < count) {\n    const idx = Math.floor(Math.random() * words.length);\n    indices.add(idx);\n  }\n  return Array.from(indices)\n    .sort((a, b) => a - b)\n    .map(idx => ({ index: idx + 1, word: words[idx]! })); // 1-indexed for display\n}\n\n/**\n * Validate that the user's confirmation words match the mnemonic.\n */\nexport function validateConfirmation(\n  mnemonic: string,\n  confirmations: Array<{ index: number; word: string }>,\n): boolean {\n  const words = mnemonic.trim().split(/\\s+/);\n  return confirmations.every(\n    ({ index, word }) => words[index - 1]?.toLowerCase() === word.toLowerCase(),\n  );\n}\n\n/**\n * Get the storage backend description (for display to user).\n */\nexport function getStorageInfo(): { backend: 'keychain' | 'file'; path?: string } {\n  if (isMacOS()) {\n    return { backend: 'keychain' };\n  }\n  return { backend: 'file', path: FALLBACK_PATH };\n}\n"],"mappings":"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"}