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:
parent
c20a10d480
commit
900e1ef586
1 changed files with 101 additions and 0 deletions
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue