Skip to content

fix(key-wallet-manager): enable key-wallet/getrandom for mnemonic generation - #945

Open
QuantumExplorer wants to merge 3 commits into
devfrom
claude/seed-phrase-entropy-check-32436d
Open

fix(key-wallet-manager): enable key-wallet/getrandom for mnemonic generation#945
QuantumExplorer wants to merge 3 commits into
devfrom
claude/seed-phrase-entropy-check-32436d

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Why

Prompted by an audit of seed-phrase entropy across the repo, checking for the bug class behind Milk Sad (CVE-2023-39910, Libbitcoin bx seedmt19937 seeded with 32-bit time(NULL)) and Trust Wallet's browser extension (CVE-2023-31290, 32-bit seed).

The audit came back clean. Mnemonic::generate is the single chokepoint every random wallet in the repo routes through, and it is correct on both halves of that bug class:

  • Width — the entropy buffer is sized from the word count (16/20/24/28/32 bytes) and filled completely. No truncation, no zero-padding.
  • Sourcegetrandom::getrandom goes straight to the OS CSPRNG (getentropy on Apple, getrandom(2) on Linux, BCryptGenRandom on Windows). No seeded PRNG anywhere in the path. getrandom v0.2.17 resolves with only its default feature — no custom, js, or rdrand backend, and nothing calls register_custom_getrandom!.

Supporting checks: Seed::random() fills all 64 bytes from getrandom; Wallet::new_random, WalletManager::create_wallet_with_random_mnemonic, and the FFI mnemonic_generate* entry points all delegate to Mnemonic::generate; no time-based seeding outside benchmarks; the only StdRng::seed_from_u64 calls are test helpers. BIP38's 4-byte owner_salt is spec-mandated and drawn from a CSPRNG, not a seed.

Verified empirically as well as by reading — 20,000 generations per word count through the public API gave zero collisions, all 256 values at every byte position, and χ² on bit balance landing at the degrees of freedom (e.g. 120.2 on 128 dof for 12-word).

What changed

1. key-wallet-manager could never generate a mnemonic (the one real defect)

key-wallet-manager declared key-wallet with default-features = false, which drops getrandom. Without it, Mnemonic::generate compiles to its #[cfg(not(feature = "getrandom"))] stub — one that always returns an error — so create_wallet_with_random_mnemonic could never succeed.

It was invisible in CI because workspace builds and the crate's own dev-dependency on key-wallet (default features on) both unify getrandom back on: the full test suite passed while an external consumer got a dead API. Confirmed with a crate built outside the workspace, which printed Mnemonic generation requires getrandom feature before the change and OK: generated 24 words after.

This was fail-closed — an error, never weak entropy. Not a Milk Sad-class vulnerability. key-wallet-ffi depends on key-wallet with default features, so iOS/Swift was never affected.

2. Regression guard: generate_entropy_has_expected_width_and_no_stuck_bits

For all five word counts, asserts the recovered entropy is the full BIP-39 width, round-trips to the same phrase, never collides across draws, and that every bit position takes both values. 128 samples makes a false positive a ~2^-63 event; runs in well under a second.

This catches zero-padded and stuck-bit entropy only. It does not establish how much real entropy those bits carry — see below.

For reviewers

  • The test does not verify that entropy is full-width in the information-theoretic sense. A generator expanding a 32- or 48-bit seed through a hash or PRNG — the actual defect in both CVEs — produces freely varying bits and passes every assertion. Nothing statistical at this sample size would separate it from a real CSPRNG. That property is held by construction (generate fills its buffer directly from getrandom) and only review can protect it; the test's doc comment says so explicitly. Thanks to CodeRabbit for catching that the original name and doc comment overclaimed this.
  • Not done, worth a decision: Mnemonic::generate currently exists as a runtime-erroring stub when getrandom is off. Making it compile-time-absent would turn this whole class of misconfiguration into a build failure instead of a runtime surprise, but that's a breaking API change for no_std consumers who don't need generation.

Testing

  • cargo test -p key-wallet --lib mnemonic — 47 passed
  • cargo check -p key-wallet-manager -p key-wallet-ffi — clean
  • cargo clippy -p key-wallet --lib --all-features and cargo fmt --check -p key-wallet — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enabled mnemonic generation for external consumers.
    • Ensured generated 12-, 15-, 18-, 21-, and 24-word phrases use the full expected entropy range.
  • Tests

    • Added coverage verifying mnemonic round-tripping, uniqueness, and variation across all entropy bits.

…eration

key-wallet-manager declared key-wallet with `default-features = false`,
which drops `getrandom`. Without that feature `Mnemonic::generate`
compiles to its `#[cfg(not(feature = "getrandom"))]` stub, which always
returns an error — so `create_wallet_with_random_mnemonic` could never
succeed for an external consumer of the crate.

