What changed, and why it matters
This commit is a defensive code cleanup, not an active vulnerability fix. It narrows four broad 'catch-everything' error handlers in the wallet's encrypted mnemonic storage module so they only ignore expected file/JSON problems. Unexpected errors (including serious ones like memory exhaustion or user cancellation) now surface instead of being silently swallowed. The change improves future bug detection and reliability but does not by itself create or close a known exploit.
Treat as a routine hardening improvement. Review whether any production workflows rely on the old silent-swallow behavior for unexpected errors, and ensure monitoring can surface newly propagated exceptions on the K210 device. No urgent patch or incident response is indicated.
Security signals we found
Broad exception suppression narrowed to expected (OSError, ValueError)
Unexpected errors now propagate instead of being silently swallowed
Decrypt and write paths retain broad catches, now explicitly commented
Extensive regression tests added for fallback vs. propagation behavior
No change to cryptographic algorithms, key handling, or storage format
Evidence from the diff
In src/krux/encryption.py, MnemonicStorage.init and store_encrypted_kef previously used bare except: clauses around SD/flash reads and JSON loads. The patch replaces them with except (OSError, ValueError):, preserving the fallback-to-empty/fresh-write behavior for missing files, unreadable media, and malformed JSON. The decrypt and write paths intentionally keep broad catches and are now documented. The test file adds 12 unit tests verifying that OSError/ValueError still trigger the fallback while unexpected exceptions (RuntimeError) propagate. This is a hardening/refactoring change that reduces error masking.
Changed components
src/krux/encryption.py:MnemonicStorage.__init__src/krux/encryption.py:MnemonicStorage.store_encrypted_keftests/test_encryption.pyInspect captured patch +195 / −4
diff --git a/src/krux/encryption.py b/src/krux/encryption.py
index be3f5f9..f669937 100644
--- a/src/krux/encryption.py
+++ b/src/krux/encryption.py
@@ -42,12 +42,14 @@ class MnemonicStorage:
try:
with SDHandler() as sd:
self.stored_sd = json.loads(sd.read(MNEMONICS_FILE))
- except:
+ except (OSError, ValueError):
+ # missing/unreadable SD card or malformed JSON -> start empty
pass
try:
with open(FLASH_PATH_STR % MNEMONICS_FILE, "r") as f:
self.stored = json.loads(f.read())
- except:
+ except (OSError, ValueError):
+ # missing/unreadable flash file or malformed JSON -> start empty
pass
def _deprecated_decrypt(self, key, salt, iterations, mode, payload):
@@ -73,6 +75,8 @@ class MnemonicStorage:
plaintext = kef._unpad(decryptor.decrypt(payload), pkcs_pad=False)
return plaintext.decode()
except:
+ # broad on purpose: any failure here means a wrong key or
+ # incompatible legacy ciphertext -> return None
return None
def list_mnemonics(self, sd_card=False):
@@ -119,7 +123,8 @@ class MnemonicStorage:
contents = sd.read(MNEMONICS_FILE)
orig_len = len(contents)
mnemonics = json.loads(contents)
- except:
+ except (OSError, ValueError):
+ # no existing/readable file or malformed JSON -> write fresh
orig_len = 0
# save the new MNEMONICS_FILE
@@ -132,13 +137,15 @@ class MnemonicStorage:
contents += " " * (orig_len - len(contents))
sd.write(MNEMONICS_FILE, contents)
except:
+ # broad on purpose: any failure to save means the store failed
return False
else:
try:
# load current MNEMONICS_FILE
with open(FLASH_PATH_STR % MNEMONICS_FILE, "r") as f:
mnemonics = json.loads(f.read())
- except:
+ except (OSError, ValueError):
+ # no existing/readable file or malformed JSON -> write fresh
pass
try:
# save the new MNEMONICS_FILE
@@ -146,6 +153,7 @@ class MnemonicStorage:
mnemonics[mnemonic_id] = {"b64_kef": b64_kef}
f.write(json.dumps(mnemonics))
except:
+ # broad on purpose: any failure to save means the store failed
return False
return True
diff --git a/tests/test_encryption.py b/tests/test_encryption.py
index 607ca0a..ef430a8 100644
--- a/tests/test_encryption.py
+++ b/tests/test_encryption.py
@@ -612,3 +612,186 @@ def test_customize_pbkdf2_iterations_create_and_decode(m5stickv):
plaintext = decryptor.decrypt(cpl, version)
words = bip39.mnemonic_from_bytes(plaintext)
assert words == TEST_WORDS
+
+
+# ---------------------------------------------------------------------------
+# Mnemonic-storage file-load error handling.
+#
+# The four read/load fallbacks below catch only the file/JSON errors they
+# expect (OSError, ValueError), matching the OSError convention already used in
+# sd_card.py. The behaviour for a missing/unreadable file or malformed JSON is
+# unchanged ("storage starts empty" / "first store still writes"); the change
+# is that an *unexpected* error is no longer silently swallowed -- it now
+# propagates, so real bugs stop hiding. Each "propagates_unexpected_error" test
+# is the one that fails on the old bare-except code.
+# ---------------------------------------------------------------------------
+
+
+# --- __init__ SD load (self.stored_sd) ---
+
+
+def test_init_sd_load_propagates_unexpected_error(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ mocker.patch("krux.encryption.SDHandler", side_effect=RuntimeError("unexpected"))
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ with pytest.raises(RuntimeError):
+ MnemonicStorage()
+
+
+def test_init_sd_load_oserror_starts_empty(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ mocker.patch("krux.encryption.SDHandler", side_effect=OSError("no card"))
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ storage = MnemonicStorage()
+ assert storage.stored_sd == {}
+
+
+def test_init_sd_load_malformed_json_starts_empty(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ sd = mocker.MagicMock()
+ sd.read.return_value = "not valid json {{{"
+ sdhandler = mocker.MagicMock()
+ sdhandler.return_value.__enter__.return_value = sd
+ mocker.patch("krux.encryption.SDHandler", new=sdhandler)
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ storage = MnemonicStorage()
+ assert storage.stored_sd == {}
+
+
+# --- __init__ flash load (self.stored) ---
+
+
+def test_init_flash_load_propagates_unexpected_error(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ # SD load fails with an expected error so only the flash load can raise.
+ mocker.patch("krux.encryption.SDHandler", side_effect=OSError)
+ mocker.patch("krux.encryption.open", side_effect=RuntimeError("unexpected"))
+ with pytest.raises(RuntimeError):
+ MnemonicStorage()
+
+
+def test_init_flash_load_oserror_starts_empty(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ mocker.patch("krux.encryption.SDHandler", side_effect=OSError)
+ mocker.patch("krux.encryption.open", side_effect=OSError("missing"))
+ storage = MnemonicStorage()
+ assert storage.stored == {}
+
+
+def test_init_flash_load_malformed_json_starts_empty(m5stickv, mocker):
+ from krux.encryption import MnemonicStorage
+
+ mocker.patch("krux.encryption.SDHandler", side_effect=OSError)
+ with patch(
+ "krux.encryption.open", new=mocker.mock_open(read_data="not valid json {{{")
+ ):
+ storage = MnemonicStorage()
+ assert storage.stored == {}
+
+
+# --- store_encrypted_kef SD read-before-write ---
+
+
+def test_store_sd_read_propagates_unexpected_error(
+ m5stickv, mocker, mock_file_operations
+):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ mocker.patch("krux.sd_card.SDHandler.read", side_effect=RuntimeError("unexpected"))
+ with patch("krux.sd_card.open", new=mocker.mock_open(read_data="{}")):
+ with pytest.raises(RuntimeError):
+ storage.store_encrypted_kef("KEFecbID", KEF_ENVELOPE_ECB, sd_card=True)
+
+
+def test_store_sd_read_oserror_still_writes(m5stickv, mocker, mock_file_operations):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ mocker.patch("krux.sd_card.SDHandler.read", side_effect=OSError("missing"))
+ with patch("krux.sd_card.open", new=mocker.mock_open(read_data="{}")) as m:
+ success = storage.store_encrypted_kef(
+ "KEFecbID", KEF_ENVELOPE_ECB, sd_card=True
+ )
+ assert success is True
+ m().write.assert_called_once_with(KEF_ECBENTROPY_ONLY_JSON)
+
+
+def test_store_sd_read_malformed_json_still_writes(
+ m5stickv, mocker, mock_file_operations
+):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ mocker.patch("krux.sd_card.SDHandler.read", return_value="not valid json {{{")
+ with patch("krux.sd_card.open", new=mocker.mock_open(read_data="{}")) as m:
+ success = storage.store_encrypted_kef(
+ "KEFecbID", KEF_ENVELOPE_ECB, sd_card=True
+ )
+ assert success is True
+ m().write.assert_called_once_with(KEF_ECBENTROPY_ONLY_JSON)
+
+
+# --- store_encrypted_kef flash read-before-write ---
+
+
+def test_store_flash_read_propagates_unexpected_error(m5stickv, mocker):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ write_handle = mocker.mock_open()
+ mocker.patch(
+ "krux.encryption.open",
+ side_effect=[RuntimeError("unexpected"), write_handle.return_value],
+ )
+ with pytest.raises(RuntimeError):
+ storage.store_encrypted_kef("KEFecbID", KEF_ENVELOPE_ECB, sd_card=False)
+
+
+def test_store_flash_read_oserror_still_writes(m5stickv, mocker):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ write_handle = mocker.mock_open()
+ mocker.patch(
+ "krux.encryption.open",
+ side_effect=[OSError("missing"), write_handle.return_value],
+ )
+ success = storage.store_encrypted_kef("KEFecbID", KEF_ENVELOPE_ECB, sd_card=False)
+ assert success is True
+ write_handle().write.assert_called_once_with(KEF_ECBENTROPY_ONLY_JSON)
+
+
+def test_store_flash_read_malformed_json_still_writes(m5stickv, mocker):
+ from krux.krux_settings import Settings
+ from krux.encryption import MnemonicStorage
+
+ with patch("krux.encryption.open", new=mocker.mock_open(read_data="{}")):
+ storage = MnemonicStorage()
+ Settings().encryption.version = "AES-ECB"
+ read_handle = mocker.mock_open(read_data="not valid json {{{")
+ write_handle = mocker.mock_open()
+ mocker.patch(
+ "krux.encryption.open",
+ side_effect=[read_handle.return_value, write_handle.return_value],
+ )
+ success = storage.store_encrypted_kef("KEFecbID", KEF_ENVELOPE_ECB, sd_card=False)
+ assert success is True
+ write_handle().write.assert_called_once_with(KEF_ECBENTROPY_ONLY_JSON)
Why this scored 34/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.