Embit update: Improved BIP39 mnemonic validation (#759)
What changed, and why it matters
This commit tightens how a Bitcoin wallet tool (Krux) checks recovery phrases. Previously, the code accepted phrases with extra spaces, tabs, newlines, or commas between words because it used a loose 'strip and split' approach. Now it requires words to be separated by exactly one ordinary space. The change is defensive: malformed phrases that a user might accidentally type are now rejected, reducing the chance of accepting an invalid or unexpectedly interpreted recovery phrase.
Treat this as a hardening improvement rather than an active vulnerability. Review the embit submodule bump to confirm it contains the matching stricter `mnemonic_to_bytes` behavior. Ensure downstream UI guidance tells users that recovery phrases must use single spaces only, and consider whether any existing user backups rely on previously accepted malformed formatting.
Security signals we found
Stricter input validation for BIP39 mnemonic parsing
Rejection of whitespace-normalized parsing that could mask malformed user input
Alignment with upstream embit validation behavior
New unit tests covering leading/trailing/double spaces and alternative separators
Evidence from the diff
The patch changes src/krux/bip39.py so that k_mnemonic_bytes() splits the mnemonic on a single space (mnemonic.split(' ')) instead of stripping whitespace and splitting on any whitespace (mnemonic.strip().split()). This makes leading/trailing spaces, double spaces, newlines, commas, semicolons, and similar formatting invalid. The test file is updated to construct test mnemonics without trailing spaces and adds a new test (test_mnemonic_with_formatting_issues) asserting that such malformed inputs are rejected by both Krux and upstream embit. The vendor/embit submodule is also updated, presumably to an embit version with matching stricter validation.
Changed components
src/krux/bip39.pytests/test_bip39.pyvendor/embit (submodule update)Inspect captured patch +43 / −3
diff --git a/src/krux/bip39.py b/src/krux/bip39.py
index ba2ffe0..2b1ccc9 100644
--- a/src/krux/bip39.py
+++ b/src/krux/bip39.py
@@ -20,7 +20,7 @@ def k_mnemonic_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WORD
Verifies the mnemonic checksum and returns it in bytes
Equivalent to embit.bip39.mnemonic_to_bytes
"""
- words = mnemonic.strip().split()
+ words = mnemonic.split(" ")
if len(words) % 3 != 0 or not 12 <= len(words) <= 24:
raise ValueError("Invalid recovery phrase")
diff --git a/tests/test_bip39.py b/tests/test_bip39.py
index 38acd24..f3c7217 100644
--- a/tests/test_bip39.py
+++ b/tests/test_bip39.py
@@ -10,7 +10,7 @@ from krux import bip39 as kruxbip39
def test_one_word_mnemonics():
for numwords in (12, 15, 18, 21, 24):
for word in WORDLIST:
- mnemonic = (word + " ") * numwords
+ mnemonic = (word + " ") * (numwords - 1) + word
assert kruxbip39.k_mnemonic_is_valid(mnemonic) == bip39.mnemonic_is_valid(
mnemonic
)
@@ -101,3 +101,43 @@ def test_invalid_mnemonic_length():
for case in cases:
with pytest.raises(ValueError, match="Invalid recovery phrase"):
kruxbip39.k_mnemonic_bytes(case)
+
+
+def test_mnemonic_with_formatting_issues():
+ # Use a valid 12-word mnemonic for testing
+ valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+ # Verify the base mnemonic is valid
+ assert kruxbip39.k_mnemonic_is_valid(valid_mnemonic) == True
+
+ # Test cases with formatting issues that should be invalid
+ invalid_cases = [
+ # Leading spaces
+ " " + valid_mnemonic,
+ " " + valid_mnemonic,
+ # Trailing spaces
+ valid_mnemonic + " ",
+ valid_mnemonic + " ",
+ # Double spaces between words
+ valid_mnemonic.replace(" ", " ", 1), # One double space
+ valid_mnemonic.replace(" ", " "), # All double spaces
+ # Newlines instead of spaces
+ valid_mnemonic.replace(" ", "\n", 1), # One newline
+ valid_mnemonic.replace(" ", "\n"), # All newlines
+ # Commas instead of spaces
+ valid_mnemonic.replace(" ", ",", 1), # One comma
+ valid_mnemonic.replace(" ", ","), # All commas
+ # Semicolons instead of spaces
+ valid_mnemonic.replace(" ", ";", 1), # One semicolon
+ valid_mnemonic.replace(" ", ";"), # All semicolons
+ # Mixed formatting issues
+ " " + valid_mnemonic.replace(" ", " ", 3) + " ",
+ ]
+
+ for case in invalid_cases:
+ assert (
+ kruxbip39.k_mnemonic_is_valid(case) == False
+ ), f"Krux: Expected invalid for: {repr(case)}"
+ assert (
+ bip39.mnemonic_is_valid(case) == False
+ ), f"Embit: Expected invalid for: {repr(case)}"
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.