From 188628ed5184e28084a3d09fd47a56a421d1b812 Mon Sep 17 00:00:00 2001 From: CaverneCrypto <150951860+CaverneCrypto@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:49:31 +0100 Subject: [PATCH 1/2] feat: add Kurihara n-of-m threshold XOR secret-sharing core Adds the pure, UI-less core of an n-of-m threshold backup for BIP39 mnemonics (Kurihara et al., ISC 2008). Any n of m shares reconstruct the secret; strictly fewer than n reveal nothing (information-theoretic confidentiality up to the threshold). Each share is the size of the secret and re-encodes to a valid BIP39 mnemonic -- a true threshold generalization of the existing m-of-m Mnemonic XOR (SeedXOR), which has no loss tolerance. - src/krux/kurihara.py: scheme (split / reconstruct / regenerate a lost share) via Gaussian elimination over GF(2); no hardware dependency (randomness injected; embit imported lazily for the BIP39 glue). - tests/test_kurihara.py: 20 tests, 100% coverage of the new module. UI integration into the existing Mnemonic XOR page is intended as a follow-up so the security-critical math can be reviewed in isolation. Reference: https://doi.org/10.5281/zenodo.20734041 (CC BY 4.0) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/krux/kurihara.py | 327 +++++++++++++++++++++++++++++++++++++++++ tests/test_kurihara.py | 215 +++++++++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 src/krux/kurihara.py create mode 100644 tests/test_kurihara.py diff --git a/src/krux/kurihara.py b/src/krux/kurihara.py new file mode 100644 index 000000000..7fb18534a --- /dev/null +++ b/src/krux/kurihara.py @@ -0,0 +1,327 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2026 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. +""" +Kurihara ideal (k, n)-threshold XOR secret sharing. + +Reference scheme: J. Kurihara, S. Kiyomoto, K. Fukushima, T. Tanaka, +"A New (k, n)-Threshold Secret Sharing Scheme and Its Extension", ISC 2008, +https://eprint.iacr.org/2008/409 + +BIP39 adaptation, pen-and-paper derivation, security-property verification and +ready-to-print templates: CaverneCrypto, "Bitcoin Mnemonic Backup via Threshold +XOR (n-of-m): A pen-and-paper adaptation of the Kurihara scheme to BIP39", 2026, +https://doi.org/10.5281/zenodo.20734041 (CC BY 4.0). + +This is the pure, UI-less core of an n-of-m threshold backup. It splits a secret +(BIP39 entropy) into m shares such that any n of them reconstruct it, while any +n - 1 reveal nothing (perfect, information-theoretic confidentiality up to the +threshold). Each share is exactly the size of the secret, so it re-encodes to a +valid BIP39 mnemonic of the same word count -- a true threshold generalization +of Krux's existing m-of-m "Mnemonic XOR" (SeedXOR), which has no loss tolerance. + +Only XOR is used; there is no finite-field arithmetic beyond GF(2). The module +has no hardware dependency: the randomness source is injected into generate(), +and embit is imported lazily only for the BIP39 glue helpers. + +Construction (with fragment 0 fixed to zero by convention): + + W[i][pos] = XOR over t of R[t][(t*i + pos) mod prime] + XOR fragment[(pos - i) mod prime] + +where prime is the smallest prime >= m, the fragments are the prime-1 pieces of +the secret, and the R[t] are (n-1) sets of prime uniform random values. +""" + +from binascii import hexlify + +# Standard BIP39 entropy sizes, in bits +VALID_ENTROPY_BITS = (128, 160, 192, 224, 256) + + +def xor_bytes(*chunks): + """XOR several byte sequences of equal length""" + out = bytearray(len(chunks[0])) + for chunk in chunks: + for i in range(len(chunk)): + out[i] ^= chunk[i] + return bytes(out) + + +def is_prime(num): + """Return True if num is a prime number""" + if num < 2: + return False + if num < 4: + return True + if num % 2 == 0: + return False + i = 3 + while i * i <= num: + if num % i == 0: + return False + i += 2 + return True + + +def smallest_prime_gte(num): + """Return the smallest prime greater than or equal to num""" + candidate = num if num > 2 else 2 + while not is_prime(candidate): + candidate += 1 + return candidate + + +class Share: + """One share of the split. + + part_id: 1-based share number (S_1, S_2, ...). + pieces: list of prime-1 byte chunks of d_bytes each. + meta: dict of instance parameters, shared across sibling shares. + """ + + def __init__(self, part_id, pieces, meta): + self.part_id = part_id + self.pieces = pieces + self.meta = meta + + def to_bytes(self): + """Concatenated pieces: exactly num_bits, a valid BIP39 entropy""" + return b"".join(self.pieces) + + +class KuriharaScheme: + """Kurihara n-of-m ideal threshold XOR secret-sharing scheme""" + + def __init__(self, n, m, num_bits): + if not 2 <= n <= m: + raise ValueError("Need 2 <= n <= m") + if num_bits not in VALID_ENTROPY_BITS: + raise ValueError("Not a standard BIP39 entropy size") + self.n = n + self.m = m + self.num_bits = num_bits + self.prime = smallest_prime_gte(m) + self.num_fragments = self.prime - 1 + if ( + num_bits % self.num_fragments != 0 + or (num_bits // self.num_fragments) % 8 != 0 + ): + raise ValueError("num_bits does not split into byte-aligned fragments") + self.d_bytes = num_bits // self.num_fragments // 8 + + def _split_secret(self, secret): + """Split the secret into prime-1 fragments; fragment 0 is zero""" + zero = bytes(self.d_bytes) + fragments = [zero] + for j in range(self.num_fragments): + fragments.append(secret[j * self.d_bytes : (j + 1) * self.d_bytes]) + return fragments + + def _gen_random(self, randfunc): + """Draw (n-1) sets of prime random values""" + return [ + [randfunc(self.d_bytes) for _ in range(self.prime)] + for _ in range(self.n - 1) + ] + + def _piece(self, i, pos, fragments, randoms): + """Compute the piece W[i][pos] from the Kurihara formula""" + terms = [] + for t in range(self.n - 1): + terms.append(randoms[t][(t * i + pos) % self.prime]) + terms.append(fragments[(pos - i) % self.prime]) + return xor_bytes(*terms) + + def generate(self, secret, randfunc): + """Build m shares with threshold n. + + secret: num_bits / 8 bytes of entropy to protect. + randfunc: callable(nbytes) -> bytes, the injected randomness source + (e.g. the device TRNG, or dice). Required so the core stays free of any + hardware dependency and stays deterministic under test. + """ + if len(secret) != self.num_bits // 8: + raise ValueError("Secret must be %d bytes" % (self.num_bits // 8)) + fragments = self._split_secret(secret) + randoms = self._gen_random(randfunc) + instance = hexlify(randfunc(4)).decode() + meta = { + "instance": instance, + "n": self.n, + "m": self.m, + "num_bits": self.num_bits, + "prime": self.prime, + } + shares = [] + for i in range(self.m): + pieces = [ + self._piece(i, pos, fragments, randoms) + for pos in range(self.num_fragments) + ] + shares.append(Share(i + 1, pieces, dict(meta))) + return shares + + def reconstruct(self, shares): + """Reconstruct the secret from at least n shares""" + self._check_coalition(shares) + fragments, _ = self._solve([(s.part_id - 1, s.pieces) for s in shares]) + out = [] + for j in range(1, self.prime): + out.append(fragments[j]) + return b"".join(out) + + def reconstruct_lost(self, shares, lost_id): + """Regenerate the lost share S_{lost_id} (1-based) from n others. + + The result is bit-for-bit identical to the original share thanks to the + shift symmetry of the random values: any choice for the residual free + variables yields the same recomputed pieces. + """ + self._check_coalition(shares) + if not 1 <= lost_id <= self.m: + raise ValueError("Share id out of range") + i_lost = lost_id - 1 + for s in shares: + if s.part_id - 1 == i_lost: + raise ValueError("Lost share is already in the coalition") + fragments, randoms = self._solve([(s.part_id - 1, s.pieces) for s in shares]) + pieces = [ + self._piece(i_lost, pos, fragments, randoms) + for pos in range(self.num_fragments) + ] + return Share(lost_id, pieces, dict(shares[0].meta)) + + def _check_coalition(self, shares): + """Ensure enough shares, all from the same instance""" + if len(shares) < self.n: + raise ValueError("Need %d shares" % self.n) + ref = shares[0].meta.get("instance") + for s in shares[1:]: + if s.meta.get("instance") != ref: + raise ValueError("Shares come from different instances") + + def _solve(self, observed): + """Solve the Kurihara linear system for the observed pieces. + + Each piece is one linear equation over GF(2^d). The GF(2) coefficient + row is a set of active column indices (XOR of two rows = symmetric + difference), which keeps the elimination free of big-integer bitmasks + and portable to MicroPython. + + Column layout: + R[t][ell] -> t * prime + ell + fragment[j] -> num_r + (j - 1) (fragment 0 is absorbed) + + Returns (fragments, randoms). Structurally free variables stay zero; + this affects neither the fragments nor any regenerated share piece. + """ + num_r = (self.n - 1) * self.prime + num_vars = num_r + self.num_fragments + + equations = [] + for i, pieces in observed: + for pos in range(self.num_fragments): + cols = set() + for t in range(self.n - 1): + cols.add(t * self.prime + (t * i + pos) % self.prime) + k_idx = (pos - i) % self.prime + if k_idx != 0: + cols.add(num_r + (k_idx - 1)) + equations.append([cols, pieces[pos]]) + + pivot_of = {} + used = set() + for col in range(num_vars): + pivot = None + for row in range(len(equations)): + if row not in used and col in equations[row][0]: + pivot = row + break + if pivot is None: + continue + pivot_of[col] = pivot + used.add(pivot) + p_cols, p_val = equations[pivot] + for row in range(len(equations)): + if row != pivot and col in equations[row][0]: + equations[row][0] = equations[row][0] ^ p_cols + equations[row][1] = xor_bytes(equations[row][1], p_val) + + zero = bytes(self.d_bytes) + fragments = [zero] * self.prime + for j in range(1, self.prime): + col = num_r + (j - 1) + if col not in pivot_of: + raise ValueError("Fragment undetermined: not enough shares?") + fragments[j] = equations[pivot_of[col]][1] + + randoms = [[zero] * self.prime for _ in range(self.n - 1)] + for t in range(self.n - 1): + for ell in range(self.prime): + col = t * self.prime + ell + if col in pivot_of: + randoms[t][ell] = equations[pivot_of[col]][1] + + return fragments, randoms + + +# BIP39 glue. A share's concatenated pieces are exactly num_bits, i.e. a +# standard BIP39 entropy, so each share re-encodes to a valid mnemonic of the +# same word count. embit is imported lazily so the core above stays dependency +# free (mirrors krux.pages.home_pages.mnemonic_xor). + + +def entropy_to_mnemonic(entropy): + """Encode BIP39 entropy bytes as a mnemonic""" + from embit.bip39 import mnemonic_from_bytes + + return mnemonic_from_bytes(entropy) + + +def mnemonic_to_entropy(mnemonic): + """Decode a BIP39 mnemonic to its entropy bytes""" + from embit.bip39 import mnemonic_to_bytes + + return mnemonic_to_bytes(mnemonic.strip()) + + +def share_to_mnemonic(share): + """Encode a share as a BIP39 mnemonic (its pieces, concatenated)""" + return entropy_to_mnemonic(share.to_bytes()) + + +def share_from_mnemonic(part_id, mnemonic, n, m, num_bits, instance="input"): + """Decode a BIP39 mnemonic into a Share for the given parameters""" + entropy = mnemonic_to_entropy(mnemonic) + if len(entropy) * 8 != num_bits: + raise ValueError("Mnemonic entropy size does not match num_bits") + prime = smallest_prime_gte(m) + d_bytes = num_bits // (prime - 1) // 8 + pieces = [entropy[pos * d_bytes : (pos + 1) * d_bytes] for pos in range(prime - 1)] + meta = { + "instance": instance, + "n": n, + "m": m, + "num_bits": num_bits, + "prime": prime, + } + return Share(part_id, pieces, meta) diff --git a/tests/test_kurihara.py b/tests/test_kurihara.py new file mode 100644 index 000000000..149e4ab72 --- /dev/null +++ b/tests/test_kurihara.py @@ -0,0 +1,215 @@ +import itertools +import pytest +from src.krux.kurihara import ( + KuriharaScheme, + xor_bytes, + is_prime, + smallest_prime_gte, + entropy_to_mnemonic, + mnemonic_to_entropy, + share_to_mnemonic, + share_from_mnemonic, +) + +# Profiles exercised: (n, m, num_bits) +PROFILES = [(2, 3, 128), (2, 5, 256), (3, 5, 256), (4, 5, 256), (2, 2, 128)] + + +def counter_randfunc(seed=0): + """Deterministic, non-crypto byte source for reproducible test vectors.""" + state = [seed & 0xFF] + + def randfunc(nbytes): + out = bytearray(nbytes) + for i in range(nbytes): + state[0] = (state[0] * 1103515245 + 12345) & 0xFF + out[i] = state[0] + return bytes(out) + + return randfunc + + +def test_xor_bytes(): + assert xor_bytes(b"\x0f", b"\xf0") == b"\xff" + assert xor_bytes(b"\xff", b"\xff", b"\xff") == b"\xff" + assert xor_bytes(b"\xaa\x55", b"\x55\xaa") == b"\xff\xff" + + +def test_is_prime(): + assert not is_prime(1) + assert is_prime(2) + assert is_prime(3) + assert not is_prime(4) + assert is_prime(7) + assert not is_prime(9) + assert not is_prime(25) + + +def test_smallest_prime_gte(): + assert smallest_prime_gte(1) == 2 + assert smallest_prime_gte(2) == 2 + assert smallest_prime_gte(3) == 3 + assert smallest_prime_gte(4) == 5 + assert smallest_prime_gte(6) == 7 + assert smallest_prime_gte(9) == 11 + + +def test_rejects_bad_params(): + with pytest.raises(ValueError): + KuriharaScheme(1, 3, 128) # n < 2 + with pytest.raises(ValueError): + KuriharaScheme(4, 3, 128) # n > m + with pytest.raises(ValueError): + KuriharaScheme(2, 3, 100) # non-BIP39 entropy size + with pytest.raises(ValueError): + KuriharaScheme(3, 6, 256) # prime-1 = 6 does not divide 256 + + +def test_share_size_equals_secret(): + for n, m, num_bits in PROFILES: + sch = KuriharaScheme(n, m, num_bits) + secret = counter_randfunc(1)(num_bits // 8) + shares = sch.generate(secret, counter_randfunc(2)) + assert len(shares) == m + for share in shares: + assert len(share.to_bytes()) * 8 == num_bits + + +def test_generate_rejects_wrong_secret_length(): + sch = KuriharaScheme(3, 5, 256) + with pytest.raises(ValueError): + sch.generate(b"\x00" * 31, counter_randfunc()) + + +def test_all_coalitions_reconstruct(): + for n, m, num_bits in PROFILES: + sch = KuriharaScheme(n, m, num_bits) + secret = counter_randfunc(7)(num_bits // 8) + shares = sch.generate(secret, counter_randfunc(8)) + for combo in itertools.combinations(range(m), n): + picked = [shares[i] for i in combo] + assert sch.reconstruct(picked) == secret + + +def test_oversized_coalition_reconstructs(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(3)(32) + shares = sch.generate(secret, counter_randfunc(4)) + assert sch.reconstruct(shares) == secret + + +def test_subthreshold_rejected(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(5)(32) + shares = sch.generate(secret, counter_randfunc(6)) + with pytest.raises(ValueError): + sch.reconstruct(shares[:2]) + + +def test_mixed_instances_rejected(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(0)(32) + coalition_a = sch.generate(secret, counter_randfunc(10)) + coalition_b = sch.generate(secret, counter_randfunc(20)) + with pytest.raises(ValueError): + sch.reconstruct([coalition_a[0], coalition_a[1], coalition_b[2]]) + + +def test_duplicate_shares_rejected(): + sch = KuriharaScheme(2, 3, 128) + secret = counter_randfunc(0)(16) + shares = sch.generate(secret, counter_randfunc(1)) + # Two copies of one share are rank-deficient: a fragment stays undetermined. + with pytest.raises(ValueError): + sch.reconstruct([shares[0], shares[0]]) + + +def test_regenerate_lost_share_bit_identical(): + for n, m, num_bits in PROFILES: + if m <= n: + continue + sch = KuriharaScheme(n, m, num_bits) + secret = counter_randfunc(42)(num_bits // 8) + shares = sch.generate(secret, counter_randfunc(43)) + coalition = shares[:n] + for lost in range(n, m): + regen = sch.reconstruct_lost(coalition, lost + 1) + assert regen.pieces == shares[lost].pieces + + +def test_regenerated_share_usable(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(99)(32) + shares = sch.generate(secret, counter_randfunc(100)) + regen = sch.reconstruct_lost(shares[:3], 5) + assert sch.reconstruct([shares[0], shares[3], regen]) == secret + + +def test_reconstruct_lost_out_of_range(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(0)(32) + shares = sch.generate(secret, counter_randfunc(1)) + with pytest.raises(ValueError): + sch.reconstruct_lost(shares[:3], 0) + with pytest.raises(ValueError): + sch.reconstruct_lost(shares[:3], 6) + + +def test_reconstruct_lost_present_share_rejected(): + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(0)(32) + shares = sch.generate(secret, counter_randfunc(1)) + with pytest.raises(ValueError): + sch.reconstruct_lost(shares[:3], 1) + + +def test_each_share_is_valid_mnemonic(): + from embit import bip39 + + sch = KuriharaScheme(3, 5, 256) + secret = counter_randfunc(8)(32) + shares = sch.generate(secret, counter_randfunc(9)) + for share in shares: + mnemonic = share_to_mnemonic(share) + assert bip39.mnemonic_is_valid(mnemonic) + assert len(mnemonic.split()) == 24 + + +def test_secret_roundtrips_through_mnemonics(): + n, m, num_bits = 3, 5, 256 + sch = KuriharaScheme(n, m, num_bits) + secret = counter_randfunc(11)(32) + shares = sch.generate(secret, counter_randfunc(12)) + instance = shares[0].meta["instance"] + rebuilt = [ + share_from_mnemonic(s.part_id, share_to_mnemonic(s), n, m, num_bits, instance) + for s in shares[:n] + ] + assert sch.reconstruct(rebuilt) == secret + + +def test_mnemonic_entropy_roundtrip(): + entropy = bytes(range(16)) + mnemonic = entropy_to_mnemonic(entropy) + assert mnemonic_to_entropy(mnemonic) == entropy + + +def test_share_from_mnemonic_size_mismatch(): + sch = KuriharaScheme(2, 3, 128) + secret = counter_randfunc(0)(16) + shares = sch.generate(secret, counter_randfunc(1)) + mnemonic = share_to_mnemonic(shares[0]) # 12 words / 128 bits + with pytest.raises(ValueError): + share_from_mnemonic(1, mnemonic, 3, 5, 256) # claims 256 bits + + +def test_known_answer(): + sch = KuriharaScheme(2, 3, 128) + secret = bytes(range(16)) + shares = sch.generate(secret, counter_randfunc(0)) + assert sch.reconstruct([shares[0], shares[1]]) == secret + assert sch.reconstruct([shares[0], shares[2]]) == secret + assert sch.reconstruct([shares[1], shares[2]]) == secret + blobs = [s.to_bytes() for s in shares] + assert blobs[0] != blobs[1] + assert blobs[0] != secret From 7381e87595a0c2a42561a31992d811746606f6b3 Mon Sep 17 00:00:00 2001 From: CaverneCrypto <150951860+CaverneCrypto@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:08:39 +0100 Subject: [PATCH 2/2] feat: wire Kurihara threshold backup into the UI (split + restore) Adds the user-facing flow for the n-of-m threshold backup: - Create shares: "Threshold Split" under Backup Mnemonic -- choose m and n on the keypad, review each share as a BIP39 mnemonic, confirm they are saved. - Restore: "Restore from shares" in the login Load Mnemonic menu -- enter n shares (number + mnemonic), the seed is reconstructed and loaded via the standard load flow. Shares are derived deterministically from the secret (like Coldcard's deterministic Seed XOR). 100% test coverage of the new page (incl. an end-to-end 3-of-5 split-then-restore test); updates the three existing tests affected by the new Backup Mnemonic menu item, and adds 12-language translations (machine-filled then domain-term corrected; non-Latin locales would benefit from a native review). Co-Authored-By: Claude Opus 4.8 (1M context) --- i18n/translations/de-DE.json | 14 + i18n/translations/es-MX.json | 14 + i18n/translations/fr-FR.json | 14 + i18n/translations/ja-JP.json | 14 + i18n/translations/ko-KR.json | 14 + i18n/translations/nl-NL.json | 14 + i18n/translations/pt-BR.json | 14 + i18n/translations/ru-RU.json | 14 + i18n/translations/tr-TR.json | 14 + i18n/translations/vi-VN.json | 14 + i18n/translations/zh-CN.json | 16 +- src/krux/pages/home_pages/mnemonic_backup.py | 7 + src/krux/pages/home_pages/threshold_backup.py | 223 +++++++++++ src/krux/pages/login.py | 25 ++ src/krux/translations/__init__.py | 16 + src/krux/translations/de.py | 16 + src/krux/translations/es.py | 16 + src/krux/translations/fr.py | 16 + src/krux/translations/ja.py | 16 + src/krux/translations/ko.py | 16 + src/krux/translations/nl.py | 16 + src/krux/translations/pt.py | 16 + src/krux/translations/ru.py | 16 + src/krux/translations/tr.py | 16 + src/krux/translations/vi.py | 16 + src/krux/translations/zh.py | 16 + .../pages/home_pages/test_mnemonic_backup.py | 15 +- .../pages/home_pages/test_threshold_backup.py | 378 ++++++++++++++++++ tests/pages/test_stackbit.py | 4 + tests/pages/test_tiny_seed.py | 1 + 30 files changed, 994 insertions(+), 7 deletions(-) create mode 100644 src/krux/pages/home_pages/threshold_backup.py create mode 100644 tests/pages/home_pages/test_threshold_backup.py diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index c3783059a..1f51ff807 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Zusätzliche Entropie von der Kamera erforderlich für %s", "Address": "Adresse", "Align camera and backup plate properly.": "Richte Kamera und Sicherungsplatte richtig aus.", + "All shares must have the same number of words.": "Alle Teile müssen gleich viele Wörter haben.", "Anti-glare mode": "Blendschutzmodus", "Appearance": "Aussehen", "Are you sure?": "Bist Du sicher?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Bestätigen Sie den Tamper Check Code", "Convert Datum": "Datum konvertieren", "Could not determine change address.": "Änderungsadresse konnte nicht ermittelt werden.", + "Could not reconstruct. Check the share numbers and retry.": "Wiederherstellung fehlgeschlagen. Teilnummern prüfen und erneut versuchen.", "Create QR Code": "QR Code erstellen", "Create QR code from text?": "QR-Code aus Text erstellen?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Erstelle m Teile (m von 2 bis 5); n davon zum Wiederherstellen nötig (n von 2 bis m). Zum Beispiel 2 von 3 oder 3 von 5.", "Created:": "Erstellt:", "Current Tamper Check Code": "Aktueller Tamper Check Code", "Custom QR Code": "Benutzerdefinierter Link QR-Code", @@ -121,11 +124,13 @@ "Go": "Go", "Good entropy": "Gute Entropie", "Hardware": "Hardware", + "Have you written down all %d shares? They will not be shown again.": "Haben Sie alle %d Teile notiert? Sie werden nicht erneut angezeigt.", "Head type": "Kopfart", "Hex Public Key:": "Hex öffentlicher Schlüssel:", "Hexadecimal": "Hexadezimal", "Hide Mnemonics": "Mnemonics ausblenden", "High fees!": "Hohe Gebühren!", + "I saved all shares": "Ich habe alle Teile notiert", "ID": "ID", "ID already exists": "ID existiert bereits", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Wenn Ihr Gerätedisplay nach dieser Änderung nicht funktioniert, wird es nach 5 Sekunden automatisch mit den vorherigen Einstellungen neu gestartet.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Firmware-Dateien von der SD-Karte entfernen?", "Res. - Format": "Res. - Format", "Restore factory settings and reboot?": "Werkseinstellungen wiederherstellen und neu starten?", + "Restore from shares": "Aus Teilen wiederherstellen", "Result": "Ergebnis", "Return to QR Viewer": "Zurück zum QR-Viewer", "Review Again": "Erneut Überprüfen", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Die Einstellungen werden intern auf Flash gespeichert.", "Settings stored on SD card.": "Einstellungen auf SD-Karte gespeichert.", "Shannon's entropy:": "Shannons Entropie:", + "Share %d of %d": "Teil %d von %d", + "Share number (1 to m)": "Teilnummer (1 bis m)", + "Shares needed to restore (n), 2 to m": "Zum Wiederherstellen nötige Teile (n), 2 bis m", + "Shares you have (n), 2 to m": "Vorhandene Teile (n), 2 bis m", "Show Datum": "Datum anzeigen", "Shutdown": "Ausschalten", "Shutdown Time": "Abschaltzeit", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Einige Knoten sind nicht gehärtet:", "Spend (%d):": "Ausgabe (%d):", "Spend:": "Ausgaben:", + "Split into %d shares, any %d restore the seed?": "In %d Teile aufteilen; beliebige %d stellen die Seed wieder her?", "Standard": "Standard", "Standard mode": "Standardmodus", "Static": "Statisch", @@ -308,9 +319,12 @@ "Text": "Text", "Theme": "Thema", "Thermal": "Thermisch", + "Threshold Split": "Schwellenwert-Aufteilung", "To ensure data is unrecoverable use Wipe Device feature": "Um sicherzustellen, dass die Daten nicht wiederhergestellt werden können, verwenden Sie die Funktion 'Gerät löschen'", "Toggle Brightness": "Helligkeit umschalten", "Tools": "Werkzeuge", + "Total shares created (m), 2 to 5": "Erstellte Teile (m), 2 bis 5", + "Total shares to create (m), 2 to 5": "Zu erstellende Teile (m), 2 bis 5", "Touch Threshold": "Berühre Schwellenwert", "Touchscreen": "Touchscreen", "Try more?": "Weiter versuchen?", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 0e3960ba2..ac5988800 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Se requiere entropía adicional de la cámara para %s", "Address": "Dirección", "Align camera and backup plate properly.": "Alinea la cámara y la placa de respaldo correctamente.", + "All shares must have the same number of words.": "Todas las partes deben tener el mismo número de palabras.", "Anti-glare mode": "Modo antirreflejo", "Appearance": "Apariencia", "Are you sure?": "¿Estás seguro?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Confirmar el código de verificación", "Convert Datum": "Convertir dato", "Could not determine change address.": "No se pudo determinar la dirección de cambio.", + "Could not reconstruct. Check the share numbers and retry.": "No se pudo reconstruir. Verifica los números de parte y reintenta.", "Create QR Code": "Crear código QR", "Create QR code from text?": "¿Crear código QR a partir de texto?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Crea m partes (m de 2 a 5); necesitas n para restaurar (n de 2 a m). Por ejemplo 2 de 3 o 3 de 5.", "Created:": "Creado:", "Current Tamper Check Code": "Código de verificación actual", "Custom QR Code": "Código QR personalizado", @@ -121,11 +124,13 @@ "Go": "Ir", "Good entropy": "Buena entropía", "Hardware": "Hardware", + "Have you written down all %d shares? They will not be shown again.": "¿Anotaste las %d partes? No se mostrarán de nuevo.", "Head type": "Tipo de cabezal", "Hex Public Key:": "Clave Pública Hexadecimal:", "Hexadecimal": "Hexadecimal", "Hide Mnemonics": "Ocultar Mnemónicos", "High fees!": "¡Tarifas altas!", + "I saved all shares": "Guardé todas las partes", "ID": "Identificador", "ID already exists": "ID ya existe", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Si la pantalla de su dispositivo no funciona después de este cambio, se reiniciará automáticamente con la configuración anterior después de 5 segundos.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "¿Eliminar archivos de firmware de la tarjeta SD?", "Res. - Format": "Res. - Formato", "Restore factory settings and reboot?": "¿Restablecer a la configuración de fábrica y reiniciar?", + "Restore from shares": "Restaurar desde partes", "Result": "Resultados", "Return to QR Viewer": "Volver al QR", "Review Again": "Revisar nuevamente", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Ajustes almacenados internamente en flash.", "Settings stored on SD card.": "Configuración almacenada en la tarjeta SD.", "Shannon's entropy:": "Entropía de Shannon:", + "Share %d of %d": "Parte %d de %d", + "Share number (1 to m)": "Número de parte (1 a m)", + "Shares needed to restore (n), 2 to m": "Partes necesarias para restaurar (n), 2 a m", + "Shares you have (n), 2 to m": "Partes que tienes (n), 2 a m", "Show Datum": "Mostrar dato", "Shutdown": "Apagar", "Shutdown Time": "Tiempo de Apagado", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Algunos nodos no están endurecidos:", "Spend (%d):": "Gastos (%d):", "Spend:": "Gasto:", + "Split into %d shares, any %d restore the seed?": "¿Dividir en %d partes; %d cualesquiera restauran la seed?", "Standard": "Estándar", "Standard mode": "Modo estándar", "Static": "Estático", @@ -308,9 +319,12 @@ "Text": "Texto", "Theme": "Tema", "Thermal": "Térmico", + "Threshold Split": "División por umbral", "To ensure data is unrecoverable use Wipe Device feature": "Para garantizar que los datos no se puedan recuperar, utiliza la función de borrar dispositivo", "Toggle Brightness": "Alternar Brillo", "Tools": "Herramientas", + "Total shares created (m), 2 to 5": "Partes creadas (m), 2 a 5", + "Total shares to create (m), 2 to 5": "Partes a crear (m), 2 a 5", "Touch Threshold": "Umbral Táctil", "Touchscreen": "Pantalla Táctil", "Try more?": "¿Intentar con mas?", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index 78fc38eee..e25f87ea7 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Entropie supplémentaire de la caméra requise pour %s", "Address": "Adresse", "Align camera and backup plate properly.": "Alignez correctement la caméra et plaque de sauvegarde.", + "All shares must have the same number of words.": "Toutes les parts doivent avoir le même nombre de mots.", "Anti-glare mode": "Mode anti-reflets", "Appearance": "Apparence", "Are you sure?": "Es-tu sûr ?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Confirmer le code de non compromis", "Convert Datum": "Convertir le datum", "Could not determine change address.": "Impossible de déterminer l'adresse de monnaie.", + "Could not reconstruct. Check the share numbers and retry.": "Reconstruction impossible. Vérifiez les numéros de part et réessayez.", "Create QR Code": "Créer un QR Code", "Create QR code from text?": "Créer un code QR à partir de texte ?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Créez m parts (m de 2 à 5) ; il en faut n pour restaurer (n de 2 à m). Par exemple 2 sur 3 ou 3 sur 5.", "Created:": "Créé :", "Current Tamper Check Code": "Code de non compromis actuel", "Custom QR Code": "Code QR personnalisé", @@ -121,11 +124,13 @@ "Go": "OK", "Good entropy": "Bonne entropie", "Hardware": "Matériel", + "Have you written down all %d shares? They will not be shown again.": "Avez-vous noté les %d parts ? Elles ne seront plus affichées.", "Head type": "Type de tête", "Hex Public Key:": "Clé publique hexadécimale :", "Hexadecimal": "Hexadécimal", "Hide Mnemonics": "Masquer les mnémoniques", "High fees!": "Frais élevés !", + "I saved all shares": "J'ai noté toutes les parts", "ID": "ID", "ID already exists": "Id existe déjà", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "En cas d'échec d'affichage, l'appareil redémarrera automatiquement après 5 secondes utilisant les paramètres précédents.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Supprimer les fichiers micrologiciel de la carte SD ?", "Res. - Format": "Rés. - Format", "Restore factory settings and reboot?": "Restaurer les paramètres d'usine et redémarrer ?", + "Restore from shares": "Restaurer depuis les parts", "Result": "Résultat", "Return to QR Viewer": "Retour au visualiseur QR", "Review Again": "Revérifier", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Paramètres stockés en interne sur flash.", "Settings stored on SD card.": "Paramètres stockés sur la carte SD.", "Shannon's entropy:": "Entropie de Shannon :", + "Share %d of %d": "Part %d sur %d", + "Share number (1 to m)": "Numéro de part (1 à m)", + "Shares needed to restore (n), 2 to m": "Parts nécessaires pour restaurer (n), 2 à m", + "Shares you have (n), 2 to m": "Parts en votre possession (n), 2 à m", "Show Datum": "Afficher le datum", "Shutdown": "Éteindre", "Shutdown Time": "Delai d'Arrêt", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Certains nœuds ne sont pas durcis :", "Spend (%d):": "Dépense (%d) :", "Spend:": "Dépense :", + "Split into %d shares, any %d restore the seed?": "Diviser en %d parts ; %d suffisent à restaurer la seed ?", "Standard": "Standard", "Standard mode": "Mode standard", "Static": "Statique", @@ -308,9 +319,12 @@ "Text": "Texte", "Theme": "Thème", "Thermal": "Thermique", + "Threshold Split": "Partage à seuil", "To ensure data is unrecoverable use Wipe Device feature": "Pour assurer que les données soient irrécupérables, utilisez la fonctionnalité 'Effacer l'appareil'", "Toggle Brightness": "Ajuster la luminosité", "Tools": "Outils", + "Total shares created (m), 2 to 5": "Nombre de parts créées (m), 2 à 5", + "Total shares to create (m), 2 to 5": "Nombre de parts à créer (m), 2 à 5", "Touch Threshold": "Sensibilité", "Touchscreen": "Écran Tactile", "Try more?": "Réessayer ?", diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 4806accdf..cb17c91f6 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "%sにはカメラからの追加エントロピーが必要です", "Address": "アドレス", "Align camera and backup plate properly.": "カメラとバックプレートを正しく整列させてください.", + "All shares must have the same number of words.": "すべてのシェアは同じ単語数でなければなりません。", "Anti-glare mode": "アンチグレアモード", "Appearance": "外観", "Are you sure?": "よろしいですか?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "改ざんチェックコードの確認", "Convert Datum": "データムの変換", "Could not determine change address.": "変更先住所を特定できませんでした.", + "Could not reconstruct. Check the share numbers and retry.": "復元できませんでした。シェア番号を確認して再試行してください。", "Create QR Code": "QRコードを作成", "Create QR code from text?": "テキストからQRコードを作成しますか?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "m 個のシェアを作成し (m は 2〜5)、復元には n 個が必要です (n は 2〜m)。例: 3 個中 2 個、または 5 個中 3 個。", "Created:": "作成されました:", "Current Tamper Check Code": "現在の改ざんチェックコード", "Custom QR Code": "カスタムQRコード", @@ -121,11 +124,13 @@ "Go": "行く", "Good entropy": "良いentropy", "Hardware": "ハードウェア", + "Have you written down all %d shares? They will not be shown again.": "%d 個のシェアをすべて控えましたか? 再表示されません。", "Head type": "ヘッドタイプ", "Hex Public Key:": "Hex公開キー:", "Hexadecimal": "エクサデシマル", "Hide Mnemonics": "Mnemonicsを隠す", "High fees!": "高い手数料!", + "I saved all shares": "すべてのシェアを保存しました", "ID": "ID", "ID already exists": "IDはすでに存在します", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "この変更後にデバイスのディスプレイが動作しない場合、5秒後に以前の設定で自動的に再起動します.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "SDカードからファームウェアファイルを削除しますか?", "Res. - Format": "Res. - フォーマット", "Restore factory settings and reboot?": "初期化を復元して再起動しますか?", + "Restore from shares": "シェアから復元", "Result": "結果", "Return to QR Viewer": "QRビューワーに戻る", "Review Again": "もう一度確認する", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "設定はフラッシュメモリに内部保存されています.", "Settings stored on SD card.": "設定はSDカードに保存されています.", "Shannon's entropy:": "シャノンのエントロピー:", + "Share %d of %d": "シェア %d / %d", + "Share number (1 to m)": "シェア番号 (1〜m)", + "Shares needed to restore (n), 2 to m": "復元に必要なシェア数 (n)、2〜m", + "Shares you have (n), 2 to m": "手元のシェア数 (n)、2〜m", "Show Datum": "データムを表示", "Shutdown": "シャットダウン", "Shutdown Time": "シャットダウン時間", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "一部のノードは硬化されていません:", "Spend (%d):": "支出(%d):", "Spend:": "支出:", + "Split into %d shares, any %d restore the seed?": "%d 個のシェアに分割しますか? 任意の %d 個で seed を復元できます。", "Standard": "標準", "Standard mode": "標準モード", "Static": "静止画", @@ -308,9 +319,12 @@ "Text": "テキスト", "Theme": "テーマ", "Thermal": "サーマル", + "Threshold Split": "しきい値分割", "To ensure data is unrecoverable use Wipe Device feature": "データが復元不可能であることを確実にするには、デバイス消去機能を使用してください", "Toggle Brightness": "明るさを切り替える", "Tools": "ツール", + "Total shares created (m), 2 to 5": "作成済みシェア数 (m)、2〜5", + "Total shares to create (m), 2 to 5": "作成するシェア数 (m)、2〜5", "Touch Threshold": "タッチスレッショルド", "Touchscreen": "タッチスクリーン", "Try more?": "もっと試してみますか?", diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index 6c00a9b97..73cf03064 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "%s 에 필요한 카메라의 추가 엔트로피", "Address": "주소", "Align camera and backup plate properly.": "카메라와 보조 플레이트를 올바르게 정렬하십시오.", + "All shares must have the same number of words.": "모든 조각은 단어 수가 같아야 합니다.", "Anti-glare mode": "눈부심 방지 모드", "Appearance": "디스플레이", "Are you sure?": "계속하시겠습니까?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "탬퍼 체크 코드 확인", "Convert Datum": "날짜 변환", "Could not determine change address.": "변경 주소를 확인할 수 없습니다.", + "Could not reconstruct. Check the share numbers and retry.": "복원할 수 없습니다. 조각 번호를 확인하고 다시 시도하세요.", "Create QR Code": "QR 코드 생성", "Create QR code from text?": "문자 메시지로 QR 코드를 만드시겠어요?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "m개의 조각을 만들고 (m은 2~5), 복원에는 n개가 필요합니다 (n은 2~m). 예: 3개 중 2개 또는 5개 중 3개.", "Created:": "생성됨:", "Current Tamper Check Code": "현재 탬퍼 체크 코드", "Custom QR Code": "사용자 지정 QR 코드", @@ -121,11 +124,13 @@ "Go": "선택", "Good entropy": "엔트로피가 충분합니다", "Hardware": "하드웨어", + "Have you written down all %d shares? They will not be shown again.": "%d개의 조각을 모두 적어두었나요? 다시 표시되지 않습니다.", "Head type": "헤드 종류", "Hex Public Key:": "16진수 공개키:", "Hexadecimal": "16진수", "Hide Mnemonics": "니모닉 숨기기", "High fees!": "수수료가 높습니다!", + "I saved all shares": "모든 조각을 저장했습니다", "ID": "ID", "ID already exists": "아이디가 이미 존재합니다", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "이 변경 후에도 장치 디스플레이가 작동하지 않으면 5초 후에 이전 설정으로 자동으로 재부팅됩니다.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "SD카드에서 펌웨어 파일을 제거하시겠습니까?", "Res. - Format": "Res. - 형식", "Restore factory settings and reboot?": "공장 설정을 복원하고 재부팅하시겠습니까?", + "Restore from shares": "조각에서 복원", "Result": "결과", "Return to QR Viewer": "QR 뷰어로 돌아가기", "Review Again": "다시 검토", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "설정은 플래시에서 내부적으로 저장됩니다.", "Settings stored on SD card.": "SD 카드에 저장된 설정.", "Shannon's entropy:": "Shannon의 엔트로피:", + "Share %d of %d": "조각 %d / %d", + "Share number (1 to m)": "조각 번호 (1~m)", + "Shares needed to restore (n), 2 to m": "복원에 필요한 조각 수 (n), 2~m", + "Shares you have (n), 2 to m": "보유한 조각 수 (n), 2~m", "Show Datum": "대추 표시", "Shutdown": "종료", "Shutdown Time": "자동 종료시간", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "일부 노드가 경화되지 않습니다:", "Spend (%d):": "Spend (%d):", "Spend:": "지출:", + "Split into %d shares, any %d restore the seed?": "%d개의 조각으로 나눌까요? 임의의 %d개로 seed를 복원합니다.", "Standard": "표준", "Standard mode": "표준 모드", "Static": "Static", @@ -308,9 +319,12 @@ "Text": "텍스트", "Theme": "테마", "Thermal": "Thermal", + "Threshold Split": "임계값 분할", "To ensure data is unrecoverable use Wipe Device feature": "데이터 복구가 불가능하도록 장치 전체지우기 기능을 사용하십시오", "Toggle Brightness": "밝기 전환", "Tools": "도구", + "Total shares created (m), 2 to 5": "생성된 조각 수 (m), 2~5", + "Total shares to create (m), 2 to 5": "생성할 조각 수 (m), 2~5", "Touch Threshold": "터치 민감도", "Touchscreen": "터치스크린", "Try more?": "더 하시겠습니까?", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 00ddb338a..ec40ac246 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Extra entropie van camera vereist voor %s", "Address": "Adres", "Align camera and backup plate properly.": "Richt de camera en back-upplaat op de juiste manier.", + "All shares must have the same number of words.": "Alle delen moeten hetzelfde aantal woorden hebben.", "Anti-glare mode": "Anti-verblindingsmodus", "Appearance": "Uiterlijk", "Are you sure?": "Weet je het zeker?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Bevestig de sabotagecontrolecode", "Convert Datum": "Datum converteren", "Could not determine change address.": "Kan adreswijziging niet bepalen.", + "Could not reconstruct. Check the share numbers and retry.": "Reconstructie mislukt. Controleer de deelnummers en probeer opnieuw.", "Create QR Code": "QR-code aanmaken", "Create QR code from text?": "QR-code maken van tekst?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Maak m delen (m van 2 tot 5); n ervan nodig om te herstellen (n van 2 tot m). Bijvoorbeeld 2-van-3 of 3-van-5.", "Created:": "Aangemaakt:", "Current Tamper Check Code": "Huidige sabotagecontrolecode", "Custom QR Code": "Aangepaste QR-code", @@ -121,11 +124,13 @@ "Go": "Ga", "Good entropy": "Goede entropie", "Hardware": "Hardware", + "Have you written down all %d shares? They will not be shown again.": "Heb je alle %d delen genoteerd? Ze worden niet opnieuw getoond.", "Head type": "Type gereedschapskop", "Hex Public Key:": "Hex publieke sleutel:", "Hexadecimal": "Hexadecimaal", "Hide Mnemonics": "Verberg geheugensteunen", "High fees!": "Hoge kosten!", + "I saved all shares": "Ik heb alle delen genoteerd", "ID": "ID", "ID already exists": "ID bestaat al", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Als het scherm van uw apparaat na deze wijziging niet werkt, wordt het na 5 seconden automatisch opnieuw opgestart met de vorige instellingen.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Firmwarebestanden van SD kaart verwijderen?", "Res. - Format": "Res. - Formaat", "Restore factory settings and reboot?": "Fabrieksinstellingen herstellen en opnieuw opstarten?", + "Restore from shares": "Herstellen uit delen", "Result": "Resultaat", "Return to QR Viewer": "Terug naar QR-lezer", "Review Again": "Opnieuw Beoordelen", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Instellingen intern opgeslagen op flitser.", "Settings stored on SD card.": "Instellingen opgeslagen op SD kaart.", "Shannon's entropy:": "Shannon's entropie:", + "Share %d of %d": "Deel %d van %d", + "Share number (1 to m)": "Deelnummer (1 tot m)", + "Shares needed to restore (n), 2 to m": "Benodigde delen om te herstellen (n), 2 tot m", + "Shares you have (n), 2 to m": "Delen die je hebt (n), 2 tot m", "Show Datum": "Toon datum", "Shutdown": "Afsluiten", "Shutdown Time": "Uitschakelingstijd", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Sommige knooppunten zijn niet gehard:", "Spend (%d):": "Uitgaven (%d):", "Spend:": "Uitgaven:", + "Split into %d shares, any %d restore the seed?": "Splitsen in %d delen; willekeurige %d herstellen de seed?", "Standard": "Standaard", "Standard mode": "Standaardmodus", "Static": "Statisch", @@ -308,9 +319,12 @@ "Text": "Tekst", "Theme": "Thema", "Thermal": "Thermisch", + "Threshold Split": "Drempelsplitsing", "To ensure data is unrecoverable use Wipe Device feature": "Gebruik de functie 'Apparaat wissen' om te zorgen dat de gegevens onherstelbaar zijn", "Toggle Brightness": "Helderheid schakelen", "Tools": "Hulpmiddelen", + "Total shares created (m), 2 to 5": "Gemaakte delen (m), 2 tot 5", + "Total shares to create (m), 2 to 5": "Te maken delen (m), 2 tot 5", "Touch Threshold": "Aanraak gevoeligheid", "Touchscreen": "Aanraakscherm", "Try more?": "Meer proberen?", diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index ce6a25e94..851ee60be 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Entropia adicional da câmera é necessária para %s", "Address": "Endereço", "Align camera and backup plate properly.": "Alinhe a câmera e a placa de backup corretamente.", + "All shares must have the same number of words.": "Todas as partes devem ter o mesmo número de palavras.", "Anti-glare mode": "Modo antirreflexo", "Appearance": "Aparência", "Are you sure?": "Tem certeza?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Confirmar código de verificação de integridade", "Convert Datum": "Converter dados", "Could not determine change address.": "Não foi possível determinar endereços de troco.", + "Could not reconstruct. Check the share numbers and retry.": "Não foi possível reconstruir. Verifique os números das partes e tente novamente.", "Create QR Code": "Criar Código QR", "Create QR code from text?": "Criar código QR a partir de texto?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Crie m partes (m de 2 a 5); são necessárias n para restaurar (n de 2 a m). Por exemplo 2 de 3 ou 3 de 5.", "Created:": "Criado:", "Current Tamper Check Code": "Código atual de verificação de integridade", "Custom QR Code": "Código QR personalizado", @@ -121,11 +124,13 @@ "Go": "Ir", "Good entropy": "Boa entropia", "Hardware": "Hardware", + "Have you written down all %d shares? They will not be shown again.": "Você anotou todas as %d partes? Elas não serão mostradas novamente.", "Head type": "Tipo de cabeçote", "Hex Public Key:": "Chave pública hexadecimal:", "Hexadecimal": "Hexadecimal", "Hide Mnemonics": "Ocultar Mnemônicos", "High fees!": "Taxas altas!", + "I saved all shares": "Anotei todas as partes", "ID": "ID", "ID already exists": "ID já existe", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Se a tela do seu dispositivo não funcionar após essa alteração, ele será reiniciado automaticamente com as configurações anteriores após 5 segundos.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Excluir arquivos de firmware do cartão SD?", "Res. - Format": "Res. - Formato", "Restore factory settings and reboot?": "Restaurar as configurações de fábrica e reiniciar?", + "Restore from shares": "Restaurar a partir de partes", "Result": "Resultado", "Return to QR Viewer": "Retornar ao Visualizador de QR", "Review Again": "Revisar Novamente", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Configurações armazenadas internamente na flash.", "Settings stored on SD card.": "Configurações armazenadas no cartão SD.", "Shannon's entropy:": "Entropia de Shannon:", + "Share %d of %d": "Parte %d de %d", + "Share number (1 to m)": "Número da parte (1 a m)", + "Shares needed to restore (n), 2 to m": "Partes necessárias para restaurar (n), 2 a m", + "Shares you have (n), 2 to m": "Partes que você tem (n), 2 a m", "Show Datum": "Mostrar dados", "Shutdown": "Desligar", "Shutdown Time": "Tempo de desligamento", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Alguns nós não são hardened:", "Spend (%d):": "Gastos (%d):", "Spend:": "Gasto:", + "Split into %d shares, any %d restore the seed?": "Dividir em %d partes; quaisquer %d restauram a seed?", "Standard": "Padrão", "Standard mode": "Modo padrão", "Static": "Estático", @@ -308,9 +319,12 @@ "Text": "Texto", "Theme": "Tema", "Thermal": "Térmica", + "Threshold Split": "Divisão por limiar", "To ensure data is unrecoverable use Wipe Device feature": "Para garantir que os dados sejam irrecuperáveis, use o recurso Limpar Dispositivo", "Toggle Brightness": "Alternar brilho", "Tools": "Ferramentas", + "Total shares created (m), 2 to 5": "Partes criadas (m), 2 a 5", + "Total shares to create (m), 2 to 5": "Partes a criar (m), 2 a 5", "Touch Threshold": "Limiar de Toque", "Touchscreen": "Touchscreen", "Try more?": "Tentar mais?", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index 61a8901af..3118acfa2 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Требуется дополнительная энтропия от камеры для %s", "Address": "Адрес", "Align camera and backup plate properly.": "Правильно совместите камеру и резервную пластину.", + "All shares must have the same number of words.": "Все части должны иметь одинаковое число слов.", "Anti-glare mode": "Антибликовый режим", "Appearance": "Внешний Вид", "Are you sure?": "Вы уверены?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Подтвердите код проверки вскрытия", "Convert Datum": "Преобразовать датум", "Could not determine change address.": "Не удалось определить адрес изменения.", + "Could not reconstruct. Check the share numbers and retry.": "Не удалось восстановить. Проверьте номера частей и повторите.", "Create QR Code": "Создать QR-код", "Create QR code from text?": "Создать QR-код из текста?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Создайте m частей (m от 2 до 5); для восстановления нужно n (n от 2 до m). Например 2 из 3 или 3 из 5.", "Created:": "Создано:", "Current Tamper Check Code": "Текущий код проверки вскрытия", "Custom QR Code": "Пользовательский QR-код", @@ -121,11 +124,13 @@ "Go": "OK", "Good entropy": "Хорошая энтропия", "Hardware": "Аппаратное Обеспечение", + "Have you written down all %d shares? They will not be shown again.": "Вы записали все %d частей? Они больше не будут показаны.", "Head type": "Тип головки", "Hex Public Key:": "Шестнадцатеричный Публичный Ключ:", "Hexadecimal": "Шестнадцатеричный", "Hide Mnemonics": "Скрыть мнемоники", "High fees!": "Высокие комиссии!", + "I saved all shares": "Я записал все части", "ID": "Идентификатор", "ID already exists": "ID уже существует", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Если после этого изменения дисплей устройства не работает, он автоматически перезагрузится с предыдущими настройками через 5 секунд.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Удалить файлы прошивки с SD-карты?", "Res. - Format": "Разреш. - Формат", "Restore factory settings and reboot?": "Восстановить заводские настройки и перезагрузить?", + "Restore from shares": "Восстановить из частей", "Result": "Результат", "Return to QR Viewer": "Вернуться к QR-просмотрщику", "Review Again": "Проверить снова", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Настройки хранятся во флэш-памяти.", "Settings stored on SD card.": "Настройки сохранены на SD-карте.", "Shannon's entropy:": "Энтропия Шеннона:", + "Share %d of %d": "Часть %d из %d", + "Share number (1 to m)": "Номер части (1 до m)", + "Shares needed to restore (n), 2 to m": "Частей нужно для восстановления (n), от 2 до m", + "Shares you have (n), 2 to m": "Имеющиеся части (n), от 2 до m", "Show Datum": "Показать датум", "Shutdown": "Выключить", "Shutdown Time": "Время выключения", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Некоторые узлы не укреплены:", "Spend (%d):": "Расход (%d):", "Spend:": "Расход:", + "Split into %d shares, any %d restore the seed?": "Разделить на %d частей; любые %d восстановят seed?", "Standard": "Стандартный", "Standard mode": "Стандартный режим", "Static": "Static / Статическое оборудование", @@ -308,9 +319,12 @@ "Text": "Текст", "Theme": "Тема", "Thermal": "Термальный", + "Threshold Split": "Пороговое разделение", "To ensure data is unrecoverable use Wipe Device feature": "Для гарантии невосстановления данных используйте функцию Очистки Устройства", "Toggle Brightness": "Регулировка Яркости", "Tools": "Инструменты", + "Total shares created (m), 2 to 5": "Сколько частей создано (m), от 2 до 5", + "Total shares to create (m), 2 to 5": "Сколько частей создать (m), от 2 до 5", "Touch Threshold": "Чувствительность", "Touchscreen": "Тачскрин", "Try more?": "Попробовать ещё?", diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 3c9f98b7f..be73ca1dc 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "%s için kameradan gelen ek entropi gerekli", "Address": "Adres", "Align camera and backup plate properly.": "Kamerayı ve yedek plakay'ı düzgün bir şekilde hizalayın.", + "All shares must have the same number of words.": "Tüm parçalar aynı sayıda kelimeye sahip olmalı.", "Anti-glare mode": "Parlama Önleyici Mod", "Appearance": "Görünüm", "Are you sure?": "Emin misiniz?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Kurcalama Kontrol Kodunu Onayla", "Convert Datum": "Veriyi Dönüştür", "Could not determine change address.": "Değişiklik adresi belirlenemedi.", + "Could not reconstruct. Check the share numbers and retry.": "Yeniden oluşturulamadı. Parça numaralarını kontrol edip tekrar deneyin.", "Create QR Code": "QR Kodu Oluştur", "Create QR code from text?": "Metinden QR kodu oluşturulsun mu?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "m parça oluşturun (m, 2 ila 5); geri yüklemek için n tanesi gerekir (n, 2 ila m). Örneğin 3 parçadan 2 veya 5 parçadan 3.", "Created:": "Oluşturuldu:", "Current Tamper Check Code": "Mevcut Kurcalama Kontrol Kodu", "Custom QR Code": "Özel QR Kodu", @@ -121,11 +124,13 @@ "Go": "Seç", "Good entropy": "Yeterli entropi", "Hardware": "Donanım", + "Have you written down all %d shares? They will not be shown again.": "%d parçanın tümünü yazdınız mı? Tekrar gösterilmeyecekler.", "Head type": "Başlık türü", "Hex Public Key:": "Hex Public Key:", "Hexadecimal": "Onaltılık", "Hide Mnemonics": "Mnemonic'leri Gizle", "High fees!": "Yüksek ücret!", + "I saved all shares": "Tüm parçaları kaydettim", "ID": "ID", "ID already exists": "ID zaten var", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Bu değişiklikten sonra cihazınızın ekranı çalışmazsa, 5 saniye sonra önceki ayarlarla otomatik olarak yeniden başlatılır.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "SD Karttan donanım yazılımı dosyaları kaldırılsın mı?", "Res. - Format": "Çözünürlüğü Sıfırla", "Restore factory settings and reboot?": "Fabrika ayarlarına geri dönüp ve yeniden başlatılsın mı?", + "Restore from shares": "Parçalardan geri yükle", "Result": "Sonuç", "Return to QR Viewer": "QR Görüntüleyiciye Dön", "Review Again": "Tekrar İncele", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Ayarlar dahili olarak flaşta saklanır.", "Settings stored on SD card.": "Ayarlar SD karta kaydedildi.", "Shannon's entropy:": "Shannon entropisi:", + "Share %d of %d": "Parça %d / %d", + "Share number (1 to m)": "Parça numarası (1 ila m)", + "Shares needed to restore (n), 2 to m": "Geri yüklemek için gereken parça (n), 2 ila m", + "Shares you have (n), 2 to m": "Elinizdeki parça sayısı (n), 2 ila m", "Show Datum": "Referans Noktasını Göster", "Shutdown": "Kapat", "Shutdown Time": "Kapanma Süresi", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Bazı düğümler sertleştirilmemiş:", "Spend (%d):": "Harcama (%d):", "Spend:": "Harcama:", + "Split into %d shares, any %d restore the seed?": "%d parçaya bölünsün mü; herhangi %d tanesi seed'i geri yükler?", "Standard": "Standart", "Standard mode": "Standart Mod", "Static": "Statik", @@ -308,9 +319,12 @@ "Text": "Metin", "Theme": "Tema", "Thermal": "Termal", + "Threshold Split": "Eşik bölme", "To ensure data is unrecoverable use Wipe Device feature": "Verilerin geri kullanılamaz olduğundan emin olmak için Cihazı Sil özelliğini kullanın", "Toggle Brightness": "Parlaklığı Değiştir", "Tools": "Araçlar", + "Total shares created (m), 2 to 5": "Oluşturulan parça sayısı (m), 2 ila 5", + "Total shares to create (m), 2 to 5": "Oluşturulacak parça sayısı (m), 2 ila 5", "Touch Threshold": "Dokunma Eşiği", "Touchscreen": "Dokunmatik ekran", "Try more?": "Daha fazla kez denensin mi?", diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 31b7f7cd5..3272bcef0 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "Entropy bổ sung từ máy ảnh cần thiết cho %s", "Address": "Địa chỉ", "Align camera and backup plate properly.": "Căn chỉnh camera và tấm dự phòng đúng cách.", + "All shares must have the same number of words.": "Tất cả các phần phải có cùng số từ.", "Anti-glare mode": "Chế độ chống lóa", "Appearance": "Giao diện", "Are you sure?": "Bạn có chắc không?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "Xác nhận mã kiểm tra giả mạo", "Convert Datum": "Chuyển đổi dữ liệu", "Could not determine change address.": "Không thể xác định địa chỉ thay đổi.", + "Could not reconstruct. Check the share numbers and retry.": "Không thể tái tạo. Kiểm tra số thứ tự phần và thử lại.", "Create QR Code": "Tạo mã QR", "Create QR code from text?": "Tạo mã QR từ văn bản?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "Tạo m phần (m từ 2 đến 5); cần n phần để khôi phục (n từ 2 đến m). Ví dụ 2/3 hoặc 3/5.", "Created:": "Tạo:", "Current Tamper Check Code": "Mã kiểm tra giả mạo hiện tại", "Custom QR Code": "Mã QR tùy chỉnh", @@ -121,11 +124,13 @@ "Go": "Chọn", "Good entropy": "Entropy tốt", "Hardware": "Phần cứng", + "Have you written down all %d shares? They will not be shown again.": "Bạn đã ghi lại tất cả %d phần chưa? Chúng sẽ không hiển thị lại.", "Head type": "Loại đầu", "Hex Public Key:": "Khóa công cộng Hex:", "Hexadecimal": "Thập lục phân", "Hide Mnemonics": "Ẩn Mnemonics", "High fees!": "Phí cao!", + "I saved all shares": "Tôi đã lưu tất cả các phần", "ID": "ID", "ID already exists": "Id đã tồn tại", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "Nếu màn hình thiết bị của bạn không hoạt động sau khi thay đổi này, màn hình sẽ tự động khởi động lại với các cài đặt trước đó sau 5 giây.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "Xóa các tệp firmware khỏi Thẻ SD?", "Res. - Format": "Độ phân giải - Định dạng", "Restore factory settings and reboot?": "Khôi phục cài đặt gốc và khởi động lại?", + "Restore from shares": "Khôi phục từ các phần", "Result": "Kết quả", "Return to QR Viewer": "Quay lại Trình xem QR", "Review Again": "Xem lại lần nữa", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "Cài đặt được lưu trữ nội bộ trên đèn flash.", "Settings stored on SD card.": "Cài đặt được lưu trên thẻ SD.", "Shannon's entropy:": "Entropy của Shannon:", + "Share %d of %d": "Phần %d trên %d", + "Share number (1 to m)": "Số thứ tự phần (1 đến m)", + "Shares needed to restore (n), 2 to m": "Số phần cần để khôi phục (n), 2 đến m", + "Shares you have (n), 2 to m": "Số phần bạn có (n), 2 đến m", "Show Datum": "Hiển thị dữ liệu", "Shutdown": "Tắt máy", "Shutdown Time": "Thời gian tắt máy", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "Một số nút không được làm cứng:", "Spend (%d):": "Chi tiêu (%d):", "Spend:": "Chi tiêu:", + "Split into %d shares, any %d restore the seed?": "Chia thành %d phần; bất kỳ %d phần nào cũng khôi phục seed?", "Standard": "Tiêu chuẩn", "Standard mode": "Chế độ Tiêu chuẩn", "Static": "Tĩnh", @@ -308,9 +319,12 @@ "Text": "Văn bản", "Theme": "Chủ đề", "Thermal": "Nhiệt", + "Threshold Split": "Chia theo ngưỡng", "To ensure data is unrecoverable use Wipe Device feature": "Sử dụng tính năng Xóa dữ liệu trên thiết bị để đảm bảo dữ liệu không thể phục hồi", "Toggle Brightness": "Chuyển đổi độ sáng", "Tools": "Công cụ", + "Total shares created (m), 2 to 5": "Số phần đã tạo (m), 2 đến 5", + "Total shares to create (m), 2 to 5": "Số phần cần tạo (m), 2 đến 5", "Touch Threshold": "Ngưỡng cảm ứng", "Touchscreen": "Màn hình cảm ứng", "Try more?": "Thử thêm nữa?", diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index 3a2f0d23e..df0def8f4 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -19,6 +19,7 @@ "Additional entropy from camera required for %s": "%s需要摄像头的额外熵", "Address": "地址", "Align camera and backup plate properly.": "正确对齐摄像头和背板.", + "All shares must have the same number of words.": "所有份额的单词数必须相同。", "Anti-glare mode": "防闪模式", "Appearance": "界面", "Are you sure?": "确定?", @@ -48,8 +49,10 @@ "Confirm Tamper Check Code": "确认防篡改检查码", "Convert Datum": "转换基准", "Could not determine change address.": "无法确定更改地址.", + "Could not reconstruct. Check the share numbers and retry.": "无法重建。请检查份额编号后重试。", "Create QR Code": "创建二维码", "Create QR code from text?": "从短信创建二维码?", + "Create m shares (m from 2 to 5) and need n of them to restore (n from 2 to m). For example 2-of-3 or 3-of-5.": "创建 m 份(m 为 2 到 5),恢复需要其中 n 份(n 为 2 到 m)。例如 3 取 2 或 5 取 3。", "Created:": "已创建:", "Current Tamper Check Code": "当前防篡改检查码", "Custom QR Code": "自定义二维码", @@ -121,11 +124,13 @@ "Go": "去", "Good entropy": "良好的熵", "Hardware": "硬件", + "Have you written down all %d shares? They will not be shown again.": "您已记录全部 %d 份了吗?将不再显示。", "Head type": "头部类型", "Hex Public Key:": "十六进制公钥:", "Hexadecimal": "十六进制", "Hide Mnemonics": "隐藏助记词", "High fees!": "高费用!", + "I saved all shares": "我已保存所有份额", "ID": "ID", "ID already exists": "ID 已存在", "If your device display does not work after this change, it will automatically reboot with previous settings after 5 seconds.": "如果您的设备显示屏在此更改后无法正常工作,它将在5秒后使用以前的设置自动重新启动.", @@ -232,6 +237,7 @@ "Remove firmware files from SD Card?": "从 SD 卡中删除固件文件?", "Res. - Format": "分辨率 - 格式", "Restore factory settings and reboot?": "恢复出厂设置并重新设备?", + "Restore from shares": "从份额恢复", "Result": "結果", "Return to QR Viewer": "返回二维码查看器", "Review Again": "再次审核", @@ -265,6 +271,10 @@ "Settings stored internally on flash.": "设置存储在 Flash 内部.", "Settings stored on SD card.": "设置存储在SD卡上.", "Shannon's entropy:": "香农熵:", + "Share %d of %d": "第 %d 份,共 %d 份", + "Share number (1 to m)": "份额编号(1 到 m)", + "Shares needed to restore (n), 2 to m": "恢复所需的份数 (n),2 到 m", + "Shares you have (n), 2 to m": "您拥有的份数 (n),2 到 m", "Show Datum": "显示基准", "Shutdown": "关机", "Shutdown Time": "关机时间", @@ -284,6 +294,7 @@ "Some nodes are not hardened:": "有些节点未硬化:", "Spend (%d):": "花费 (%d):", "Spend:": "花费", + "Split into %d shares, any %d restore the seed?": "拆分为 %d 份?任意 %d 份即可恢复 seed。", "Standard": "标准", "Standard mode": "标准模式", "Static": "Static 静态?", @@ -308,9 +319,12 @@ "Text": "文本", "Theme": "主题", "Thermal": "热敏", + "Threshold Split": "阈值分割", "To ensure data is unrecoverable use Wipe Device feature": "要确保数据不可恢复,请使用擦除设备功能", "Toggle Brightness": "调整亮度", "Tools": "工具", + "Total shares created (m), 2 to 5": "已创建的份数 (m),2 到 5", + "Total shares to create (m), 2 to 5": "要创建的份数 (m),2 到 5", "Touch Threshold": "触摸阈值", "Touchscreen": "触摸屏", "Try more?": "再次尝试?", @@ -331,8 +345,8 @@ "User's Data": "用户数据", "Value %s out of range: [%s, %s]": "值 %s 超出范围:[ %s,%s ]", "Verifying…": "验证中…", - "Vertical": "垂直", "Version": "版本", + "Vertical": "垂直", "Via Camera": "通过摄像头", "Via D20": "通过 D20", "Via D6": "通过 D6", diff --git a/src/krux/pages/home_pages/mnemonic_backup.py b/src/krux/pages/home_pages/mnemonic_backup.py index 88b1f50d0..723a46af0 100644 --- a/src/krux/pages/home_pages/mnemonic_backup.py +++ b/src/krux/pages/home_pages/mnemonic_backup.py @@ -41,11 +41,18 @@ def mnemonic(self): (t("QR Code"), self.qr_code_backup), (t("Encrypted"), self.encrypt_mnemonic_menu), (t("Other Formats"), self.other_backup_formats), + (t("Threshold Split"), self.threshold_split), ], ) submenu.run_loop() return MENU_CONTINUE + def threshold_split(self): + """Handler for the 'Threshold Split' backup item (Kurihara n-of-m)""" + from .threshold_backup import ThresholdBackup + + return ThresholdBackup(self.ctx).split() + def qr_code_backup(self): """Handler for the 'QR Code Backup' menu item""" submenu = Menu( diff --git a/src/krux/pages/home_pages/threshold_backup.py b/src/krux/pages/home_pages/threshold_backup.py new file mode 100644 index 000000000..25a27879f --- /dev/null +++ b/src/krux/pages/home_pages/threshold_backup.py @@ -0,0 +1,223 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2026 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. +import hashlib + +from .. import Menu, LETTERS, MENU_CONTINUE, MENU_EXIT, DIGITS, ESC_KEY +from ..mnemonic_loader import MnemonicLoader +from ...krux_settings import t +from ...kurihara import ( + KuriharaScheme, + share_to_mnemonic, + share_from_mnemonic, + entropy_to_mnemonic, +) + + +class ThresholdBackup(MnemonicLoader): + """Split or restore a mnemonic with an n-of-m threshold (Kurihara). + + A true threshold generalization of Mnemonic XOR (Seed XOR): any n of the m + shares reconstruct the seed, while fewer than n reveal nothing, and each + share is itself a valid BIP39 mnemonic. Unlike m-of-m Seed XOR, this + tolerates losing up to m - n shares. + + Shares are derived deterministically from the secret (so repeating the split + yields the same shares, like Coldcard's deterministic Seed XOR); the masks + stay unpredictable to anyone holding fewer than n shares. + """ + + def __init__(self, ctx): + super().__init__(ctx) + self.captured_words = None + + @staticmethod + def _deterministic_randfunc(entropy): + """Reproducible mask stream keyed by the secret entropy""" + counter = [0] + + def randfunc(nbytes): + out = b"" + while len(out) < nbytes: + out += hashlib.sha256( + b"krux-threshold" + entropy + counter[0].to_bytes(4, "big") + ).digest() + counter[0] += 1 + return out[:nbytes] + + return randfunc + + def _capture_int(self, title): + """Capture a positive integer from the numeric keypad (None if cancelled)""" + value = self.capture_from_keypad(title, [DIGITS]) + if value in (ESC_KEY, ""): + return None + return int(value) + + def _info(self, text): + """Show an already-translated centered message and wait for a button""" + self.ctx.display.clear() + self.ctx.display.draw_centered_text(text) + self.ctx.input.wait_for_button() + + # ----- Split ----- + + def _choose_scheme(self, num_bits): + """Ask for m then n on the keypad, retrying with guidance if invalid""" + while True: + m = self._capture_int(t("Total shares to create (m), 2 to 5")) + if m is None: + return None + n = self._capture_int(t("Shares needed to restore (n), 2 to m")) + if n is None: + return None + if 2 <= m <= 5 and 2 <= n <= m: + return KuriharaScheme(n, m, num_bits) + self._info( + t( + "Create m shares (m from 2 to 5) and need n of them to " + "restore (n from 2 to m). For example 2-of-3 or 3-of-5." + ) + ) + + def split(self): + """Split the current mnemonic into threshold shares for backup""" + from embit.bip39 import mnemonic_to_bytes + + mnemonic = self.ctx.wallet.key.mnemonic + entropy = mnemonic_to_bytes(mnemonic) + num_bits = len(entropy) * 8 + + scheme = self._choose_scheme(num_bits) + if scheme is None: + return MENU_CONTINUE + + self.ctx.display.clear() + if not self.prompt( + t("Split into %d shares, any %d restore the seed?") % (scheme.m, scheme.n), + self.ctx.display.height() // 2, + ): + return MENU_CONTINUE + + shares = scheme.generate(entropy, self._deterministic_randfunc(entropy)) + self._browse_shares(shares, scheme.m) + return MENU_CONTINUE + + def _show_share(self, share, m): + """Display one share full-screen as a BIP39 mnemonic""" + self.display_mnemonic( + share_to_mnemonic(share), + title=t("Share %d of %d") % (share.part_id, m), + ) + self.ctx.input.wait_for_button() + return MENU_CONTINUE + + def _browse_shares(self, shares, m): + """Browse the shares, then confirm they were written down""" + while True: + items = [ + ( + t("Share %d of %d") % (share.part_id, m), + lambda share=share: self._show_share(share, m), + ) + for share in shares + ] + items.append((t("I saved all shares"), lambda: MENU_EXIT)) + menu = Menu(self.ctx, items) + index, _ = menu.run_loop() + if index == menu.back_index: + return + if self.prompt( + t( + "Have you written down all %d shares? " + "They will not be shown again." + ) + % m, + self.ctx.display.height() // 2, + ): + return + + # ----- Restore ----- + + def _load_key_from_words(self, words, charset=LETTERS, new=False): + """Capture hook for recover(): keep the entered words and exit the loader""" + self.captured_words = words + return MENU_EXIT + + def _collect_shares(self, n, m): + """Collect n shares (number + mnemonic each); None on cancel/mismatch""" + from embit.bip39 import mnemonic_to_bytes + + shares = [] + num_bits = None + for _ in range(n): + part_id = self._capture_int(t("Share number (1 to m)")) + if part_id is None or not 1 <= part_id <= m: + return None + self.captured_words = None + self.load_key() + if not self.captured_words: + return None + mnemonic = " ".join(self.captured_words) + bits = len(mnemonic_to_bytes(mnemonic)) * 8 + if num_bits is None: + num_bits = bits + try: + shares.append(share_from_mnemonic(part_id, mnemonic, n, m, num_bits)) + except ValueError: + self._info(t("All shares must have the same number of words.")) + return None + return shares + + def restore(self): + """Reconstruct a mnemonic from n threshold shares. + + Returns the recovered BIP39 mnemonic (str), or None if cancelled. The + caller loads it through the normal flow (e.g. Login._load_key_from_words), + so this works both at login and with a key already loaded. + """ + m = self._capture_int(t("Total shares created (m), 2 to 5")) + if m is None: + return None + n = self._capture_int(t("Shares you have (n), 2 to m")) + if n is None: + return None + if not 2 <= m <= 5 or not 2 <= n <= m: + self._info( + t( + "Create m shares (m from 2 to 5) and need n of them to " + "restore (n from 2 to m). For example 2-of-3 or 3-of-5." + ) + ) + return None + + shares = self._collect_shares(n, m) + if shares is None: + return None + + try: + num_bits = len(shares[0].to_bytes()) * 8 + secret = KuriharaScheme(n, m, num_bits).reconstruct(shares) + except ValueError: + self._info(t("Could not reconstruct. Check the share numbers and retry.")) + return None + + return entropy_to_mnemonic(secret) diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 8bc7c7dad..df1c782a5 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -86,6 +86,31 @@ def __init__(self, ctx): ), ) + def load_key(self): + """Load Mnemonic menu, extended with threshold-share restore""" + 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), + (t("Restore from shares"), self.restore_from_threshold), + ], + ) + index, status = submenu.run_loop() + if index == submenu.back_index: + return MENU_CONTINUE + return status + + def restore_from_threshold(self): + """Restore a mnemonic from Kurihara threshold shares, then load it""" + from .home_pages.threshold_backup import ThresholdBackup + + recovered = ThresholdBackup(self.ctx).restore() + if recovered is None: + return MENU_CONTINUE + return self._load_key_from_words(recovered.split()) + def new_key(self): """Handler for the 'new mnemonic' menu item""" submenu = Menu( diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index d8f2a711f..ce6b55ddf 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -53,6 +53,7 @@ 4121028614, 3270727197, 900375497, + 554208378, 2693258820, 3857613120, 1056821534, @@ -82,8 +83,10 @@ 422237057, 1464900930, 3625040530, + 1050286303, 4094072796, 167798282, + 151143812, 678449760, 1347214433, 3513215254, @@ -155,11 +158,13 @@ 602716148, 1198393582, 133139382, + 1223967801, 3384260162, 3563141210, 2691246967, 3903754133, 3876651191, + 1207043213, 299066170, 2880010062, 3327483300, @@ -266,6 +271,7 @@ 1557093280, 2817311427, 2365886561, + 3039658947, 348570661, 3091105710, 2745908239, @@ -299,6 +305,10 @@ 563836138, 712533907, 1959451368, + 673866375, + 739215088, + 2243033315, + 3283470859, 1843401008, 1825881236, 3656120779, @@ -318,6 +328,8 @@ 2863098142, 2090568351, 1260825919, + 929509282, + 3917592017, 1075810813, 2272013587, 1232757391, @@ -341,9 +353,12 @@ 2612594937, 1454688268, 1180180513, + 1774609729, 2258131455, 2700207481, 725348723, + 998496752, + 3328460981, 3684696112, 2978718564, 2732611775, @@ -365,6 +380,7 @@ 4003084591, 3846217531, 1889659487, + 1358960330, 4191058607, 1254681955, 525309547, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index af68bcfe8..3e4d1934b 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -41,6 +41,7 @@ "Zusätzliche Entropie von der Kamera erforderlich für %s", "Adresse", "Richte Kamera und Sicherungsplatte richtig aus.", + "Alle Teile müssen gleich viele Wörter haben.", "Blendschutzmodus", "Aussehen", "Bist Du sicher?", @@ -70,8 +71,10 @@ "Bestätigen Sie den Tamper Check Code", "Datum konvertieren", "Änderungsadresse konnte nicht ermittelt werden.", + "Wiederherstellung fehlgeschlagen. Teilnummern prüfen und erneut versuchen.", "QR Code erstellen", "QR-Code aus Text erstellen?", + "Erstelle m Teile (m von 2 bis 5); n davon zum Wiederherstellen nötig (n von 2 bis m). Zum Beispiel 2 von 3 oder 3 von 5.", "Erstellt:", "Aktueller Tamper Check Code", "Benutzerdefinierter Link QR-Code", @@ -143,11 +146,13 @@ "Go", "Gute Entropie", "Hardware", + "Haben Sie alle %d Teile notiert? Sie werden nicht erneut angezeigt.", "Kopfart", "Hex öffentlicher Schlüssel:", "Hexadezimal", "Mnemonics ausblenden", "Hohe Gebühren!", + "Ich habe alle Teile notiert", "ID", "ID existiert bereits", "Wenn Ihr Gerätedisplay nach dieser Änderung nicht funktioniert, wird es nach 5 Sekunden automatisch mit den vorherigen Einstellungen neu gestartet.", @@ -254,6 +259,7 @@ "Firmware-Dateien von der SD-Karte entfernen?", "Res. - Format", "Werkseinstellungen wiederherstellen und neu starten?", + "Aus Teilen wiederherstellen", "Ergebnis", "Zurück zum QR-Viewer", "Erneut Überprüfen", @@ -287,6 +293,10 @@ "Die Einstellungen werden intern auf Flash gespeichert.", "Einstellungen auf SD-Karte gespeichert.", "Shannons Entropie:", + "Teil %d von %d", + "Teilnummer (1 bis m)", + "Zum Wiederherstellen nötige Teile (n), 2 bis m", + "Vorhandene Teile (n), 2 bis m", "Datum anzeigen", "Ausschalten", "Abschaltzeit", @@ -306,6 +316,8 @@ "Einige Knoten sind nicht gehärtet:", "Ausgabe (%d):", "Ausgaben:", + "In %d Teile aufteilen; beliebige %d stellen die Seed wieder her?", + "Standard", "Standardmodus", "Statisch", "Statistiken für Nerds", @@ -329,9 +341,12 @@ "Text", "Thema", "Thermisch", + "Schwellenwert-Aufteilung", "Um sicherzustellen, dass die Daten nicht wiederhergestellt werden können, verwenden Sie die Funktion 'Gerät löschen'", "Helligkeit umschalten", "Werkzeuge", + "Erstellte Teile (m), 2 bis 5", + "Zu erstellende Teile (m), 2 bis 5", "Berühre Schwellenwert", "Touchscreen", "Weiter versuchen?", @@ -353,6 +368,7 @@ "Wert %S außerhalb des Bereichs: [ %s, %s]", "Überprüfung…", "Version", + "Vertikal", "Via Kamera", "Via D20", "Via D6", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index 6e493038e..4fc833a1d 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -41,6 +41,7 @@ "Se requiere entropía adicional de la cámara para %s", "Dirección", "Alinea la cámara y la placa de respaldo correctamente.", + "Todas las partes deben tener el mismo número de palabras.", "Modo antirreflejo", "Apariencia", "¿Estás seguro?", @@ -70,8 +71,10 @@ "Confirmar el código de verificación", "Convertir dato", "No se pudo determinar la dirección de cambio.", + "No se pudo reconstruir. Verifica los números de parte y reintenta.", "Crear código QR", "¿Crear código QR a partir de texto?", + "Crea m partes (m de 2 a 5); necesitas n para restaurar (n de 2 a m). Por ejemplo 2 de 3 o 3 de 5.", "Creado:", "Código de verificación actual", "Código QR personalizado", @@ -143,11 +146,13 @@ "Ir", "Buena entropía", "Hardware", + "¿Anotaste las %d partes? No se mostrarán de nuevo.", "Tipo de cabezal", "Clave Pública Hexadecimal:", "Hexadecimal", "Ocultar Mnemónicos", "¡Tarifas altas!", + "Guardé todas las partes", "Identificador", "ID ya existe", "Si la pantalla de su dispositivo no funciona después de este cambio, se reiniciará automáticamente con la configuración anterior después de 5 segundos.", @@ -254,6 +259,7 @@ "¿Eliminar archivos de firmware de la tarjeta SD?", "Res. - Formato", "¿Restablecer a la configuración de fábrica y reiniciar?", + "Restaurar desde partes", "Resultados", "Volver al QR", "Revisar nuevamente", @@ -287,6 +293,10 @@ "Ajustes almacenados internamente en flash.", "Configuración almacenada en la tarjeta SD.", "Entropía de Shannon:", + "Parte %d de %d", + "Número de parte (1 a m)", + "Partes necesarias para restaurar (n), 2 a m", + "Partes que tienes (n), 2 a m", "Mostrar dato", "Apagar", "Tiempo de Apagado", @@ -306,6 +316,8 @@ "Algunos nodos no están endurecidos:", "Gastos (%d):", "Gasto:", + "¿Dividir en %d partes; %d cualesquiera restauran la seed?", + "Estándar", "Modo estándar", "Estático", "Estadísticas para Entendidos", @@ -329,9 +341,12 @@ "Texto", "Tema", "Térmico", + "División por umbral", "Para garantizar que los datos no se puedan recuperar, utiliza la función de borrar dispositivo", "Alternar Brillo", "Herramientas", + "Partes creadas (m), 2 a 5", + "Partes a crear (m), 2 a 5", "Umbral Táctil", "Pantalla Táctil", "¿Intentar con mas?", @@ -353,6 +368,7 @@ "Valor %s fuera del rango: [ %s, %s]", "Verificando…", "Versión", + "Vertical", "Desde Cámara", "Vía D20", "Vía D6", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index 2a9057091..3cf7356f4 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -41,6 +41,7 @@ "Entropie supplémentaire de la caméra requise pour %s", "Adresse", "Alignez correctement la caméra et plaque de sauvegarde.", + "Toutes les parts doivent avoir le même nombre de mots.", "Mode anti-reflets", "Apparence", "Es-tu sûr\u2009?", @@ -70,8 +71,10 @@ "Confirmer le code de non compromis", "Convertir le datum", "Impossible de déterminer l'adresse de monnaie.", + "Reconstruction impossible. Vérifiez les numéros de part et réessayez.", "Créer un QR Code", "Créer un code QR à partir de texte\u2009?", + "Créez m parts (m de 2 à 5) ; il en faut n pour restaurer (n de 2 à m). Par exemple 2 sur 3 ou 3 sur 5.", "Créé\u2009:", "Code de non compromis actuel", "Code QR personnalisé", @@ -143,11 +146,13 @@ "OK", "Bonne entropie", "Matériel", + "Avez-vous noté les %d parts ? Elles ne seront plus affichées.", "Type de tête", "Clé publique hexadécimale\u2009:", "Hexadécimal", "Masquer les mnémoniques", "Frais élevés\u2009!", + "J'ai noté toutes les parts", "ID", "Id existe déjà", "En cas d'échec d'affichage, l'appareil redémarrera automatiquement après 5 secondes utilisant les paramètres précédents.", @@ -254,6 +259,7 @@ "Supprimer les fichiers micrologiciel de la carte SD\u2009?", "Rés. - Format", "Restaurer les paramètres d'usine et redémarrer\u2009?", + "Restaurer depuis les parts", "Résultat", "Retour au visualiseur QR", "Revérifier", @@ -287,6 +293,10 @@ "Paramètres stockés en interne sur flash.", "Paramètres stockés sur la carte SD.", "Entropie de Shannon\u2009:", + "Part %d sur %d", + "Numéro de part (1 à m)", + "Parts nécessaires pour restaurer (n), 2 à m", + "Parts en votre possession (n), 2 à m", "Afficher le datum", "Éteindre", "Delai d'Arrêt", @@ -306,6 +316,8 @@ "Certains nœuds ne sont pas durcis :", "Dépense (%d)\u2009:", "Dépense\u2009:", + "Diviser en %d parts ; %d suffisent à restaurer la seed ?", + "Standard", "Mode standard", "Statique", "Statistiques pour les geeks", @@ -329,9 +341,12 @@ "Texte", "Thème", "Thermique", + "Partage à seuil", "Pour assurer que les données soient irrécupérables, utilisez la fonctionnalité 'Effacer l'appareil'", "Ajuster la luminosité", "Outils", + "Nombre de parts créées (m), 2 à 5", + "Nombre de parts à créer (m), 2 à 5", "Sensibilité", "Écran Tactile", "Réessayer\u2009?", @@ -353,6 +368,7 @@ "Valeur %s hors de portée: [%s, %s]", "Vérification…", "Version", + "Vertical", "Par caméra", "Via D20", "Via D6", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index c03342041..9236b4b53 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -41,6 +41,7 @@ "%sにはカメラからの追加エントロピーが必要です", "アドレス", "カメラとバックプレートを正しく整列させてください.", + "すべてのシェアは同じ単語数でなければなりません。", "アンチグレアモード", "外観", "よろしいですか?", @@ -70,8 +71,10 @@ "改ざんチェックコードの確認", "データムの変換", "変更先住所を特定できませんでした.", + "復元できませんでした。シェア番号を確認して再試行してください。", "QRコードを作成", "テキストからQRコードを作成しますか?", + "m 個のシェアを作成し (m は 2〜5)、復元には n 個が必要です (n は 2〜m)。例: 3 個中 2 個、または 5 個中 3 個。", "作成されました:", "現在の改ざんチェックコード", "カスタムQRコード", @@ -143,11 +146,13 @@ "行く", "良いentropy", "ハードウェア", + "%d 個のシェアをすべて控えましたか? 再表示されません。", "ヘッドタイプ", "Hex公開キー:", "エクサデシマル", "Mnemonicsを隠す", "高い手数料!", + "すべてのシェアを保存しました", "ID", "IDはすでに存在します", "この変更後にデバイスのディスプレイが動作しない場合、5秒後に以前の設定で自動的に再起動します.", @@ -254,6 +259,7 @@ "SDカードからファームウェアファイルを削除しますか?", "Res. - フォーマット", "初期化を復元して再起動しますか?", + "シェアから復元", "結果", "QRビューワーに戻る", "もう一度確認する", @@ -287,6 +293,10 @@ "設定はフラッシュメモリに内部保存されています.", "設定はSDカードに保存されています.", "シャノンのエントロピー:", + "シェア %d / %d", + "シェア番号 (1〜m)", + "復元に必要なシェア数 (n)、2〜m", + "手元のシェア数 (n)、2〜m", "データムを表示", "シャットダウン", "シャットダウン時間", @@ -306,6 +316,8 @@ "一部のノードは硬化されていません:", "支出(%d):", "支出:", + "%d 個のシェアに分割しますか? 任意の %d 個で seed を復元できます。", + "標準", "標準モード", "静止画", "オタクのための統計", @@ -329,9 +341,12 @@ "テキスト", "テーマ", "サーマル", + "しきい値分割", "データが復元不可能であることを確実にするには、デバイス消去機能を使用してください", "明るさを切り替える", "ツール", + "作成済みシェア数 (m)、2〜5", + "作成するシェア数 (m)、2〜5", "タッチスレッショルド", "タッチスクリーン", "もっと試してみますか?", @@ -353,6 +368,7 @@ "値%sが範囲外です: [ %s, %s]", "認証中…", "バージョン", + "縦向き", "カメラ経由", "D20経由", "D6経由", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index d9556c94a..e0e5c2e83 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -41,6 +41,7 @@ "%s 에 필요한 카메라의 추가 엔트로피", "주소", "카메라와 보조 플레이트를 올바르게 정렬하십시오.", + "모든 조각은 단어 수가 같아야 합니다.", "눈부심 방지 모드", "디스플레이", "계속하시겠습니까?", @@ -70,8 +71,10 @@ "탬퍼 체크 코드 확인", "날짜 변환", "변경 주소를 확인할 수 없습니다.", + "복원할 수 없습니다. 조각 번호를 확인하고 다시 시도하세요.", "QR 코드 생성", "문자 메시지로 QR 코드를 만드시겠어요?", + "m개의 조각을 만들고 (m은 2~5), 복원에는 n개가 필요합니다 (n은 2~m). 예: 3개 중 2개 또는 5개 중 3개.", "생성됨:", "현재 탬퍼 체크 코드", "사용자 지정 QR 코드", @@ -143,11 +146,13 @@ "선택", "엔트로피가 충분합니다", "하드웨어", + "%d개의 조각을 모두 적어두었나요? 다시 표시되지 않습니다.", "헤드 종류", "16진수 공개키:", "16진수", "니모닉 숨기기", "수수료가 높습니다!", + "모든 조각을 저장했습니다", "ID", "아이디가 이미 존재합니다", "이 변경 후에도 장치 디스플레이가 작동하지 않으면 5초 후에 이전 설정으로 자동으로 재부팅됩니다.", @@ -254,6 +259,7 @@ "SD카드에서 펌웨어 파일을 제거하시겠습니까?", "Res. - 형식", "공장 설정을 복원하고 재부팅하시겠습니까?", + "조각에서 복원", "결과", "QR 뷰어로 돌아가기", "다시 검토", @@ -287,6 +293,10 @@ "설정은 플래시에서 내부적으로 저장됩니다.", "SD 카드에 저장된 설정.", "Shannon의 엔트로피:", + "조각 %d / %d", + "조각 번호 (1~m)", + "복원에 필요한 조각 수 (n), 2~m", + "보유한 조각 수 (n), 2~m", "대추 표시", "종료", "자동 종료시간", @@ -306,6 +316,8 @@ "일부 노드가 경화되지 않습니다:", "Spend (%d):", "지출:", + "%d개의 조각으로 나눌까요? 임의의 %d개로 seed를 복원합니다.", + "표준", "표준 모드", "Static", "전문가를 위한 통계", @@ -329,9 +341,12 @@ "텍스트", "테마", "Thermal", + "임계값 분할", "데이터 복구가 불가능하도록 장치 전체지우기 기능을 사용하십시오", "밝기 전환", "도구", + "생성된 조각 수 (m), 2~5", + "생성할 조각 수 (m), 2~5", "터치 민감도", "터치스크린", "더 하시겠습니까?", @@ -353,6 +368,7 @@ "%s는 [%s, %s] 범위를 벗어났습니다", "확인…", "버전", + "세로", "카메라", "20면체 주사위", "일반 주사위", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index c3def0d70..c895264bb 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -41,6 +41,7 @@ "Extra entropie van camera vereist voor %s", "Adres", "Richt de camera en back-upplaat op de juiste manier.", + "Alle delen moeten hetzelfde aantal woorden hebben.", "Anti-verblindingsmodus", "Uiterlijk", "Weet je het zeker?", @@ -70,8 +71,10 @@ "Bevestig de sabotagecontrolecode", "Datum converteren", "Kan adreswijziging niet bepalen.", + "Reconstructie mislukt. Controleer de deelnummers en probeer opnieuw.", "QR-code aanmaken", "QR-code maken van tekst?", + "Maak m delen (m van 2 tot 5); n ervan nodig om te herstellen (n van 2 tot m). Bijvoorbeeld 2-van-3 of 3-van-5.", "Aangemaakt:", "Huidige sabotagecontrolecode", "Aangepaste QR-code", @@ -143,11 +146,13 @@ "Ga", "Goede entropie", "Hardware", + "Heb je alle %d delen genoteerd? Ze worden niet opnieuw getoond.", "Type gereedschapskop", "Hex publieke sleutel:", "Hexadecimaal", "Verberg geheugensteunen", "Hoge kosten!", + "Ik heb alle delen genoteerd", "ID", "ID bestaat al", "Als het scherm van uw apparaat na deze wijziging niet werkt, wordt het na 5 seconden automatisch opnieuw opgestart met de vorige instellingen.", @@ -254,6 +259,7 @@ "Firmwarebestanden van SD kaart verwijderen?", "Res. - Formaat", "Fabrieksinstellingen herstellen en opnieuw opstarten?", + "Herstellen uit delen", "Resultaat", "Terug naar QR-lezer", "Opnieuw Beoordelen", @@ -287,6 +293,10 @@ "Instellingen intern opgeslagen op flitser.", "Instellingen opgeslagen op SD kaart.", "Shannon's entropie:", + "Deel %d van %d", + "Deelnummer (1 tot m)", + "Benodigde delen om te herstellen (n), 2 tot m", + "Delen die je hebt (n), 2 tot m", "Toon datum", "Afsluiten", "Uitschakelingstijd", @@ -306,6 +316,8 @@ "Sommige knooppunten zijn niet gehard:", "Uitgaven (%d):", "Uitgaven:", + "Splitsen in %d delen; willekeurige %d herstellen de seed?", + "Standaard", "Standaardmodus", "Statisch", "Statistieken voor nerds", @@ -329,9 +341,12 @@ "Tekst", "Thema", "Thermisch", + "Drempelsplitsing", "Gebruik de functie 'Apparaat wissen' om te zorgen dat de gegevens onherstelbaar zijn", "Helderheid schakelen", "Hulpmiddelen", + "Gemaakte delen (m), 2 tot 5", + "Te maken delen (m), 2 tot 5", "Aanraak gevoeligheid", "Aanraakscherm", "Meer proberen?", @@ -353,6 +368,7 @@ "Waarde %s is buiten bereik: [%s, %s]", "Controleren…", "Versie", + "Verticaal", "Via camera", "Via D20", "Via D6", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index c494f6736..e3a695a0e 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -41,6 +41,7 @@ "Entropia adicional da câmera é necessária para %s", "Endereço", "Alinhe a câmera e a placa de backup corretamente.", + "Todas as partes devem ter o mesmo número de palavras.", "Modo antirreflexo", "Aparência", "Tem certeza?", @@ -70,8 +71,10 @@ "Confirmar código de verificação de integridade", "Converter dados", "Não foi possível determinar endereços de troco.", + "Não foi possível reconstruir. Verifique os números das partes e tente novamente.", "Criar Código QR", "Criar código QR a partir de texto?", + "Crie m partes (m de 2 a 5); são necessárias n para restaurar (n de 2 a m). Por exemplo 2 de 3 ou 3 de 5.", "Criado:", "Código atual de verificação de integridade", "Código QR personalizado", @@ -143,11 +146,13 @@ "Ir", "Boa entropia", "Hardware", + "Você anotou todas as %d partes? Elas não serão mostradas novamente.", "Tipo de cabeçote", "Chave pública hexadecimal:", "Hexadecimal", "Ocultar Mnemônicos", "Taxas altas!", + "Anotei todas as partes", "ID", "ID já existe", "Se a tela do seu dispositivo não funcionar após essa alteração, ele será reiniciado automaticamente com as configurações anteriores após 5 segundos.", @@ -254,6 +259,7 @@ "Excluir arquivos de firmware do cartão SD?", "Res. - Formato", "Restaurar as configurações de fábrica e reiniciar?", + "Restaurar a partir de partes", "Resultado", "Retornar ao Visualizador de QR", "Revisar Novamente", @@ -287,6 +293,10 @@ "Configurações armazenadas internamente na flash.", "Configurações armazenadas no cartão SD.", "Entropia de Shannon:", + "Parte %d de %d", + "Número da parte (1 a m)", + "Partes necessárias para restaurar (n), 2 a m", + "Partes que você tem (n), 2 a m", "Mostrar dados", "Desligar", "Tempo de desligamento", @@ -306,6 +316,8 @@ "Alguns nós não são hardened:", "Gastos (%d):", "Gasto:", + "Dividir em %d partes; quaisquer %d restauram a seed?", + "Padrão", "Modo padrão", "Estático", "Estatísticas para nerds", @@ -329,9 +341,12 @@ "Texto", "Tema", "Térmica", + "Divisão por limiar", "Para garantir que os dados sejam irrecuperáveis, use o recurso Limpar Dispositivo", "Alternar brilho", "Ferramentas", + "Partes criadas (m), 2 a 5", + "Partes a criar (m), 2 a 5", "Limiar de Toque", "Touchscreen", "Tentar mais?", @@ -353,6 +368,7 @@ "Valor %s fora do intervalo: [%s, %s]", "Checando…", "Versão", + "Vertical", "Pela Câmera", "Via D20", "Via D6", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index 418ea16cb..a147cae6e 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -41,6 +41,7 @@ "Требуется дополнительная энтропия от камеры для %s", "Адрес", "Правильно совместите камеру и резервную пластину.", + "Все части должны иметь одинаковое число слов.", "Антибликовый режим", "Внешний Вид", "Вы уверены?", @@ -70,8 +71,10 @@ "Подтвердите код проверки вскрытия", "Преобразовать датум", "Не удалось определить адрес изменения.", + "Не удалось восстановить. Проверьте номера частей и повторите.", "Создать QR-код", "Создать QR-код из текста?", + "Создайте m частей (m от 2 до 5); для восстановления нужно n (n от 2 до m). Например 2 из 3 или 3 из 5.", "Создано:", "Текущий код проверки вскрытия", "Пользовательский QR-код", @@ -143,11 +146,13 @@ "OK", "Хорошая энтропия", "Аппаратное Обеспечение", + "Вы записали все %d частей? Они больше не будут показаны.", "Тип головки", "Шестнадцатеричный Публичный Ключ:", "Шестнадцатеричный", "Скрыть мнемоники", "Высокие комиссии!", + "Я записал все части", "Идентификатор", "ID уже существует", "Если после этого изменения дисплей устройства не работает, он автоматически перезагрузится с предыдущими настройками через 5 секунд.", @@ -254,6 +259,7 @@ "Удалить файлы прошивки с SD-карты?", "Разреш. - Формат", "Восстановить заводские настройки и перезагрузить?", + "Восстановить из частей", "Результат", "Вернуться к QR-просмотрщику", "Проверить снова", @@ -287,6 +293,10 @@ "Настройки хранятся во флэш-памяти.", "Настройки сохранены на SD-карте.", "Энтропия Шеннона:", + "Часть %d из %d", + "Номер части (1 до m)", + "Частей нужно для восстановления (n), от 2 до m", + "Имеющиеся части (n), от 2 до m", "Показать датум", "Выключить", "Время выключения", @@ -306,6 +316,8 @@ "Некоторые узлы не укреплены:", "Расход (%d):", "Расход:", + "Разделить на %d частей; любые %d восстановят seed?", + "Стандартный", "Стандартный режим", "Static / Статическое оборудование", "Статистика для Гиков", @@ -329,9 +341,12 @@ "Текст", "Тема", "Термальный", + "Пороговое разделение", "Для гарантии невосстановления данных используйте функцию Очистки Устройства", "Регулировка Яркости", "Инструменты", + "Сколько частей создано (m), от 2 до 5", + "Сколько частей создать (m), от 2 до 5", "Чувствительность", "Тачскрин", "Попробовать ещё?", @@ -353,6 +368,7 @@ "Значение %s вне диапозона: [%s, %s]", "Верификация…", "Версия", + "Вертикальный", "С Помощью Камеры", "С Помощью D20", "С Помощью D6", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index f4fd01e59..852268fb5 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -41,6 +41,7 @@ "%s için kameradan gelen ek entropi gerekli", "Adres", "Kamerayı ve yedek plakay'ı düzgün bir şekilde hizalayın.", + "Tüm parçalar aynı sayıda kelimeye sahip olmalı.", "Parlama Önleyici Mod", "Görünüm", "Emin misiniz?", @@ -70,8 +71,10 @@ "Kurcalama Kontrol Kodunu Onayla", "Veriyi Dönüştür", "Değişiklik adresi belirlenemedi.", + "Yeniden oluşturulamadı. Parça numaralarını kontrol edip tekrar deneyin.", "QR Kodu Oluştur", "Metinden QR kodu oluşturulsun mu?", + "m parça oluşturun (m, 2 ila 5); geri yüklemek için n tanesi gerekir (n, 2 ila m). Örneğin 3 parçadan 2 veya 5 parçadan 3.", "Oluşturuldu:", "Mevcut Kurcalama Kontrol Kodu", "Özel QR Kodu", @@ -143,11 +146,13 @@ "Seç", "Yeterli entropi", "Donanım", + "%d parçanın tümünü yazdınız mı? Tekrar gösterilmeyecekler.", "Başlık türü", "Hex Public Key:", "Onaltılık", "Mnemonic'leri Gizle", "Yüksek ücret!", + "Tüm parçaları kaydettim", "ID", "ID zaten var", "Bu değişiklikten sonra cihazınızın ekranı çalışmazsa, 5 saniye sonra önceki ayarlarla otomatik olarak yeniden başlatılır.", @@ -254,6 +259,7 @@ "SD Karttan donanım yazılımı dosyaları kaldırılsın mı?", "Çözünürlüğü Sıfırla", "Fabrika ayarlarına geri dönüp ve yeniden başlatılsın mı?", + "Parçalardan geri yükle", "Sonuç", "QR Görüntüleyiciye Dön", "Tekrar İncele", @@ -287,6 +293,10 @@ "Ayarlar dahili olarak flaşta saklanır.", "Ayarlar SD karta kaydedildi.", "Shannon entropisi:", + "Parça %d / %d", + "Parça numarası (1 ila m)", + "Geri yüklemek için gereken parça (n), 2 ila m", + "Elinizdeki parça sayısı (n), 2 ila m", "Referans Noktasını Göster", "Kapat", "Kapanma Süresi", @@ -306,6 +316,8 @@ "Bazı düğümler sertleştirilmemiş:", "Harcama (%d):", "Harcama:", + "%d parçaya bölünsün mü; herhangi %d tanesi seed'i geri yükler?", + "Standart", "Standart Mod", "Statik", "İnekler İçin İstatistikler", @@ -329,9 +341,12 @@ "Metin", "Tema", "Termal", + "Eşik bölme", "Verilerin geri kullanılamaz olduğundan emin olmak için Cihazı Sil özelliğini kullanın", "Parlaklığı Değiştir", "Araçlar", + "Oluşturulan parça sayısı (m), 2 ila 5", + "Oluşturulacak parça sayısı (m), 2 ila 5", "Dokunma Eşiği", "Dokunmatik ekran", "Daha fazla kez denensin mi?", @@ -353,6 +368,7 @@ "%s değeri aralık dışında: [%s, %s]", "Doğrulanıyor…", "Sürüm", + "Dikey", "Kamera Aracılığıyla", "D20 Aracılığıyla", "D6 Aracılığıyla", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index 2da615282..2253427cb 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -41,6 +41,7 @@ "Entropy bổ sung từ máy ảnh cần thiết cho %s", "Địa chỉ", "Căn chỉnh camera và tấm dự phòng đúng cách.", + "Tất cả các phần phải có cùng số từ.", "Chế độ chống lóa", "Giao diện", "Bạn có chắc không?", @@ -70,8 +71,10 @@ "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.", + "Không thể tái tạo. Kiểm tra số thứ tự phần và thử lại.", "Tạo mã QR", "Tạo mã QR từ văn bản?", + "Tạo m phần (m từ 2 đến 5); cần n phần để khôi phục (n từ 2 đến m). Ví dụ 2/3 hoặc 3/5.", "Tạo:", "Mã kiểm tra giả mạo hiện tại", "Mã QR tùy chỉnh", @@ -143,11 +146,13 @@ "Chọn", "Entropy tốt", "Phần cứng", + "Bạn đã ghi lại tất cả %d phần chưa? Chúng sẽ không hiển thị lại.", "Loại đầu", "Khóa công cộng Hex:", "Thập lục phân", "Ẩn Mnemonics", "Phí cao!", + "Tôi đã lưu tất cả các phần", "ID", "Id đã tồn tại", "Nếu màn hình thiết bị của bạn không hoạt động sau khi thay đổi này, màn hình sẽ tự động khởi động lại với các cài đặt trước đó sau 5 giây.", @@ -254,6 +259,7 @@ "Xóa các tệp firmware khỏi Thẻ SD?", "Độ phân giải - Định dạng", "Khôi phục cài đặt gốc và khởi động lại?", + "Khôi phục từ các phần", "Kết quả", "Quay lại Trình xem QR", "Xem lại lần nữa", @@ -287,6 +293,10 @@ "Cài đặt được lưu trữ nội bộ trên đèn flash.", "Cài đặt được lưu trên thẻ SD.", "Entropy của Shannon:", + "Phần %d trên %d", + "Số thứ tự phần (1 đến m)", + "Số phần cần để khôi phục (n), 2 đến m", + "Số phần bạn có (n), 2 đến m", "Hiển thị dữ liệu", "Tắt máy", "Thời gian tắt máy", @@ -306,6 +316,8 @@ "Một số nút không được làm cứng:", "Chi tiêu (%d):", "Chi tiêu:", + "Chia thành %d phần; bất kỳ %d phần nào cũng khôi phục seed?", + "Tiêu chuẩn", "Chế độ Tiêu chuẩn", "Tĩnh", "Số liệu thống kê cho Mọt sách", @@ -329,9 +341,12 @@ "Văn bản", "Chủ đề", "Nhiệt", + "Chia theo ngưỡng", "Sử dụng tính năng Xóa dữ liệu trên thiết bị để đảm bảo dữ liệu không thể phục hồi", "Chuyển đổi độ sáng", "Công cụ", + "Số phần đã tạo (m), 2 đến 5", + "Số phần cần tạo (m), 2 đến 5", "Ngưỡng cảm ứng", "Màn hình cảm ứng", "Thử thêm nữa?", @@ -353,6 +368,7 @@ "Giá trị %s ngoài phạm vi: [ %s, %s]", "Xác minh…", "Phiên Bản", + "Dọc", "Qua máy ảnh", "Qua xúc xắc 20 mặt", "Qua xúc xắc 6 mặt", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index ae11284bf..4b71d46ba 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -41,6 +41,7 @@ "%s需要摄像头的额外熵", "地址", "正确对齐摄像头和背板.", + "所有份额的单词数必须相同。", "防闪模式", "界面", "确定?", @@ -70,8 +71,10 @@ "确认防篡改检查码", "转换基准", "无法确定更改地址.", + "无法重建。请检查份额编号后重试。", "创建二维码", "从短信创建二维码?", + "创建 m 份(m 为 2 到 5),恢复需要其中 n 份(n 为 2 到 m)。例如 3 取 2 或 5 取 3。", "已创建:", "当前防篡改检查码", "自定义二维码", @@ -143,11 +146,13 @@ "去", "良好的熵", "硬件", + "您已记录全部 %d 份了吗?将不再显示。", "头部类型", "十六进制公钥:", "十六进制", "隐藏助记词", "高费用!", + "我已保存所有份额", "ID", "ID 已存在", "如果您的设备显示屏在此更改后无法正常工作,它将在5秒后使用以前的设置自动重新启动.", @@ -254,6 +259,7 @@ "从 SD 卡中删除固件文件?", "分辨率 - 格式", "恢复出厂设置并重新设备?", + "从份额恢复", "結果", "返回二维码查看器", "再次审核", @@ -287,6 +293,10 @@ "设置存储在 Flash 内部.", "设置存储在SD卡上.", "香农熵:", + "第 %d 份,共 %d 份", + "份额编号(1 到 m)", + "恢复所需的份数 (n),2 到 m", + "您拥有的份数 (n),2 到 m", "显示基准", "关机", "关机时间", @@ -306,6 +316,8 @@ "有些节点未硬化:", "花费 (%d):", "花费", + "拆分为 %d 份?任意 %d 份即可恢复 seed。", + "标准", "标准模式", "Static 静态?", "极客统计数据", @@ -329,9 +341,12 @@ "文本", "主题", "热敏", + "阈值分割", "要确保数据不可恢复,请使用擦除设备功能", "调整亮度", "工具", + "已创建的份数 (m),2 到 5", + "要创建的份数 (m),2 到 5", "触摸阈值", "触摸屏", "再次尝试?", @@ -353,6 +368,7 @@ "值 %s 超出范围:[ %s,%s ]", "验证中…", "版本", + "垂直", "通过摄像头", "通过 D20", "通过 D6", diff --git a/tests/pages/home_pages/test_mnemonic_backup.py b/tests/pages/home_pages/test_mnemonic_backup.py index 31ee7c918..052cf92fe 100644 --- a/tests/pages/home_pages/test_mnemonic_backup.py +++ b/tests/pages/home_pages/test_mnemonic_backup.py @@ -38,6 +38,7 @@ def test_mnemonic_words(mocker, m5stickv, tdata): BUTTON_ENTER, # Leave Page 1 BUTTON_PAGE_PREV, # change to btn Back BUTTON_ENTER, # click on back to return to Mnemonic Backup + BUTTON_PAGE, # to Threshold Split BUTTON_PAGE, # change to btn Back BUTTON_ENTER, # click on back to return to home init screen ], @@ -55,6 +56,7 @@ def test_mnemonic_words(mocker, m5stickv, tdata): BUTTON_ENTER, # Leave Page 2 BUTTON_PAGE_PREV, # change to btn Back BUTTON_ENTER, # click on back to return to Mnemonic Backup + BUTTON_PAGE, # to Threshold Split BUTTON_PAGE, # change to btn Back BUTTON_ENTER, # click on back to return to home init screen ], @@ -73,6 +75,7 @@ def test_mnemonic_words(mocker, m5stickv, tdata): BUTTON_ENTER, # Print BUTTON_PAGE_PREV, # change to btn Back BUTTON_ENTER, # click on back to return to Mnemonic Backup + BUTTON_PAGE, # to Threshold Split BUTTON_PAGE, # change to btn Back BUTTON_ENTER, # click on back to return to home init screen ], @@ -365,7 +368,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): + [ # 0, # QR code leave press won't read index 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), ( @@ -378,7 +381,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): + [ # 0, # QR code leave press won't read index 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), # Print @@ -393,7 +396,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): # 0, # QR code leave press won't read index 0, # Print 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), ( @@ -407,7 +410,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): # 0, # QR code leave press won't read index 0, # Print 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), # Decline to print @@ -422,7 +425,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): # 0, # QR code leave press won't read index 1, # Decline to print 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), ( @@ -436,7 +439,7 @@ def test_mnemonic_standard_qr_touch(mocker, amigo, tdata): # 0, # QR code leave press won't read index 1, # Decline to print 4, # Back to Mnemonic Backup - 3, # Back to home init screen + 4, # Back to home init screen ], ), ] diff --git a/tests/pages/home_pages/test_threshold_backup.py b/tests/pages/home_pages/test_threshold_backup.py new file mode 100644 index 000000000..6491ae18c --- /dev/null +++ b/tests/pages/home_pages/test_threshold_backup.py @@ -0,0 +1,378 @@ +from .. import create_ctx + +MNEMONIC_24 = ( + "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon art" +) + + +def _wallet(): + from krux.key import Key, TYPE_SINGLESIG + from krux.wallet import Wallet + from embit.networks import NETWORKS + + return Wallet(Key(MNEMONIC_24, TYPE_SINGLESIG, NETWORKS["test"])) + + +def _page(mocker, wallet=None): + """ThresholdBackup with a context whose buttons never run out.""" + from krux.pages.home_pages.threshold_backup import ThresholdBackup + from krux.input import BUTTON_ENTER + + ctx = create_ctx(mocker, [], wallet=wallet) + ctx.input.wait_for_button = mocker.MagicMock(return_value=BUTTON_ENTER) + return ThresholdBackup(ctx), ctx + + +def test_deterministic_randfunc(mocker, m5stickv): + from krux.pages.home_pages.threshold_backup import ThresholdBackup + + rf = ThresholdBackup._deterministic_randfunc(b"\x01" * 32) + first = rf(8) + # reproducible across fresh instances with the same entropy + again = ThresholdBackup._deterministic_randfunc(b"\x01" * 32)(8) + assert first == again + # different entropy -> different stream + assert ThresholdBackup._deterministic_randfunc(b"\x02" * 32)(8) != first + # asking for more than one digest worth of bytes refills the stream + assert len(ThresholdBackup._deterministic_randfunc(b"\x03" * 32)(40)) == 40 + + +def test_split_generates_and_browses(mocker, m5stickv): + from krux.pages import MENU_CONTINUE + + page, _ = _page(mocker, _wallet()) + # m=3 then n=2 + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", "2"]) + mocker.patch.object(page, "prompt", return_value=True) + browse = mocker.patch.object(page, "_browse_shares") + + assert page.split() == MENU_CONTINUE + # _browse_shares received 3 shares + shares = browse.call_args[0][0] + assert len(shares) == 3 + assert browse.call_args[0][1] == 3 + + +def test_split_cancel_at_params(mocker, m5stickv): + from krux.pages import MENU_CONTINUE + + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", return_value="") # cancel + browse = mocker.patch.object(page, "_browse_shares") + assert page.split() == MENU_CONTINUE + browse.assert_not_called() + + +def test_split_decline_confirmation(mocker, m5stickv): + from krux.pages import MENU_CONTINUE + + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", "2"]) + mocker.patch.object(page, "prompt", return_value=False) + browse = mocker.patch.object(page, "_browse_shares") + assert page.split() == MENU_CONTINUE + browse.assert_not_called() + + +def test_choose_scheme_invalid_then_valid(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + # m=9 (invalid) -> guidance -> m=3, n=2 (valid) + mocker.patch.object(page, "capture_from_keypad", side_effect=["9", "2", "3", "2"]) + info = mocker.patch.object(page, "_info") + scheme = page._choose_scheme(256) + assert (scheme.n, scheme.m) == (2, 3) + info.assert_called_once() + + +def test_browse_shares_saved_confirms(mocker, m5stickv): + from krux.pages.home_pages.threshold_backup import ThresholdBackup + from krux.pages import Menu, MENU_EXIT + from krux.kurihara import KuriharaScheme + + page, _ = _page(mocker, _wallet()) + entropy = bytes(32) + shares = KuriharaScheme(2, 3, 256).generate( + entropy, ThresholdBackup._deterministic_randfunc(entropy) + ) + mocker.patch.object(page, "display_mnemonic") + # select the "I saved all shares" item (index == number of shares) + mocker.patch.object(Menu, "run_loop", return_value=(len(shares), MENU_EXIT)) + confirm = mocker.patch.object(page, "prompt", return_value=True) + page._browse_shares(shares, 3) + confirm.assert_called_once() + + +def test_browse_shares_back_exits(mocker, m5stickv): + from krux.pages.home_pages.threshold_backup import ThresholdBackup + from krux.pages import Menu, MENU_EXIT + from krux.kurihara import KuriharaScheme + + page, _ = _page(mocker, _wallet()) + entropy = bytes(32) + shares = KuriharaScheme(2, 3, 256).generate( + entropy, ThresholdBackup._deterministic_randfunc(entropy) + ) + mocker.patch.object(page, "display_mnemonic") + # autospec so the side effect can read the menu's own back_index + mocker.patch.object( + Menu, + "run_loop", + autospec=True, + side_effect=lambda self, **kw: (self.back_index, MENU_EXIT), + ) + prompt = mocker.patch.object(page, "prompt") + page._browse_shares(shares, 3) + prompt.assert_not_called() + + +def test_show_share_displays(mocker, m5stickv): + from krux.pages.home_pages.threshold_backup import ThresholdBackup + from krux.kurihara import KuriharaScheme + from krux.pages import MENU_CONTINUE + + page, _ = _page(mocker, _wallet()) + entropy = bytes(32) + shares = KuriharaScheme(2, 3, 256).generate( + entropy, ThresholdBackup._deterministic_randfunc(entropy) + ) + disp = mocker.patch.object(page, "display_mnemonic") + assert page._show_share(shares[0], 3) == MENU_CONTINUE + disp.assert_called_once() + + +def _make_shares(n, m): + from krux.pages.home_pages.threshold_backup import ThresholdBackup + from krux.kurihara import KuriharaScheme + from embit.bip39 import mnemonic_to_bytes + + entropy = mnemonic_to_bytes(MNEMONIC_24) + shares = KuriharaScheme(n, m, 256).generate( + entropy, ThresholdBackup._deterministic_randfunc(entropy) + ) + return shares, entropy + + +def _wire_recover(mocker, page, share_words, part_ids, m, n): + """Drive capture_from_keypad (m, n, part_ids) and load_key (share words).""" + mocker.patch.object( + page, "capture_from_keypad", side_effect=[str(m), str(n)] + part_ids + ) + pending = list(share_words) + + def fake_load_key(): + page.captured_words = pending.pop(0) + + mocker.patch.object(page, "load_key", side_effect=fake_load_key) + + +def test_restore_returns_mnemonic(mocker, m5stickv): + from krux.kurihara import share_to_mnemonic + + page, _ = _page(mocker, _wallet()) + shares, _ = _make_shares(2, 3) + words = [ + share_to_mnemonic(shares[0]).split(), + share_to_mnemonic(shares[1]).split(), + ] + _wire_recover(mocker, page, words, ["1", "2"], m=3, n=2) + assert page.restore() == MNEMONIC_24 + + +def test_restore_cancel_params(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", return_value="") + assert page.restore() is None + + +def test_restore_cancel_n(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", ""]) + assert page.restore() is None + + +def test_restore_invalid_params(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["9", "2"]) + info = mocker.patch.object(page, "_info") + assert page.restore() is None + info.assert_called_once() + + +def test_restore_cancel_part_id(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", "2", ""]) + assert page.restore() is None + + +def test_restore_cancel_word_entry(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", "2", "1"]) + + def empty_load_key(): + page.captured_words = None + + mocker.patch.object(page, "load_key", side_effect=empty_load_key) + assert page.restore() is None + + +def test_restore_length_mismatch(mocker, m5stickv): + from krux.kurihara import share_to_mnemonic + + page, _ = _page(mocker, _wallet()) + shares, _ = _make_shares(2, 3) + # second share is a 12-word mnemonic -> size mismatch + twelve = ( + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" + ).split() + words = [share_to_mnemonic(shares[0]).split(), twelve] + _wire_recover(mocker, page, words, ["1", "2"], m=3, n=2) + info = mocker.patch.object(page, "_info") + assert page.restore() is None + info.assert_called_once() + + +def test_restore_reconstruct_failure(mocker, m5stickv): + from krux.kurihara import share_to_mnemonic + + page, _ = _page(mocker, _wallet()) + shares, _ = _make_shares(2, 3) + # same share twice -> rank-deficient -> reconstruct raises + words = [ + share_to_mnemonic(shares[0]).split(), + share_to_mnemonic(shares[0]).split(), + ] + _wire_recover(mocker, page, words, ["1", "1"], m=3, n=2) + info = mocker.patch.object(page, "_info") + assert page.restore() is None + info.assert_called_once() + + +def test_load_key_from_words_hook(mocker, m5stickv): + from krux.pages import MENU_EXIT + + page, _ = _page(mocker, _wallet()) + assert page._load_key_from_words(["a", "b"]) == MENU_EXIT + assert page.captured_words == ["a", "b"] + + +def test_info_shows_message(mocker, m5stickv): + page, ctx = _page(mocker, _wallet()) + page._info("hello") + ctx.display.clear.assert_called() + ctx.display.draw_centered_text.assert_called_once() + + +def test_choose_scheme_cancel_n(mocker, m5stickv): + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["3", ""]) + assert page._choose_scheme(256) is None + + +# ----- integration wrappers ----- + + +def test_backup_menu_threshold_split(mocker, m5stickv): + from krux.pages.home_pages.mnemonic_backup import MnemonicsView + from krux.pages.home_pages import threshold_backup as tb_mod + from krux.pages import MENU_CONTINUE + + ctx = create_ctx(mocker, [], wallet=_wallet()) + split = mocker.patch.object( + tb_mod.ThresholdBackup, "split", return_value=MENU_CONTINUE + ) + assert MnemonicsView(ctx).threshold_split() == MENU_CONTINUE + split.assert_called_once() + + +def test_login_restore_loads_recovered(mocker, m5stickv): + from krux.pages.login import Login + from krux.pages.home_pages import threshold_backup as tb_mod + + ctx = create_ctx(mocker, []) + login = Login(ctx) + mocker.patch.object(tb_mod.ThresholdBackup, "restore", return_value=MNEMONIC_24) + loaded = mocker.patch.object(login, "_load_key_from_words", return_value="LOADED") + assert login.restore_from_threshold() == "LOADED" + loaded.assert_called_once_with(MNEMONIC_24.split()) + + +def test_login_restore_cancelled(mocker, m5stickv): + from krux.pages.login import Login + from krux.pages.home_pages import threshold_backup as tb_mod + from krux.pages import MENU_CONTINUE + + ctx = create_ctx(mocker, []) + login = Login(ctx) + mocker.patch.object(tb_mod.ThresholdBackup, "restore", return_value=None) + assert login.restore_from_threshold() == MENU_CONTINUE + + +def test_login_load_key_returns_status(mocker, m5stickv): + from krux.pages.login import Login + from krux.pages import Menu + + ctx = create_ctx(mocker, []) + login = Login(ctx) + # select a load source (index 0, not Back) -> load_key returns its status + mocker.patch.object(Menu, "run_loop", return_value=(0, "STATUS")) + assert login.load_key() == "STATUS" + + +def test_login_load_key_back(mocker, m5stickv): + from krux.pages.login import Login + from krux.pages import Menu, MENU_CONTINUE + + ctx = create_ctx(mocker, []) + login = Login(ctx) + mocker.patch.object( + Menu, + "run_loop", + autospec=True, + side_effect=lambda self, **kw: (self.back_index, MENU_CONTINUE), + ) + assert login.load_key() == MENU_CONTINUE + + +def test_e2e_split_3of5_then_restore(mocker, m5stickv): + """End-to-end: split a seed 3-of-5, then rebuild it from any 3 shares.""" + from krux.kurihara import share_to_mnemonic + from krux.key import Key, TYPE_SINGLESIG + from embit.networks import NETWORKS + + # 1) SPLIT the wallet's mnemonic into 5 shares, threshold 3 + page, _ = _page(mocker, _wallet()) + mocker.patch.object(page, "capture_from_keypad", side_effect=["5", "3"]) + mocker.patch.object(page, "prompt", return_value=True) + captured = {} + mocker.patch.object( + page, + "_browse_shares", + side_effect=lambda shares, m: captured.update(shares=shares, m=m), + ) + page.split() + assert captured["m"] == 5 and len(captured["shares"]) == 5 + + # the 5 mnemonics the user would write down, one per share + saved = {s.part_id: share_to_mnemonic(s) for s in captured["shares"]} + + # 2) RESTORE from ANY 3 of the 5 (here parts 1, 3 and 5), as if at login + fresh, _ = _page(mocker, _wallet()) + chosen = [1, 3, 5] + _wire_recover( + mocker, + fresh, + [saved[i].split() for i in chosen], + [str(i) for i in chosen], + m=5, + n=3, + ) + recovered = fresh.restore() + + # 3) It is exactly the original wallet (same mnemonic AND same fingerprint) + assert recovered == MNEMONIC_24 + original_fp = Key(MNEMONIC_24, TYPE_SINGLESIG, NETWORKS["test"]).fingerprint + rebuilt_fp = Key(recovered, TYPE_SINGLESIG, NETWORKS["test"]).fingerprint + assert rebuilt_fp == original_fp diff --git a/tests/pages/test_stackbit.py b/tests/pages/test_stackbit.py index 7be313213..77adfd69f 100644 --- a/tests/pages/test_stackbit.py +++ b/tests/pages/test_stackbit.py @@ -21,6 +21,7 @@ def test_export_mnemonic_stackbit_standard(mocker, m5stickv, tdata): BUTTON_ENTER, # Select "Back" from Stackbit submenu *([BUTTON_PAGE] * 2), # Go to "Back" in Other Formats BUTTON_ENTER, # Select "Back" from Other Formats + BUTTON_PAGE, # Go to "Threshold Split" in mnemonic menu BUTTON_PAGE, # Go to "Back" in mnemonic menu BUTTON_ENTER, # Select "Back" ], @@ -57,6 +58,7 @@ def test_export_mnemonic_stackbit_standard_amigo(mocker, amigo, tdata): BUTTON_ENTER, # Select "Back" from Stackbit submenu *([BUTTON_PAGE] * 2), # Go to "Back" in Other Formats BUTTON_ENTER, # Select "Back" from Other Formats + BUTTON_PAGE, # Go to "Threshold Split" in mnemonic menu BUTTON_PAGE, # Go to "Back" in mnemonic menu BUTTON_ENTER, # Select "Back" ], @@ -96,6 +98,7 @@ def test_export_mnemonic_stackbit_vertical(mocker, amigo, tdata): BUTTON_ENTER, # Select "Back" from Stackbit submenu *([BUTTON_PAGE] * 2), # Go to "Back" in Other Formats BUTTON_ENTER, # Select "Back" from Other Formats + BUTTON_PAGE, # Go to "Threshold Split" in mnemonic menu BUTTON_PAGE, # Go to "Back" in mnemonic menu BUTTON_ENTER, # Select "Back" ], @@ -136,6 +139,7 @@ def test_export_mnemonic_stackbit_vertical_compact(mocker, m5stickv, tdata): BUTTON_ENTER, # Select "Back" from Stackbit submenu *([BUTTON_PAGE] * 2), # Go to "Back" in Other Formats BUTTON_ENTER, # Select "Back" from Other Formats + BUTTON_PAGE, # Go to "Threshold Split" in mnemonic menu BUTTON_PAGE, # Go to "Back" in mnemonic menu BUTTON_ENTER, # Select "Back" ], diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py index 42fe57273..003692abe 100644 --- a/tests/pages/test_tiny_seed.py +++ b/tests/pages/test_tiny_seed.py @@ -24,6 +24,7 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata): BUTTON_PAGE, # Go to "Back" BUTTON_ENTER, # click on back to return Mnemonic Backup BUTTON_PAGE, + BUTTON_PAGE, BUTTON_ENTER, # click on back to return to home init screen ], ]