theseus/bundled-addons/aegis/lib/import-derive.js
Local Dev 9e7e9d5e8b Aegis 0.31.0: chain adapters stop trusting what they should check
Start of the next batch; 0.30.0 is published.

- Tron panel sends signed whatever /wallet/createtransaction returned while
  the approval showed the local request. The returned bytes are now decoded
  and must be one transfer from this wallet, to that address, for that
  amount, with a matching txID - checked at plan time and again at signing.
- Importing a Solana wallet from a seed phrase threw on every attempt (a
  mis-parenthesised `new require("crypto").createHmac` plus a bare require
  of an ESM-only subpath). SLIP-0010 now uses Node's HMAC, as chain-sol does.
- Imported BCH wallets put token-bearing UTXOs into coin selection. They are
  excluded, as in the HD wallet, and the balance counts what can be spent.
- WizardConnect dropped the token from each spent output before signing, so
  under SIGHASH_UTXOS every signature of a token transaction was invalid.
- A wallet disposed while a refresh was in flight re-armed its poll timer.
- BCMR registry content is bounded before it reaches the panel: control and
  bidi characters stripped, lengths capped, decimals 0-18, icons https/ipfs
  only, registries https only.
2026-10-04 01:38:50 +02:00

287 lines
14 KiB
JavaScript

