What changed, and why it matters
This change relaxes a safety rule in the Krux hardware wallet firmware. Previously, if a user scanned a BIP39 passphrase QR code containing non-English/non-ASCII characters (for example accented letters or symbols), Krux would block it with an error. Now it shows a warning and lets the user proceed anyway. The risk is that different wallet software may interpret those special characters slightly differently, so the user could end up with a different Bitcoin key than expected and potentially lose access to funds. The change is intentional and documented, not a hidden bug.
Review whether the relaxed policy is acceptable for the project's threat model. If retained, consider additional safeguards such as requiring explicit confirmation on a second screen, showing the raw passphrase bytes, or documenting the exact normalization behavior. Users should be advised to use ASCII-only passphrases or to pre-normalize non-ASCII passphrases to NFKD before creating QR codes.
Security signals we found
Relaxation of input validation for BIP39 passphrase
Non-ASCII passphrase now allowed after warning instead of blocked
BIP39 NFKD normalization not performed due to firmware size constraints
Cross-wallet key derivation mismatch risk explicitly acknowledged in UI and docs
User-facing warning added in 12 languages
Evidence from the diff
The commit modifies PassphraseEditor.load_passphrase_menu() in src/krux/pages/wallet_settings.py. The previous behavior rejected any passphrase byte > 126 by calling flash_error(“Failed to load”) and looping. The new behavior displays a warning that non-ASCII characters were detected and that Krux cannot guarantee other wallets will derive the same key, then prompts the user to proceed. If the user declines, it continues the loop. The change is accompanied by i18n string additions, documentation updates noting BIP39 NFKD normalization cannot be performed due to firmware size constraints, and updated tests that now expect the prompt path instead of rejection. This is a UX/policy change rather than a code vulnerability, but it increases the chance of cross-wallet seed derivation mismatch for non-ASCII passphrases.
Changed components
src/krux/pages/wallet_settings.pyBIP39 passphrase loading flowInternationalization strings and compiled translation arraysUser documentation for loading a mnemonicInspect captured patch +53 / −11
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62d1956..802cf5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ This device shares similarities with the WonderMV but stands out with its larger
### New Device Support: WonderK PRO
From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Created by an entrepreneur who loves the Krux project, the WonderK follows in the footsteps of the WonderMV, but boasts a larger 2.8" display! Computer simulator for the WonderK device is also included.
+### Changed BIP39 Passphrase Validation
+Krux now displays a warning instead of blocking QR-encoded passphrases that contain non-ASCII characters. Users are encouraged to use QR codes containing only ASCII passphrases or non-ASCII that have already been normalized to NFKD.
+
### Other Bug Fixes and Improvements
- Added backtick ` to keypad
- Bugfix: Screensaver not activating in menu pages without statusbar
diff --git a/docs/getting-started/usage/loading-a-mnemonic.en.md b/docs/getting-started/usage/loading-a-mnemonic.en.md
index bacd30b..cef1e0a 100644
--- a/docs/getting-started/usage/loading-a-mnemonic.en.md
+++ b/docs/getting-started/usage/loading-a-mnemonic.en.md
@@ -161,7 +161,11 @@ It is possible to change any of the **wallet's attributes** (it will be possible
<img src="../../../img/maixpy_m5stickv/passphrase-load-options-250.png" align="right" class="m5stickv">
<img src="../../../img/maixpy_amigo/passphrase-load-options-300.png" align="right" class="amigo">
-You can type or scan a BIP39 passphrase. When typing, swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change keypads if your device has a touchscreen. You can also hold the button `PAGE` or `PREVIOUS` when navigating among letters while typing text to fast forward or backward. For scanning, you can also create a QR code from your offline passphrase using the [Datum tool](../features/tools.md/#datum-tool).
+You can type or scan a BIP39 passphrase. When typing, swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change keypads if your device has a touchscreen. Hold `PAGE` or `PREVIOUS` to move quickly through letters, and hold `ENTER` to jump to the next keypad.
+
+For scanning, you can generate an offline passphrase QR code using the [Datum tool](../features/tools.md/#datum-tool).
+
+**Note**: [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed) requires passphrases to be NFKD-normalized. Due to firmware size constraints, Krux cannot perform normalization internally. We therefore recommend using only ASCII QR codes or ensuring any non-ASCII are already normalized to NFKD.
<div style="clear: both"></div>
diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json
index 76ddc6c..20532c1 100644
--- a/i18n/translations/de-DE.json
+++ b/i18n/translations/de-DE.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Neue Firmware erkannt.",
"No": "Nein",
"No Passphrase": "Keine Passphrase",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Nicht-ASCII-Zeichen wurden in Ihrer Passphrase erkannt. Krux kann nicht garantieren, dass andere Wallets den gleichen Schlüssel ableiten.",
"Not enough rolls!": "Nicht genug Würfe!",
"Numbers": "Zahlen",
"Octal": "Oktal",
diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json
index 5941558..92a5c8b 100644
--- a/i18n/translations/es-MX.json
+++ b/i18n/translations/es-MX.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Nuevo firmware detectado.",
"No": "No",
"No Passphrase": "Sin Passphrase",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Se detectaron caracteres no ASCII en su frase de contraseña. Krux no puede garantizar que otros monederos obtengan la misma clave.",
"Not enough rolls!": "¡No hay suficientes tiradas!",
"Numbers": "Números",
"Octal": "Octales",
diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json
index a0a36f0..b76693c 100644
--- a/i18n/translations/fr-FR.json
+++ b/i18n/translations/fr-FR.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Nouveau micrologiciel détecté.",
"No": "Non",
"No Passphrase": "Pas de phrase secrète",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Des caractères non ASCII ont été détectés dans votre phrase secrète. Krux ne peut garantir que d'autres portefeuilles obtiendront la même clé.",
"Not enough rolls!": "Pas assez de jets !",
"Numbers": "Nombres",
"Octal": "Octale",
diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json
index 4c2d7b4..c668969 100644
--- a/i18n/translations/ja-JP.json
+++ b/i18n/translations/ja-JP.json
@@ -181,6 +181,7 @@
"New firmware detected.": "新しいファームウェアが検出されました.",
"No": "いいえ",
"No Passphrase": "パスフレーズなし",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "パスフレーズに非ASCII文字が検出されました.Kruxは、他のウォレットが同じキーを取得することを保証することはできません.",
"Not enough rolls!": "ロールが足りません!",
"Numbers": "数字",
"Octal": "Octal",
diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json
index 0cf4250..c5ae009 100644
--- a/i18n/translations/ko-KR.json
+++ b/i18n/translations/ko-KR.json
@@ -181,6 +181,7 @@
"New firmware detected.": "새로운 펌웨어가 감지되었습니다.",
"No": "아니요",
"No Passphrase": "패스프레이즈 없음",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "암호에서 ASCII가 아닌 문자가 감지되었습니다. Krux는 다른 지갑이 동일한 키를 파생할 것이라고 보장할 수 없습니다.",
"Not enough rolls!": "주사위 횟수가 충분하지 않습니다!",
"Numbers": "시드문구 순번",
"Octal": "8진수",
diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json
index 0b997df..6796d8c 100644
--- a/i18n/translations/nl-NL.json
+++ b/i18n/translations/nl-NL.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Nieuwe firmware gevonden.",
"No": "Nee",
"No Passphrase": "Geen wachtwoordzin",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Er zijn niet-ASCII-tekens gedetecteerd in uw wachtwoordzin. Krux kan niet garanderen dat andere portefeuilles dezelfde sleutel zullen afleiden.",
"Not enough rolls!": "Niet genoeg gedobbeld!",
"Numbers": "Nummers",
"Octal": "Octaal",
diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json
index 09d5e3a..60b9b16 100644
--- a/i18n/translations/pt-BR.json
+++ b/i18n/translations/pt-BR.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Novo firmware detectado.",
"No": "Não",
"No Passphrase": "Sem Senha",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Caracteres não ASCII foram detectados em sua senha. A Krux não pode garantir que outras carteiras obtenham a mesma chave.",
"Not enough rolls!": "Jogadas insuficientes!",
"Numbers": "Números",
"Octal": "Octal",
diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json
index 6acb2b1..afc5ef5 100644
--- a/i18n/translations/ru-RU.json
+++ b/i18n/translations/ru-RU.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Обнаружена новая прошивка.",
"No": "Нет",
"No Passphrase": "Нет Фраза-пароль",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "В вашей парольной фразе обнаружены символы, отличные от ASCII. Krux не может гарантировать, что другие кошельки получат тот же ключ.",
"Not enough rolls!": "Недостаточно бросков!",
"Numbers": "Числа",
"Octal": "Восьмеричный",
diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json
index 588ec91..9e2ccf2 100644
--- a/i18n/translations/tr-TR.json
+++ b/i18n/translations/tr-TR.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Yeni donanım yazılımı tespit edildi.",
"No": "Hayır",
"No Passphrase": "Parola Yok",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Parolanızda ASCII olmayan karakterler algılandı. Krux, diğer cüzdanların aynı anahtarı türeteceğini garanti edemez.",
"Not enough rolls!": "Yeterli zar atışı yok!",
"Numbers": "Numaralar",
"Octal": "Sekizlik",
diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json
index 7a86932..22a1e4d 100644
--- a/i18n/translations/vi-VN.json
+++ b/i18n/translations/vi-VN.json
@@ -181,6 +181,7 @@
"New firmware detected.": "Phát hiện phần sụn mới.",
"No": "Không",
"No Passphrase": "Không có cụm mật khẩu",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Các ký tự không phải ASCII đã được phát hiện trong cụm mật khẩu của bạn. Krux không thể đảm bảo rằng các ví khác sẽ lấy được cùng một khóa.",
"Not enough rolls!": "Không đủ số lần quay!",
"Numbers": "Số",
"Octal": "Bát phân",
diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json
index 212e24e..f7205d0 100644
--- a/i18n/translations/zh-CN.json
+++ b/i18n/translations/zh-CN.json
@@ -181,6 +181,7 @@
"New firmware detected.": "检测到新固件.",
"No": "否",
"No Passphrase": "无 Passphrase ",
+ "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "在您的密码短语中检测到非ASCII字符.Krux无法保证其他钱包将获得相同的密钥.",
"Not enough rolls!": "投掷次数不足!",
"Numbers": "数字",
"Octal": "八进制",
diff --git a/src/krux/pages/wallet_settings.py b/src/krux/pages/wallet_settings.py
index bc3e967..459d6f3 100644
--- a/src/krux/pages/wallet_settings.py
+++ b/src/krux/pages/wallet_settings.py
@@ -92,8 +92,19 @@ class PassphraseEditor(Page):
continue
# Check if passphrase string is within ascii range
if any(byte > 126 for byte in passphrase.encode()):
- self.flash_error(t("Failed to load"))
- continue
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ t(
+ "Non-ASCII characters were detected in your passphrase. "
+ "Krux cannot guarantee that other wallets will derive the "
+ "same key."
+ )
+ )
+ if not self.prompt(
+ t("Proceed?"),
+ BOTTOM_PROMPT_LINE,
+ ):
+ continue
from ..themes import theme
from ..key import Key
diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py
index e0900cc..31c3f4a 100644
--- a/src/krux/translations/__init__.py
+++ b/src/krux/translations/__init__.py
@@ -215,6 +215,7 @@ ref_array = [
3634967631,
4063104189,
2325721074,
+ 2474845199,
4092516657,
3089363813,
1577637745,
diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py
index 8802adf..4bf1c87 100644
--- a/src/krux/translations/de.py
+++ b/src/krux/translations/de.py
@@ -203,6 +203,7 @@ translation_array = [
"Neue Firmware erkannt.",
"Nein",
"Keine Passphrase",
+ "Nicht-ASCII-Zeichen wurden in Ihrer Passphrase erkannt. Krux kann nicht garantieren, dass andere Wallets den gleichen Schlüssel ableiten.",
"Nicht genug Würfe!",
"Zahlen",
"Oktal",
diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py
index 835afe8..8ea9b1f 100644
--- a/src/krux/translations/es.py
+++ b/src/krux/translations/es.py
@@ -203,6 +203,7 @@ translation_array = [
"Nuevo firmware detectado.",
"No",
"Sin Passphrase",
+ "Se detectaron caracteres no ASCII en su frase de contraseña. Krux no puede garantizar que otros monederos obtengan la misma clave.",
"¡No hay suficientes tiradas!",
"Números",
"Octales",
diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py
index 5bb187c..55ca8d2 100644
--- a/src/krux/translations/fr.py
+++ b/src/krux/translations/fr.py
@@ -203,6 +203,7 @@ translation_array = [
"Nouveau micrologiciel détecté.",
"Non",
"Pas de phrase secrète",
+ "Des caractères non ASCII ont été détectés dans votre phrase secrète. Krux ne peut garantir que d'autres portefeuilles obtiendront la même clé.",
"Pas assez de jets\u2009!",
"Nombres",
"Octale",
diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py
index 85bd471..7ae0c5e 100644
--- a/src/krux/translations/ja.py
+++ b/src/krux/translations/ja.py
@@ -203,6 +203,7 @@ translation_array = [
"新しいファームウェアが検出されました.",
"いいえ",
"パスフレーズなし",
+ "パスフレーズに非ASCII文字が検出されました.Kruxは、他のウォレットが同じキーを取得することを保証することはできません.",
"ロールが足りません!",
"数字",
"Octal",
diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py
index 3007c6e..323856d 100644
--- a/src/krux/translations/ko.py
+++ b/src/krux/translations/ko.py
@@ -203,6 +203,7 @@ translation_array = [
"새로운 펌웨어가 감지되었습니다.",
"아니요",
"패스프레이즈 없음",
+ "암호에서 ASCII가 아닌 문자가 감지되었습니다. Krux는 다른 지갑이 동일한 키를 파생할 것이라고 보장할 수 없습니다.",
"주사위 횟수가 충분하지 않습니다!",
"시드문구 순번",
"8진수",
diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py
index cab0979..2a08967 100644
--- a/src/krux/translations/nl.py
+++ b/src/krux/translations/nl.py
@@ -203,6 +203,7 @@ translation_array = [
"Nieuwe firmware gevonden.",
"Nee",
"Geen wachtwoordzin",
+ "Er zijn niet-ASCII-tekens gedetecteerd in uw wachtwoordzin. Krux kan niet garanderen dat andere portefeuilles dezelfde sleutel zullen afleiden.",
"Niet genoeg gedobbeld!",
"Nummers",
"Octaal",
diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py
index 04a718f..39e4802 100644
--- a/src/krux/translations/pt.py
+++ b/src/krux/translations/pt.py
@@ -203,6 +203,7 @@ translation_array = [
"Novo firmware detectado.",
"Não",
"Sem Senha",
+ "Caracteres não ASCII foram detectados em sua senha. A Krux não pode garantir que outras carteiras obtenham a mesma chave.",
"Jogadas insuficientes!",
"Números",
"Octal",
diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py
index a728898..a90aa47 100644
--- a/src/krux/translations/ru.py
+++ b/src/krux/translations/ru.py
@@ -203,6 +203,7 @@ translation_array = [
"Обнаружена новая прошивка.",
"Нет",
"Нет Фраза-пароль",
+ "В вашей парольной фразе обнаружены символы, отличные от ASCII. Krux не может гарантировать, что другие кошельки получат тот же ключ.",
"Недостаточно бросков!",
"Числа",
"Восьмеричный",
diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py
index 0efac34..fd63291 100644
--- a/src/krux/translations/tr.py
+++ b/src/krux/translations/tr.py
@@ -203,6 +203,7 @@ translation_array = [
"Yeni donanım yazılımı tespit edildi.",
"Hayır",
"Parola Yok",
+ "Parolanızda ASCII olmayan karakterler algılandı. Krux, diğer cüzdanların aynı anahtarı türeteceğini garanti edemez.",
"Yeterli zar atışı yok!",
"Numaralar",
"Sekizlik",
diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py
index 6423f52..893e663 100644
--- a/src/krux/translations/vi.py
+++ b/src/krux/translations/vi.py
@@ -203,6 +203,7 @@ translation_array = [
"Phát hiện phần sụn mới.",
"Không",
"Không có cụm mật khẩu",
+ "Các ký tự không phải ASCII đã được phát hiện trong cụm mật khẩu của bạn. Krux không thể đảm bảo rằng các ví khác sẽ lấy được cùng một khóa.",
"Không đủ số lần quay!",
"Số",
"Bát phân",
diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py
index 44ba6c2..b94f405 100644
--- a/src/krux/translations/zh.py
+++ b/src/krux/translations/zh.py
@@ -203,6 +203,7 @@ translation_array = [
"检测到新固件.",
"否",
"无 Passphrase ",
+ "在您的密码短语中检测到非ASCII字符.Krux无法保证其他钱包将获得相同的密钥.",
"投掷次数不足!",
"数字",
"八进制",
diff --git a/tests/pages/test_wallet_settings.py b/tests/pages/test_wallet_settings.py
index 659eacb..5e8c319 100644
--- a/tests/pages/test_wallet_settings.py
+++ b/tests/pages/test_wallet_settings.py
@@ -199,10 +199,11 @@ def test_qr_passphrase_fails_on_encrypted_non_ascii_bytes(mocker, m5stickv, tdat
def test_passphrase_non_ascii_validation(m5stickv, mocker, tdata):
from krux.pages.wallet_settings import PassphraseEditor
from krux.pages import Menu, MENU_EXIT
+ from krux.input import BUTTON_PAGE_PREV
# Test non-ASCII passphrase rejection
- NON_ASCII_PASSPHRASE = "Test™" # Contains non-ASCII character
- ctx = create_ctx(mocker, None, tdata.SINGLESIG_12_WORD_KEY)
+ NON_ASCII_PASSPHRASE = "Testá" # Contains non-ASCII character
+ ctx = create_ctx(mocker, [BUTTON_PAGE_PREV], tdata.SINGLESIG_12_WORD_KEY)
passphrase_editor = PassphraseEditor(ctx)
# Mock the Menu's run_loop to return non-ASCII passphrase first, then exit
@@ -216,18 +217,18 @@ def test_passphrase_non_ascii_validation(m5stickv, mocker, tdata):
side_effect=menu_returns,
)
- # Mock flash_error to track if it was called
- flash_error_spy = mocker.spy(passphrase_editor, "flash_error")
+ # Mock prompt to track if it was called
+ prompt_spy = mocker.spy(passphrase_editor, "prompt")
result = passphrase_editor.load_passphrase_menu(
tdata.SINGLESIG_12_WORD_KEY.mnemonic
)
- # Verify that flash_error was called with the ASCII error message
- flash_error_spy.assert_called()
+ # Verify that it was called
+ prompt_spy.assert_called()
# Get the actual call arguments
- call_args = flash_error_spy.call_args[0][0]
- assert call_args == "Failed to load"
+ call_args = prompt_spy.call_args[0][0]
+ assert call_args == "Proceed?"
# Verify that the method returned None (exited without accepting passphrase)
assert result is None
Why this scored 37/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.