feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect

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.
This commit is contained in:
Local Dev 2026-09-09 10:33:21 +02:00
parent c20a10d480
commit 900e1ef586

View file

@ -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}`);
}