// Per-chain address derivation for imported wallets. Every helper turns
// either a BIP39 mnemonic (+ path) OR a raw private key (chain-native
// format — WIF for UTXO chains, hex for account chains, base58 for Solana)
// into the canonical address that chain uses.
//
// Deps arrive from index.js loadDeps() so nothing here has to know about
// npm packages — same "hand it in" pattern the other adapters use.
module.exports = function makeImportDerive({
HDKey, secp256k1, ed25519, sha256, sha512, hmac, ripemd160, keccak_256, blake2b,
cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39,
dgbCore,
}) {
// Sia's key derivation lives in lib/sia/sia.js — reuse it here so
// imported SC wallets end up with byte-identical addresses to what
// Sia Central Lite or walletd would show for the same seed.
const sia = blake2b ? require("./sia/sia.js")({ ed25519, blake2b }) : null;
// Taproot needs bitcoinjs-lib's schnorr backend installed via initEccLib,
// which is process-global and must happen once. This module used to rely on
// chain-btc.js (and, before it went lazy, lib/dgb/core/address.js) doing it
// at load time — a hidden dependency on an unrelated module's import order,
// which broke the moment either stopped being eagerly loaded. Own it here.
let eccInstalled = false;
function ensureEcc() {
if (eccInstalled) return;
try { if (bitcoinjs && bitcoinjs.initEccLib && ecc) bitcoinjs.initEccLib(ecc); } catch { /* p2tr will report it */ }
eccInstalled = true;
}
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const fromHex = (h) => {
const s = String(h || "").replace(/^0x/i, "");
const out = new Uint8Array(s.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
return out;
};
const hash160 = (b) => ripemd160(sha256(b));
// BIP39 mnemonic → 64-byte seed hex. Same wire format the vault-derive
// path stores, so keystore-mirrored seeds land in wallet-imports.enc
// identically whether they came from a mnemonic or hex directly.
function mnemonicToSeedHex(m) {
if (!bip39.validateMnemonic(m)) throw new Error("invalid BIP39 mnemonic");
return toHex(bip39.mnemonicToSeedSync(m));
}
// ---- BTC ------------------------------------------------------------------
const BTC_NET = {
mainnet: bitcoinjs.networks.bitcoin,
testnet3: bitcoinjs.networks.testnet,
signet: bitcoinjs.networks.testnet, // signet uses testnet params here
};
function btcAddressFromNode(node, path, network) {
const net = BTC_NET[network];
if (!net) throw new Error(`unknown BTC network ${network}`);
// Purpose byte in the path decides the address type. m/84' -> bech32,
// m/49' -> P2SH-P2WPKH, m/86' -> P2TR, m/44' -> P2PKH.
const m = /^m\/(\d+)'/.exec(String(path || ""));
const purpose = m ? Number(m[1]) : 84;
const pk = Buffer.from(node.publicKey);
if (purpose === 86) {
// Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey.
const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
}
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
}
function deriveBtcFromSeed(seedHex, path, network) {
const bip32 = bip32Factory(ecc);
const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), BTC_NET[network]).derivePath(path);
return btcAddressFromNode(node, path, network);
}
function deriveBtcFromWif(wif, network, hint) {
const ECPair = ecpairFactory(ecc);
const kp = ECPair.fromWIF(wif, BTC_NET[network]);
// WIF alone doesn't tell us the address family; caller passes hint = 44/49/84/86.
const purpose = hint || 84;
const pk = kp.publicKey;
const net = BTC_NET[network];
if (purpose === 86) {
const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
}
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
}
// ---- DGB (mirrors BTC pattern with digibyte params) -----------------------
function digibyteNetwork() {
if (!dgbCore) throw new Error("DGB adapter not available");
return dgbCore.digibyte;
}
// DigiByte's 2018-19 official mobile wallets (BreadWallet forks) built the
// master node with HMAC-SHA512 key "DigiByte seed" rather than BIP32's
// "Bitcoin seed", so the same words yield a different key tree and a
// standard scan finds nothing. Pass hmacKey to recover those.
// Ported from Digibyte.X/dgb-wallet @ 989a2696.
const HMAC_BITCOIN_SEED = "Bitcoin seed";
const HMAC_DIGIBYTE_SEED = "DigiByte seed";
function dgbRootFromSeed(seedBytes, net, hmacKey) {
const bip32 = bip32Factory(ecc);
if (!hmacKey || hmacKey === HMAC_BITCOIN_SEED) return bip32.fromSeed(Buffer.from(seedBytes), net);
if (!hmac || !sha512) throw new Error("this build cannot derive the legacy DigiByte master key");
const I = hmac(sha512, new TextEncoder().encode(hmacKey), Buffer.from(seedBytes));
return bip32.fromPrivateKey(Buffer.from(I.slice(0, 32)), Buffer.from(I.slice(32)), net);
}
function deriveDgbFromSeed(seedHex, path, hmacKey) {
const net = digibyteNetwork();
const node = dgbRootFromSeed(fromHex(seedHex), net, hmacKey).derivePath(path);
const pk = Buffer.from(node.publicKey);
const m = /^m\/(\d+)'/.exec(String(path || ""));
const purpose = m ? Number(m[1]) : 84;
if (purpose === 86) {
const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
}
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
}
function deriveDgbFromWif(wif, hint) {
const ECPair = ecpairFactory(ecc);
const net = digibyteNetwork();
const kp = ECPair.fromWIF(wif, net);
const purpose = hint || 84;
const pk = kp.publicKey;
ensureEcc();
if (purpose === 86) return bitcoinjs.payments.p2tr({ internalPubkey: pk.slice(1, 33), network: net }).address;
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
}
// ---- ETH (EIP-55 checksummed 0x address) ----------------------------------
function ethAddressFromPubkey(pubUncompressed64) {
// Strip the 0x04 prefix if present so we hash just the 64 raw bytes.
const raw = pubUncompressed64.length === 65 ? pubUncompressed64.slice(1) : pubUncompressed64;
const h = keccak_256(raw);
const addr20 = h.slice(-20);
const hex = toHex(addr20);
// EIP-55 checksum
const hashOfLower = toHex(keccak_256(new TextEncoder().encode(hex)));
let out = "0x";
for (let i = 0; i < hex.length; i++) {
out += parseInt(hashOfLower[i], 16) >= 8 ? hex[i].toUpperCase() : hex[i];
}
return out;
}
function deriveEthFromSeed(seedHex, path) {
const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path);
// secp256k1.getPublicKey with compressed=false gives 65 bytes (04||X||Y).
const pub = secp256k1.getPublicKey(node.privateKey, false);
return ethAddressFromPubkey(pub);
}
function deriveEthFromPrivHex(hex) {
const priv = fromHex(hex);
if (priv.length !== 32) throw new Error("ETH private key must be 32 bytes hex");
const pub = secp256k1.getPublicKey(priv, false);
return ethAddressFromPubkey(pub);
}
// ---- TRX (T... base58check, network 0x41) --------------------------------
function tronAddressFromPubkey(pubUncompressed65) {
const raw = pubUncompressed65.length === 65 ? pubUncompressed65.slice(1) : pubUncompressed65;
const h = keccak_256(raw);
const last20 = h.slice(-20);
const versioned = new Uint8Array(21);
versioned[0] = 0x41; // Tron mainnet address prefix — same for Nile testnet
versioned.set(last20, 1);
return base58check.encodeCheck(versioned);
}
function deriveTrxFromSeed(seedHex, path) {
const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path);
const pub = secp256k1.getPublicKey(node.privateKey, false);
return tronAddressFromPubkey(pub);
}
function deriveTrxFromPrivHex(hex) {
const priv = fromHex(hex);
if (priv.length !== 32) throw new Error("TRX private key must be 32 bytes hex");
const pub = secp256k1.getPublicKey(priv, false);
return tronAddressFromPubkey(pub);
}
// ---- SOL (base58 pubkey, ed25519) ----------------------------------------
// SLIP-0010 ed25519 hardened derivation. Slightly different HD scheme
// from BIP32 secp256k1 — every step is hardened, index >= 0x80000000.
function slip0010DeriveEd25519(seed, path) {
const HMAC_KEY = new TextEncoder().encode("ed25519 seed");
const parts = String(path).split("/").slice(1);
// HMAC-SHA512(HMAC_KEY, seed) → I = I_L || I_R, sk = I_L, cc = I_R; each
// step is HMAC-SHA512(cc, 0x00 || sk || idx). Node's own HMAC, same as
// chain-sol.js: the add-on runs in Electron main, and the previous
// `new require("crypto").createHmac` plus a bare require of an ESM-only
// @noble/hashes subpath threw on every Solana seed import.
const nodeCrypto = require("node:crypto");
const hmac = (_h, key, data) => new Uint8Array(nodeCrypto.createHmac("sha512", Buffer.from(key)).update(Buffer.from(data)).digest());
const sha512 = null;
let I = hmac(sha512, HMAC_KEY, seed);
let sk = I.slice(0, 32); let cc = I.slice(32);
for (const seg of parts) {
const m = /^(\d+)'?$/.exec(seg);
if (!m) throw new Error(`bad path segment: ${seg}`);
const idx = (Number(m[1]) | 0x80000000) >>> 0;
const data = new Uint8Array(1 + 32 + 4);
data[0] = 0;
data.set(sk, 1);
data[33] = (idx >>> 24) & 0xff; data[34] = (idx >>> 16) & 0xff;
data[35] = (idx >>> 8) & 0xff; data[36] = idx & 0xff;
I = hmac(sha512, cc, data);
sk = I.slice(0, 32); cc = I.slice(32);
}
return sk;
}
function deriveSolFromSeed(seedHex, path) {
const sk = slip0010DeriveEd25519(fromHex(seedHex), path);
const pub = ed25519.getPublicKey(sk);
return base58check.encodeBase58(pub);
}
function deriveSolFromPrivHex(hex) {
const priv = fromHex(hex);
if (priv.length !== 32 && priv.length !== 64) throw new Error("SOL private key must be 32 or 64 bytes hex");
const seed = priv.length === 64 ? priv.slice(0, 32) : priv;
const pub = ed25519.getPublicKey(seed);
return base58check.encodeBase58(pub);
}
function deriveSolFromBase58(b58) {
const bytes = base58check.decodeBase58(b58);
if (bytes.length !== 32 && bytes.length !== 64) throw new Error("SOL private key base58 must decode to 32 or 64 bytes");
const seed = bytes.length === 64 ? bytes.slice(0, 32) : bytes;
const pub = ed25519.getPublicKey(seed);
return base58check.encodeBase58(pub);
}
// ---- SC (Siacoin) --------------------------------------------------------
// Sia's walletd + Sia Central Lite Wallet both use a 32-byte root seed.
// Sia Central Lite exports it as a BIP39 12-word mnemonic (PBKDF2 →
// 64-byte seed → first 32 bytes = root); walletd's API accepts the raw
// 32-byte hex. Address at index N: standardUnlockHash(ed25519.pub(
// blake2b(root32 || u64le(N))
// )) — see lib/sia/sia.js:keyFromSeed for the byte layout.
function deriveScRootFromMnemonic(m) {
// BIP39 → 512-bit master seed; Sia Central takes the FIRST 32 bytes as
// the walletd root. Trimming the tail keeps addresses identical to
// what sialite.com and Sia Central mobile derive for the same phrase.
const fullSeedHex = mnemonicToSeedHex(m);
return fullSeedHex.slice(0, 64);
}
function deriveScRootFromHex(seedHex) {
const s = String(seedHex || "").trim().toLowerCase().replace(/^0x/, "");
if (!/^[0-9a-f]{64}$/.test(s)) throw new Error("SC seed hex must be exactly 32 bytes (64 hex chars)");
return s;
}
function deriveScAddressFromSeed(seedHex, index) {
if (!sia) throw new Error("SC derive unavailable (blake2b dep not passed)");
const root = fromHex(seedHex);
if (root.length !== 32) throw new Error("SC root seed must be 32 bytes");
const idx = Number(index || 0);
if (!Number.isInteger(idx) || idx < 0) throw new Error("SC index must be a non-negative integer");
const k = sia.keyFromSeed(root, idx);
return k.address; // 76 hex chars, canonical Sia address form
}
function deriveScFromMnemonic(mnemonic, index) {
return { seedHex: deriveScRootFromMnemonic(mnemonic), address: deriveScAddressFromSeed(deriveScRootFromMnemonic(mnemonic), index) };
}
function deriveScFromSeedHex(seedHex, index) {
const s = deriveScRootFromHex(seedHex);
return { seedHex: s, address: deriveScAddressFromSeed(s, index) };
}
return {
mnemonicToSeedHex,
btc: { fromSeed: deriveBtcFromSeed, fromWif: deriveBtcFromWif },
dgb: {
fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif,
HMAC_BITCOIN_SEED, HMAC_DIGIBYTE_SEED,
},
eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex },
trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex },
sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 },
sc: { fromMnemonic: deriveScFromMnemonic, fromSeedHex: deriveScFromSeedHex, addressAt: deriveScAddressFromSeed },
};
};