SeedQR & XOR Seed
This commit is contained in:
parent
457d3bd8a3
commit
ea619e0596
@ -25,6 +25,7 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
## 1.2.4Q - 2024-08-xx
|
||||
|
||||
- New Feature: Seed XOR with SeedQR
|
||||
- New Feature: (BB)QR file share
|
||||
- Bugfix: Properly clear LCD screen after BBQR is shown
|
||||
- Bugfix: Writing to empty slot B caused broken card reader
|
||||
|
||||
@ -8,7 +8,7 @@ import ckcc, pyb, version, uasyncio, sys, uos
|
||||
from uhashlib import sha256
|
||||
from uasyncio import sleep_ms
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from utils import imported, problem_file_line, get_filesize
|
||||
from utils import imported, problem_file_line, get_filesize, encode_seed_qr
|
||||
from utils import xfp2str, B2A, addr_fmt_label, txid_from_fname
|
||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
|
||||
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt
|
||||
@ -719,7 +719,7 @@ async def export_seedqr(*a):
|
||||
words = bip39.b2a_words(sv.raw).split(' ')
|
||||
|
||||
dis.busy_bar(False)
|
||||
qr = ''.join('%04d'% bip39.get_word_index(w) for w in words)
|
||||
qr = encode_seed_qr(words)
|
||||
|
||||
del words
|
||||
|
||||
|
||||
@ -2,12 +2,21 @@
|
||||
#
|
||||
# decoders.py - Convert QR (or text) values into useful bitcoin-related objects.
|
||||
#
|
||||
import ngu, bip39, ure
|
||||
# included in Q builds only, not Mk4 --> manifest_q1.py
|
||||
#
|
||||
import ngu, bip39, ure, stash
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from exceptions import QRDecodeExplained
|
||||
from bbqr import TYPE_LABELS
|
||||
from utils import decode_bip21_text
|
||||
|
||||
|
||||
def decode_seed_qr(data):
|
||||
# SeedQR: 4 digit groups of index into word list
|
||||
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
||||
words = [bip39.wordlist_en[int(n)] for n in parts]
|
||||
return words
|
||||
|
||||
def txn_decoding_taster(txt):
|
||||
# look at first 4 bytes, and assume it's txn version number (LE32 0x1 or 0x2), then decode
|
||||
# - working in normal RAM, won't handle full sized txn
|
||||
@ -66,17 +75,15 @@ def decode_secret(got):
|
||||
taste = got.strip().lower()
|
||||
|
||||
if taste.isdigit():
|
||||
# SeedQR: 4 digit groups of index into word list
|
||||
parts = [taste[pos:pos+4] for pos in range(0, len(taste), 4)]
|
||||
try:
|
||||
assert len(parts) in (12, 18, 24)
|
||||
words = [bip39.wordlist_en[int(n)] for n in parts]
|
||||
words = decode_seed_qr(taste)
|
||||
except:
|
||||
raise ValueError('corrupt SeedQR?')
|
||||
assert len(words) in stash.SEED_LEN_OPTS, "seed len"
|
||||
return 'words', words
|
||||
|
||||
words = taste.strip().split(' ')
|
||||
if len(words) in [ 12, 18, 24]:
|
||||
if len(words) in stash.SEED_LEN_OPTS:
|
||||
# looks like bip-39 words, decode and re-expand
|
||||
idx = [bip39.get_word_index(w) for w in words]
|
||||
return 'words', [bip39.wordlist_en[n] for n in idx]
|
||||
|
||||
@ -59,7 +59,7 @@ def bip85_derive(picked, index):
|
||||
|
||||
if picked in (0,1,2):
|
||||
# BIP-39 seed phrases (we only support English)
|
||||
num_words = (12, 18, 24)[picked]
|
||||
num_words = stash.SEED_LEN_OPTS[picked]
|
||||
width = (16, 24, 32)[picked] # of bytes
|
||||
path = "m/83696968h/39h/0h/{num_words}h/{index}h".format(num_words=num_words, index=index)
|
||||
s_mode = 'words'
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
# - has GPIO signal "??" which is multipurpose on its own pin
|
||||
# - this chip chosen because it can disable RF interaction
|
||||
#
|
||||
import utime, ngu, ndef
|
||||
import utime, ngu, ndef, stash
|
||||
from uasyncio import sleep_ms
|
||||
import uasyncio as asyncio
|
||||
from ustruct import pack, unpack
|
||||
@ -693,7 +693,7 @@ class NFCHandler:
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode().strip() # from memory view
|
||||
split_msg = msg.split(" ")
|
||||
if len(split_msg) in (12, 18, 24):
|
||||
if len(split_msg) in stash.SEED_LEN_OPTS:
|
||||
winner = split_msg
|
||||
break
|
||||
|
||||
|
||||
@ -48,8 +48,14 @@ class QRDisplaySingle(UserInteraction):
|
||||
|
||||
# can fail if not enough space in QR
|
||||
self.qr_data = uqr.make(msg, min_version=2,
|
||||
max_version=11 if not has_qwerty else 25,
|
||||
encoding=enc)
|
||||
max_version=11 if not has_qwerty else 25,
|
||||
encoding=enc)
|
||||
|
||||
def idx_hint(self):
|
||||
# draw_qr_display takes this and renders hint in the top right corner
|
||||
# this member function decides what type of hint will be shown
|
||||
# numbers, letters, etc.
|
||||
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
||||
|
||||
def redraw(self):
|
||||
# Redraw screen.
|
||||
@ -66,10 +72,9 @@ class QRDisplaySingle(UserInteraction):
|
||||
self.calc_qr(body)
|
||||
|
||||
# draw display
|
||||
idx_hint = str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
||||
dis.busy_bar(False)
|
||||
dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum,
|
||||
self.sidebar, idx_hint, self.invert)
|
||||
self.sidebar, self.idx_hint(), self.invert)
|
||||
|
||||
async def interact_bare(self):
|
||||
from glob import NFC, dis
|
||||
@ -116,4 +121,10 @@ class QRDisplaySingle(UserInteraction):
|
||||
await self.interact_bare()
|
||||
the_ux.pop()
|
||||
|
||||
|
||||
class XORQRDisplaySingle(QRDisplaySingle):
|
||||
def idx_hint(self):
|
||||
if len(self.addrs) > 1:
|
||||
return chr(65+int(self.start_n + self.idx))
|
||||
|
||||
# EOF
|
||||
|
||||
@ -15,6 +15,9 @@ from uhashlib import sha256
|
||||
from utils import swab32, call_later_ms, B2A
|
||||
|
||||
|
||||
SEED_LEN_OPTS = [12, 18, 24]
|
||||
|
||||
|
||||
class ZeroSecretException(ValueError):
|
||||
# raised when there is no secret or secret is zero
|
||||
pass
|
||||
@ -43,7 +46,7 @@ def len_to_numwords(vlen):
|
||||
|
||||
def numwords_to_len(num_words):
|
||||
# map number of BIP-39 seed words to length of binary secret
|
||||
assert num_words in [12, 18, 24]
|
||||
assert num_words in SEED_LEN_OPTS
|
||||
return (num_words * 8) // 6
|
||||
|
||||
class SecretStash:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# utils.py - Misc utils. My favourite kind of source file.
|
||||
#
|
||||
import gc, sys, ustruct, ngu, chains, ure, time
|
||||
import gc, sys, ustruct, ngu, chains, ure, time, bip39
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import a2b_base64, b2a_base64
|
||||
@ -615,13 +615,26 @@ def datetime_to_str(dt, fmt="%d-%02d-%02d %02d:%02d:%02d"):
|
||||
dts = fmt % (y, mo, d, h, mi, s)
|
||||
return dts + " UTC"
|
||||
|
||||
def censor_address(addr):
|
||||
# We don't like to show the user multisig addresses because we cannot be certain
|
||||
# they are valid and could actually be signed. And yet, dont blank too many
|
||||
# spots or else an attacker could grind out a suitable replacement.
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
def txid_from_fname(fname):
|
||||
if len(fname) >= 64:
|
||||
txid = fname[:64]
|
||||
try:
|
||||
a2b_hex(txid)
|
||||
return txid
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def url_decode(u):
|
||||
# expand control chars from %XX and '+'
|
||||
# - equiv to urllib.parse.unquote_plus
|
||||
# - ure.sub is missing, so not being clever here.
|
||||
# - give up on syntax errors, and return unchanged
|
||||
import ure
|
||||
|
||||
u = u.replace('+', ' ')
|
||||
while 1:
|
||||
pos = u.find('%')
|
||||
@ -685,19 +698,7 @@ def decode_bip21_text(got):
|
||||
|
||||
raise ValueError('not bip-21')
|
||||
|
||||
def censor_address(addr):
|
||||
# We don't like to show the user multisig addresses because we cannot be certain
|
||||
# they are valid and could actually be signed. And yet, dont blank too many
|
||||
# spots or else an attacker could grind out a suitable replacement.
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
def txid_from_fname(fname):
|
||||
if len(fname) >= 64:
|
||||
txid = fname[:64]
|
||||
try:
|
||||
a2b_hex(txid)
|
||||
return txid
|
||||
except: pass
|
||||
return None
|
||||
def encode_seed_qr(words):
|
||||
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -605,11 +605,16 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
|
||||
|
||||
assert num_words and prompt and done_cb
|
||||
|
||||
words = ['' for _ in range(num_words)]
|
||||
def redraw_words(wrds=None):
|
||||
if not wrds:
|
||||
wrds = ['' for _ in range(num_words)]
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, 0, prompt, invert=1)
|
||||
pos = ux_draw_words(2 if num_words != 24 else 1, num_words, words)
|
||||
dis.clear()
|
||||
dis.text(None, 0, prompt, invert=1)
|
||||
p = ux_draw_words(2 if num_words != 24 else 1, num_words, wrds)
|
||||
return wrds, p
|
||||
|
||||
words, pos = redraw_words()
|
||||
|
||||
word_num = 0
|
||||
value = ''
|
||||
@ -647,7 +652,28 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
|
||||
|
||||
commit = False
|
||||
final = (word_num == num_words)
|
||||
if ch == KEY_ENTER:
|
||||
if ch == KEY_QR:
|
||||
try:
|
||||
got = await QRScannerInteraction.scan('Scan seed from a QR code')
|
||||
what, vals = decode_qr_result(got, expect_secret=True)
|
||||
except QRDecodeExplained:
|
||||
redraw_words(words)
|
||||
continue
|
||||
if what != "words":
|
||||
err_msg = "Must be seed words, not %s" % what
|
||||
elif num_words != len(vals[0]):
|
||||
err_msg = "Must be seed of length %d, not %s" % (num_words, len(vals[0]))
|
||||
else:
|
||||
words = vals[0]
|
||||
# offer just the actual imported csum if user deletes csum word
|
||||
last_words = [words[-1]]
|
||||
word_num = num_words
|
||||
|
||||
# needs redraw, empty on error with error below
|
||||
# if success, qr imported words shown to user
|
||||
redraw_words(words)
|
||||
|
||||
elif ch == KEY_ENTER:
|
||||
if final:
|
||||
break
|
||||
commit = True
|
||||
|
||||
@ -5,13 +5,14 @@
|
||||
# - for secret spliting on paper
|
||||
# - all combination of partial XOR seed phrases are working wallets
|
||||
#
|
||||
import stash, ngu, bip39, random
|
||||
import stash, ngu, bip39, version
|
||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_render_words
|
||||
from ux import show_qr_code
|
||||
from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed
|
||||
from glob import settings
|
||||
from actions import goto_top_menu
|
||||
from version import has_qwerty
|
||||
from charcodes import KEY_CANCEL
|
||||
from utils import encode_seed_qr
|
||||
from charcodes import KEY_CANCEL, KEY_QR
|
||||
|
||||
|
||||
def xor(*args):
|
||||
@ -110,6 +111,15 @@ Otherwise, press OK to continue.'''.format(n=num_parts), escape='2')
|
||||
if await ux_confirm("Stop and forget those words?"):
|
||||
return
|
||||
continue
|
||||
if ch == KEY_QR:
|
||||
qrs = []
|
||||
for wl in word_parts:
|
||||
qrs.append(encode_seed_qr(wl))
|
||||
|
||||
from qrs import XORQRDisplaySingle
|
||||
o = XORQRDisplaySingle(qrs, True, 0, sidebar=None)
|
||||
await o.interact_bare()
|
||||
continue
|
||||
|
||||
for ws, part in enumerate(word_parts):
|
||||
ch = await word_quiz(part, title='Word %s%%d is?' % chr(65+ws))
|
||||
@ -127,6 +137,7 @@ import_xor_parts = []
|
||||
async def xor_all_done(new_words):
|
||||
# So we have another part, might be done or not.
|
||||
global import_xor_parts
|
||||
chk_words = None
|
||||
import_xor_parts.append(new_words)
|
||||
target_words = len(new_words)
|
||||
|
||||
@ -138,7 +149,8 @@ async def xor_all_done(new_words):
|
||||
|
||||
msg = "You've entered %d parts so far.\n\n" % num_parts
|
||||
if num_parts >= 2:
|
||||
chk_word = bip39.b2a_words(seed).split(' ')[-1]
|
||||
chk_words = bip39.b2a_words(seed).split(' ')
|
||||
chk_word = chk_words[-1]
|
||||
msg += "If you stop now, the %dth word of the XOR-combined seed phrase\nwill be:\n\n" % target_words
|
||||
msg += "%d: %s\n\n" % (target_words, chk_word)
|
||||
|
||||
@ -153,51 +165,53 @@ async def xor_all_done(new_words):
|
||||
msg += " Or (2) if done with all words."
|
||||
escape += "2"
|
||||
|
||||
ch = await ux_show_story(msg, strict_escape=True, escape='12x'+KEY_CANCEL, sensitive=True)
|
||||
if ch == 'x':
|
||||
# give up
|
||||
import_xor_parts.clear() # concern: we are contaminated w/ secrets
|
||||
return None
|
||||
while True:
|
||||
ch = await ux_show_story(msg, strict_escape=True, escape=escape, sensitive=True, hint_icons=KEY_QR)
|
||||
if ch == 'x':
|
||||
# give up
|
||||
import_xor_parts.clear() # concern: we are contaminated w/ secrets
|
||||
elif chk_words and ch == KEY_QR:
|
||||
rv = encode_seed_qr(chk_words)
|
||||
await show_qr_code(rv, True, msg="SeedQR")
|
||||
continue
|
||||
elif ch == '1':
|
||||
# do another list of words
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)),
|
||||
target_words, done_cb=xor_all_done)
|
||||
else:
|
||||
nxt = XORWordNestMenu(num_words=target_words, done_cb=xor_all_done)
|
||||
the_ux.push(nxt)
|
||||
|
||||
elif ch == '1':
|
||||
# do another list of words
|
||||
if has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)),
|
||||
target_words, done_cb=xor_all_done)
|
||||
else:
|
||||
nxt = XORWordNestMenu(num_words=target_words, done_cb=xor_all_done)
|
||||
the_ux.push(nxt)
|
||||
elif ch == '2':
|
||||
# done; import on temp basis, or be the main secret
|
||||
from pincodes import pa
|
||||
enc = stash.SecretStash.encode(seed_phrase=seed)
|
||||
|
||||
elif ch == '2':
|
||||
# done; import on temp basis, or be the main secret
|
||||
from pincodes import pa
|
||||
enc = stash.SecretStash.encode(seed_phrase=seed)
|
||||
|
||||
if pa.is_secret_blank():
|
||||
# save it since they have no other secret
|
||||
set_seed_value(encoded=enc)
|
||||
# update menu contents now that wallet defined
|
||||
goto_top_menu(first_time=True)
|
||||
else:
|
||||
# set as ephemeral seed, maybe save it too
|
||||
# below is super costly as we need to bip32 generate master secret from entropy bytes
|
||||
# only need XFPs for UI
|
||||
# xfps = [
|
||||
# xfp2str(swab32(
|
||||
# stash.SecretStash.decode(stash.SecretStash.encode(seed_phrase=i))[2].my_fp()
|
||||
# ))
|
||||
# for i in enc_parts
|
||||
# ]
|
||||
await set_ephemeral_seed(
|
||||
enc,
|
||||
meta='SeedXOR(%d parts, check: "%s")' % (
|
||||
num_parts, chk_word
|
||||
if pa.is_secret_blank():
|
||||
# save it since they have no other secret
|
||||
set_seed_value(encoded=enc)
|
||||
# update menu contents now that wallet defined
|
||||
goto_top_menu(first_time=True)
|
||||
else:
|
||||
# set as ephemeral seed, maybe save it too
|
||||
# below is super costly as we need to bip32 generate master secret from entropy bytes
|
||||
# only need XFPs for UI
|
||||
# xfps = [
|
||||
# xfp2str(swab32(
|
||||
# stash.SecretStash.decode(stash.SecretStash.encode(seed_phrase=i))[2].my_fp()
|
||||
# ))
|
||||
# for i in enc_parts
|
||||
# ]
|
||||
await set_ephemeral_seed(
|
||||
enc,
|
||||
meta='SeedXOR(%d parts, check: "%s")' % (
|
||||
num_parts, chk_word
|
||||
)
|
||||
)
|
||||
)
|
||||
goto_top_menu()
|
||||
|
||||
return None
|
||||
goto_top_menu()
|
||||
break
|
||||
|
||||
class XORWordNestMenu(WordNestMenu):
|
||||
def tr_label(self):
|
||||
@ -268,10 +282,10 @@ or press (2) for 18 words XOR.''', escape="12")
|
||||
words = bip39.b2a_words(sv.raw).split(' ')
|
||||
import_xor_parts.append(words)
|
||||
|
||||
if has_qwerty:
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
# if current loaded seed is added to xor - it is always A
|
||||
await seed_word_entry("Part %s Words" % ("B" if import_xor_parts else "A"),
|
||||
await seed_word_entry("Part %s Words" % (chr(65+len(import_xor_parts))),
|
||||
desired_num_words, done_cb=xor_all_done)
|
||||
else:
|
||||
return XORWordNestMenu(num_words=desired_num_words, done_cb=xor_all_done)
|
||||
|
||||
@ -8,6 +8,7 @@ from mnemonic import Mnemonic
|
||||
from constants import simulator_fixed_words
|
||||
from xor import prepare_test_pairs
|
||||
from test_ux import word_menu_entry, pass_word_quiz
|
||||
from charcodes import KEY_QR, KEY_RIGHT
|
||||
|
||||
wordlist = Mnemonic('english').wordlist
|
||||
|
||||
@ -54,7 +55,7 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
confirm_tmp_seed, seed_vault_enable, press_select,
|
||||
scan_a_qr, is_q1, cap_screen_qr, cap_screen):
|
||||
def doit(parts, expect, incl_self=False, save_to_vault=False,
|
||||
is_master_tmp_fail=False):
|
||||
is_master_tmp_fail=False, way=None):
|
||||
if expect is None:
|
||||
parts, expect = prepare_test_pairs(*parts)
|
||||
|
||||
@ -92,6 +93,7 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
else:
|
||||
press_select()
|
||||
|
||||
wordlist = Mnemonic('english').wordlist
|
||||
for n, part in enumerate(parts):
|
||||
if n == 0 and incl_self:
|
||||
continue
|
||||
@ -104,9 +106,25 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
else:
|
||||
assert what in scr
|
||||
|
||||
word_menu_entry(part.split())
|
||||
if way and "qr" in way:
|
||||
assert is_q1
|
||||
need_keypress(KEY_QR)
|
||||
time.sleep(.1)
|
||||
if way == "seedqr":
|
||||
qr = ''.join('%04d' % wordlist.index(w) for w in part.split())
|
||||
else:
|
||||
qr = ' '.join(w[:4] for w in part.split())
|
||||
scan_a_qr(qr)
|
||||
for _ in range(20):
|
||||
scr = cap_screen()
|
||||
if 'Valid words' in scr:
|
||||
break
|
||||
time.sleep(.1)
|
||||
press_select()
|
||||
else:
|
||||
word_menu_entry(part.split())
|
||||
|
||||
time.sleep(0.01)
|
||||
time.sleep(.1)
|
||||
title, body = cap_story()
|
||||
assert f"You've entered {n + 1} parts so far" in body
|
||||
if n+1 > 1:
|
||||
@ -123,6 +141,13 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
if expect == zeros[num_words]:
|
||||
assert 'ZERO WARNING' in body
|
||||
|
||||
if is_q1:
|
||||
need_keypress(KEY_QR)
|
||||
qr = cap_screen_qr().decode('ascii')
|
||||
parts = [qr[pos:pos + 4] for pos in range(0, len(qr), 4)]
|
||||
assert [wordlist[int(n)] for n in parts] == expect.split()
|
||||
press_select()
|
||||
|
||||
need_keypress('2')
|
||||
try:
|
||||
confirm_tmp_seed(seedvault=save_to_vault)
|
||||
@ -139,6 +164,7 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.mark.parametrize('way', ["qr", "seedqr", "classic"])
|
||||
@pytest.mark.parametrize('incl_self', [False, True])
|
||||
@pytest.mark.parametrize('seed_vault', [False, True])
|
||||
@pytest.mark.parametrize('parts, expect', [
|
||||
@ -160,8 +186,10 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
|
||||
# random generated
|
||||
*random_test_cases()
|
||||
])
|
||||
def test_import_xor(seed_vault, incl_self, parts, expect, restore_seed_xor):
|
||||
restore_seed_xor(parts, expect, incl_self, seed_vault)
|
||||
def test_import_xor(seed_vault, incl_self, parts, expect, restore_seed_xor, way, is_q1):
|
||||
if not is_q1 and "qr" in way:
|
||||
raise pytest.skip("Q only")
|
||||
restore_seed_xor(parts, expect, incl_self, seed_vault, way=way)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('incl_self', [False, True])
|
||||
@ -186,7 +214,7 @@ def test_import_xor_zeros_ones(incl_self, parts, expect, restore_seed_xor):
|
||||
@pytest.mark.parametrize('trng', [False, True])
|
||||
def test_xor_split(num_words, qty, trng, goto_home, pick_menu_item, cap_story, need_keypress,
|
||||
cap_menu, get_secrets, pass_word_quiz, set_seed_words, press_select,
|
||||
seed_story_to_words, is_q1):
|
||||
seed_story_to_words, is_q1, cap_screen_qr):
|
||||
|
||||
set_seed_words(proper[num_words])
|
||||
|
||||
@ -228,6 +256,19 @@ def test_xor_split(num_words, qty, trng, goto_home, pick_menu_item, cap_story, n
|
||||
assert all(len(prt) == num_words for prt in parts)
|
||||
chk_word = seed_story_to_words(chk_prt)[0]
|
||||
assert chk_word
|
||||
|
||||
need_keypress(KEY_QR)
|
||||
p_all = []
|
||||
for i in range(len(parts)):
|
||||
p = cap_screen_qr().decode("ascii") # SeedQR
|
||||
pparts = [p[pos:pos + 4] for pos in range(0, len(p), 4)]
|
||||
pwords = [wordlist[int(n)] for n in pparts]
|
||||
p_all.append(pwords)
|
||||
need_keypress(KEY_RIGHT)
|
||||
time.sleep(.1)
|
||||
|
||||
press_select() # exit QR display
|
||||
assert p_all == parts
|
||||
else:
|
||||
words = [ln[4:] for ln in body.split('\n') if ln[2:4] == ': ']
|
||||
parts = [words[pos:pos + num_words] for pos in range(0, num_words * qty, num_words)]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user