From 900e1ef58643f27a540874c38abe6c20e422c38f Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 9 Sep 2026 10:33:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/aegis):=200.6.1=20=E2=80=94=20in-p?= =?UTF-8?q?anel=20vault=20setup/unlock,=20BCH=20wallet=20imports,=20opt-in?= =?UTF-8?q?=20fiat=20prices,=20WizardConnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down. --- src/lib/password-vault.js | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/lib/password-vault.js b/src/lib/password-vault.js index 54b050f..ae2ae0d 100644 --- a/src/lib/password-vault.js +++ b/src/lib/password-vault.js @@ -218,3 +218,104 @@ export function listMetadata(state) { kind: e.literal ? "literal" : "generated", })); } + +// -- wallet imports ---------------------------------------------------------- +// Externally-generated wallet material (imported seeds + BIP44 paths, or raw +// WIFs) lives in its OWN encrypted file, `wallet-imports.enc`, distinct from +// the primary passwords vault. See DESIGN-wallet-multi-account-amendment.md. +// +// Same PBKDF2/AES-GCM envelope as the passwords vault, same master password, +// deliberately DIFFERENT KDF salt so the two AES keys are cryptographically +// disjoint — a leak of one key never yields the other. Blast radius: a bug +// that corrupts wallet-imports.enc cannot destroy the primary seed and vice +// versa. +// +// Derivation rule (design §3): imports are used DIRECTLY — +// `HDKey.fromMasterSeed(seed).derive(path)` OR the raw WIF's private key. +// No HKDF steps. The `path` field is the disambiguator: when present, honour +// it verbatim so the same seed + path produces the same address across +// Electron Cash, mainnet-js, Deviant's keystore, and Theseus. + +const IMPORTS_VERSION = "silentmode/wallet-imports/1"; + +function writeImportsFile(path, { salt, iv, ct, iters }) { + const raw = { + version: VAULT_VERSION, + kdf: { name: "PBKDF2", iters, salt: bytesToHex(salt) }, + iv: bytesToHex(iv), + ciphertext: bytesToHex(ct), + }; + writeFileSync(path + ".tmp", JSON.stringify(raw), "utf8"); + renameSync(path + ".tmp", path); +} + +export function importsExist(path) { return existsSync(path); } + +// Create an empty imports file. Called the first time the user adds an +// external wallet. Uses a fresh salt so its AES key is unrelated to the +// passwords vault's. +export async function createImports(path, masterPw) { + const salt = wc.getRandomValues(new Uint8Array(16)); + const iv = wc.getRandomValues(new Uint8Array(12)); + const key = await deriveVaultKey(masterPw, salt, PBKDF2_ITERS); + const plaintext = { version: IMPORTS_VERSION, generated: dateStamp(), accounts: {} }; + const ct = new Uint8Array(await wc.subtle.encrypt( + { name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(plaintext)))); + writeImportsFile(path, { salt, iv, ct, iters: PBKDF2_ITERS }); + return { key, accounts: {}, _salt: salt, _iters: PBKDF2_ITERS }; +} + +// Unlock the imports file with the same master password used for the primary +// vault. Salt is the imports file's own — the key differs from vaultState.key. +export async function unlockImports(path, masterPw) { + const raw = JSON.parse(readFileSync(path, "utf8")); + if (raw.version !== VAULT_VERSION) throw new Error(`imports version ${raw.version} not supported`); + const salt = hexToBytes(raw.kdf.salt); + const iv = hexToBytes(raw.iv); + const ct = hexToBytes(raw.ciphertext); + const key = await deriveVaultKey(masterPw, salt, raw.kdf.iters); + let pt; + try { pt = new Uint8Array(await wc.subtle.decrypt({ name: "AES-GCM", iv }, key, ct)); } + catch { throw new Error("bad master password"); } + const plaintext = JSON.parse(dec.decode(pt)); + return { + key, + accounts: plaintext.accounts || {}, + _salt: salt, _iters: raw.kdf.iters, + }; +} + +export async function saveImports(path, state) { + const iv = wc.getRandomValues(new Uint8Array(12)); + const plaintext = { version: IMPORTS_VERSION, generated: dateStamp(), accounts: state.accounts || {} }; + const ct = new Uint8Array(await wc.subtle.encrypt( + { name: "AES-GCM", iv }, state.key, enc.encode(JSON.stringify(plaintext)))); + writeImportsFile(path, { salt: state._salt, iv, ct, iters: state._iters }); +} + +// Public metadata for the imports UI. Never returns seeds/WIFs. +export function listImportsMetadata(state) { + return Object.entries(state.accounts || {}).map(([id, a]) => ({ + id, + kind: a.kind, + cashaddr: a.cashaddr, + label: a.label, + category: a.category || "", + source: a.source || "", + path: a.path || "", + createdAt: a.createdAt || 0, + })); +} + +// Signer material for one imported wallet. Callers hold this in RAM for the +// duration of a single signing operation and drop it. Only ever exposed to +// add-ons that already have vault-derive-equivalent trust (i.e. the wallet +// add-on that consumes imports). +export function getImportSigner(state, id) { + const a = (state.accounts || {})[id]; + if (!a) throw new Error("no such import"); + if (a.kind === "seed") return { kind: "seed", seed: a.seed, path: a.path }; + if (a.kind === "wif") return { kind: "wif", wif: a.wif }; + throw new Error(`unknown import kind: ${a.kind}`); +} +