Repository navigation
Conversation
…ble word count (v2) Replace PBKDF2-HMAC-SHA256 with memory-hard Argon2id (64 MiB, t=3, p=1) so GPU/ASIC farms cannot grind phrase collisions orders of magnitude faster than defenders. Normalize addresses before hashing (trim whitespace, lowercase 0x-hex) so EIP-55 casing cannot change a phrase. Version the salt (human-checkphrase-v2) with optional domain-separation context, and make word count configurable (3-11, default 5). Applied across Rust, JS (now async via hash-wasm), Dart, and the Python reference; test vectors regenerated with normalization, word-count, and context cases. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5a51238. Configure here.
| @@ -1,5 +1,5 @@ | |||
| name: human_checksum | |||
| description: A tool to generate human-readable checksums from cryptocurrency addresses using the BIP-39 word list and PBKDF2 | |||
| description: A tool to generate human-readable checksums from cryptocurrency addresses using the BIP-39 word list and Argon2id | |||
There was a problem hiding this comment.
Unused crypto dependency remains
Low Severity
The Dart implementation no longer imports or uses package:crypto after switching from PBKDF2 to Pointycastle Argon2id, but crypto: ^3.0.3 is still listed under direct dependencies in pubspec.yaml.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5a51238. Configure here.
| throw new RangeError( | ||
| `wordCount must be between ${MIN_WORD_COUNT} and ${MAX_WORD_COUNT}, got ${wordCount}`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Non-integer wordCount accepted
Medium Severity
addressToChecksum only bounds-checks wordCount with numeric comparisons and never requires an integer. Values such as NaN pass validation, and fractional counts can yield a phrase length that does not match the configured wordCount, unlike the Rust, Dart, and Python APIs that only accept integer word counts.
Reviewed by Cursor Bugbot for commit 5a51238. Configure here.
|
Yeah not sure on that, would like to see if we can put the previous on the keystone HW wallet If we need 64MB memory say goodbye to HW wallet support But I haven't tried the previous version on HW either - I need to run some experiments, previous vs now |


Summary
This PR upgrades the checkphrase construction from PBKDF2-HMAC-SHA256 to a
memory-hard KDF and fixes several structural weaknesses, across all four
implementations (Rust, JavaScript, Dart, Python reference).
This is a breaking change: every checkphrase changes. The scheme is
versioned as
human-checkphrase-v2via the salt.Argon2id replaces PBKDF2 (the security fix)
PBKDF2 iterations impose only compute cost, and SHA-256 is the most
hardware-accelerated function in existence: a consumer GPU computes
~250,000 PBKDF2-40k guesses/second (~3,500x faster than the ~14 ms a wallet
pays per phrase), and mining ASICs widen the gap by several more orders of
magnitude. The old README's "GPU = 10x CPU" estimate significantly understated
this.
Argon2id (RFC 9106) with 64 MiB memory, 3 passes, parallelism 1 bounds
attacker throughput by memory bandwidth instead of core count, collapsing the
attacker/defender gap to roughly two orders of magnitude. Defender cost stays
UI-friendly at ~50–150 ms per phrase. Parallelism is fixed at 1 because
Argon2's output depends on it; this keeps all ports byte-identical.
argon2cratehash-wasm(browser-compatible;addressToChecksumis nowasync)Argon2BytesGeneratorargon2-cffi(new shared reference modulescripts/checkphrase.py)Address normalization
Addresses are canonicalized before hashing: surrounding whitespace is
stripped, and
0x-prefixed hex addresses are lowercased so EIP-55 checksumcasing cannot produce two different phrases for the same Ethereum address.
Base58/bech32/SS58 are case-sensitive and pass through unchanged; callers
must supply canonical forms (e.g. lowercase bech32). A test vector pins the
fact that lowercased base58 is deliberately a different address.
Versioned, domain-separable salt
The salt is now the scheme version string
human-checkphrase-v2, optionallyextended with a caller-supplied context:
human-checkphrase-v2|<context>(e.g.
eip155:1). The version enables clean future migrations; the contextlets deployments domain-separate phrases per chain. Context is optional so
uncoordinated wallets still agree by default.
Configurable word count
Word count is now a parameter (3–11, default 5). Five words (55 bits) remain
appropriate for verification; 8+ words are recommended if phrases are ever
used as lookup identifiers, where multi-target collision attacks apply. The
floor comes from Argon2's 4-byte minimum output, the ceiling from the u128
bit-slicing in the Rust port.
address_to_checksum_with_options(address, word_list, word_count, context)(existing
address_to_checksumkeeps its signature and defaults)addressToChecksum(address, wordList, { wordCount, context })addressToChecksum(address, wordCount: ..., context: ...)Other changes
test-vectors/checksums.json): 149 casesincluding normalization variants, word counts 3/4/6/8/11, and context
vectors. The suite shrank from 1171 cases because full 2048-word coverage
at ~100 ms+ per Argon2 hash would make test runs (especially pure-Dart,
~500 ms/hash) take 10+ minutes; wordlist consistency across packages is
already enforced by
scripts/sync_wordlists.py --checkinrun_tests.sh.Vectors may carry optional
wordCount/contextfields; implementationsdefault to 5 words / no context when absent.
and an honest security analysis (verification vs. lookup threat models,
multi-target attack costs, realistic GPU throughput numbers).
hex,hmac,pbkdf2, andreqwestdependencies (
reqwestwas unused before this PR too); bench now comparesArgon2id against bare SHA-256.
scripts/words.pydemo now delegates to the shared referenceimplementation instead of duplicating the (old) scheme.
.gitignore: added.venvand__pycache__.Test plan
./run_tests.shpasses: wordlist sync check, Rust (7 tests), JavaScript(14 tests), Dart (7 tests), all validating the same 149 shared vectors
Python for all vectors, including normalization/word-count/context cases
0X-uppercase, and whitespace variants of thesame Ethereum address produce identical phrases; lowercased base58
produces a different phrase
targets; drop to 32 MiB if needed (breaking, so decide before release)
existing consumers (all phrases change)
Note
High Risk
This is a breaking cryptographic migration: all phrases change, JS consumers must adopt async
addressToChecksum, and wallet integrators must align on v2 parameters, normalization, and optional context or users will see mismatched phrases.Overview
Breaking: every checkphrase changes. The scheme is versioned as
human-checkphrase-v2(salt) and replaces PBKDF2-HMAC-SHA256 with Argon2id (64 MiB, t=3, p=1) across Rust, JavaScript, Dart, and a new Python reference (scripts/checkphrase.py).Before hashing, addresses are normalized (trim; lowercase
0xhex for EIP-55 consistency). Phrases can use 3–11 words (default 5) and optional domain context (human-checkphrase-v2|<context>), exposed viaaddress_to_checksum_with_options/ JSChecksumOptions/ Dart named params. JavaScriptaddressToChecksumis now async (hash-wasm).Shared test vectors are regenerated for v2 (smaller suite due to Argon2 cost) with normalization, word-count, and context cases. Docs and benches are updated; Rust drops PBKDF2/
reqwestand related deps.Reviewed by Cursor Bugbot for commit 5a51238. Configure here.