What changed, and why it matters
This commit fixes a layout bug in Krux, a Bitcoin hardware-wallet tool, that affected how mnemonic (seed phrase) words and their numeric equivalents are printed on thermal paper. Long words could previously wrap or collide with adjacent columns, making a paper backup harder to read or potentially ambiguous. The fix changes the print format from three narrow columns to two wider columns for word backups, and keeps three fixed-width columns for decimal/hex/octal number backups. It also moves some internal label constants to a shared location and corrects a device error when showing backups as word numbers. There is no evidence in the commit of a security vulnerability such as secret leakage or remote code execution.
Treat as a routine bug-fix/UX improvement. Reviewers may verify that the new 12-character fixed column width accommodates the longest BIP39 English word ('accomplice', 10 letters) plus the index prefix, and that numeric backups still fit within the printer's line width. No security-specific action is required.
Security signals we found
No secret exposure, memory corruption, or cryptographic weakness visible in the diff.
Change is purely presentational: paper-backup column layout and padding.
No network, input parsing, or privilege changes.
No vendor security framing or CVE references in commit message or changelog.
Evidence from the diff
The patch refactors print_mnemonic_text() in src/krux/pages/print_page.py. Previously the routine always printed 3 columns of index:word pairs with hard-coded padding (10 and 21 character thresholds). Long BIP39 words (e.g., ‘panther’, ‘kitchen’) could exceed that width and run into the next column. The new logic uses 2 columns for plain-word backups and 3 columns for numeric (DEC/HEX/OCT) backups, with a fixed 12-character column width and trailing-space padding only between columns. Constants BASE_DEC_SUFFIX, BASE_HEX_SUFFIX, BASE_OCT_SUFFIX were moved from Utils to the pages package root to avoid circular imports and are now imported where needed. A keyword-argument call fix in show_mnemonic() and MnemonicLoader resolves a positional-argument mismatch that caused a device error when displaying word-number backups. Tests were updated to match the new 2-column word output and wider 3-column numeric output.
Changed components
src/krux/pages/print_page.pysrc/krux/pages/home_pages/mnemonic_backup.pysrc/krux/pages/mnemonic_loader.pysrc/krux/pages/__init__.pysrc/krux/pages/utils.pytests/pages/home_pages/test_mnemonic_backup.pyInspect captured patch +70 / −58
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8befd58..f22cd7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ Krux can now apply XOR operations on entropy bytes between a loaded mnemonic and
- Remove "Reboot" option and status bar, when empty, from Login menu
- Optimized Datum show: total pages are no longer visible, and navigation no longer wraps from the first page to the last or vice versa; better memory management and handling for large (~100K) files.
- Enabled swipe up/down gestures on keypads, menus, and QR transcribe
+- Fix mnemonic thermal printing when long words span multiple lines
# Changelog 25.09.0 - September 2025
diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py
index ac88f9b..b3c7f25 100644
--- a/src/krux/pages/__init__.py
+++ b/src/krux/pages/__init__.py
@@ -81,6 +81,10 @@ EXTRA_MNEMONIC_LENGTH_FLAG = 48
SWIPE_R_CHAR = "»"
SWIPE_L_CHAR = "«"
+BASE_DEC_SUFFIX = "DEC"
+BASE_HEX_SUFFIX = "HEX"
+BASE_OCT_SUFFIX = "OCT"
+
class Page:
"""Represents a page in the app, with helper methods for common display and
diff --git a/src/krux/pages/home_pages/mnemonic_backup.py b/src/krux/pages/home_pages/mnemonic_backup.py
index 582c13b..a0581ca 100644
--- a/src/krux/pages/home_pages/mnemonic_backup.py
+++ b/src/krux/pages/home_pages/mnemonic_backup.py
@@ -95,7 +95,9 @@ class MnemonicsView(Page):
def show_mnemonic(self, mnemonic, suffix="", display_mnemonic=None):
"""Displays only the mnemonic words or indexes"""
- self.display_mnemonic(mnemonic, suffix, display_mnemonic)
+ self.display_mnemonic(
+ mnemonic, suffix=suffix, display_mnemonic=display_mnemonic
+ )
self.ctx.input.wait_for_button()
# Avoid printing text on a cnc
@@ -115,6 +117,7 @@ class MnemonicsView(Page):
def display_mnemonic_numbers(self):
"""Handler for the 'numbers' menu item"""
from ..utils import Utils
+ from .. import BASE_DEC_SUFFIX, BASE_HEX_SUFFIX, BASE_OCT_SUFFIX
submenu = Menu(
self.ctx,
@@ -123,7 +126,7 @@ class MnemonicsView(Page):
t("Decimal"),
lambda: self.show_mnemonic(
self.ctx.wallet.key.mnemonic,
- Utils.BASE_DEC_SUFFIX,
+ BASE_DEC_SUFFIX,
Utils.get_mnemonic_numbers(
self.ctx.wallet.key.mnemonic, Utils.BASE_DEC
),
@@ -133,7 +136,7 @@ class MnemonicsView(Page):
t("Hexadecimal"),
lambda: self.show_mnemonic(
self.ctx.wallet.key.mnemonic,
- Utils.BASE_HEX_SUFFIX,
+ BASE_HEX_SUFFIX,
Utils.get_mnemonic_numbers(
self.ctx.wallet.key.mnemonic, Utils.BASE_HEX
),
@@ -143,7 +146,7 @@ class MnemonicsView(Page):
t("Octal"),
lambda: self.show_mnemonic(
self.ctx.wallet.key.mnemonic,
- Utils.BASE_OCT_SUFFIX,
+ BASE_OCT_SUFFIX,
Utils.get_mnemonic_numbers(
self.ctx.wallet.key.mnemonic, Utils.BASE_OCT
),
diff --git a/src/krux/pages/mnemonic_loader.py b/src/krux/pages/mnemonic_loader.py
index b2bd0e3..a0c09b8 100644
--- a/src/krux/pages/mnemonic_loader.py
+++ b/src/krux/pages/mnemonic_loader.py
@@ -29,6 +29,9 @@ from . import (
MENU_CONTINUE,
ESC_KEY,
LETTERS,
+ BASE_DEC_SUFFIX,
+ BASE_HEX_SUFFIX,
+ BASE_OCT_SUFFIX,
)
from ..display import BOTTOM_PROMPT_LINE
from ..qr import FORMAT_UR
@@ -468,15 +471,15 @@ class MnemonicLoader(Page):
DIGITS_OCT: Utils.BASE_OCT,
}
suffix_dict = {
- DIGITS: Utils.BASE_DEC_SUFFIX,
- DIGITS_HEX: Utils.BASE_HEX_SUFFIX,
- DIGITS_OCT: Utils.BASE_OCT_SUFFIX,
+ DIGITS: BASE_DEC_SUFFIX,
+ DIGITS_HEX: BASE_HEX_SUFFIX,
+ DIGITS_OCT: BASE_OCT_SUFFIX,
}
numbers_str = Utils.get_mnemonic_numbers(mnemonic, charset_type[charset])
self.display_mnemonic(
mnemonic,
- suffix_dict[charset],
- numbers_str,
+ suffix=suffix_dict[charset],
+ display_mnemonic=numbers_str,
fingerprint=Key.extract_fingerprint(mnemonic),
)
if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
diff --git a/src/krux/pages/print_page.py b/src/krux/pages/print_page.py
index 9ab0a92..81daeaa 100644
--- a/src/krux/pages/print_page.py
+++ b/src/krux/pages/print_page.py
@@ -73,23 +73,24 @@ class PrintPage(Page):
def print_mnemonic_text(self, mnemonic, suffix=""):
"""Prints Mnemonics words as text"""
+ from . import BASE_DEC_SUFFIX, BASE_HEX_SUFFIX, BASE_OCT_SUFFIX
+
self.ctx.display.clear()
self.ctx.display.draw_hcentered_text(
t("Printing…"), self.ctx.display.height() // 2
)
self.printer.print_string("BIP39" + " " + suffix + "\n\n")
words = mnemonic.split(" ")
- lines = len(words) // 3
+ cols = 3 if suffix in (BASE_DEC_SUFFIX, BASE_HEX_SUFFIX, BASE_OCT_SUFFIX) else 2
+ lines = len(words) // cols
for i in range(lines):
- index = i + 1
- string = str(index) + ":" + words[index - 1] + " "
- while len(string) < 10:
- string += " "
- index += lines
- string += str(index) + ":" + words[index - 1] + " "
- while len(string) < 21:
- string += " "
- index += lines
- string += str(index) + ":" + words[index - 1] + "\n"
- self.printer.print_string(string)
+ parts = []
+ for c in range(cols):
+ index = i + 1 + c * lines
+ part = str(index) + ":" + words[index - 1]
+ # pad all but last column
+ if c < cols - 1:
+ part = part + " " * (12 - len(part)) # fixed width of 12
+ parts.append(part)
+ self.printer.print_string("".join(parts) + "\n")
self.printer.feed(4)
diff --git a/src/krux/pages/utils.py b/src/krux/pages/utils.py
index 356bf66..73447e8 100644
--- a/src/krux/pages/utils.py
+++ b/src/krux/pages/utils.py
@@ -32,10 +32,6 @@ class Utils(Page):
BASE_HEX = 16
BASE_OCT = 8
- BASE_DEC_SUFFIX = "DEC"
- BASE_HEX_SUFFIX = "HEX"
- BASE_OCT_SUFFIX = "OCT"
-
def __init__(self, ctx):
super().__init__(ctx, None)
diff --git a/tests/pages/home_pages/test_mnemonic_backup.py b/tests/pages/home_pages/test_mnemonic_backup.py
index d654f0f..31ee7c9 100644
--- a/tests/pages/home_pages/test_mnemonic_backup.py
+++ b/tests/pages/home_pages/test_mnemonic_backup.py
@@ -89,7 +89,7 @@ def test_mnemonic_words(mocker, m5stickv, tdata):
mnemonics.mnemonic()
mnemonics.display_mnemonic.assert_called_with(
- ctx.wallet.key.mnemonic, "Mnemonic", None
+ ctx.wallet.key.mnemonic, suffix="Mnemonic", display_mnemonic=None
)
assert ctx.input.wait_for_button.call_count == len(case[2])
@@ -681,14 +681,18 @@ def test_print_mnemonic_other_words(mocker, amigo, tdata):
# brush badge sing still venue panther kitchen please help panel bundle excess sign couch stove increase human once effort candy goat top tiny major"
printer.print_string.assert_has_calls(
[
- mocker.call("1:brush 9:help 17:human\n"),
- mocker.call("2:badge 10:panel 18:once\n"),
- mocker.call("3:sing 11:bundle 19:effort\n"),
- mocker.call("4:still 12:excess 20:candy\n"),
- mocker.call("5:venue 13:sign 21:goat\n"),
- mocker.call("6:panther 14:couch 22:top\n"),
- mocker.call("7:kitchen 15:stove 23:tiny\n"),
- mocker.call("8:please 16:increase 24:major\n"),
+ mocker.call("1:brush 13:sign\n"),
+ mocker.call("2:badge 14:couch\n"),
+ mocker.call("3:sing 15:stove\n"),
+ mocker.call("4:still 16:increase\n"),
+ mocker.call("5:venue 17:human\n"),
+ mocker.call("6:panther 18:once\n"),
+ mocker.call("7:kitchen 19:effort\n"),
+ mocker.call("8:please 20:candy\n"),
+ mocker.call("9:help 21:goat\n"),
+ mocker.call("10:panel 22:top\n"),
+ mocker.call("11:bundle 23:tiny\n"),
+ mocker.call("12:excess 24:major\n"),
]
)
@@ -729,14 +733,14 @@ def test_print_mnemonic_numbers_decimal(mocker, amigo, tdata):
# brush badge sing still venue panther kitchen please help panel bundle excess sign couch stove increase human once effort candy goat top tiny major"
printer.print_string.assert_has_calls(
[
- mocker.call("1:234 9:857 17:887\n"),
- mocker.call("2:140 10:1277 18:1237\n"),
- mocker.call("3:1611 11:243 19:566\n"),
- mocker.call("4:1711 12:629 20:267\n"),
- mocker.call("5:1940 13:1603 21:801\n"),
- mocker.call("6:1279 14:392 22:1832\n"),
- mocker.call("7:985 15:1718 23:1812\n"),
- mocker.call("8:1332 16:918 24:1076\n"),
+ mocker.call("1:234 9:857 17:887\n"),
+ mocker.call("2:140 10:1277 18:1237\n"),
+ mocker.call("3:1611 11:243 19:566\n"),
+ mocker.call("4:1711 12:629 20:267\n"),
+ mocker.call("5:1940 13:1603 21:801\n"),
+ mocker.call("6:1279 14:392 22:1832\n"),
+ mocker.call("7:985 15:1718 23:1812\n"),
+ mocker.call("8:1332 16:918 24:1076\n"),
]
)
@@ -779,14 +783,14 @@ def test_print_mnemonic_numbers_hex(mocker, amigo, tdata):
# brush badge sing still venue panther kitchen please help panel bundle excess sign couch stove increase human once effort candy goat top tiny major"
printer.print_string.assert_has_calls(
[
- mocker.call("1:EA 9:359 17:377\n"),
- mocker.call("2:8C 10:4FD 18:4D5\n"),
- mocker.call("3:64B 11:F3 19:236\n"),
- mocker.call("4:6AF 12:275 20:10B\n"),
- mocker.call("5:794 13:643 21:321\n"),
- mocker.call("6:4FF 14:188 22:728\n"),
- mocker.call("7:3D9 15:6B6 23:714\n"),
- mocker.call("8:534 16:396 24:434\n"),
+ mocker.call("1:EA 9:359 17:377\n"),
+ mocker.call("2:8C 10:4FD 18:4D5\n"),
+ mocker.call("3:64B 11:F3 19:236\n"),
+ mocker.call("4:6AF 12:275 20:10B\n"),
+ mocker.call("5:794 13:643 21:321\n"),
+ mocker.call("6:4FF 14:188 22:728\n"),
+ mocker.call("7:3D9 15:6B6 23:714\n"),
+ mocker.call("8:534 16:396 24:434\n"),
]
)
@@ -829,14 +833,14 @@ def test_print_mnemonic_numbers_oct(mocker, amigo, tdata):
# brush badge sing still venue panther kitchen please help panel bundle excess sign couch stove increase human once effort candy goat top tiny major"
printer.print_string.assert_has_calls(
[
- mocker.call("1:352 9:1531 17:1567\n"),
- mocker.call("2:214 10:2375 18:2325\n"),
- mocker.call("3:3113 11:363 19:1066\n"),
- mocker.call("4:3257 12:1165 20:413\n"),
- mocker.call("5:3624 13:3103 21:1441\n"),
- mocker.call("6:2377 14:610 22:3450\n"),
- mocker.call("7:1731 15:3266 23:3424\n"),
- mocker.call("8:2464 16:1626 24:2064\n"),
+ mocker.call("1:352 9:1531 17:1567\n"),
+ mocker.call("2:214 10:2375 18:2325\n"),
+ mocker.call("3:3113 11:363 19:1066\n"),
+ mocker.call("4:3257 12:1165 20:413\n"),
+ mocker.call("5:3624 13:3103 21:1441\n"),
+ mocker.call("6:2377 14:610 22:3450\n"),
+ mocker.call("7:1731 15:3266 23:3424\n"),
+ mocker.call("8:2464 16:1626 24:2064\n"),
]
)
Why this scored 18/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.