What changed, and why it matters
This commit adds a new 'Mnemonic XOR' (SeedXOR) feature to Krux, a Bitcoin hardware wallet. It lets users combine two seed phrases using a mathematical XOR operation to create a new seed, or split an existing seed into two parts. The code also refactors existing mnemonic-loading code into a shared base class. There is no indication this is a security fix; it is a new feature implementation.
Review the SeedXOR implementation for correctness against the Coldcard specification, ensure entropy validation cannot be bypassed, and verify that the `MnemonicLoader` refactor did not introduce regressions in mnemonic loading or key derivation. No immediate patching is indicated because this is a feature addition, not a disclosed vulnerability fix.
Security signals we found
New cryptographic operation (XOR on mnemonic entropy) added to wallet UI
Input/output entropy validation rejects all-zeros and all-ones results
Length-mismatch guard prevents leaking longer entropy bytes
Refactor of mnemonic loading into shared base class could affect login attack surface
No vendor disclosure of a vulnerability or security fix
Evidence from the diff
The commit introduces MnemonicXOR, a new UI flow under Wallet settings that performs entropy-level XOR between the currently loaded BIP39 mnemonic and a second user-supplied mnemonic, following Coldcard’s SeedXOR scheme. It validates input/output entropies against all-zeros/all-ones, enforces equal word counts, and verifies the resulting mnemonic checksum. The change also extracts mnemonic-loading logic from Login into a new MnemonicLoader base class used by both Login and MnemonicXOR. Tests cover known Coldcard SeedXOR test vectors, length mismatch errors, and low-entropy rejection.
Changed components
src/krux/pages/home_pages/mnemonic_xor.pysrc/krux/pages/home_pages/home.pysrc/krux/pages/login.pysrc/krux/pages/mnemonic_loader.pysrc/krux/pages/__init__.pyi18n translationstests/pages/home_pages/test_mnemonic_xor.pyInspect captured patch +1768 / −473
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34fc00a..8befd58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@ Krux can now recognize and work with a wider range of SD cards that were previou
### Discontinued Support for Maix Bit Device
The Maix Bit device has long been discouraged due to its poor-quality camera. Starting with this release, we are discontinuing support and it will no longer be included in future builds. The support and parameters for building its firmware from source, however, will be kept.
+### Mnemonic XOR
+Krux can now apply XOR operations on entropy bytes between a loaded mnemonic and another chosen one, similar to the Coinkite's `SeedXOR` protocol.
+
### Other Bug Fixes and Improvements
- Touchscreen test added in Tools for detection check
- wbits for deflate-decompress window set to 10 bits to match KEF spec.
diff --git a/docs/getting-started/features/mnemonic-xor.en.md b/docs/getting-started/features/mnemonic-xor.en.md
new file mode 100644
index 0000000..2412cf5
--- /dev/null
+++ b/docs/getting-started/features/mnemonic-xor.en.md
@@ -0,0 +1,82 @@
+# What's the Mnemonic XOR?
+
+It's a implementation of [XOR logical gate](https://www.geeksforgeeks.org/digital-logic/xor-gate/) to operate, indefinitely, an [exclusive OR](https://en.wikipedia.org/wiki/Exclusive_or) upon a loaded mnemonic, based on [Coinkite's SeedXOR](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
+
+# How it works
+
+To derive a new mnemonic (and thus, a new seed) from other mnemonics, an operation occurs with the **mnemonic's entropy bytes**.
+Operate a XOR between them will derive a new **entropy bytes** that will be converted to a new mnemonic and then, to a new seed:
+
+<img src="../../../img/mnemonic_xor.png" align="center">
+
+- We get two different mnemonics (A and B), extract their *entropy bytes*;
+- validate the input entropies to avoid useless or dangerous operations:
+ - `A XOR B = A`: it will not change the XORed mnemonic;
+ - `A XOR B = A'` where `A'` is a "inverse" version of `A`;
+- once the inputs are checked, krux will apply a XOR between the *entropy bytes*;
+- validate the output entropy (same as above);
+- "convert" the valid *entropy bytes* output to a new mnemonic (C);
+- the user then can apply a password (optional) and get a new **master seed**.
+
+# Split and recover shares
+
+You can split a mnemonic into two separate mnemonics (or "shares") using the XOR operation. Neither share reveals any information about the original secret on its own. The original mnemonic can only be recovered when **both shares are combined**.
+
+### Core Principle
+
+Now we show a basic step
+If `A XOR B = C`, then `B XOR C = A`.
+
+- **A**: Your original mnemonic (the secret to protect);
+- **B**: A newly generated, random mnemonic (Share 1);
+- **C**: The resulting mnemonic from the XOR operation (Share 2).
+
+---
+
+## Step-by-Step Guide to Splitting Your Mnemonic
+
+### Phase 1: The Splitting Process
+
+#### Step 1: Generate a Random Share (Mnemonic B)
+
+1. Generate a new, random mnemonic from dice rolls or an image;
+2. **CRITICAL**: This mnemonic must have the same number of words (12 or 24) as your original mnemonic (A).
+
+#### Step 2: Backup Mnemonic B
+
+- Use a safe method to backup mnemonic B.
+
+#### Step 3: Perform the XOR Operation
+
+1. Load mnemonic A, go to **Wallet -> Mnemonic XOR** and load mnemonic B to be XORed with A;
+2. The resulting entropy of `A XOR B` will be used to create the second share, mnemonic C.
+
+#### Step 4: Back Up Mnemonic C
+
+1. Go to **Backup** and choose your favorite secure method to backup mnemonic C
+---
+
+### Phase 2: Verification & Finalization
+
+> ⚠️ **DO NOT SKIP THIS PHASE**
+
+#### Step 5: Verify the Recovery Process
+
+**This is non-negotiable.** Before relying on the system or destroying the original mnemonic, perform a test:
+
+1. Retrieve mnemonic B and C backups;
+2. Load one of them and XOR it with the other;
+3. Verify that the resulting mnemonic matches the original mnemonic A.
+
+#### Step 6: Destroy the Original
+
+⚠️ **Only after you have successfully verified in Step 5** that the recovery works perfectly should you securely destroy the original mnemonic (A).
+
+---
+
+## Important Notes
+
+- **Keep shares separate**: Store mnemonic B and mnemonic C in different secure locations;
+- **Both shares required**: Neither share alone provides any information about the original mnemonic;
+- **Always verify**: Test the recovery process before destroying the original;
+- **Same word count**: All three mnemonics (A, B, and C) must have the same number of words.
diff --git a/docs/img/mnemonic_xor.png b/docs/img/mnemonic_xor.png
new file mode 100644
index 0000000..caa7e2b
Binary files /dev/null and b/docs/img/mnemonic_xor.png differ
diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json
index 52b000f..a02818c 100644
--- a/i18n/translations/de-DE.json
+++ b/i18n/translations/de-DE.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "X-Koordinaten spiegeln",
"Missing signature file": "Fehlende Signaturdatei",
"Mnemonic": "Mnemonic",
+ "Mnemonic XOR": "Mnemonisches XOR",
"Mnemonic and passphrase will be kept.": "Mnemotechnik und Passphrase werden beibehalten.",
"Modified:": "Geändert:",
"Native Segwit - 84 would be assumed": "Native Segwit - 84 würde angenommen",
@@ -345,6 +346,9 @@
"Word %d": "Wort %d",
"Word Numbers": "Wortnummern",
"Words": "Wörter",
+ "XOR Result": "XOR-Ergebnis",
+ "XOR With": "XOR mit",
+ "XOR current mnemonic with another one ?": "XOR-Strommnemotechnik mit einer anderen?",
"Yes": "Ja",
"Zoomed mode": "Zoommodus",
"binary:": "binär:",
@@ -368,4 +372,4 @@
"to utf8": "zu utf8",
"unknown": "unbekannt",
"was NOT FOUND in the first %d addresses": "wurde in den ersten %d Adressen nicht gefunden"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json
index c74700c..18b6ec2 100644
--- a/i18n/translations/es-MX.json
+++ b/i18n/translations/es-MX.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Espejo de coordenadas X",
"Missing signature file": "Falta archivo de firma",
"Mnemonic": "Mnemónico",
+ "Mnemonic XOR": "XOR mnemónico",
"Mnemonic and passphrase will be kept.": "Mnemónico y passphrase se mantendrán.",
"Modified:": "Modificado:",
"Native Segwit - 84 would be assumed": "Segwit nativo - 84 se supondría",
@@ -345,6 +346,9 @@
"Word %d": "Palabra %d",
"Word Numbers": "Números de Palabra",
"Words": "Palabras",
+ "XOR Result": "Resultado XOR",
+ "XOR With": "XOR Con",
+ "XOR current mnemonic with another one ?": "¿XOR mnemotécnico actual con otro ?",
"Yes": "Sí",
"Zoomed mode": "Modo ampliado",
"binary:": "binario:",
@@ -368,4 +372,4 @@
"to utf8": "a utf8",
"unknown": "desconocido",
"was NOT FOUND in the first %d addresses": "NO FUE ENCONTRADO en las primeras %d direcciones"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json
index d51dcf0..f1c6976 100644
--- a/i18n/translations/fr-FR.json
+++ b/i18n/translations/fr-FR.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Refléter coordonnées X",
"Missing signature file": "Fichier de signature manquant",
"Mnemonic": "Mnémonique",
+ "Mnemonic XOR": "Mnémonique XOR",
"Mnemonic and passphrase will be kept.": "Mnémonique et phrase secrète seront conservés.",
"Modified:": "Modifié :",
"Native Segwit - 84 would be assumed": "Native Segwit - 84 serait supposé",
@@ -345,6 +346,9 @@
"Word %d": "Mot %d",
"Word Numbers": "Numéros de mots",
"Words": "Mots",
+ "XOR Result": "Résultat XOR",
+ "XOR With": "XOR avec",
+ "XOR current mnemonic with another one ?": "XOR mnémonique actuel avec un autre ?",
"Yes": "Oui",
"Zoomed mode": "Mode zoomé",
"binary:": "binaire :",
@@ -368,4 +372,4 @@
"to utf8": "vers utf8",
"unknown": "inconnu",
"was NOT FOUND in the first %d addresses": "INTROUVABLE dans les %d premières adresses"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json
index fe8dc2a..4832282 100644
--- a/i18n/translations/ja-JP.json
+++ b/i18n/translations/ja-JP.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "ミラーX座標",
"Missing signature file": "署名ファイルが欠落しています",
"Mnemonic": "Mnemonic",
+ "Mnemonic XOR": "ニーモニックXOR",
"Mnemonic and passphrase will be kept.": "Mnemonicとパスフレーズは保持されます.",
"Modified:": "修正されました:",
"Native Segwit - 84 would be assumed": "ネイティブSegwit - 84が仮定されます",
@@ -345,6 +346,9 @@
"Word %d": "単語 %d",
"Word Numbers": "単語番号",
"Words": "単語",
+ "XOR Result": "XOR結果",
+ "XOR With": "XOR With",
+ "XOR current mnemonic with another one ?": "XOR現在のニーモニックと別のニーモニック?",
"Yes": "はい",
"Zoomed mode": "ズームモード",
"binary:": "バイナリ:",
@@ -368,4 +372,4 @@
"to utf8": "utf 8へ",
"unknown": "不明",
"was NOT FOUND in the first %d addresses": "最初の%dアドレスに見つかりませんでした"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json
index 49fd789..0064a73 100644
--- a/i18n/translations/ko-KR.json
+++ b/i18n/translations/ko-KR.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "미러 X 좌표",
"Missing signature file": "서명 파일이 누락되었습니다",
"Mnemonic": "니모닉",
+ "Mnemonic XOR": "니모닉 XOR",
"Mnemonic and passphrase will be kept.": "니모닉과 암호는 유지됩니다.",
"Modified:": "수정되었습니다:",
"Native Segwit - 84 would be assumed": "네이티브 세그윗 - BIP84를 적용합니다",
@@ -345,6 +346,9 @@
"Word %d": "%d 단어",
"Word Numbers": "단어 번호(1-2048)",
"Words": "시드문구",
+ "XOR Result": "XOR 결과",
+ "XOR With": "XOR With",
+ "XOR current mnemonic with another one ?": "다른 XOR 전류 니모닉과 함께?",
"Yes": "예",
"Zoomed mode": "확대 모드",
"binary:": "바이너리:",
@@ -368,4 +372,4 @@
"to utf8": "utf8로",
"unknown": "알 수 없음",
"was NOT FOUND in the first %d addresses": "첫 번째 %d개의 주소에서 찾을 수 없습니다"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json
index 9252f38..a6307bf 100644
--- a/i18n/translations/nl-NL.json
+++ b/i18n/translations/nl-NL.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "X-coördinaten spiegelen",
"Missing signature file": "Handtekening bestand mist",
"Mnemonic": "Geheugensteun",
+ "Mnemonic XOR": "Mnemonic XOR",
"Mnemonic and passphrase will be kept.": "Geheugensteun en wachtwoord worden bewaard.",
"Modified:": "Aangepast:",
"Native Segwit - 84 would be assumed": "Native Segwit - 84 zal worden gebruikt",
@@ -345,6 +346,9 @@
"Word %d": "Woord %d",
"Word Numbers": "Woord nummers",
"Words": "Woorden",
+ "XOR Result": "XOR-resultaat",
+ "XOR With": "XOR met",
+ "XOR current mnemonic with another one ?": "XOR huidig geheugensteuntje met een ander ?",
"Yes": "Yes",
"Zoomed mode": "Ingezoomde modus",
"binary:": "binair:",
@@ -368,4 +372,4 @@
"to utf8": "naar utf8",
"unknown": "onbekend",
"was NOT FOUND in the first %d addresses": "werd NIET GEVONDEN in de eerste %d adressen"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json
index 28880cc..7fcf3e6 100644
--- a/i18n/translations/pt-BR.json
+++ b/i18n/translations/pt-BR.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Coordenadas X espelhadas",
"Missing signature file": "Arquivo de assinatura não encontrado",
"Mnemonic": "Mnemônico",
+ "Mnemonic XOR": "XOR Mnemônico",
"Mnemonic and passphrase will be kept.": "Mnemônico e senha serão mantidos.",
"Modified:": "Alterado:",
"Native Segwit - 84 would be assumed": "Segwit nativo - 84 seria assumido",
@@ -345,6 +346,9 @@
"Word %d": "Palavra %d",
"Word Numbers": "Números das Palavras",
"Words": "Palavras",
+ "XOR Result": "Resultado XOR",
+ "XOR With": "XOR com",
+ "XOR current mnemonic with another one ?": "XOR atual mnemônico com outro ?",
"Yes": "Sim",
"Zoomed mode": "Modo ampliado",
"binary:": "binário:",
@@ -368,4 +372,4 @@
"to utf8": "para utf8",
"unknown": "desconhecida",
"was NOT FOUND in the first %d addresses": "NÃO FOI ENCONTRADO nos primeiros %d endereços"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json
index 58ef802..e031065 100644
--- a/i18n/translations/ru-RU.json
+++ b/i18n/translations/ru-RU.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Координаты зеркала X",
"Missing signature file": "Отсутствует файл подписи",
"Mnemonic": "Мнемоника",
+ "Mnemonic XOR": "Мнемонический XOR",
"Mnemonic and passphrase will be kept.": "Мнемоника и парольная фраза будут сохранены.",
"Modified:": "Изменено:",
"Native Segwit - 84 would be assumed": "Native Segwit - 84 будет принято",
@@ -345,6 +346,9 @@
"Word %d": "Слово %d",
"Word Numbers": "Числа Слов",
"Words": "Слова",
+ "XOR Result": "Результат XOR",
+ "XOR With": "XOR с",
+ "XOR current mnemonic with another one ?": "XOR current mnemonic с другой ?",
"Yes": "Да",
"Zoomed mode": "Режим масштабирования",
"binary:": "двоичный:",
@@ -368,4 +372,4 @@
"to utf8": "to utf8",
"unknown": "неизвестный",
"was NOT FOUND in the first %d addresses": "нЕ НАЙДЕНО в первых %d адресах"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json
index c0bf4cd..199153c 100644
--- a/i18n/translations/tr-TR.json
+++ b/i18n/translations/tr-TR.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Ayna X Koordinatları",
"Missing signature file": "İmza dosyası eksik",
"Mnemonic": "Mnemonic",
+ "Mnemonic XOR": "Anımsatıcı XOR",
"Mnemonic and passphrase will be kept.": "Mnemonik ve parola tutulacaktır.",
"Modified:": "Değiştirildi:",
"Native Segwit - 84 would be assumed": "Yerel Segwit - 84 varsayılacaktır",
@@ -345,6 +346,9 @@
"Word %d": "Kelime %d",
"Word Numbers": "Kelime Numaraları",
"Words": "Kelimeler",
+ "XOR Result": "XOR Sonucu",
+ "XOR With": "XOR ile",
+ "XOR current mnemonic with another one ?": "XOR akım anımsatıcı başka bir tane ile?",
"Yes": "Evet",
"Zoomed mode": "Zoom Mod",
"binary:": "ikili:",
@@ -368,4 +372,4 @@
"to utf8": "utf8 'e",
"unknown": "bilinmiyor",
"was NOT FOUND in the first %d addresses": "ilk %d adreste BULUNAMADI"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json
index 9ee3a80..d46f6c3 100644
--- a/i18n/translations/vi-VN.json
+++ b/i18n/translations/vi-VN.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "Tọa độ gương X",
"Missing signature file": "Thiếu tập tin chữ ký",
"Mnemonic": "Mã mnemonic",
+ "Mnemonic XOR": "Mnemonic XOR",
"Mnemonic and passphrase will be kept.": "Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.",
"Modified:": "Đã sửa đổi:",
"Native Segwit - 84 would be assumed": "Native Segwit - 84 sẽ được giả định",
@@ -345,6 +346,9 @@
"Word %d": "Kí tự %d",
"Word Numbers": "Từ số",
"Words": "Từ ngữ",
+ "XOR Result": "Kết quả XOR",
+ "XOR With": "XOR với",
+ "XOR current mnemonic with another one ?": "Từ gợi nhớ hiện tại của XOR với từ gợi nhớ khác ?",
"Yes": "Đúng",
"Zoomed mode": "Chế độ thu phóng",
"binary:": "nhị phân:",
@@ -368,4 +372,4 @@
"to utf8": "đến utf8",
"unknown": "không rõ",
"was NOT FOUND in the first %d addresses": "kHÔNG TÌM THẤY trong %d địa chỉ đầu tiên"
-}
\ No newline at end of file
+}
diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json
index 924fb1b..a984135 100644
--- a/i18n/translations/zh-CN.json
+++ b/i18n/translations/zh-CN.json
@@ -171,6 +171,7 @@
"Mirror X Coordinates": "镜像X坐标",
"Missing signature file": "缺少签名文件",
"Mnemonic": "助记词",
+ "Mnemonic XOR": "助记符异或",
"Mnemonic and passphrase will be kept.": "助记词和密码将被保留.",
"Modified:": "修改时间:",
"Native Segwit - 84 would be assumed": "假定为原生 Segwit - 84",
@@ -345,6 +346,9 @@
"Word %d": "词 %d",
"Word Numbers": "单词序号",
"Words": "单词",
+ "XOR Result": "XOR结果",
+ "XOR With": "XOR With",
+ "XOR current mnemonic with another one ?": "XOR当前助记符与其他助记符?",
"Yes": "是",
"Zoomed mode": "放大模式",
"binary:": "二进制:",
@@ -368,4 +372,4 @@
"to utf8": "to_utf8()",
"unknown": "未知",
"was NOT FOUND in the first %d addresses": "在前 %d 个地址中未找到"
-}
\ No newline at end of file
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index f6583ad..2e486cf 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -114,6 +114,7 @@ nav:
- Tiny Seed and other metal plates: getting-started/features/tinyseed.en.md
- Empirical Entropy Measurement: getting-started/features/entropy.en.md
- Tamper Detection: getting-started/features/tamper-detection.en.md
+ - Mnemonic XOR: getting-started/features/mnemonic-xor.en.md
- Interface:
- Navigation overview: getting-started/navigation.en.md
- Settings: getting-started/settings.en.md
diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py
index 3d26bd4..cd42214 100644
--- a/src/krux/pages/__init__.py
+++ b/src/krux/pages/__init__.py
@@ -328,7 +328,12 @@ class Page:
done = True
def display_mnemonic(
- self, mnemonic: str, suffix="", display_mnemonic: str = None, fingerprint=""
+ self,
+ mnemonic: str,
+ title=None,
+ suffix="",
+ display_mnemonic: str = None,
+ fingerprint="",
):
"""Displays the 12 or 24-word list of words to the user"""
from ..wallet import is_double_mnemonic
@@ -343,7 +348,11 @@ class Page:
suffix += "*"
if fingerprint:
fingerprint = "\n" + fingerprint
- header = "BIP39 {}{}".format(suffix, fingerprint)
+ header = (
+ "BIP39 {}{}".format(suffix, fingerprint)
+ if not title
+ else "{} {}{}".format(title, suffix, fingerprint)
+ )
self.ctx.display.clear()
self.ctx.display.draw_hcentered_text(header)
if fingerprint:
diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py
index 2354fca..bc694c8 100644
--- a/src/krux/pages/home_pages/home.py
+++ b/src/krux/pages/home_pages/home.py
@@ -163,6 +163,21 @@ class Home(Page):
bip85.export()
return MENU_CONTINUE
+ def mnemonic_xor(self):
+ """Handler for the 'Mnemonic XOR' menu item"""
+ if not self.prompt(
+ t("XOR current mnemonic with another one ?"),
+ self.ctx.display.height() // 2,
+ ):
+ return MENU_CONTINUE
+
+ from .mnemonic_xor import MnemonicXOR
+
+ mnemonic_xor = MnemonicXOR(self.ctx)
+ mnemonic_xor.load()
+
+ return MENU_CONTINUE
+
def wallet(self):
"""Handler for the 'wallet' menu item"""
@@ -173,6 +188,7 @@ class Home(Page):
(t("Passphrase"), self.passphrase),
(t("Customize"), self.customize),
("BIP85", self.bip85),
+ (t("Mnemonic XOR"), self.mnemonic_xor),
],
)
submenu.run_loop()
@@ -225,7 +241,6 @@ class Home(Page):
return (None, FORMAT_NONE, psbt_filename)
def _sign_menu(self, signer, psbt_filename, outputs):
-
submenu = Menu(
self.ctx,
[
diff --git a/src/krux/pages/home_pages/mnemonic_xor.py b/src/krux/pages/home_pages/mnemonic_xor.py
new file mode 100644
index 0000000..525056e
--- /dev/null
+++ b/src/krux/pages/home_pages/mnemonic_xor.py
@@ -0,0 +1,199 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2025 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from .. import Menu, LETTERS, MENU_CONTINUE, MENU_EXIT
+from ..login import MnemonicLoader
+from ...krux_settings import Settings, t
+from ...display import BOTTOM_PROMPT_LINE, FONT_HEIGHT
+from ...themes import theme
+from ...key import Key
+from ...wallet import Wallet
+
+
+class MnemonicXOR(MnemonicLoader):
+ """
+ UI to apply a exclusive-or operation between the current mnemonic entropy with
+ chosen mnemonic entropies
+ """
+
+ # See implementation reference at
+ # https://github.com/Coldcard/firmware/blob/2445b4d4350d0aad0f4ef8f966697a67ab9ecbdc/shared/utils.py#L752
+ @staticmethod
+ def _xor_bytes(a: bytes, b: bytes) -> bytearray:
+ """XOR two byte sequences of equal length"""
+
+ # All sequences should have same length because it would
+ # reveal the last bytes from the longer bytestring as they are.
+ # In the case of mnemonics, it might reveal last 12 words.
+ if len(a) != len(b):
+ raise ValueError("Sequences should have same length")
+
+ out = bytearray(len(b))
+ for i in range(len(b)):
+ out[i] = a[i] ^ b[i]
+
+ return out
+
+ @staticmethod
+ def _validate_entropy(entropy: bytes | bytearray) -> None:
+ """Check for low entropy (all zeros or all ones)"""
+ # TODO: apply a shannon low entropy check for XOR
+ all_zeros = bytes(len(entropy))
+ all_ones = b"\xff" * len(entropy)
+ if entropy in (all_zeros, all_ones):
+ raise ValueError("Low entropy mnemonic")
+
+ @staticmethod
+ def _check_last_word(words):
+ _words = words.split(" ")
+ main_words = _words[:-1]
+ last_word = _words[-1:][0]
+
+ if last_word not in Key.get_final_word_candidates(main_words):
+ raise ValueError("Invalid checksum word: %s" % last_word)
+
+ def xor_with_current_mnemonic(self, mnemonic_to_xor: str) -> str:
+ """XOR current mnemonic with a new one following SeedXOR implementation"""
+ from embit.bip39 import mnemonic_from_bytes, mnemonic_to_bytes
+
+ # Validate same word count
+ current_word_count = len(self.ctx.wallet.key.mnemonic.split())
+ to_xor_word_count = len(mnemonic_to_xor.split())
+ if current_word_count != to_xor_word_count:
+ raise ValueError("Mnemonics should have same length")
+
+ # Convert and validate entropies
+ entropy_a = mnemonic_to_bytes(self.ctx.wallet.key.mnemonic)
+ entropy_b = mnemonic_to_bytes(mnemonic_to_xor)
+ self._validate_entropy(entropy_a)
+ self._validate_entropy(entropy_b)
+
+ # XOR and validate result
+ new_entropy = self._xor_bytes(entropy_a, entropy_b)
+ self._validate_entropy(new_entropy)
+
+ return mnemonic_from_bytes(new_entropy)
+
+ def _display_key_info(self, mnemonic: str, fingerprint: str, title: str) -> None:
+ """Display mnemonic or fingerprint based on hide_mnemonic setting"""
+
+ if not Settings().security.hide_mnemonic:
+ self.display_mnemonic(
+ mnemonic,
+ title=title,
+ fingerprint=fingerprint,
+ )
+ else:
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ title + " " + fingerprint,
+ color=theme.highlight_color,
+ )
+
+ def load(self):
+ """Menu for XOR the current mnemonic with a share"""
+ menu = Menu(
+ self.ctx,
+ [
+ (t("Via Camera"), self.load_key_from_camera),
+ (t("Via Manual Input"), self.load_key_from_manual_input),
+ (t("From Storage"), self.load_mnemonic_from_storage),
+ ],
+ )
+
+ menu.run_loop()
+ return MENU_EXIT
+
+ def _load_key_from_words(self, words, charset=LETTERS, new=False):
+ """
+ Similar method from krux.pages.login.Login without loading a key,
+ instead, it add the bytes from a mnemonic's entropy to the list of entropies
+ """
+
+ # Memorize the current fingerprint to use it later
+ current_fingerprint = self.ctx.wallet.key.fingerprint_hex_str(True)
+
+ # Show mnemonic which will be used for XOR with current one
+ mnemonic_to_xor = " ".join(words)
+ key_to_xor = Key(
+ mnemonic_to_xor,
+ self.ctx.wallet.key.policy_type,
+ self.ctx.wallet.key.network,
+ "",
+ self.ctx.wallet.key.account_index,
+ self.ctx.wallet.key.script_type,
+ )
+ # Memorize the fingerprint to XOR to use later
+ fingerprint_to_xor = key_to_xor.fingerprint_hex_str(True)
+
+ # Show the mnemonic to XOR with
+ self._display_key_info(
+ mnemonic_to_xor,
+ key_to_xor.fingerprint_hex_str(True),
+ t("XOR With") + ":",
+ )
+
+ if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
+ return MENU_CONTINUE
+
+ xored_mnemonic = self.xor_with_current_mnemonic(key_to_xor.mnemonic)
+ xored_key = Key(
+ xored_mnemonic,
+ self.ctx.wallet.key.policy_type,
+ self.ctx.wallet.key.network,
+ "",
+ self.ctx.wallet.key.account_index,
+ self.ctx.wallet.key.script_type,
+ )
+
+ # Display XOR operation and resulting fingerprint:
+ offset_y = self.ctx.display.height() // 2
+ offset_y -= FONT_HEIGHT * 3
+ self.ctx.display.clear()
+ for element in [current_fingerprint, "XOR", fingerprint_to_xor, "="]:
+ self.ctx.display.draw_hcentered_text(element, offset_y, theme.fg_color)
+ offset_y += FONT_HEIGHT
+ self.ctx.display.draw_hcentered_text(
+ xored_key.fingerprint_hex_str(True), offset_y, theme.highlight_color
+ )
+
+ if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
+ return MENU_CONTINUE
+
+ # If last word do not match, raise an error
+ MnemonicXOR._check_last_word(xored_mnemonic)
+
+ # Show the XOR result
+ self._display_key_info(
+ xored_mnemonic,
+ xored_key.fingerprint_hex_str(True),
+ t("XOR Result") + ":",
+ )
+
+ if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE):
+ self.ctx.wallet = Wallet(xored_key)
+ self.flash_text(
+ t("%s: loaded!") % xored_key.fingerprint_hex_str(True),
+ highlight_prefix=":",
+ )
+
+ return MENU_EXIT
diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py
index ac99206..6813b9b 100644
--- a/src/krux/pages/login.py
+++ b/src/krux/pages/login.py
@@ -23,21 +23,17 @@
import sys
from embit.networks import NETWORKS
-from embit.wordlists.bip39 import WORDLIST
from . import (
- Page,
Menu,
- DIGITS,
MENU_CONTINUE,
MENU_EXIT,
- ESC_KEY,
LETTERS,
EXTRA_MNEMONIC_LENGTH_FLAG,
choose_len_mnemonic,
)
+from .mnemonic_loader import MnemonicLoader
from ..display import DEFAULT_PADDING, FONT_HEIGHT, BOTTOM_PROMPT_LINE
from ..krux_settings import Settings
-from ..qr import FORMAT_UR
from ..key import (
Key,
P2WPKH,
@@ -64,7 +60,7 @@ MASK256 = (1 << 256) - 1
MASK128 = (1 << 128) - 1
-class Login(Page):
+class Login(MnemonicLoader):
"""Represents the login page of the app"""
# Used on boot.py when changing the locale on Settings
@@ -95,69 +91,6 @@ class Login(Page):
),
)
- def load_key(self):
- """Handler for the 'load mnemonic' menu item"""
- submenu = Menu(
- self.ctx,
- [
- (t("Via Camera"), self.load_key_from_camera),
- (t("Via Manual Input"), self.load_key_from_manual_input),
- (t("From Storage"), self.load_mnemonic_from_storage),
- ],
- )
- index, status = submenu.run_loop()
- if index == submenu.back_index:
- return MENU_CONTINUE
- return status
-
- def load_key_from_camera(self):
- """Handler for the 'load mnemonic'>'via camera' menu item"""
- submenu = Menu(
- self.ctx,
- [
- (t("QR Code"), self.load_key_from_qr_code),
- ("Tinyseed", lambda: self.load_key_from_tiny_seed_image("Tinyseed")),
- (
- "OneKey KeyTag",
- lambda: self.load_key_from_tiny_seed_image("OneKey KeyTag"),
- ),
- (
- t("Binary Grid"),
- lambda: self.load_key_from_tiny_seed_image("Binary Grid"),
- ),
- ],
- )
- index, status = submenu.run_loop()
- if index == submenu.back_index:
- return MENU_CONTINUE
- return status
-
- def load_key_from_manual_input(self):
- """Handler for the 'load mnemonic'>'via manual input' menu item"""
- submenu = Menu(
- self.ctx,
- [
- (t("Words"), self.load_key_from_text),
- (t("Word Numbers"), self.pre_load_key_from_digits),
- ("Tinyseed (Bits)", self.load_key_from_tiny_seed),
- ("Stackbit 1248", self.load_key_from_1248),
- ],
- )
- index, status = submenu.run_loop()
- if index == submenu.back_index:
- return MENU_CONTINUE
- return status
-
- def load_mnemonic_from_storage(self):
- """Handler to 'load mnemonic'>'from storage"""
- from .encryption_ui import LoadEncryptedMnemonic
-
- encrypted_mnemonics = LoadEncryptedMnemonic(self.ctx)
- words = encrypted_mnemonics.load_from_storage()
- if words == MENU_CONTINUE:
- return MENU_CONTINUE
- return self._load_key_from_words(words)
-
def new_key(self):
"""Handler for the 'new mnemonic' menu item"""
submenu = Menu(
@@ -413,395 +346,6 @@ class Login(Page):
self.ctx.wallet = Wallet(key)
return MENU_EXIT
- def _confirm_key_from_digits(self, mnemonic, charset):
- from .utils import Utils
-
- charset_type = {
- DIGITS: Utils.BASE_DEC,
- DIGITS_HEX: Utils.BASE_HEX,
- DIGITS_OCT: Utils.BASE_OCT,
- }
- suffix_dict = {
- DIGITS: Utils.BASE_DEC_SUFFIX,
- DIGITS_HEX: Utils.BASE_HEX_SUFFIX,
- DIGITS_OCT: Utils.BASE_OCT_SUFFIX,
- }
- numbers_str = Utils.get_mnemonic_numbers(mnemonic, charset_type[charset])
- self.display_mnemonic(
- mnemonic,
- suffix_dict[charset],
- numbers_str,
- fingerprint=Key.extract_fingerprint(mnemonic),
- )
- if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
- return MENU_CONTINUE
- self.ctx.display.clear()
-
- return None
-
- def auto_complete_qr_words(self, words):
- """Ensure all words are in the wordlist, autocomplete if possible"""
- for i, word in enumerate(words):
- if word not in WORDLIST:
- word_lower = word.lower()
- # Try to autocomplete the word
- auto_complete = False
- for list_word in WORDLIST:
- if list_word.startswith(word_lower):
- words[i] = list_word
- auto_complete = True
- break
- if not auto_complete:
- # Mark as invalid and clear the words list to indicate failure
- return []
- return words
-
- def load_key_from_qr_code(self):
- """Handler for the 'via qr code' menu item"""
- from .qr_capture import QRCodeCapture
- from .encryption_ui import decrypt_kef
-
- qr_capture = QRCodeCapture(self.ctx)
- data, qr_format = qr_capture.qr_capture_loop()
- if data is None:
- self.flash_error(t("Failed to load"))
- return MENU_CONTINUE
-
- try:
- data = decrypt_kef(self.ctx, data)
- except KeyError:
- self.flash_error(t("Failed to decrypt"))
- return MENU_CONTINUE
- except ValueError:
- # ValueError=not KEF or declined to decrypt
- pass
-
- words = []
- if qr_format == FORMAT_UR:
- from urtypes.crypto.bip39 import BIP39
-
- words = BIP39.from_cbor(data.cbor).words
- else:
- try:
- data_str = data.decode() if not isinstance(data, str) else data
- words = data_str.split() if " " in data_str else []
- if len(words) in (12, 24):
- words = self.auto_complete_qr_words(words)
- else:
- words = []
- except:
- pass
-
- if not words:
- data_bytes = ""
- try:
- data_bytes = (
- data.encode("latin-1") if isinstance(data, str) else data
- )
- except:
- try:
- data_bytes = (
- data.encode("shift-jis") if isinstance(data, str) else data
- )
- except:
- pass
-
- # CompactSeedQR format
- if len(data_bytes) in (16, 32):
- from embit.bip39 import mnemonic_from_bytes
-
- words = mnemonic_from_bytes(data_bytes).split()
- # SeedQR format
- elif len(data_bytes) in (48, 96):
- words = [
- WORDLIST[int(data_bytes[i : i + 4])]
- for i in range(0, len(data_bytes), 4)
- ]
-
- if not words or (len(words) != 12 and len(words) != 24):
- self.flash_error(t("Invalid mnemonic length"))
- return MENU_CONTINUE
- return self._load_key_from_words(words)
-
- def _load_key_from_keypad(
- self,
- title,
- charset,
- to_word,
- autocomplete_fn=None,
- possible_keys_fn=None,
- new=False,
- len_mnemonic=None,
- ):
- words = []
- self.ctx.display.draw_hcentered_text(title)
- if self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
- while len(words) < 24:
- if new:
- if len(words) == len_mnemonic - 1:
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(
- t(
- "Leave blank if you'd like Krux to pick a valid final word"
- )
- )
- self.ctx.input.wait_for_button()
- elif len(words) == len_mnemonic:
- break
- else:
- if len(words) == 12:
- self.ctx.display.clear()
- if self.prompt(t("Done?"), self.ctx.display.height() // 2):
- break
-
- word = ""
- word_num = ""
- while True:
- word_num = ""
-
- # if new and last word, lead input to a valid mnemonic
- if new and len(words) == len_mnemonic - 1:
- finalwords = Key.get_final_word_candidates(words)
- word = self.capture_from_keypad(
- t("Word %d") % (len(words) + 1),
- [charset],
- lambda x: autocomplete_fn(x, finalwords),
- lambda x: possible_keys_fn(x, finalwords),
- )
- else:
- word = self.capture_from_keypad(
- t("Word %d") % (len(words) + 1),
- [charset],
- autocomplete_fn,
- possible_keys_fn,
- )
-
- if word == ESC_KEY:
- return MENU_CONTINUE
-
- # If 'new' and the last 'word' is blank,
- # pick a random final word that is a valid checksum
- if new and word == "" and len(words) == len_mnemonic - 1:
- break
-
- if to_word is not None:
- word_num = word
- word = to_word(word)
-
- if word not in WORDLIST:
- word = ""
-
- if word != "":
- break
-
- if word not in WORDLIST and word == "":
- word = Key.pick_final_word(self.ctx.input.entropy, words)
-
- self.ctx.display.clear()
- if word_num in (word, ""):
- word_num = ""
- else:
- word_num += ": "
- if self.prompt(
- str(len(words) + 1) + ".\n\n" + word_num + word + "\n\n",
- self.ctx.display.height() // 2,
- highlight_prefix=":",
- ):
- words.append(word)
-
- return self._load_key_from_words(words, charset, new)
-
- return MENU_CONTINUE
-
- def load_key_from_text(self, new=False):
- """Handler for both 'new/load mnemonic'>[...]>'via words' menu items"""
- from .mnemonic_editor import MnemonicEditor
-
- if new:
- len_mnemonic = choose_len_mnemonic(self.ctx)
- if not len_mnemonic:
- return MENU_CONTINUE
- title = t("Enter %d BIP39 words.") % len_mnemonic
- else:
- len_mnemonic = None
- title = t("Enter each word of your BIP39 mnemonic.")
-
- mnemonic_editor = MnemonicEditor(self.ctx)
- mnemonic_editor.compute_search_ranges()
-
- return self._load_key_from_keypad(
- title,
- LETTERS,
- None,
- autocomplete_fn=mnemonic_editor.autocomplete,
- possible_keys_fn=mnemonic_editor.possible_letters,
- new=new,
- len_mnemonic=len_mnemonic,
- )
-
- def pre_load_key_from_digits(self):
- """Handler for the 'load mnemonic'>'via numbers' menu item"""
- submenu = Menu(
- self.ctx,
- [
- (t("Decimal"), self.load_key_from_digits),
- (t("Hexadecimal"), self.load_key_from_hexadecimal),
- (t("Octal"), self.load_key_from_octal),
- ],
- )
- index, status = submenu.run_loop()
- if index == submenu.back_index:
- return MENU_CONTINUE
- return status
-
- def load_key_from_octal(self):
- """Handler for the 'load mnemonic'>'via numbers'>'octal' submenu item"""
- title = t(
- "Enter each word of your BIP39 mnemonic as a number in octal from 1 to 4000."
- )
-
- def autocomplete(prefix):
- # 256 in decimal is 400 in octal
- if len(prefix) == 4 or (len(prefix) == 3 and int(prefix, 8) > 256):
- return prefix
- return None
-
- def to_word(user_input):
- if user_input:
- word_num = int(user_input, 8)
- if 0 < word_num <= 2048:
- return WORDLIST[word_num - 1]
- return ""
-
- def possible_letters(prefix):
- if prefix == "":
- return DIGITS_OCT.replace("0", "")
- if prefix == "400":
- return "0"
- return DIGITS_OCT
-
- return self._load_key_from_keypad(
- title,
- DIGITS_OCT,
- to_word,
- autocomplete_fn=autocomplete,
- possible_keys_fn=possible_letters,
- )
-
- def load_key_from_hexadecimal(self):
- """Handler for the 'load mnemonic'>'via numbers'>'hexadecimal' submenu item"""
- title = t(
- "Enter each word of your BIP39 mnemonic as a number in hexadecimal from 1 to 800."
- )
-
- def autocomplete(prefix):
- # 128 decimal is 0x80
- if len(prefix) == 3 or (len(prefix) == 2 and int(prefix, 16) > 128):
- return prefix
- return None
-
- def to_word(user_input):
- if user_input:
- word_num = int(user_input, 16)
- if 0 < word_num <= 2048:
- return WORDLIST[word_num - 1]
- return ""
-
- def possible_letters(prefix):
- if prefix == "":
- return DIGITS_HEX.replace("0", "")
- if prefix == "80":
- return "0"
- return DIGITS_HEX
-
- return self._load_key_from_keypad(
- title,
- DIGITS_HEX,
- to_word,
- autocomplete_fn=autocomplete,
- possible_keys_fn=possible_letters,
- )
-
- def load_key_from_digits(self):
- """Handler for the 'load mnemonic'>'via numbers'>'decimal' submenu item"""
- title = t("Enter each word of your BIP39 mnemonic as a number from 1 to 2048.")
-
- def autocomplete(prefix):
- if len(prefix) == 4 or (len(prefix) == 3 and int(prefix) > 204):
- return prefix
- return None
-
- def to_word(user_input):
- if user_input:
- word_num = int(user_input)
- if 0 < word_num <= 2048:
- return WORDLIST[word_num - 1]
- return ""
-
- def possible_letters(prefix):
- if prefix == "":
- return DIGITS.replace("0", "")
- if prefix == "204":
- return DIGITS.replace("9", "")
- return DIGITS
-
- return self._load_key_from_keypad(
- title,
- DIGITS,
- to_word,
- autocomplete_fn=autocomplete,
- possible_keys_fn=possible_letters,
- )
-
- def load_key_from_1248(self):
- """Menu handler to load key from Stackbit 1248 sheet metal storage method"""
- from .stack_1248 import Stackbit
-
- stackbit = Stackbit(self.ctx)
- words = stackbit.enter_1248()
- del stackbit
- if words is not None:
- return self._load_key_from_words(words)
- return MENU_CONTINUE
-
- def load_key_from_tiny_seed(self):
- """Menu handler to manually load key from Tinyseed sheet metal storage method"""
- from .tiny_seed import TinySeed
-
- len_mnemonic = choose_len_mnemonic(self.ctx)
- if not len_mnemonic:
- return MENU_CONTINUE
-
- tiny_seed = TinySeed(self.ctx)
- words = tiny_seed.enter_tiny_seed(len_mnemonic == 24)
- del tiny_seed
- if words is not None:
- return self._load_key_from_words(words)
- return MENU_CONTINUE
-
- def load_key_from_tiny_seed_image(self, grid_type="Tinyseed"):
- """Menu handler to scan key from Tinyseed sheet metal storage method"""
- from .tiny_seed import TinyScanner
-
- len_mnemonic = choose_len_mnemonic(self.ctx)
- if not len_mnemonic:
- return MENU_CONTINUE
-
- intro = t("Paint punched dots black so they can be detected.") + " "
- intro += t("Use a black background surface.") + " "
- intro += t("Align camera and backup plate properly.")
- self.ctx.display.draw_hcentered_text(intro)
- if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
- return MENU_CONTINUE
-
- tiny_scanner = TinyScanner(self.ctx, grid_type)
- words = tiny_scanner.scanner(len_mnemonic == 24)
- del tiny_scanner
- if words is None:
- self.flash_error(t("Failed to load"))
- return MENU_CONTINUE
- return self._load_key_from_words(words)
-
def tools(self):
"""Handler for the 'Tools' menu item"""
from .tools import Tools
diff --git a/src/krux/pages/mnemonic_loader.py b/src/krux/pages/mnemonic_loader.py
new file mode 100644
index 0000000..e005be3
--- /dev/null
+++ b/src/krux/pages/mnemonic_loader.py
@@ -0,0 +1,504 @@
+# The MIT License (MIT)
+
+
+# Copyright (c) 2021-2024 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from embit.wordlists.bip39 import WORDLIST
+from . import (
+ Page,
+ Menu,
+ DIGITS,
+ MENU_CONTINUE,
+ ESC_KEY,
+ LETTERS,
+ choose_len_mnemonic,
+)
+from ..display import BOTTOM_PROMPT_LINE
+from ..qr import FORMAT_UR
+from ..key import Key
+from ..krux_settings import t
+
+
+DIGITS_HEX = "0123456789ABCDEF"
+DIGITS_OCT = "01234567"
+
+DOUBLE_MNEMONICS_MAX_TRIES = 200
+MASK256 = (1 << 256) - 1
+MASK128 = (1 << 128) - 1
+
+
+class MnemonicLoader(Page):
+ """Base class for loading mnemonic (to be used for Login and MnemonicXOR)"""
+
+ def load_key(self):
+ """Handler for the 'load mnemonic' menu item"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("Via Camera"), self.load_key_from_camera),
+ (t("Via Manual Input"), self.load_key_from_manual_input),
+ (t("From Storage"), self.load_mnemonic_from_storage),
+ ],
+ )
+ index, status = submenu.run_loop()
+ if index == submenu.back_index:
+ return MENU_CONTINUE
+ return status
+
+ def load_key_from_camera(self):
+ """Handler for the 'load mnemonic'>'via camera' menu item"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("QR Code"), self.load_key_from_qr_code),
+ ("Tinyseed", lambda: self.load_key_from_tiny_seed_image("Tinyseed")),
+ (
+ "OneKey KeyTag",
+ lambda: self.load_key_from_tiny_seed_image("OneKey KeyTag"),
+ ),
+ (
+ t("Binary Grid"),
+ lambda: self.load_key_from_tiny_seed_image("Binary Grid"),
+ ),
+ ],
+ )
+ index, status = submenu.run_loop()
+ if index == submenu.back_index:
+ return MENU_CONTINUE
+ return status
+
+ def load_key_from_manual_input(self):
+ """Handler for the 'load mnemonic'>'via manual input' menu item"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("Words"), self.load_key_from_text),
+ (t("Word Numbers"), self.pre_load_key_from_digits),
+ ("Tinyseed (Bits)", self.load_key_from_tiny_seed),
+ ("Stackbit 1248", self.load_key_from_1248),
+ ],
+ )
+ index, status = submenu.run_loop()
+ if index == submenu.back_index:
+ return MENU_CONTINUE
+ return status
+
+ def load_mnemonic_from_storage(self):
+ """Handler to 'load mnemonic'>'from storage"""
+ from .encryption_ui import LoadEncryptedMnemonic
+
+ encrypted_mnemonics = LoadEncryptedMnemonic(self.ctx)
+ words = encrypted_mnemonics.load_from_storage()
+ if words == MENU_CONTINUE:
+ return MENU_CONTINUE
+ return self._load_key_from_words(words)
+
+ def _load_key_from_words(self, words, charset=LETTERS, new=False):
+ raise NotImplementedError
+
+ def load_key_from_text(self, new=False):
+ """Handler for both 'new/load mnemonic'>[...]>'via words' menu items"""
+ from .mnemonic_editor import MnemonicEditor
+
+ if new:
+ len_mnemonic = choose_len_mnemonic(self.ctx)
+ if not len_mnemonic:
+ return MENU_CONTINUE
+ title = t("Enter %d BIP39 words.") % len_mnemonic
+ else:
+ len_mnemonic = None
+ title = t("Enter each word of your BIP39 mnemonic.")
+
+ mnemonic_editor = MnemonicEditor(self.ctx)
+ mnemonic_editor.compute_search_ranges()
+
+ return self._load_key_from_keypad(
+ title,
+ LETTERS,
+ None,
+ autocomplete_fn=mnemonic_editor.autocomplete,
+ possible_keys_fn=mnemonic_editor.possible_letters,
+ new=new,
+ len_mnemonic=len_mnemonic,
+ )
+
+ def pre_load_key_from_digits(self):
+ """Handler for the 'load mnemonic'>'via numbers' menu item"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("Decimal"), self.load_key_from_digits),
+ (t("Hexadecimal"), self.load_key_from_hexadecimal),
+ (t("Octal"), self.load_key_from_octal),
+ ],
+ )
+ index, status = submenu.run_loop()
+ if index == submenu.back_index:
+ return MENU_CONTINUE
+ return status
+
+ def load_key_from_octal(self):
+ """Handler for the 'load mnemonic'>'via numbers'>'octal' submenu item"""
+ title = t(
+ "Enter each word of your BIP39 mnemonic as a number in octal from 1 to 4000."
+ )
+
+ def autocomplete(prefix):
+ # 256 in decimal is 400 in octal
+ if len(prefix) == 4 or (len(prefix) == 3 and int(prefix, 8) > 256):
+ return prefix
+ return None
+
+ def to_word(user_input):
+ if user_input:
+ word_num = int(user_input, 8)
+ if 0 < word_num <= 2048:
+ return WORDLIST[word_num - 1]
+ return ""
+
+ def possible_letters(prefix):
+ if prefix == "":
+ return DIGITS_OCT.replace("0", "")
+ if prefix == "400":
+ return "0"
+ return DIGITS_OCT
+
+ return self._load_key_from_keypad(
+ title,
+ DIGITS_OCT,
+ to_word,
+ autocomplete_fn=autocomplete,
+ possible_keys_fn=possible_letters,
+ )
+
+ def load_key_from_hexadecimal(self):
+ """Handler for the 'load mnemonic'>'via numbers'>'hexadecimal' submenu item"""
+ title = t(
+ "Enter each word of your BIP39 mnemonic as a number in hexadecimal from 1 to 800."
+ )
+
+ def autocomplete(prefix):
+ # 128 decimal is 0x80
+ if len(prefix) == 3 or (len(prefix) == 2 and int(prefix, 16) > 128):
+ return prefix
+ return None
+
+ def to_word(user_input):
+ if user_input:
+ word_num = int(user_input, 16)
+ if 0 < word_num <= 2048:
+ return WORDLIST[word_num - 1]
+ return ""
+
+ def possible_letters(prefix):
+ if prefix == "":
+ return DIGITS_HEX.replace("0", "")
+ if prefix == "80":
+ return "0"
+ return DIGITS_HEX
+
+ return self._load_key_from_keypad(
+ title,
+ DIGITS_HEX,
+ to_word,
+ autocomplete_fn=autocomplete,
+ possible_keys_fn=possible_letters,
+ )
+
+ def load_key_from_digits(self):
+ """Handler for the 'load mnemonic'>'via numbers'>'decimal' submenu item"""
+ title = t("Enter each word of your BIP39 mnemonic as a number from 1 to 2048.")
+
+ def autocomplete(prefix):
+ if len(prefix) == 4 or (len(prefix) == 3 and int(prefix) > 204):
+ return prefix
+ return None
+
+ def to_word(user_input):
+ if user_input:
+ word_num = int(user_input)
+ if 0 < word_num <= 2048:
+ return WORDLIST[word_num - 1]
+ return ""
+
+ def possible_letters(prefix):
+ if prefix == "":
+ return DIGITS.replace("0", "")
+ if prefix == "204":
+ return DIGITS.replace("9", "")
+ return DIGITS
+
+ return self._load_key_from_keypad(
+ title,
+ DIGITS,
+ to_word,
+ autocomplete_fn=autocomplete,
+ possible_keys_fn=possible_letters,
+ )
+
+ def load_key_from_1248(self):
+ """Menu handler to load key from Stackbit 1248 sheet metal storage method"""
+ from .stack_1248 import Stackbit
+
+ stackbit = Stackbit(self.ctx)
+ words = stackbit.enter_1248()
+ del stackbit
+ if words is not None:
+ return self._load_key_from_words(words)
+ return MENU_CONTINUE
+
+ def load_key_from_tiny_seed(self):
+ """Menu handler to manually load key from Tinyseed sheet metal storage method"""
+ from .tiny_seed import TinySeed
+
+ len_mnemonic = choose_len_mnemonic(self.ctx)
+ if not len_mnemonic:
+ return MENU_CONTINUE
+
+ tiny_seed = TinySeed(self.ctx)
+ words = tiny_seed.enter_tiny_seed(len_mnemonic == 24)
+ del tiny_seed
+ if words is not None:
+ return self._load_key_from_words(words)
+ return MENU_CONTINUE
+
+ def load_key_from_tiny_seed_image(self, grid_type="Tinyseed"):
+ """Menu handler to scan key from Tinyseed sheet metal storage method"""
+ from .tiny_seed import TinyScanner
+
+ len_mnemonic = choose_len_mnemonic(self.ctx)
+ if not len_mnemonic:
+ return MENU_CONTINUE
+
+ intro = t("Paint punched dots black so they can be detected.") + " "
+ intro += t("Use a black background surface.") + " "
+ intro += t("Align camera and backup plate properly.")
+ self.ctx.display.draw_hcentered_text(intro)
+ if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
+ return MENU_CONTINUE
+
+ tiny_scanner = TinyScanner(self.ctx, grid_type)
+ words = tiny_scanner.scanner(len_mnemonic == 24)
+ del tiny_scanner
+ if words is None:
+ self.flash_error(t("Failed to load"))
+ return MENU_CONTINUE
+ return self._load_key_from_words(words)
+
+ def load_key_from_qr_code(self):
+ """Handler for the 'via qr code' menu item"""
+ from .qr_capture import QRCodeCapture
+ from .encryption_ui import decrypt_kef
+
+ qr_capture = QRCodeCapture(self.ctx)
+ data, qr_format = qr_capture.qr_capture_loop()
+ if data is None:
+ self.flash_error(t("Failed to load"))
+ return MENU_CONTINUE
+
+ try:
+ data = decrypt_kef(self.ctx, data)
+ except KeyError:
+ self.flash_error(t("Failed to decrypt"))
+ return MENU_CONTINUE
+ except ValueError:
+ # ValueError=not KEF or declined to decrypt
+ pass
+
+ words = []
+ if qr_format == FORMAT_UR:
+ from urtypes.crypto.bip39 import BIP39
+
+ words = BIP39.from_cbor(data.cbor).words
+ else:
+ try:
+ data_str = data.decode() if not isinstance(data, str) else data
+ words = data_str.split() if " " in data_str else []
+ if len(words) in (12, 24):
+ words = self.auto_complete_qr_words(words)
+ else:
+ words = []
+ except:
+ pass
+
+ if not words:
+ data_bytes = ""
+ try:
+ data_bytes = (
+ data.encode("latin-1") if isinstance(data, str) else data
+ )
+ except:
+ try:
+ data_bytes = (
+ data.encode("shift-jis") if isinstance(data, str) else data
+ )
+ except:
+ pass
+
+ # CompactSeedQR format
+ if len(data_bytes) in (16, 32):
+ from embit.bip39 import mnemonic_from_bytes
+
+ words = mnemonic_from_bytes(data_bytes).split()
+ # SeedQR format
+ elif len(data_bytes) in (48, 96):
+ words = [
+ WORDLIST[int(data_bytes[i : i + 4])]
+ for i in range(0, len(data_bytes), 4)
+ ]
+
+ if not words or (len(words) != 12 and len(words) != 24):
+ self.flash_error(t("Invalid mnemonic length"))
+ return MENU_CONTINUE
+ return self._load_key_from_words(words)
+
+ def _load_key_from_keypad(
+ self,
+ title,
+ charset,
+ to_word,
+ autocomplete_fn=None,
+ possible_keys_fn=None,
+ new=False,
+ len_mnemonic=None,
+ ):
+ words = []
+ self.ctx.display.draw_hcentered_text(title)
+ if self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
+ while len(words) < 24:
+ if new:
+ if len(words) == len_mnemonic - 1:
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t(
+ "Leave blank if you'd like Krux to pick a valid final word"
+ )
+ )
+ self.ctx.input.wait_for_button()
+ elif len(words) == len_mnemonic:
+ break
+ else:
+ if len(words) == 12:
+ self.ctx.display.clear()
+ if self.prompt(t("Done?"), self.ctx.display.height() // 2):
+ break
+
+ word = ""
+ word_num = ""
+ while True:
+ word_num = ""
+
+ # if new and last word, lead input to a valid mnemonic
+ if new and len(words) == len_mnemonic - 1:
+ finalwords = Key.get_final_word_candidates(words)
+ word = self.capture_from_keypad(
+ t("Word %d") % (len(words) + 1),
+ [charset],
+ lambda x: autocomplete_fn(x, finalwords),
+ lambda x: possible_keys_fn(x, finalwords),
+ )
+ else:
+ word = self.capture_from_keypad(
+ t("Word %d") % (len(words) + 1),
+ [charset],
+ autocomplete_fn,
+ possible_keys_fn,
+ )
+
+ if word == ESC_KEY:
+ return MENU_CONTINUE
+
+ # If 'new' and the last 'word' is blank,
+ # pick a random final word that is a valid checksum
+ if new and word == "" and len(words) == len_mnemonic - 1:
+ break
+
+ if to_word is not None:
+ word_num = word
+ word = to_word(word)
+
+ if word not in WORDLIST:
+ word = ""
+
+ if word != "":
+ break
+
+ if word not in WORDLIST and word == "":
+ word = Key.pick_final_word(self.ctx.input.entropy, words)
+
+ self.ctx.display.clear()
+ if word_num in (word, ""):
+ word_num = ""
+ else:
+ word_num += ": "
+ if self.prompt(
+ str(len(words) + 1) + ".\n\n" + word_num + word + "\n\n",
+ self.ctx.display.height() // 2,
+ highlight_prefix=":",
+ ):
+ words.append(word)
+
+ return self._load_key_from_words(words, charset, new)
+
+ return MENU_CONTINUE
+
+ def _confirm_key_from_digits(self, mnemonic, charset):
+ from .utils import Utils
+
+ charset_type = {
+ DIGITS: Utils.BASE_DEC,
+ DIGITS_HEX: Utils.BASE_HEX,
+ DIGITS_OCT: Utils.BASE_OCT,
+ }
+ suffix_dict = {
+ DIGITS: Utils.BASE_DEC_SUFFIX,
+ DIGITS_HEX: Utils.BASE_HEX_SUFFIX,
+ DIGITS_OCT: Utils.BASE_OCT_SUFFIX,
+ }
+ numbers_str = Utils.get_mnemonic_numbers(mnemonic, charset_type[charset])
+ self.display_mnemonic(
+ mnemonic,
+ suffix_dict[charset],
+ numbers_str,
+ fingerprint=Key.extract_fingerprint(mnemonic),
+ )
+ if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
+ return MENU_CONTINUE
+ self.ctx.display.clear()
+
+ return None
+
+ def auto_complete_qr_words(self, words):
+ """Ensure all words are in the wordlist, autocomplete if possible"""
+ for i, word in enumerate(words):
+ if word not in WORDLIST:
+ word_lower = word.lower()
+ # Try to autocomplete the word
+ auto_complete = False
+ for list_word in WORDLIST:
+ if list_word.startswith(word_lower):
+ words[i] = list_word
+ auto_complete = True
+ break
+ if not auto_complete:
+ # Mark as invalid and clear the words list to indicate failure
+ return []
+ return words
diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py
index 0b38b7c..c382ae9 100644
--- a/src/krux/translations/__init__.py
+++ b/src/krux/translations/__init__.py
@@ -50,6 +50,7 @@ ref_array = [
2995482424,
2415648848,
1043817877,
+ 1610434839,
3439746594,
4121028614,
3270727197,
@@ -79,6 +80,7 @@ ref_array = [
3119547911,
1187826970,
4011811253,
+ 1860915234,
422237057,
1464900930,
3625040530,
@@ -86,6 +88,7 @@ ref_array = [
167798282,
678449760,
1347214433,
+ 1590681747,
3513215254,
1741764813,
3585411775,
@@ -100,6 +103,7 @@ ref_array = [
4102535566,
3200465747,
2940689088,
+ 3164577772,
1712856005,
4150351825,
3278654271,
@@ -172,6 +176,8 @@ ref_array = [
237577215,
4122897393,
640219121,
+ 173596861,
+ 149326735,
3218124392,
3000888649,
3264569915,
@@ -206,6 +212,7 @@ ref_array = [
3928301843,
1948316555,
1443208255,
+ 3782283369,
1237332019,
4265479636,
2939797024,
@@ -380,6 +387,7 @@ ref_array = [
3742424146,
2965123464,
1303016265,
+ 55888997,
619317523,
1688347225,
3673250051,
diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py
index 9970e8a..b6a393d 100644
--- a/src/krux/translations/de.py
+++ b/src/krux/translations/de.py
@@ -38,6 +38,7 @@ translation_array = [
"Konto",
"Konto #0 würde angenommen",
"Kontoindex",
+ "Einen Anteil hinzufügen",
"Wallet-Passphrase hinzufügen oder ändern?",
"Zusätzliche Entropie von der Kamera erforderlich für %s",
"Adresse",
@@ -67,6 +68,7 @@ translation_array = [
"Überprüfen, ob diese Adresse zu dieser Wallet gehört?",
"Überprüfte %d Adresse ohne Übereinstimmungen.",
"SD-Karte wird gesucht…",
+ "Anteile bereinigen",
"Bestätigen Sie den Tamper Check Code",
"Datum konvertieren",
"Änderungsadresse konnte nicht ermittelt werden.",
@@ -74,6 +76,7 @@ translation_array = [
"QR-Code aus Text erstellen?",
"Erstellt:",
"Aktueller Tamper Check Code",
+ "Die aktuelle Mnemonic und die Passphrase werden nicht beibehalten.",
"Benutzerdefinierter Link QR-Code",
"Custom Text",
"Anpassen",
@@ -88,6 +91,7 @@ translation_array = [
"Tiefe pro Durchgang",
"Derivation-Pfad",
"BIP85-Entropie ableiten?",
+ "Anteile ableiten",
"Deskriptor-Adressen",
"Gerätetests",
"Bildschirm",
@@ -160,6 +164,8 @@ translation_array = [
"Ungültige Wallet:",
"Umkehren",
"Invertierte Farben",
+ "Es werden alle geladenen Anteile gelöscht",
+ "Es werden der aktuelle Schlüssel, die Passphrase und der Deskriptor gelöscht.",
"KEF-verschlüsselt",
"Schlüssel",
"Schlüssel wurde nicht zur Verfügung gestellt",
@@ -194,6 +200,7 @@ translation_array = [
"Fehlende Signaturdatei",
"Mnemonic",
"Mnemotechnik und Passphrase werden beibehalten.",
+ "Die Mnemonics werden mit der aktuellen Mnemonic (SeedXOR) XOR-verknüpft.",
"Geändert:",
"Native Segwit - 84 würde angenommen",
"Netzwerk",
@@ -368,6 +375,7 @@ translation_array = [
"Wortnummern",
"Wörter",
"Ja",
+ "Sie können bis zu %d Anteile hinzufügen",
"Zoommodus",
"binär:",
"failed:",
diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py
index 114f3ba..9dc5134 100644
--- a/src/krux/translations/es.py
+++ b/src/krux/translations/es.py
@@ -38,6 +38,7 @@ translation_array = [
"Cuenta",
"Se supondría que la cuenta #0",
"Índice de la cuenta",
+ "Agregar una participación",
"¿Añadir o cambiar passphrase de la cartera?",
"Se requiere entropía adicional de la cámara para %s",
"Dirección",
@@ -67,6 +68,7 @@ translation_array = [
"¿Verificar que la dirección pertenece a esta cartera?",
"Comprobado %d direcciones sin coincidencias.",
"Buscando tarjeta SD…",
+ "Limpiar participaciones",
"Confirmar el código de verificación",
"Convertir dato",
"No se pudo determinar la dirección de cambio.",
@@ -74,6 +76,7 @@ translation_array = [
"¿Crear código QR a partir de texto?",
"Creado:",
"Código de verificación actual",
+ "La mnemotécnica actual y la frase de contraseña no se conservarán.",
"Código QR personalizado",
"Texto Personalizado",
"Personalizar",
@@ -88,6 +91,7 @@ translation_array = [
"Profundidad por Pasada",
"Ruta de derivación",
"¿Derivar entropía BIP85?",
+ "Derivar participaciones",
"Direcciones del descriptor",
"Pruebas del dispositivo",
"Pantalla",
@@ -160,6 +164,8 @@ translation_array = [
"Cartera inválida:",
"Invertir",
"Colores Invertidos",
+ "Se limpiarán todas las participaciones cargadas",
+ "Se eliminarán la clave actual, la frase de contraseña y el descriptor.",
"Kef encriptado",
"Clave",
"No se proporcionó la clave",
@@ -194,6 +200,7 @@ translation_array = [
"Falta archivo de firma",
"Mnemónico",
"Mnemónico y passphrase se mantendrán.",
+ "Las mnemotécnicas se XORarán con la mnemotécnica actual (SeedXOR).",
"Modificado:",
"Segwit nativo - 84 se supondría",
"Red",
@@ -368,6 +375,7 @@ translation_array = [
"Números de Palabra",
"Palabras",
"Sí",
+ "Puedes agregar hasta %d participaciones",
"Modo ampliado",
"binario:",
"failed:",
diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py
index 1358e45..cb96b3d 100644
--- a/src/krux/translations/fr.py
+++ b/src/krux/translations/fr.py
@@ -38,6 +38,7 @@ translation_array = [
"Compte",
"Le compte n °0 serait supposé",
"Index du compte",
+ "Ajouter une part",
"Ajoutez ou modifiez la phrase secrète\u2009?",
"Entropie supplémentaire de la caméra requise pour %s",
"Adresse",
@@ -67,6 +68,7 @@ translation_array = [
"Vérifiez que l'adresse appartient à ce portefeuille\u2009?",
"%d adresses vérifiées sans correspondance.",
"Recherche de carte SD…",
+ "Nettoyer les parts",
"Confirmer le code de non compromis",
"Convertir le datum",
"Impossible de déterminer l'adresse de monnaie.",
@@ -74,6 +76,7 @@ translation_array = [
"Créer un code QR à partir de texte\u2009?",
"Créé\u2009:",
"Code de non compromis actuel",
+ "La mnémonique et la phrase secrète actuelles ne seront pas conservées.",
"Code QR personnalisé",
"Texte personnalisé",
"Personnaliser",
@@ -88,6 +91,7 @@ translation_array = [
"Profondeur par passage",
"Chemin de dérivation",
"Dériver l'entropie BIP85\u2009?",
+ "Dériver des parts",
"Adresses du descripteur",
"Tests de l'appareil",
"Affichage",
@@ -160,6 +164,8 @@ translation_array = [
"Portefeuille invalide\u2009:",
"Inverser",
"Couleurs inversées",
+ "Toutes les parts chargées seront supprimées",
+ "La clé actuelle, la phrase secrète et le descripteur seront supprimés.",
"KEF chiffré",
"Clé",
"La clé n'a pas été fournie",
@@ -194,6 +200,7 @@ translation_array = [
"Fichier de signature manquant",
"Mnémonique",
"Mnémonique et phrase secrète seront conservés.",
+ "Les mnémoniques seront combinées par XOR avec la mnémonique actuelle (SeedXOR).",
"Modifié\u2009:",
"Native Segwit - 84 serait supposé",
"Réseau",
@@ -368,6 +375,7 @@ translation_array = [
"Numéros de mots",
"Mots",
"Oui",
+ "Vous pouvez ajouter jusqu’à %d parts",
"Mode zoomé",
"binaire\u2009:",
"echoué",
diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py
index 0b01479..6316f40 100644
--- a/src/krux/translations/ja.py
+++ b/src/krux/translations/ja.py
@@ -38,6 +38,7 @@ translation_array = [
"アカウント",
"アカウント#0は仮定されます",
"アカウントインデックス",
+ "シェアを追加",
"ウォレットのパスフレーズを追加または変更しますか?",
"%sにはカメラからの追加エントロピーが必要です",
"アドレス",
@@ -67,6 +68,7 @@ translation_array = [
"このアドレスがこのウォレットに属しているか確認しますか?",
"%d のアドレスを確認しましたが、一致するものはありませんでした.",
"SDカードを確認しています…",
+ "シェアを消去",
"改ざんチェックコードの確認",
"データムの変換",
"変更先住所を特定できませんでした.",
@@ -74,6 +76,7 @@ translation_array = [
"テキストからQRコードを作成しますか?",
"作成されました:",
"現在の改ざんチェックコード",
+ "現在のニーモニックとパスフレーズは保持されません。",
"カスタムQRコード",
"カスタムテキスト",
"カスタマイズする",
@@ -88,6 +91,7 @@ translation_array = [
"パスごとの深さ",
"導出パス",
"BIP85エントロピーを導出しますか?",
+ "シェアを導出",
"ディスクリプタアドレス",
"デバイステスト",
"ディスプレイ",
@@ -160,6 +164,8 @@ translation_array = [
"無効なウォレット:",
"反転する",
"反転した色",
+ "読み込まれたすべてのシェアを消去します",
+ "現在のキー、パスフレーズ、ディスクリプタを削除します。",
"暗号化されたKEF",
"キー",
"キーが提供されていません",
@@ -194,6 +200,7 @@ translation_array = [
"署名ファイルが欠落しています",
"Mnemonic",
"Mnemonicとパスフレーズは保持されます.",
+ "ニーモニックは現在のニーモニック(SeedXOR)とXORされます。",
"修正されました:",
"ネイティブSegwit - 84が仮定されます",
"ネットワーク",
@@ -368,6 +375,7 @@ translation_array = [
"単語番号",
"単語",
"はい",
+ "最大 %d 個のシェアを追加できます",
"ズームモード",
"バイナリ:",
"失敗",
diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py
index e08303a..d2e49b9 100644
--- a/src/krux/translations/ko.py
+++ b/src/krux/translations/ko.py
@@ -38,6 +38,7 @@ translation_array = [
"계정",
"계정 #0이 가정됩니다",
"계정 인덱스",
+ "쉐어 추가",
"패스프레이즈를 추가하거나 변경하시겠습니까?",
"%s 에 필요한 카메라의 추가 엔트로피",
"주소",
@@ -67,6 +68,7 @@ translation_array = [
"해당 주소가 이 지갑에 속하는지 확인하시겠습니까?",
"일치하는 주소가 없는 %d 개를 확인했습니다.",
"SD 카드 확인 중…",
+ "쉐어 정리",
"탬퍼 체크 코드 확인",
"날짜 변환",
"변경 주소를 확인할 수 없습니다.",
@@ -74,6 +76,7 @@ translation_array = [
"문자 메시지로 QR 코드를 만드시겠어요?",
"생성됨:",
"현재 탬퍼 체크 코드",
+ "현재 니모닉과 패스프레이즈는 유지되지 않습니다.",
"사용자 지정 QR 코드",
"사용자 텍스트",
"사용자 정의",
@@ -88,6 +91,7 @@ translation_array = [
"Depth Per Pass",
"파생 경로",
"BIP85 엔트로피를 유독하시겠습니까?",
+ "쉐어 파생",
"디스크립터 주소",
"장치 테스트",
"디스플레이",
@@ -160,6 +164,8 @@ translation_array = [
"지갑이 잘못되었습니다:",
"반전",
"반전된 색상",
+ "로드된 모든 쉐어가 삭제됩니다",
+ "현재 키, 패스프레이즈 및 디스크립터가 삭제됩니다.",
"KEF 암호화됨",
"키",
"키가 제공되지 않았습니다",
@@ -194,6 +200,7 @@ translation_array = [
"서명 파일이 누락되었습니다",
"니모닉",
"니모닉과 암호는 유지됩니다.",
+ "니모닉은 현재 니모닉(SeedXOR)과 XOR 연산됩니다.",
"수정되었습니다:",
"네이티브 세그윗 - BIP84를 적용합니다",
"네트워크",
@@ -368,6 +375,7 @@ translation_array = [
"단어 번호(1-2048)",
"시드문구",
"예",
+ "최대 %d개의 쉐어를 추가할 수 있습니다",
"확대 모드",
"바이너리:",
"실패",
diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py
index 2f6ce75..9e04f9c 100644
--- a/src/krux/translations/nl.py
+++ b/src/krux/translations/nl.py
@@ -38,6 +38,7 @@ translation_array = [
"Account",
"Account #0 zou worden aangenomen",
"Accountindex",
+ "Een aandeel toevoegen",
"Wachtwoordzin voor portemonnee toevoegen of wijzigen?",
"Extra entropie van camera vereist voor %s",
"Adres",
@@ -67,6 +68,7 @@ translation_array = [
"Controleer of dit adres bij deze portemonnee hoort?",
"%d adressen gecontroleerd zonder overeenkomsten.",
"Controleren op SD-kaart…",
+ "Aandelen opschonen",
"Bevestig de sabotagecontrolecode",
"Datum converteren",
"Kan adreswijziging niet bepalen.",
@@ -74,6 +76,7 @@ translation_array = [
"QR-code maken van tekst?",
"Aangemaakt:",
"Huidige sabotagecontrolecode",
+ "De huidige mnemonic en wachtwoordzin worden niet behouden.",
"Aangepaste QR-code",
"Aangepaste tekst",
"Aanpassen",
@@ -88,6 +91,7 @@ translation_array = [
"Diepte per pas",
"Afleidingspad",
"BIP85-entropie afleiden?",
+ "Aandelen afleiden",
"Descriptoradressen",
"Apparaattests",
"Weergave",
@@ -160,6 +164,8 @@ translation_array = [
"Ongeldige portemonnee:",
"Omkeren",
"Omgekeerde kleuren",
+ "Alle geladen aandelen worden verwijderd",
+ "De huidige sleutel, wachtwoordzin en beschrijver worden verwijderd.",
"KEF versleuteld",
"Sleutel",
"Sleutel niet verstrekt",
@@ -194,6 +200,7 @@ translation_array = [
"Handtekening bestand mist",
"Geheugensteun",
"Geheugensteun en wachtwoord worden bewaard.",
+ "De mnemonics worden met de huidige mnemonic (SeedXOR) via XOR gecombineerd.",
"Aangepast:",
"Native Segwit - 84 zal worden gebruikt",
"Netwerk",
@@ -368,6 +375,7 @@ translation_array = [
"Woord nummers",
"Woorden",
"Yes",
+ "U kunt maximaal %d aandelen toevoegen",
"Ingezoomde modus",
"binair:",
"mislukt",
diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py
index 5bd8ed4..35ce69b 100644
--- a/src/krux/translations/pt.py
+++ b/src/krux/translations/pt.py
@@ -38,6 +38,7 @@ translation_array = [
"Conta",
"A conta #0 seria assumida",
"Índice da Conta",
+ "Adicionar uma parte",
"Adicionar ou alterar a senha da carteira?",
"Entropia adicional da câmera é necessária para %s",
"Endereço",
@@ -67,6 +68,7 @@ translation_array = [
"Checar se o endereço pertence a esta carteira?",
"%d endereços checados sem correspondência.",
"Procurando por cartão SD…",
+ "Limpar partes",
"Confirmar código de verificação de integridade",
"Converter dados",
"Não foi possível determinar endereços de troco.",
@@ -74,6 +76,7 @@ translation_array = [
"Criar código QR a partir de texto?",
"Criado:",
"Código atual de verificação de integridade",
+ "A mnemônica atual e a senha não serão mantidas.",
"Código QR personalizado",
"Texto Personalizado",
"Personalizar",
@@ -88,6 +91,7 @@ translation_array = [
"Profundidade por passe",
"Caminho de Derivação",
"Derivar entropia BIP85?",
+ "Derivar partes",
"Endereços do descritor",
"Testes do Dispositivo",
"Display",
@@ -160,6 +164,8 @@ translation_array = [
"Carteira inválida:",
"Inverter",
"Cores invertidas",
+ "Todas as partes carregadas serão limpas",
+ "A chave atual, a senha e o descritor serão excluídos.",
"KEF criptografado",
"Chave",
"A chave não foi fornecida",
@@ -194,6 +200,7 @@ translation_array = [
"Arquivo de assinatura não encontrado",
"Mnemônico",
"Mnemônico e senha serão mantidos.",
+ "As mnemônicas serão XORadas com a mnemônica atual (SeedXOR).",
"Alterado:",
"Segwit nativo - 84 seria assumido",
"Rede",
@@ -368,6 +375,7 @@ translation_array = [
"Números das Palavras",
"Palavras",
"Sim",
+ "Você pode adicionar até %d partes",
"Modo ampliado",
"binário:",
"falha",
diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py
index b90a392..8d9af0b 100644
--- a/src/krux/translations/ru.py
+++ b/src/krux/translations/ru.py
@@ -38,6 +38,7 @@ translation_array = [
"Учетная запись",
"Будет принят счет №0",
"Индекс счета",
+ "Добавить долю",
"Добавить или изменить пароль кошелька?",
"Требуется дополнительная энтропия от камеры для %s",
"Адрес",
@@ -67,6 +68,7 @@ translation_array = [
"Проверить, что адрес принадлежит этому кошельку?",
"Проверено %d адресов без совпадений.",
"Проверка SD-карты…",
+ "Очистить доли",
"Подтвердите код проверки вскрытия",
"Преобразовать датум",
"Не удалось определить адрес изменения.",
@@ -74,6 +76,7 @@ translation_array = [
"Создать QR-код из текста?",
"Создано:",
"Текущий код проверки вскрытия",
+ "Текущая мнемоника и парольная фраза не будут сохранены.",
"Пользовательский QR-код",
"Произвольный текст",
"Настроить",
@@ -88,6 +91,7 @@ translation_array = [
"Глубина за Проход",
"Путь деривации",
"Вывести энтропию BIP85?",
+ "Произвести доли",
"Адреса дескрипторов",
"Испытания устройства",
"Дисплеи",
@@ -160,6 +164,8 @@ translation_array = [
"Неверный кошелек:",
"Инвертировать",
"Перевернутые цвета",
+ "Все загруженные доли будут удалены",
+ "Будут удалены текущий ключ, парольная фраза и дескриптор.",
"Зашифровано KEF",
"Ключ",
"Ключ не предоставлен",
@@ -194,6 +200,7 @@ translation_array = [
"Отсутствует файл подписи",
"Мнемоника",
"Мнемоника и парольная фраза будут сохранены.",
+ "Мнемоника(и) будет(ут) объединена(ы) по XOR с текущей мнемоникой (SeedXOR).",
"Изменено:",
"Native Segwit - 84 будет принято",
"Сеть",
@@ -368,6 +375,7 @@ translation_array = [
"Числа Слов",
"Слова",
"Да",
+ "Можно добавить до %d долей",
"Режим масштабирования",
"двоичный:",
"неудачно",
diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py
index d485662..0422469 100644
--- a/src/krux/translations/tr.py
+++ b/src/krux/translations/tr.py
@@ -38,6 +38,7 @@ translation_array = [
"Hesap",
"#0 numaralı hesap varsayılacaktır",
"Hesap Endeksi",
+ "Pay ekle",
"Cüzdan parolası eklensin mi veya değiştirilsin mi?",
"%s için kameradan gelen ek entropi gerekli",
"Adres",
@@ -67,6 +68,7 @@ translation_array = [
"Bu adresin, bu cüzdana ait olduğunu kontrol et?",
"Eşleşmeyen %d adres kontrol edildi.",
"SD kart kontrol ediliyor…",
+ "Payları temizle",
"Kurcalama Kontrol Kodunu Onayla",
"Veriyi Dönüştür",
"Değişiklik adresi belirlenemedi.",
@@ -74,6 +76,7 @@ translation_array = [
"Metinden QR kodu oluşturulsun mu?",
"Oluşturuldu:",
"Mevcut Kurcalama Kontrol Kodu",
+ "Geçerli anımsatıcı ve parola saklanmayacak.",
"Özel QR Kodu",
"Özel metin",
"Özelleştir",
@@ -88,6 +91,7 @@ translation_array = [
"Geçiş Başına Derinlik",
"Türetim Yolu",
"BIP85 entropisi türetilsin mi?",
+ "Payları türet",
"Tanımlayıcı Adresler",
"Cihaz Testleri",
"Ekran",
@@ -160,6 +164,8 @@ translation_array = [
"Geçersiz cüzdan:",
"Ters Çevir",
"Ters Renkler",
+ "Yüklenen tüm paylar temizlenecek",
+ "Geçerli Anahtar, Parola ve Tanımlayıcı silinecek.",
"Kef Şifreli",
"Anahtar",
"Anahtar sağlanmadı",
@@ -194,6 +200,7 @@ translation_array = [
"İmza dosyası eksik",
"Mnemonic",
"Mnemonik ve parola tutulacaktır.",
+ "Anımsatıcı(lar), mevcut anımsatıcı (SeedXOR) ile XOR yapılacak.",
"Değiştirildi:",
"Yerel Segwit - 84 varsayılacaktır",
"Ağ",
@@ -368,6 +375,7 @@ translation_array = [
"Kelime Numaraları",
"Kelimeler",
"Evet",
+ "%d adede kadar pay ekleyebilirsiniz",
"Zoom Mod",
"ikili:",
"Kalınan: ",
diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py
index 2eb51c6..5f496cb 100644
--- a/src/krux/translations/vi.py
+++ b/src/krux/translations/vi.py
@@ -38,6 +38,7 @@ translation_array = [
"Tài khoản",
"Tài khoản #0 sẽ được giả định",
"Chỉ mục tài khoản",
+ "Thêm phần chia sẻ",
"Thêm hoặc thay đổi cụm mật khẩu ví?",
"Entropy bổ sung từ máy ảnh cần thiết cho %s",
"Địa chỉ",
@@ -67,6 +68,7 @@ translation_array = [
"Kiểm tra địa chỉ đó có thuộc về ví này không?",
"Đã kiểm tra %d địa chỉ không khớp.",
"Đang kiểm tra thẻ SD…",
+ "Xóa phần chia sẻ",
"Xác nhận mã kiểm tra giả mạo",
"Chuyển đổi dữ liệu",
"Không thể xác định địa chỉ thay đổi.",
@@ -74,6 +76,7 @@ translation_array = [
"Tạo mã QR từ văn bản?",
"Tạo:",
"Mã kiểm tra giả mạo hiện tại",
+ "Cụm từ ghi nhớ và mật khẩu hiện tại sẽ không được giữ lại.",
"Mã QR tùy chỉnh",
"Văn bản tùy chọn",
"Tùy chỉnh",
@@ -88,6 +91,7 @@ translation_array = [
"Độ sâu mỗi lần cắt CNC",
"Đường dẫn phái sinh",
"Suy ra entropy BIP85?",
+ "Tạo phần chia sẻ",
"Địa chỉ người mô tả",
"Kiểm tra thiết bị",
"Hiển thị",
@@ -160,6 +164,8 @@ translation_array = [
"Ví không hợp lệ:",
"Đảo ngược",
"Màu đảo ngược",
+ "Tất cả các phần chia sẻ đã tải sẽ bị xóa",
+ "Khóa hiện tại, mật khẩu và mô tả sẽ bị xóa.",
"Đã mã hóa KEF",
"Chìa khóa",
"Khóa không được cung cấp",
@@ -194,6 +200,7 @@ translation_array = [
"Thiếu tập tin chữ ký",
"Mã mnemonic",
"Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.",
+ "Cụm từ ghi nhớ sẽ được XOR với cụm từ ghi nhớ hiện tại (SeedXOR).",
"Đã sửa đổi:",
"Native Segwit - 84 sẽ được giả định",
"Mạng lưới",
@@ -368,6 +375,7 @@ translation_array = [
"Từ số",
"Từ ngữ",
"Đúng",
+ "Bạn có thể thêm tối đa %d phần chia sẻ",
"Chế độ thu phóng",
"nhị phân:",
"trượt",
diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py
index babb8bd..dbd29a9 100644
--- a/src/krux/translations/zh.py
+++ b/src/krux/translations/zh.py
@@ -38,6 +38,7 @@ translation_array = [
"账户",
"将假定为账户 #0",
"账户索引",
+ "添加份额",
"添加或更改钱包密码?",
"%s需要摄像头的额外熵",
"地址",
@@ -67,6 +68,7 @@ translation_array = [
"检查该地址是否属于此钱包?",
"已检查 %d 个不匹配的地址.",
"检查卡…",
+ "清理份额",
"确认防篡改检查码",
"转换基准",
"无法确定更改地址.",
@@ -74,6 +76,7 @@ translation_array = [
"从短信创建二维码?",
"已创建:",
"当前防篡改检查码",
+ "当前助记词和密码短语将不会被保留。",
"自定义二维码",
"自定义文本",
"自定义",
@@ -88,6 +91,7 @@ translation_array = [
"每次通过的深度",
"源路径",
"导出BIP85熵?",
+ "派生份额",
"描述符地址",
"设备测试",
"显示",
@@ -160,6 +164,8 @@ translation_array = [
"无效钱包:",
"反转",
"反转颜色",
+ "将清除所有已加载的份额",
+ "将删除当前密钥、密码短语和描述符。",
"KEF加密",
"密钥",
"未提供密钥",
@@ -194,6 +200,7 @@ translation_array = [
"缺少签名文件",
"助记词",
"助记词和密码将被保留.",
+ "助记词将与当前助记词(SeedXOR)进行异或运算。",
"修改时间:",
"假定为原生 Segwit - 84",
"网络",
@@ -368,6 +375,7 @@ translation_array = [
"单词序号",
"单词",
"是",
+ "您最多可以添加 %d 个份额",
"放大模式",
"二进制:",
"失败",
diff --git a/tests/pages/home_pages/test_home.py b/tests/pages/home_pages/test_home.py
index 79a3ef3..2ee00ea 100644
--- a/tests/pages/home_pages/test_home.py
+++ b/tests/pages/home_pages/test_home.py
@@ -31,6 +31,28 @@ def tdata(mocker):
SIGNING_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
ACTION_MNEMONIC = "action action action action action action action action action action action action"
+ TEST_XOR_24_WORD_MNEMONIC_1 = "romance wink lottery autumn shop bring dawn tongue range crater truth ability miss spice fitness easy legal release recall obey exchange recycle dragon room"
+ TEST_XOR_24_WORD_MNEMONIC_2 = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge"
+ TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT = "defy island room gas rookie easily blame travel school excess egg unable since milk mother grace rocket case fence photo decorate idle junior cross"
+ TEST_XOR_24_WORD_MNEMONIC_3 = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate"
+ TEST_XOR_24_WORD_MNEMONIC_RESULT = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor"
+
+ TEST_XOR_12_WORD_MNEMONIC_1 = (
+ "romance wink lottery autumn shop bring dawn tongue range crater truth ability"
+ )
+ TEST_XOR_12_WORD_MNEMONIC_2 = (
+ "boat unfair shell violin tree robust open ride visual forest vintage approve"
+ )
+ TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT = (
+ "person bitter door winner candy polar proud fringe early have bulb apple"
+ )
+ TEST_XOR_12_WORD_MNEMONIC_3 = (
+ "lion misery divide hurry latin fluid camp advance illegal lab pyramid unhappy"
+ )
+ TEST_XOR_12_WORD_MNEMONIC_RESULT = (
+ "cannon opinion leader nephew found yard metal galaxy crouch between real trade"
+ )
+
SINGLESIG_12_WORD_KEY = Key(TEST_12_WORD_MNEMONIC, TYPE_SINGLESIG, NETWORKS["main"])
SINGLESIG_24_WORD_KEY = Key(TEST_24_WORD_MNEMONIC, TYPE_SINGLESIG, NETWORKS["main"])
MULTISIG_12_WORD_KEY = Key(
@@ -135,6 +157,16 @@ def tdata(mocker):
[
"TEST_12_WORD_MNEMONIC",
"TEST_24_WORD_MNEMONIC",
+ "TEST_XOR_12_WORD_MNEMONIC_1",
+ "TEST_XOR_12_WORD_MNEMONIC_2",
+ "TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT",
+ "TEST_XOR_12_WORD_MNEMONIC_3",
+ "TEST_XOR_12_WORD_MNEMONIC_RESULT",
+ "TEST_XOR_24_WORD_MNEMONIC_1",
+ "TEST_XOR_24_WORD_MNEMONIC_2",
+ "TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT",
+ "TEST_XOR_24_WORD_MNEMONIC_3",
+ "TEST_XOR_24_WORD_MNEMONIC_RESULT",
"SIGNING_MNEMONIC",
"SINGLESIG_12_WORD_KEY",
"SINGLESIG_24_WORD_KEY",
@@ -180,6 +212,16 @@ def tdata(mocker):
)(
TEST_12_WORD_MNEMONIC,
TEST_24_WORD_MNEMONIC,
+ TEST_XOR_12_WORD_MNEMONIC_1,
+ TEST_XOR_12_WORD_MNEMONIC_2,
+ TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ TEST_XOR_12_WORD_MNEMONIC_3,
+ TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ TEST_XOR_24_WORD_MNEMONIC_1,
+ TEST_XOR_24_WORD_MNEMONIC_2,
+ TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ TEST_XOR_24_WORD_MNEMONIC_3,
+ TEST_XOR_24_WORD_MNEMONIC_RESULT,
SIGNING_MNEMONIC,
SINGLESIG_12_WORD_KEY,
SINGLESIG_24_WORD_KEY,
@@ -466,7 +508,7 @@ def test_load_bip85_from_wallet_menu(mocker, amigo, tdata):
BUTTON_ENTER, # Load words
BUTTON_PAGE_PREV, # Move to "< Back"
BUTTON_ENTER, # Leave BIP85
- BUTTON_PAGE, # Move to "Back"
+ *([BUTTON_PAGE] * 2), # Move to "Back"
BUTTON_ENTER, # Exit
]
@@ -481,6 +523,65 @@ def test_load_bip85_from_wallet_menu(mocker, amigo, tdata):
assert ctx.wallet.key.fingerprint_hex_str() == INDEX_1_B85_FINGERPRINT
+def test_load_xor_not_derive(mocker, amigo, tdata):
+ from krux.pages.home_pages.home import Home
+ from embit.networks import NETWORKS
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BUTTON_SEQUENCE = [
+ *([BUTTON_PAGE] * 4),
+ BUTTON_ENTER,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ ]
+
+ key = Key(tdata.TEST_XOR_24_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, BUTTON_SEQUENCE, wallet)
+ home = Home(ctx)
+ home.wallet()
+
+ assert ctx.wallet.key.fingerprint.hex() == "e51c20a3"
+ assert ctx.input.wait_for_button.call_count == len(BUTTON_SEQUENCE)
+
+
+def test_load_xor_from_wallet_menu(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.home import Home
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = [
+ *([BUTTON_PAGE] * 4), # Go to Mnemonic XOR
+ BUTTON_ENTER, # Enter Mnemonic XOR
+ BUTTON_ENTER, # Accept Derive
+ BUTTON_PAGE_PREV, # Go to back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE, # Move to Back
+ BUTTON_ENTER, # Press Back
+ ]
+
+ key = Key(
+ tdata.TEST_XOR_24_WORD_MNEMONIC_1,
+ TYPE_SINGLESIG,
+ NETWORKS["test"],
+ )
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, BTN_SEQUENCE, wallet=wallet)
+ home = Home(ctx)
+ home.wallet()
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert ctx.wallet.key.fingerprint_hex_str() == "e51c20a3"
+
+
def test_load_address_view(mocker, amigo, tdata):
from krux.pages.home_pages.home import Home
from krux.wallet import Wallet
diff --git a/tests/pages/home_pages/test_mnemonic_xor.py b/tests/pages/home_pages/test_mnemonic_xor.py
new file mode 100644
index 0000000..d6d4e8a
--- /dev/null
+++ b/tests/pages/home_pages/test_mnemonic_xor.py
@@ -0,0 +1,697 @@
+import pytest
+from .test_home import tdata
+from .. import create_ctx
+
+
+def test_xor_bytes(mocker, m5stickv):
+ from src.krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+
+ # bytes_0 XOR bytest_1 = result_bytes
+ cases = [
+ # Basic cases for single bytes
+ (b"\x00", b"\x00", b"\x00"),
+ (b"\x00", b"\x01", b"\x01"),
+ (b"\x00", b"\x10", b"\x10"),
+ (b"\x10", b"\x10", b"\x00"),
+ (b"\x11", b"\x10", b"\x01"),
+ (b"\x11", b"\x01", b"\x10"),
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
+ # A = 5DC 7DE 420 07D 635 0E1 1BF 723 58E 194 74E 001 46E 68C 2BF 22E 3FB 5A9 59B 4BF 275 59F 210 5DF
+ # B = 411 46D 1FF 37D 3EB 2CD 106 01F 388 3E0 578 763 227 2E8 63E 105 620 4B0 733 1A2 3BD 61A 1D9 422
+ # C = 78E 4AF 18F 644 4F0 2EC 70A 3FA 100 59B 6EB 7EE 2F5 6D1 63C 52F 573 169 219 2AC 625 6FB 69C 234
+ # X = 643 71C 450 544 12E 0C0 7B3 4C6 706 7EF 4DD 08C 4BC 2B5 2BD 604 0A8 070 0B1 7B1 7ED 57E 555 3xx
+ # final word between: gas [300] - lend [3FF]
+ # correct final word: indoor [398]
+ # We had 3C9, is between the final but isn't one from CC docs
+ (
+ b"\x05\xdc\x07\xde\x04\x20\x00\x7d\x06\x35\x00\xe1\x01\xbf\x07\x23\x05\x8e\x01\x94\x07\x4e\x00\x01\x04\x6e\x06\x8c\x02\xbf\x02\x2e\x03\xfb\x05\xa9\x05\x9b\x04\xbf\x02\x75\x05\x9f\x02\x10\x05\xdf",
+ b"\x04\x11\x04\x6d\x01\xff\x03\x7d\x03\xeb\x02\xcd\x01\x06\x00\x1f\x03\x88\x03\xe0\x05\x78\x07\x63\x02\x27\x02\xe8\x06\x3e\x01\x05\x06\x20\x04\xb0\x07\x33\x01\xa2\x03\xbd\x06\x1a\x01\xd9\x04\x22",
+ b"\x01\xcd\x03\xb3\x05\xdf\x03\x00\x05\xde\x02\x2c\x00\xb9\x07\x3c\x06\x06\x02\x74\x02\x36\x07\x62\x06\x49\x04\x64\x04\x81\x03\x2b\x05\xdb\x01\x19\x02\xa8\x05\x1d\x01\xc8\x03\x85\x03\xc9\x01\xfd",
+ ),
+ (
+ b"\x01\xcd\x03\xb3\x05\xdf\x03\x00\x05\xde\x02\x2c\x00\xb9\x07\x3c\x06\x06\x02\x74\x02\x36\x07\x62\x06\x49\x04\x64\x04\x81\x03\x2b\x05\xdb\x01\x19\x02\xa8\x05\x1d\x01\xc8\x03\x85\x03\xc9\x01\xfd",
+ b"\x07\x8e\x04\xaf\x01\x8f\x06\x44\x04\xf0\x02\xec\x07\x0a\x03\xfa\x01\x00\x05\x9b\x06\xeb\x07\xee\x02\xf5\x06\xd1\x06\x3c\x05\x2f\x05\x73\x01\x69\x02\x19\x02\xac\x06\x25\x06\xfb\x06\x9c\x02\x34",
+ b"\x06\x43\x07\x1c\x04\x50\x05\x44\x01\x2e\x00\xc0\x07\xb3\x04\xc6\x07\x06\x07\xef\x04\xdd\x00\x8c\x04\xbc\x02\xb5\x02\xbd\x06\x04\x00\xa8\x00\x70\x00\xb1\x07\xb1\x07\xed\x05\x7e\x05\x55\x03\xc9",
+ ),
+ ]
+
+ n = 0
+ for case in cases:
+ print(f"Case: {n}")
+ assert MnemonicXOR._xor_bytes(case[0], case[1]) == case[2]
+ n += 1
+
+
+def test_fail_xor_bytes_different_lengths(mocker, m5stickv):
+ from src.krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+
+ a = b"\x05\xdc\x07\xde\x04\x20\x00\x7d\x06\x35\x00\xe1\x01\xbf\x07\x23\x05\x8e\x01\x94\x07\x4e\x00\x01"
+ b = b"\x04\x11\x04\x6d\x01\xff\x03\x7d\x03\xeb\x02\xcd\x01\x06\x00\x1f\x03\x88\x03\xe0\x05\x78\x07\x63\x02\x27\x02\xe8\x06\x3e\x01\x05\x06\x20\x04\xb0\x07\x33\x01\xa2\x03\xbd\x06\x1a\x01\xd9\x04\x22"
+
+ with pytest.raises(ValueError) as exc:
+ MnemonicXOR._xor_bytes(a, b)
+
+ assert str(exc.value) == "Sequences should have same length"
+
+ with pytest.raises(ValueError) as exc:
+ MnemonicXOR._xor_bytes(b, a)
+
+ assert str(exc.value) == "Sequences should have same length"
+
+
+def test_xor_with_current_mnemonic(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+
+ cases = [
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#12-words-xor-seed-example-using-3-parts
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ ),
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#24-words-xor-seed-example-using-3-parts
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_RESULT,
+ ),
+ ]
+
+ n = 0
+ for case in cases:
+ print(f"Case: {n}")
+ key = Key(case[0], TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case, wallet)
+ m = MnemonicXOR(ctx)
+ assert m.xor_with_current_mnemonic(case[1]) == case[2]
+ n += 1
+
+
+def test_fail_xor_mnemonics_different_lengths(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+
+ cases = [
+ (tdata.TEST_XOR_12_WORD_MNEMONIC_1, tdata.TEST_XOR_24_WORD_MNEMONIC_1),
+ (tdata.TEST_XOR_12_WORD_MNEMONIC_2, tdata.TEST_XOR_24_WORD_MNEMONIC_2),
+ (tdata.TEST_XOR_12_WORD_MNEMONIC_3, tdata.TEST_XOR_24_WORD_MNEMONIC_3),
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_RESULT,
+ ),
+ ]
+
+ for case in cases:
+ with pytest.raises(ValueError) as exc:
+ key = Key(case[0], TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case, wallet)
+ m = MnemonicXOR(ctx)
+ m.xor_with_current_mnemonic(case[1])
+
+ assert str(exc.value) == "Mnemonics should have same length"
+
+
+def test_menu_load_and_back(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ cases = [
+ (
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ ),
+ (
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE_PREV, # Move to "Back"
+ BUTTON_ENTER, # Press "Back"
+ ),
+ (
+ BUTTON_PAGE, # Move to "Via manual input"
+ BUTTON_ENTER, # Press "Via manual input"
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ *([BUTTON_PAGE_PREV] * 2), # Move to "Back"
+ BUTTON_ENTER, # Press "Back"
+ ),
+ (
+ *([BUTTON_PAGE] * 2), # Move to "Via storage"
+ BUTTON_ENTER, # Press "Via storage
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE, # Move to "Back"
+ BUTTON_ENTER, # Press "Back"
+ ),
+ ]
+
+ n = 0
+ for case in cases:
+ print(f"Case {n}")
+ key = Key(tdata.TEST_XOR_12_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case, wallet)
+ m = MnemonicXOR(ctx)
+ m.load()
+
+ assert ctx.wallet.key.fingerprint.hex() == "a70e2c26"
+ assert ctx.input.wait_for_button.call_count == len(case)
+ n += 1
+
+
+def test_menu_load_qrcode_and_back(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+
+ cases = [
+ # Not accept the part
+ (
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_PAGE_PREV, # Move to "No"
+ BUTTON_ENTER, # Press "No"
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE_PREV, # Move to Back
+ BUTTON_ENTER, # Press Back
+ ),
+ # Load the part, but not accept the fingerprint
+ (
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes"
+ BUTTON_PAGE_PREV, # Move to "No"
+ BUTTON_ENTER, # Press "No"
+ BUTTON_PAGE_PREV, # Move to back
+ BUTTON_ENTER, # Press back
+ BUTTON_PAGE_PREV, # Move to back
+ BUTTON_ENTER, # Press back
+ ),
+ ]
+
+ n = 0
+
+ for case in cases:
+ print(f"Case {n}")
+ key = Key(tdata.TEST_XOR_12_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case, wallet)
+
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (tdata.TEST_XOR_12_WORD_MNEMONIC_2, FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+
+ assert ctx.wallet.key.fingerprint.hex() == "a70e2c26"
+ assert ctx.input.wait_for_button.call_count == len(case)
+ n += 1
+
+
+def test_load_from_qrcode(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+
+ cases = [
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#12-words-xor-seed-example-using-3-parts
+ # Via camera, QRCode, XOR 12, 1st XOR 2nd shares
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("a70e2c26", "d9987b75"),
+ ),
+ # Via camera, QRCode, XOR 12, (1st XOR 2nd) XOR 3rd shares
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ ),
+ ("d9987b75", "60259e7d"),
+ ),
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#24-words-xor-seed-example-using-3-parts
+ # Via camera, QRCode, XOR 24, 1st XOR 2nd
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("e51c20a3", "0849dc5e"),
+ ),
+ # Via camera, QRCode, XOR 24, (1st XOR 2nd) XOR 3rd
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_RESULT,
+ ),
+ ("0849dc5e", "e335e9c4"),
+ ),
+ ]
+
+ n = 0
+
+ for case in cases:
+ print(f"Case {n}")
+ key = Key(case[1][0], TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case[0], wallet)
+
+ assert ctx.wallet.key.mnemonic == case[1][0]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][0]
+
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (case[1][1], FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+
+ assert ctx.wallet.key.mnemonic == case[1][2]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][1]
+ assert ctx.input.wait_for_button.call_count == len(case[0])
+ n += 1
+
+
+def test_load_from_qrcode_with_hide_mnemonic(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.krux_settings import Settings
+
+ cases = [
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#12-words-xor-seed-example-using-3-parts
+ # Via camera, QRCode, XOR 12, 1st XOR 2nd shares
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("a70e2c26", "d9987b75"),
+ ),
+ # Via camera, QRCode, XOR 12, (1st XOR 2nd) XOR 3rd shares
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ ),
+ ("d9987b75", "60259e7d"),
+ ),
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#24-words-xor-seed-example-using-3-parts
+ # Via camera, QRCode, XOR 24, 1st XOR 2nd
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("e51c20a3", "0849dc5e"),
+ ),
+ # Via camera, QRCode, XOR 24, (1st XOR 2nd) XOR 3rd
+ (
+ [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_RESULT,
+ ),
+ ("0849dc5e", "e335e9c4"),
+ ),
+ ]
+
+ n = 0
+
+ for case in cases:
+ print(f"Case {n}")
+ Settings().security.hide_mnemonic = True
+ key = Key(case[1][0], TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case[0], wallet)
+
+ assert ctx.wallet.key.mnemonic == case[1][0]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][0]
+
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (case[1][1], FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+
+ assert ctx.wallet.key.mnemonic == case[1][2]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][1]
+ assert ctx.input.wait_for_button.call_count == len(case[0])
+ n += 1
+
+
+def test_export_from_words(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.qr import FORMAT_NONE
+
+ cases = [
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#12-words-xor-seed-example-using-3-parts
+ # Via manual input/words, QRCode, XOR 12, 1st XOR 2nd shares
+ (
+ [
+ BUTTON_PAGE, # Move to "Via Manual Input"
+ BUTTON_ENTER, # Press "Via Manual Input"
+ BUTTON_ENTER, # Words
+ BUTTON_ENTER, # Press "Yes" for "Enter each word of your BIP39 mnemonic"
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_ENTER, # Done "Yes"
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("a70e2c26", "d9987b75"),
+ ),
+ # Via manual input/words, QRCode, XOR 12, (1st XOR 2nd) XOR 3rd shares
+ (
+ [
+ BUTTON_PAGE, # Move to "Via Manual Input"
+ BUTTON_ENTER, # Press "Via Manual Input"
+ BUTTON_ENTER, # Words
+ BUTTON_ENTER, # Press "Yes" for "Enter each word of your BIP39 mnemonic"
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_ENTER, # Done "Yes"
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_12_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_12_WORD_MNEMONIC_RESULT,
+ ),
+ ("d9987b75", "60259e7d"),
+ ),
+ # Case from https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md#24-words-xor-seed-example-using-3-parts
+ # Via manual input/words, QRCode, XOR 24, 1st XOR 2nd shares
+ (
+ [
+ BUTTON_PAGE, # Move to "Via Manual Input"
+ BUTTON_ENTER, # Press "Via Manual Input"
+ BUTTON_ENTER, # Words
+ BUTTON_ENTER, # Press "Yes" for "Enter each word of your BIP39 mnemonic"
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_PAGE_PREV, # Move to "No" (we do not finished)
+ BUTTON_ENTER, # Press "No",
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_1,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_2,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ ),
+ ("e51c20a3", "0849dc5e"),
+ ),
+ # Via manual input/words, QRCode, XOR 24, (1st XOR 2nd) XOR 3rd shares
+ (
+ [
+ BUTTON_PAGE, # Move to "Via Manual Input"
+ BUTTON_ENTER, # Press "Via Manual Input"
+ BUTTON_ENTER, # Words
+ BUTTON_ENTER, # Press "Yes" for "Enter each word of your BIP39 mnemonic"
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_PAGE_PREV, # Move to "No" (we do not finished)
+ BUTTON_ENTER, # Press "No",
+ *([BUTTON_ENTER] * 12), # Accept each word
+ BUTTON_ENTER, # Press "Yes" to accept part words
+ BUTTON_ENTER, # Press "Yes" to Proceed after see fingerprints
+ BUTTON_ENTER, # Press "Yes" to accept XORed words
+ ],
+ (
+ tdata.TEST_XOR_24_WORD_MNEMONIC_INTERMEDIARY_RESULT,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_3,
+ tdata.TEST_XOR_24_WORD_MNEMONIC_RESULT,
+ ),
+ ("0849dc5e", "e335e9c4"),
+ ),
+ ]
+
+ n = 0
+ for case in cases:
+ print(f"Case {n}")
+ key = Key(case[1][0], TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, case[0], wallet)
+
+ assert ctx.wallet.key.mnemonic == case[1][0]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][0]
+ words = case[1][1].split(" ")
+
+ m = MnemonicXOR(ctx)
+ mocker.patch.object(m, "capture_from_keypad", side_effect=words)
+ mocker.spy(m, "xor_with_current_mnemonic")
+ m.load()
+
+ m.xor_with_current_mnemonic.assert_called_once_with(case[1][1])
+ assert ctx.wallet.key.mnemonic == case[1][2]
+ assert ctx.wallet.key.fingerprint.hex() == case[2][1]
+ assert ctx.input.wait_for_button.call_count == len(case[0])
+ n += 1
+
+
+def test_export_xor_to_same_mnemonic_from_qrcode(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+
+ BTN_SEQUENCE = [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCode
+ BUTTON_ENTER, # Press "Yes" to accept part words (will raise error)
+ *([BUTTON_PAGE] * 5), # Move to back
+ BUTTON_ENTER, # Press back
+ *([BUTTON_PAGE] * 3), # Move to back
+ BUTTON_ENTER, # Press back
+ ]
+
+ key = Key(tdata.TEST_XOR_12_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, BTN_SEQUENCE, wallet)
+
+ assert ctx.wallet.key.mnemonic == tdata.TEST_XOR_12_WORD_MNEMONIC_1
+ assert ctx.wallet.key.fingerprint.hex() == "a70e2c26"
+
+ mocker.spy(ctx.display, "draw_centered_text")
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (tdata.SIGNING_MNEMONIC, FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+ ctx.display.draw_centered_text.assert_has_calls(
+ [mocker.call("Error:\nValueError('Low entropy mnemonic')", 248)],
+ any_order=True,
+ )
+
+
+def test_export_xor_to_inverted_mnemonic_from_qrcode(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+
+ ZOO = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
+ BTN_SEQUENCE = [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCodeCapture
+ BUTTON_ENTER, # Press "Yes" to accept part words (will raise error)
+ *([BUTTON_PAGE] * 5), # Move to back
+ BUTTON_ENTER, # Press back
+ *([BUTTON_PAGE] * 3), # Move to back
+ BUTTON_ENTER, # Press back
+ ]
+
+ key = Key(tdata.TEST_XOR_12_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, BTN_SEQUENCE, wallet)
+
+ assert ctx.wallet.key.mnemonic == tdata.TEST_XOR_12_WORD_MNEMONIC_1
+ assert ctx.wallet.key.fingerprint.hex() == "a70e2c26"
+
+ mocker.spy(ctx.display, "draw_centered_text")
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (ZOO, FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+ ctx.display.draw_centered_text.assert_has_calls(
+ [mocker.call("Error:\nValueError('Low entropy mnemonic')", 248)],
+ any_order=True,
+ )
+
+
+def test_export_xor_low_entropy_mnemonic_from_qrcode(mocker, amigo, tdata):
+ from embit.networks import NETWORKS
+ from krux.pages.home_pages.mnemonic_xor import MnemonicXOR
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.qr import FORMAT_NONE
+ from krux.pages.qr_capture import QRCodeCapture
+
+ DANGEROUS = (
+ "dutch aerobic know utility deer toilet siege breeze evolve sniff bike wrap"
+ )
+ BTN_SEQUENCE = [
+ BUTTON_ENTER, # Press "Via camera"
+ BUTTON_ENTER, # QRCodeCapture
+ BUTTON_ENTER, # Press "Yes" to accept part words (will raise error)
+ *([BUTTON_PAGE] * 5), # Move to back
+ BUTTON_ENTER, # Press back
+ *([BUTTON_PAGE] * 3), # Move to back
+ BUTTON_ENTER, # Press back
+ ]
+
+ key = Key(tdata.TEST_XOR_12_WORD_MNEMONIC_1, TYPE_SINGLESIG, NETWORKS["test"])
+ wallet = Wallet(key)
+ ctx = create_ctx(mocker, BTN_SEQUENCE, wallet)
+
+ assert ctx.wallet.key.mnemonic == tdata.TEST_XOR_12_WORD_MNEMONIC_1
+ assert ctx.wallet.key.fingerprint.hex() == "a70e2c26"
+
+ mocker.spy(ctx.display, "draw_centered_text")
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (DANGEROUS, FORMAT_NONE),
+ )
+
+ m = MnemonicXOR(ctx)
+ m.load()
+ ctx.display.draw_centered_text.assert_has_calls(
+ [mocker.call("Error:\nValueError('Low entropy mnemonic')", 248)],
+ any_order=True,
+ )
Why this scored 28/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.