Skip to content

feat(core): add human-assisted wallet entropy - #237

Draft
BullishNode wants to merge 8 commits into
developfrom
feat/entropy-pool
Draft

feat(core): add human-assisted wallet entropy#237
BullishNode wants to merge 8 commits into
developfrom
feat/entropy-pool

Conversation

@BullishNode

Copy link
Copy Markdown
Owner

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.

Scope boundary

This is the production finger-entropy implementation only. It ends at 3523d5fdc and deliberately excludes the BB_ENTROPY_CAPTURE research harness, logcat gesture-transcript capture, direct debug routing, debug APK, and draft debug release. Those remain isolated on research/entropy-capture-debug in this fork.

The branch is based directly on upstream SatoshiPortal/develop at ad6c1b559 after fast-forwarding this fork's develop to the same commit. It mirrors the production code in upstream draft PR SatoshiPortal/bullbitcoin-mobile#2547.

Summary

  • Generate 12-word BIP39 mnemonics from 128 bits of explicitly combined entropy.
  • Require both a fresh 64-byte operating-system CSPRNG draw and a completed 500-sample finger ceremony for every generated wallet.
  • Count only qualified pointer observations: Flutter-synthesized events, consecutive duplicate positions, and non-finite movement data cannot advance ceremony progress.
  • Require at least 10 seconds between the first and final accepted samples and movement spanning at least half of both canvas axes.
  • Mix touch-contact measurements already supplied by Flutter: minor radius, contact size, orientation, and tilt, alongside position, delta, event and arrival timing, pressure, and major radius.
  • Combine inputs through a domain-separated, length-framed SHA-512 pool with a counter and retained 256-bit state.
  • Use BDK only for deterministic Mnemonic.fromEntropy encoding; do not invoke BDK's random mnemonic constructor.
  • Fail closed if either required input is missing or the OS source fails its catastrophic sanity checks.
  • Remove CPU-jitter, system-statistics, IMU/magnetometer, time-strengthening, and ceremony-bypass paths.
  • Add no motion-sensor dependency, motion permission, camera access, microphone access, persistent gesture storage, or research logging.
  • Keep new-wallet output at the existing 12-word product standard.

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 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 checks
  • Whole-project flutter analyze --fatal-warnings --fatal-infos — no issues
  • dart fix --dry-runNothing to fix!
  • Formatting — 1,613 files unchanged
  • 856 app tests passed
  • 17 bull_ui tests passed
  • 1 catalogue test passed
  • Independent Python hashlib known-answer vector for the 500-sample SHA-512 pool construction

Draft checklist

  • Real-device wallet creation on Android
  • Real-device wallet creation on iOS
  • Create-and-restore round trip using the generated 12-word mnemonic
  • Product/security review of the mandatory finger ceremony

- 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.
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