feat(core): add human-assisted wallet entropy - #2547
Conversation
- SHA-512 mix/extract pool modelled on Bitcoin Core's RNGState::MixExtract: every source is concatenated and hashed together with the previous state (strictly additive - no source can ever reduce pool entropy), the first half of each digest is the output and the second half becomes the next secret state, with a 10ms strengthening pass on extraction - Mandatory floor: extraction refuses unless both the OS RNG (Random.secure) and a bdk RNG draw were mixed since the last extract, so seeds are never weaker than the platform CSPRNG. The two mandatory sources reach the kernel through independent bindings (Dart vs Rust getrandom) - Best-effort additive sources: CPU clock jitter (jitterentropy-style busy loop on a worker isolate), process/system stats, and IMU sensor noise via sensors_plus - New onboarding entropy ceremony: the user drags a finger on screen (VeraCrypt-style) and every raw pointer sample is mixed into the pool, with a minimalist trail, hint animation, hairline progress bar and milestone messages every 10% - New wallets are 24 words (256-bit entropy) via Mnemonic.fromEntropy; import, recovery and BIP85 paths are unchanged - Tests: known-answer vectors against an independent spec implementation, additivity property with adversarial sources, mandatory-source gating, collector failure policy, ceremony cubit pacing
- iOS: add NSMotionUsageDescription (sensors_plus accesses motion data; missing key crashes the app on first sensor read) - Bump sensors_plus 6.1.2 -> 7.1.0: 6.x never stopped the iOS magnetometer on stream cancellation (its cancel handler called stopDeviceMotionUpdates instead of stopMagnetometerUpdates) - Pause sensor sampling while the app is backgrounded via a lifecycle observer on the ceremony screen - Make the mandatory entropy gate unspoofable: only the new pool.mixMandatory (validated source identity + >=32-byte minimum) can satisfy the security floor; the UI-facing supplemental mix path never can, whatever source name it passes - Serialize mnemonic generation: collect -> mandatory mix -> extract is one queued transaction, so concurrent calls cannot interleave pool state - Memory hygiene: zeroize entropy and the bdk draw on every exit path (try/finally), dispose FFI mnemonic handles explicitly, wipe mixer input buffers and strengthening intermediates, wipe collector output after mixing, build OS RNG bytes without an intermediate list - Neutralize hardcoded '12 words' user copy in backup/recovery screens: new wallets are 24 words and the old copy instructed users to write down half their mnemonic - Accessibility: ceremony canvas gets a semantic label; taps count toward progress; after 20s a 'Continue without drawing' fallback appears so users who cannot perform gestures can still create a wallet (ceremony input is supplemental; the RNG floor is enforced at extraction) - Known-answer tests now pin extraction outputs to vectors computed by an independent Python hashlib implementation of the specification, plus gate-spoofing and short-read regression tests - Correct overclaiming doc comments: the two mandatory sources share the OS entropy root (thread_rng is userspace, OS-reseeded); they provide binding-diversity, not independent roots, and additivity is computational under SHA-512 assumptions
Source-level invariants that fail loudly instead of silently when a refactor introduces the RNG failure classes seen in the Coldcard firmware disclosure (predictable fallback binding, narrow reseed pipe, call-site drift): - fresh mnemonic generation has exactly one call site - only the generator and locator may reach the entropy pool from outside the entropy module - only the collector and generator may feed the mandatory gate - no non-secure Random anywhere in entropy or seed modules - production wiring cannot override the pool's strengthening budget
Dart's Random.secure is implemented by the Flutter engine registering dart::bin::GetEntropy, which reads /dev/urandom directly on both Android and iOS (runtime/bin/crypto_linux.cc and crypto_macos.cc in the Dart SDK) - not getrandom(2) or SecRandomCopyBytes as previously stated. Failure of the open/read throws with no fallback, so the fail-closed property is unchanged; only the mechanism description was wrong.
Entropy strategy and design rationaleThreat modelThis change is a hedge against a narrow but serious failure mode: the wallet's nominal CSPRNG path returns predictable bytes because of a platform defect, integration bug, or supply-chain compromise. It is not evidence of such a defect in BDK, Dart, Android, or iOS. The construction requires two inputs for every generated wallet:
Neither input is treated as optional. There is no predictable fallback, low-bit reseed, or path that silently continues after a source failure. This is directly motivated by the class of failure documented in Block's analysis of Coldcard's predictable fallback and 32-bit reseed: https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware The security claim is deliberately conditional: if either the OS draw or the touch transcript remains unknown to the attacker, the combined result should remain computationally unpredictable under the SHA-512 assumptions. If the entire app/runtime is malicious and can observe or replace both inputs, this design cannot help. Bitcoin Core inspirationThis is inspired by the shape of Bitcoin Core's RNG, not a line-for-line port. Bitcoin Core maintains a process-wide 256-bit RNG state. Its
Our pool follows the same useful core pattern:
We add explicit domain and length framing for ceremony start, individual samples, ceremony completion, and OS input. This prevents ambiguous concatenations and cross-purpose reuse. A completed ceremony is a one-use capability: extraction consumes it, and starting a new ceremony invalidates an older unconsumed completion. We intentionally do not copy every Bitcoin Core source. CPU jitter, system statistics, generic timestamps, and mobile IMU data were removed because we could not justify a defensible entropy estimate for them in this environment. Keeping many impressive-looking inputs without a clear threat-model contribution would be noise theater. The design instead has two understandable mandatory components. Why finger swipes instead of the cameraA camera can contain physical sensor noise, but the application normally receives frames after a device-specific image-processing pipeline rather than controlled raw sensor samples. Compression, denoising, auto-exposure, stabilization, frame buffering, and vendor behavior make the actual contribution difficult to characterize. Camera frames also create large sensitive buffers with more copies and a less convincing best-effort wiping story. Activating a camera during seed creation also has a materially different privacy and trust signal for users. Finger input is smaller and more explicit. Each sample commits to the event kind, pointer identifier, coordinates, deltas, framework timestamp, an independent monotonic-clock reading, pressure, major and minor touch radius, contact size, orientation, and tilt. Samples are serialized into fixed 112-byte buffers, mixed immediately, and then wiped on a best-effort basis. No motion-sensor plugin or additional ceremony sensor lifecycle is required. The 500-sample threshold is a pacing and freshness gate, not an estimate of 300 events' entropy. Flutter-synthesized events, consecutive duplicate positions, and non-finite movement data do not advance the gate. Completion time remains device-dependent. Taps remain accepted for users who cannot perform a continuous gesture. We make no claim such as “one bit per event.” The camera could be reconsidered as an optional additional source only if we define the exact capture pipeline, retention rules, supported platforms, and a conservative entropy argument. It should not replace the OS CSPRNG floor or the explicit human contribution merely because it yields more bytes. Why 12 wordsThe product currently uses 12-word BIP39 mnemonics. This change therefore extracts exactly 16 bytes (128 bits) and passes them to BDK's deterministic What each component actually contributes
Limitations
|
|
Claude finished @BullishNode's task in 8m 18s —— View job Review: human-assisted wallet entropy (PR #2547)
SummaryThe cryptographic core is sound. I verified the SHA-512 pool construction (domain- and length-framed inputs, monotonic counter, output from the first digest half, retained state from the second half), the fail-closed paths (no fallback when either source is missing or the OS draw fails sanity checks), and the one-use ceremony semantics — including the good detail that a short OS draw throws before mixing, so the ceremony isn't consumed by a retryable failure ( No blocking correctness or security bugs found. The findings below are flow/robustness issues in the UI layer, plus nits. Findings1. Back navigation during the 1.2 s completion pause silently drops wallet creation — After the bar fills, 2. Pool state exceptions escape into the pointer-event dispatcher —
3. Progress label shows 100% before the ceremony is complete —
4. Trail buffer retains raw gesture coordinates — The cubit carefully zeroes each 80-byte sample buffer, but 5. Screen-reader accessibility of the mandatory ceremony (product note) With TalkBack/VoiceOver, touch exploration intercepts pointer events, so delivering 300 down/move samples may be very difficult for screen-reader users — and there is deliberately no bypass. The Security assessment details
Test coverageCoverage is strong (known-answer vector, fail-closed paths, consumption semantics, state chaining, source-drift tripwires). Two small gaps worth adding: a cubit test for pointer samples arriving after the pool ceremony was externally invalidated (finding 2's scenario), and a widget/bloc test that completion actually dispatches |
|
Follow-up after adversarial gesture capture I instrumented the ceremony locally in a debug-only build (the capture harness and traces are not part of this PR; wallet generation was disabled during capture). The useful result was negative: 500 accepted pointer events are not an entropy estimate. Human emulator traces completed in roughly 6–8 seconds, a tiny repetitive gesture still passed, and one deterministic Android-injected straight swipe reached 500 accepted samples in 3.5 seconds. Commit 3523d5f therefore adds anti-degeneracy gates while preserving the original design:
These conditions improve ordinary gesture diversity but are deliberately not credited as entropy bits. They cannot distinguish a sufficiently adaptive script or protect against an OS that observes or fabricates touch input. The human input remains a supplemental hedge against a broken RNG when the rest of the input path is honest. This follow-up intentionally does not add camera/sensor permissions or re-architect the RNG stack. |
Disclaimer
We have no reason to believe that BDK has any RNG or entropy-generation defect. This is an experimental hardening project intended to reduce reliance on any single entropy path and to reduce attack surface. It is not a response to an ongoing incident, known vulnerability, or suspected defect in BDK.
Summary
Mnemonic.fromEntropyencoding; do not invoke BDK's random mnemonic constructor.Why
The goal is defense in depth against a hypothetical defect or supply-chain compromise affecting one randomness path. The human transcript is a physically distinct, deliberately uncredited input: it may preserve meaningful unpredictability if the OS RNG becomes predictable, but the implementation does not claim a fixed number of entropy bits from finger movement.
Five hundred is a conservative pacing threshold, not an assertion of 500 bits or any fixed per-event contribution. Pointer observations are correlated and device pipelines differ, so the gate rejects known low-value callbacks while the cryptographic pool mixes the complete accepted transcript.
Motion sensors were deliberately excluded after considering privacy and complexity. Their incremental entropy is difficult to quantify, they expand permissions and lifecycle behavior, and prior plugin defects demonstrate the reliability surface they add. The existing explicit finger ceremony supplies the independently motivated physical input without introducing another permission or dependency.
This is not designed to protect against a malicious application or runtime that can observe or replace both the OS draw and the pointer transcript.
Verification
make checksflutter analyze --fatal-warnings --fatal-infos— no issuesdart fix --dry-run—Nothing to fix!bull_uitests passedhashlibknown-answer vector for the 500-sample SHA-512 pool constructionDraft checklist