The breakage was invisible in CI: workspace builds and the crate's own
dev-dependency on key-wallet (default features on) both unify `getrandom`
back on, so the test suite passed while a standalone consumer got a dead
API. Verified against a crate outside the workspace — it printed
"Mnemonic generation requires getrandom feature" before this change and
generates a 24-word phrase after. key-wallet-ffi depends on key-wallet
with default features, so iOS/Swift was never affected.

This was fail-closed — it returned an error, never weak entropy.

Also add `generate_draws_full_width_entropy`, a regression guard for the
weak-seed-phrase bug class (Milk Sad / CVE-2023-39910, Trust Wallet /
CVE-2023-31290). For all five word counts it asserts the recovered
entropy is the full BIP-39 width, round-trips to the same phrase, never
collides across draws, and that every bit position takes both values —
a stuck bit being what a narrowed entropy source leaves behind.

The test covers the truncation half of that bug class only. A full-width
buffer filled from a seeded PRNG is not statistically detectable at any
practical sample size; that half is held by construction, and the test
doc comment names the invariant so review can protect it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1061b52d-8c1d-4330-9d03-8be711c4cd4a

📥 Commits

Reviewing files that changed from the base of the PR and between 686df7d and 586bd6b.

📒 Files selected for processing (1)
  • key-wallet/src/mnemonic.rs
📝 Walkthrough

Walkthrough

The change enables the getrandom feature for key-wallet and adds gated tests for entropy quality across all supported BIP-39 mnemonic sizes.

Changes

Mnemonic entropy support

Layer / File(s) Summary
Enable and validate mnemonic entropy
key-wallet-manager/Cargo.toml, key-wallet/src/mnemonic.rs
The manager enables key-wallet's getrandom feature. The gated test checks entropy width, phrase round-tripping, sample uniqueness, and variation across all entropy bit positions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: xdustinface

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling the key-wallet getrandom feature for mnemonic generation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/seed-phrase-entropy-check-32436d

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@key-wallet/src/mnemonic.rs`:
- Around line 406-421: Rename the test and its documentation around the affected
mnemonic test cases to describe only zero-padding or stuck-bit regression
coverage, removing claims that it verifies full-width entropy or detects
deterministic expansion. Preserve the assertions for round-tripping, collisions,
and varying bit positions, and keep the OS-CSPRNG guarantee stated separately as
a construction requirement of Mnemonic::generate using direct getrandom.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 75484a21-68a8-4132-a125-ca8c25981bbd

📥 Commits

Reviewing files that changed from the base of the PR and between d91ad05 and 686df7d.

📒 Files selected for processing (2)
  • key-wallet-manager/Cargo.toml
  • key-wallet/src/mnemonic.rs

Comment thread key-wallet/src/mnemonic.rs Outdated

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

… test

The test doc comment said it guarded against a full-width entropy buffer
whose bits are "zero-padding or a deterministic expansion". The
deterministic-expansion half is wrong: a generator that expands a 32- or
48-bit seed through a hash or PRNG produces freely varying bits and
passes every assertion in the test. That claim also contradicted the
comment's own closing paragraph, which already conceded exactly that
limitation.

Rename `generate_draws_full_width_entropy` ->
`generate_entropy_has_expected_width_and_no_stuck_bits` and rewrite the
doc comment to state only what the assertions establish: correct BIP-39
width, entropy round-trips to the same phrase, no collisions, no stuck
bits. The seeded-PRNG case is now called out explicitly as undetectable
at this sample size and held by construction instead, so a reader knows
it is `Mnemonic::generate` calling `getrandom` directly that review has
to protect.

Also reword the stuck-bit assertion message, which drew the same
overly broad conclusion about the entropy source.

No behavior change; assertions are untouched.

Reported by CodeRabbit on #945.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.19%. Comparing base (9cbe4e7) to head (586bd6b).
⚠️ Report is 12 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #945      +/-   ##
==========================================
+ Coverage   74.76%   75.19%   +0.42%     
==========================================
  Files         328      328              
  Lines       76593    78218    +1625     
==========================================
+ Hits        57267    58816    +1549     
- Misses      19326    19402      +76     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 48.58% <ø> (-1.14%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.35% <ø> (+0.40%) ⬆️
wallet 76.92% <100.00%> (+1.23%) ⬆️
Files with missing lines Coverage Δ
key-wallet/src/mnemonic.rs 97.17% <100.00%> (+0.67%) ⬆️

... and 44 files with indirect coverage changes

…ssage

The message passed `u8::from(count > 0)` as a trailing argument to
recover which value the bit was stuck at. Rust evaluates assert! message
arguments only on the panic path, so that line could never execute while
the test passes — it was the sole uncovered line in the patch (llvm-cov
line 470, flagged by Codecov at 96% patch coverage).

Fold the value into the format string as a captured `{count}` instead.
The count carries the same information more directly (0 ones = stuck at
0, SAMPLES ones = stuck at 1) while showing the actual distribution, and
drops a needlessly clever conversion. Patch coverage goes to 100%;
verified with `cargo llvm-cov --lib -p key-wallet`, which shows line 470
as the only uncovered line before and none in the test region after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant