What changed, and why it matters
This commit fixes how Krux handles QR codes that contain raw binary data rather than plain text. Previously, some scanned binary QR codes may have been processed incorrectly because the firmware expected text strings. The change makes the device try to decode binary QR content as text when possible, and keep it as raw bytes when it cannot. This is a bug fix for handling certain binary-encoded QR codes, not a new security vulnerability.
Review the MaixPy submodule change to confirm binary QR codes are correctly classified and that downstream consumers consistently handle bytes objects. Verify that non-decodable binary data paths do not cause crashes or unexpected behavior in other pages not touched by this patch.
Security signals we found
Input-type normalization from bytes to str
Graceful fallback for non-UTF-8 binary QR data
Firmware-level QR decoder behavior change in MaixPy submodule
Bug fix explicitly noted in CHANGELOG
Evidence from the diff
The patch updates the MaixPy firmware submodule to return bytes objects from binary QR codes, and adjusts three Krux pages to handle bytes input. In datum_tool.py, scanned bytes are decoded to str if UTF-8 decodable, otherwise left as bytes (which later display as hex). In encryption_ui.py, encryption keys scanned as bytes are decoded to str when possible, otherwise kept as bytes. In addresses.py, address data is decoded from bytes to str before parsing. Tests are added to verify both decodable and non-decodable binary QR behavior. The CHANGELOG labels this as a bug fix for binary-encoded QR code handling.
Changed components
firmware/MaixPy QR decodersrc/krux/pages/datum_tool.pysrc/krux/pages/encryption_ui.pysrc/krux/pages/home_pages/addresses.pyInspect captured patch +129 / −1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 802cf5b..97935b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont
- Added backtick ` to keypad
- Bugfix: Screensaver not activating in menu pages without statusbar
- Embit: Improved BIP39 mnemonic validation
+- Bug Fix: Corrected handling of certain binary-encoded QR codes
# Changelog 25.10.1 - October 2025
diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py
index 39c2c96..e43985b 100644
--- a/src/krux/pages/datum_tool.py
+++ b/src/krux/pages/datum_tool.py
@@ -325,6 +325,12 @@ class DatumToolMenu(Page):
title += ", UR:" + contents.type
contents = urobj_to_data(contents)
+ if isinstance(contents, bytes):
+ try:
+ contents = contents.decode()
+ except:
+ pass
+
page = DatumTool(self.ctx)
page.contents, page.title = contents, title
return page.view_contents()
diff --git a/src/krux/pages/encryption_ui.py b/src/krux/pages/encryption_ui.py
index e0bd4c1..dd13cb8 100644
--- a/src/krux/pages/encryption_ui.py
+++ b/src/krux/pages/encryption_ui.py
@@ -525,6 +525,11 @@ class EncryptionKey(Page):
return None
if len(data) > ENCRYPTION_KEY_MAX_LEN:
raise ValueError("Maximum length exceeded (%s)" % ENCRYPTION_KEY_MAX_LEN)
+ if isinstance(data, bytes):
+ try:
+ data = data.decode()
+ except:
+ pass
return data
diff --git a/src/krux/pages/home_pages/addresses.py b/src/krux/pages/home_pages/addresses.py
index 4b4a59c..fd1518b 100644
--- a/src/krux/pages/home_pages/addresses.py
+++ b/src/krux/pages/home_pages/addresses.py
@@ -313,6 +313,7 @@ class Addresses(Page):
pass
addr = None
+ data = data.decode() if isinstance(data, bytes) else data
try:
from ...wallet import parse_address
diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py
index 0f9912c..778085b 100644
--- a/tests/pages/test_datum_tool.py
+++ b/tests/pages/test_datum_tool.py
@@ -1187,3 +1187,86 @@ def test_datumtool_show_contents_button_turbo(mocker, m5stickv):
datum._show_contents()
time.sleep_ms.assert_called_with(KEY_REPEAT_DELAY_MS)
+
+
+def test_datumtoolmenu_scan_qr_binary_decodable(m5stickv, mocker):
+ """Test scan_qr when QR scanner returns binary data that can be decoded to text"""
+ from krux.pages.datum_tool import DatumToolMenu
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ # Binary data that can be decoded to UTF-8 text
+ binary_data = b"Hello World"
+
+ # Mock QR scanner to return binary data
+ mocker.patch.object(
+ QRCodeCapture, "qr_capture_loop", new=lambda self: (binary_data, 0)
+ )
+
+ # Button sequence to exit from DatumTool view
+ BTN_SEQUENCE = (
+ BUTTON_ENTER, # go Scan QR
+ BUTTON_PAGE_PREV, # to Back
+ BUTTON_ENTER, # go Back
+ BUTTON_PAGE, # to Text Entry
+ BUTTON_PAGE, # to Read File
+ BUTTON_PAGE, # to Back
+ BUTTON_ENTER, # go Back
+ )
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ page = DatumToolMenu(ctx).run()
+
+ # Verify that the button sequence was executed
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+
+ # Verify that the decoded text "Hello World" was displayed in the info box
+ # The display should show the decoded string, not the hex representation
+ display_calls = [
+ str(call) for call in ctx.display.draw_hcentered_text.call_args_list
+ ]
+ assert any(
+ '"Hello World"' in call for call in display_calls
+ ), f"Expected to find decoded text 'Hello World' in display calls, but got: {display_calls}"
+
+
+def test_datumtoolmenu_scan_qr_binary_non_decodable(m5stickv, mocker):
+ """Test scan_qr when QR scanner returns binary data that cannot be decoded to text"""
+ from krux.pages.datum_tool import DatumToolMenu
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ # Binary data that cannot be decoded to UTF-8 (invalid UTF-8 sequence)
+ # 0xFF and 0xFE are not valid UTF-8 byte sequences
+ binary_data = b"\xff\xfe\xfd\xfc"
+
+ # Mock QR scanner to return binary data
+ mocker.patch.object(
+ QRCodeCapture, "qr_capture_loop", new=lambda self: (binary_data, 0)
+ )
+
+ # Button sequence to exit from DatumTool view after scanning binary data
+ BTN_SEQUENCE = (
+ BUTTON_ENTER, # go Scan QR
+ BUTTON_PAGE_PREV, # to Back
+ BUTTON_ENTER, # go Back
+ BUTTON_PAGE, # to Text Entry
+ BUTTON_PAGE, # to Read File
+ BUTTON_PAGE, # to Back
+ BUTTON_ENTER, # go Back
+ )
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ page = DatumToolMenu(ctx).run()
+
+ # Verify that the button sequence was executed
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+
+ # Verify that the hex representation was displayed in the info box
+ # Since the data cannot be decoded to text, it should be shown as hex
+ display_calls = [
+ str(call) for call in ctx.display.draw_hcentered_text.call_args_list
+ ]
+ assert any(
+ "0xfffefdfc" in call for call in display_calls
+ ), f"Expected to find hex representation '0xfffefdfc' in display calls, but got: {display_calls}"
diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py
index 92f83ab..17f5cca 100644
--- a/tests/pages/test_encryption_ui.py
+++ b/tests/pages/test_encryption_ui.py
@@ -126,6 +126,38 @@ def test_load_key_from_qr_code(m5stickv, mocker):
assert key == "qr key"
print("case 2: load_key_from_qr_code")
+ BTN_SEQUENCE = (
+ [BUTTON_PAGE] # move to QR code key
+ + [BUTTON_ENTER] # choose QR code key
+ + [BUTTON_ENTER] # Confirm
+ )
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ key_generator = EncryptionKey(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (b"decodable bytes qr key", None),
+ )
+ key = key_generator.encryption_key()
+ assert key == "decodable bytes qr key"
+
+ print("case 3: load_key_from_qr_code")
+ BTN_SEQUENCE = (
+ [BUTTON_PAGE] # move to QR code key
+ + [BUTTON_ENTER] # choose QR code key
+ + [BUTTON_ENTER] # Confirm
+ )
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ key_generator = EncryptionKey(ctx)
+ mocker.patch.object(
+ QRCodeCapture, "qr_capture_loop", new=lambda self: (b"\xde\xad\xbe\xef", None)
+ )
+ key = key_generator.encryption_key()
+ assert key == b"\xde\xad\xbe\xef"
+ call_message = mocker.call("Key (4): 0xdeadbeef", 10, highlight_prefix=":")
+ ctx.display.draw_hcentered_text.assert_has_calls([call_message])
+
+ print("case 4: load_key_from_qr_code")
# Repeat with too much characters >ENCRYPTION_KEY_MAX_LEN
BTN_SEQUENCE = [
BUTTON_PAGE, # move to QR code key
Why this scored 36/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.