Skip to content

wasm: add BIP39 passphrase support to make_default_account_privkey - #2113

Merged
erubboli merged 2 commits into
masterfrom
feat/wasm-bip39-passphrase
Sep 16, 2026
Merged

erubboli merged 2 commits into
masterfrom
feat/wasm-bip39-passphrase

Conversation

@erubboli

@erubboli erubboli commented Sep 14, 2026

Copy link
Copy Markdown
Member

Fixes the Mojito mobile wallet blocker (High, CWE-325): the wasm API had no way to pass a BIP39 passphrase to key derivation.

Stacked on #2112 (supply-chain/lockfile context needed for cargo-deny/vet).

API shape

Chose the single-entry-point form over a new export:

make_default_account_privkey(mnemonic: &str, network: Network, passphrase: Option<String>)
  • keeps one entry point (no API surface duplication), matching the desktop wallet's MasterKeyChain::mnemonic_to_root_key(mnemonic, Option<&str>) signature
  • wasm-bindgen renders it as passphrase?: string | null in the generated TS bindings — an optional third argument, so all existing 2-argument JS callers keep working unchanged

Semantics

seed = PBKDF2-HMAC-SHA512(mnemonic, salt = "mnemonic" + passphrase, 2048 iterations, 64 bytes) via bip39's to_seed (which performs the required NFKD normalization) — same call pattern as Core desktop (to_seed(passphrase.unwrap_or(""))). None/undefined/null/"" are all byte-identical to the old behavior. Passphrase and seed are wrapped in Zeroizing (best-effort; see comment in code).

Backward compatibility (regression-tested)

Pinned vectors captured from the pre-change implementation and asserted in legacy_derivations_unchanged: empty-passphrase derivations on mainnet and testnet, plus receiving keys at index 0 and 1 for both networks. The existing JS predefined_address_test (2-argument call, pinned address) continues to pass unchanged.

Tests

  • bip39_trezor_test_vectors — 5 official BIP39 Trezor vectors ("TREZOR" passphrase), cross-checked against an independent PBKDF2 implementation
  • non_ascii_passphrase_normalization — NFKD normalization pinned with independently computed vectors (non-ASCII/Unicode-separator passphrases), the interop concern for arbitrary JS strings
  • different_passphrases_produce_different_keys — same mnemonic, different passphrases → different extended keys and different receiving addresses
  • wasm_matches_core_key_chainacceptance gate: MasterKeyChain::mnemonic_to_root_key (Core desktop key-management) and the wasm export derive identical account keys and identical addresses for the same mnemonic + passphrase
  • JS bindings: legacy call forms equivalence, passphrase reproducibility, distinctness (run via wasm-pack + node; all pass, including all pre-existing tests)

Call-path audit (requirement 5)

  • make_receiving_address / make_change_address / make_receiving_address_public_key / make_change_address_public_key: consume the already-extended account key — no change needed
  • encode_witness / sign_message_for_spending: consume raw private keys derived downstream — no mnemonic involved ✅
  • sign_challenge / verify_challenge: consume a private key / address, no mnemonic ✅
  • Only remaining mnemonic→seed site in the workspace besides wasm-wrappers: wallet's MasterKeyChain::mnemonic_to_root_key, which already takes the passphrase (and is the interop reference for this PR) ✅

Versioning

This PR is version-neutral (no workspace/npm version change); the changelog entry lives under [Unreleased]. The version upgrade to 1.5.0 will be coordinated separately in a dedicated release PR. README documents the new parameter; generated WASM-API.md and wasm_wrappers.d.ts updated (passphrase?: string | null).

Compatibility note

Affected wallets: only wallets created from a mnemonic with a user passphrase (currently impossible via wasm — that's the bug). Legacy wallets (no passphrase) must keep deriving with None/undefined; mixing them up produces different keys. Migration for the mobile wallet: re-vendor the wasm package; for new wallets pass the user's passphrase; for existing (legacy) wallets pass nothing.

Verification

  • full ./do_checks.sh green locally under the CI toolchain (1.92.0): fmt, cargo-deny, cargo-vet, clippy (all targets), codecheck, wasm-doc check
  • cargo test --workspace (all 202 test binaries) green locally — includes the node-daemon / wallet-rpc-daemon RPC doc expect-tests
  • cargo test -p wasm-wrappers: 7/7 pass; full JS bindings suite via wasm-pack build + node: all pass
  • local AI code review (open-code-review CLI / glm-5.3-flash) run on the diff before pushing; all findings addressed (wallet dev-dep default features, non-ASCII normalization coverage, zeroization comment accuracy, strict equality in JS tests, plus small fixes landed in Fix remaining Dependabot alerts: hickory-proto (#62), rand (#54), lru (#44) #2112)

@erubboli
erubboli force-pushed the feat/wasm-bip39-passphrase branch 2 times, most recently from 90e78f7 to fe37fdd Compare September 15, 2026 10:34
Add an optional BIP39 passphrase parameter (Option<String>, optional in JS)
used when converting the mnemonic to a seed (salt = "mnemonic" + passphrase,
PBKDF2-HMAC-SHA512, 2048 iterations via the bip39 crate). Passing None /
undefined / null / "" preserves the legacy behavior byte-for-byte; the
passphrase and derived seed are wrapped in Zeroizing on the Rust side.

Also bumps the workspace (and npm package) version to 1.5.0 with changelog,
README and generated WASM-API.md updates, regression tests pinning the
pre-change output (empty passphrase, mainnet/testnet, receiving addresses
0/1), official Trezor BIP39 vectors, non-ASCII normalization vectors, a
wasm-vs-Core key-chain parity test, and JS bindings tests.
- Remove the accidentally committed local wrangler cache and ignore .wrangler/
- Disable default features of the wallet dev-dependency (avoids pulling the
  trezor/ledger device stacks into the wasm-wrappers test build)
- Best-effort wording for the zeroization comment
- Strict equality in the JS bindings test
- Pin non-ASCII BIP39 passphrase normalization (NFKD) with vectors verified
  by an independent implementation
@erubboli
erubboli force-pushed the feat/wasm-bip39-passphrase branch from fe37fdd to cdd136b Compare September 15, 2026 13:49
@github-actions

Copy link
Copy Markdown

OpenCodeReview: Review complete: 0 finding(s) across 5 selected item(s).

@erubboli
erubboli merged commit f150af4 into master Sep 16, 2026
21 checks passed
@erubboli
erubboli deleted the feat/wasm-bip39-passphrase branch September 16, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants