This commit is contained in:
Peter D. Gray 2025-03-17 17:30:19 -04:00 committed by doc-hex
parent 3ebde0ea34
commit 0aa0fc4500
14 changed files with 557 additions and 32 deletions

151
docs/key-teleport.md Normal file
View File

@ -0,0 +1,151 @@
# Key Teleport
Purpose: Send a small quantity of very secret data between two COLDCARD Q systems, with
no risk of anything in the middle learning the secret.
Method: ECDH and AES-256-CTR plus an extra wrapping layer, transmitted over a mixture of
NFC, passive websites, and QR/BBQr codes.
# Protocol Overview
## Steps
- Receiver picks an EC keypair, stores it in settings, and publishes the pubkey via a QR/NFC
- Sender gets that, pickes own keypair, and does ECDH to arrive at a shared session key
- Sender picks a human-readable secret (12 words) which is independant of anything else (P key)
- The secret data (perhaps a seed phrase, XPRV, secure note, etc) is AES encryped with P key,
then encrypted + MAC added with session key
- Data packet is sent to receiver, who can reconstruct the session key via ECDH
- Prompt user for the P key to finish decoding
- Decoded secret value is saved to Seed Vault or secure notes as appropriate
- Receiver destroys EC keypair used in transfer
### When used for PSBT Multisig
- No action required on receiver
- Sender uses the pubkey of the first unsigned input as receiver's pubkey
- Same steps, but drops immediately into signing process when decoded correctly
## Notes and Limitations
- max 4k (after encoding) of data is possible due to HTTP limitations
- all transfers are "data typed" and decode only expected on COLDCARD
- Q model is required due to the use of QR codes to ulitmately get data into the COLDCARD
# Details
## Data Type Codes
The first byte encodes what the package contents (under all the encryption).
- `w` - 12/18/24 words - 16/24/32 bytes follow
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
- `n` - one or many notes export (JSON array)
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
- `p` - binary PSBT to be signed
- `P` - a more-signed binary PSBT being returned back to sender
## QR details
BBQr is always used for the QR's involved in this process, even if they
are short enough for a normal QR code. Becasuse the BBQr is being
generated on the COLDCARD, it will not be compressed and will be
Base32 encoded.
New type codes for BBQr are defined for the purposes of this application:
- `R` contains `(pubkey)` ... begins the process from receiver; pubkey is base32 of 33 bytes
- `S` contains `(pubkey)(data)` ... data from sender; all base32 encoded, and first 33 bytes
are sender's pubkey
- `E` for PSBT: `(randint)(data)` ... randint (4 bytes) indicates which a randomly
selected derived subkey from pre-shared xpub associated with receiver
All the data is encrypted with the exception of the pubkey or randint. Keep in mind
those are both nonce values picked uniquely for each transfer.
### PSBT Key Picking
When sending PSBT data, the keys involved are picked at random by the sender in range:
5000..(2^30).
This is called `randint`. The receiver's pubkey will be
.../20250317/(randint)
where `...` is the derivation used in the multisig setup for the co-signer who will
receive the package. The sender's keypair is implied by:
.../20250318/(randint)
Because both the sender and receiver have each other's XPUB they can derive
the appropriate pubkeys (and privkey for their side) without communicating anymore
more than `randint`. The sending COLDCARD will pick a new random value each time.
## Encryption Details
AES-256-CTR is used exclusively. Session key is picked via ECDH with final
key value being the SHA256 over 64 bytes of coordinate X (concat) Y.
While ECDH is enough to assure privacy from men in the middle, we
add an additional layer of encryption, using 12 BIP-39 words with
checksum (128 bits). We call this the "paranoid key" internally.
- ECDH arrives at session key
- decrypt (AES-256-CTR) the binary body of message
- verify checksum:
- final 2 bytes should be `== SHA256(decrypted body[0:-2])[-2:]`
- if not, corruption, truncation, or wrong keys
- if that decryption is correct, then prompt user for the paranoid key (12 words)
- concat paranoid key (16 bytes) with first 16 bytes of the session key and run AES-256-CTR again
- same checksum of 2 bytes of SHA256 are found inside after decryption
Encryption adds 4 bytes of overhead because of these MAC values,
but should catch truncation and bitrot. There are no other
protections against truncation as length data is not transmitted.
# Web Component
In order to "teleport" the contents of a QR code over NFC, we will
publish a static website directly from an open Github repository.
The single-page website contains javascript code which looks at the
"hash" part of the incoming URL (`window.location.hash`) and if it
meets the requirements, renders a large QR. The QR data must look like
a correctly-ecoded BBQr with one of the type codes above.
(otherwise the website could render any QR, which we don't want to
support).
The page will offer "copy to clipboard" features for the data inside
the QR as a URL (ie. same URL as shown) and as an image and of course,
the COLDCARD Q can scan from the web browser screen itself.
On the COLDCARD side, when NFC is tapped, it will offer a long
URL to this site with the data to be transfered "after the hash".
This is optional since the QR shown on the Q itself, would pass
the same data.
Since the website is running on Github, Coinkite does not have
access to IP addresses or other log details. Because the data for
teleport is "after the hash" it is never sent to Github's servers
but remains in the browser only.
# UX Details
- When the receive process is started by the user, a pubkey is picked
and stored, so that they can come back later (after a power cycle)
and make use of the data encoded by the sender. However once a package
is decoded successfully, that key is deleted.
- Sender must start by scanning the QR from a receiver. Then can pick what
to send, from secure notes to seeds and so on.
- For PSBT multisig, user must pick a single co-signer (who hasn't already
signed) and the QR is prepared for that receiver. They should get another
chance to do the same for the other possible co-signers.
- If the user opts to skip the "paranoid key" then treat it as `bytes([0x5a] * 32)`,
but still do the extra decryption and MAC check.

View File

@ -12,7 +12,8 @@ b32encode = ngu.codecs.b32_encode
b32decode = ngu.codecs.b32_decode
TYPE_LABELS = dict(P='PSBT File', T='Transaction', J='JSON', C='CBOR', U='Unicode Text',
X='Executable', B='Binary')
X='Executable', B='Binary',
R='KT Rx', S='KT Tx', E='KT PSBT')
def int2base36(n):
# convert an integer to two digits of base 36 string. 00 thu ZZ as bytes

View File

@ -136,6 +136,10 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
what = "smsg"
return what, (got,)
elif ty in 'RSE':
# key-teleport related
return 'teleport', (ty, got)
else:
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
raise QRDecodeExplained("Sorry, %s not useful." % msg)

View File

@ -39,11 +39,13 @@ if version.has_battery:
from battery import battery_idle_timeout_chooser, brightness_chooser
from q1 import scan_and_bag
from notes import make_notes_menu
from teleport import kt_start_rx
else:
battery_idle_timeout_chooser = None
brightness_chooser = None
scan_and_bag = None
make_notes_menu = None
kt_start_rx = None
#
@ -253,6 +255,7 @@ AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('Perform Selftest', f=start_selftest),
MenuItem("I Am Developer.", menu=maybe_dev_menu),
@ -362,6 +365,7 @@ AdvancedNormalMenu = [
f=drv_entro_start),
MenuItem("View Identity", f=view_ident),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem('Paper Wallets', f=make_paper_wallet),
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "

View File

@ -806,8 +806,12 @@ class Display:
else:
pat = '' # clear line
self.text(None, -3, pat)
if count == hdr.num_parts and count == 1:
# skip the BS, it's a simple one
self.progress_bar_show(1)
return
self.text(None, -3, pat)
self.text(None, -2, 'Keep scanning more...' if count < hdr.num_parts else 'Got all parts!')
self.text(None, -1, '%s: %d of %d parts' % (hdr.file_label(), count, hdr.num_parts),
dark=True)

View File

@ -1,4 +1,4 @@
# Q1/Mk4 only files; would not be needed on Mk3 or earlier.
# Q1 only files; would not be needed on Mk4
freeze_as_mpy('', [
'psram.py',
'mk4.py',
@ -18,6 +18,7 @@ freeze_as_mpy('', [
'battery.py',
'notes.py',
'calc.py',
'teleport.py',
], opt=0)
# Optimize data-like files, since no need to debug them.

View File

@ -619,7 +619,13 @@ async def import_from_other(menu, *a):
records = json.load(open(fn, 'rt'))
# We have some JSON, parsed now.
# - should dedup, but we aren't
await import_from_json(records)
await ux_dramatic_pause('Saved.', 3)
menu.update_contents()
async def import_from_json(records):
# should dedup, but we aren't
try:
assert 'coldcard_notes' in records, 'Incorrect format'
@ -634,9 +640,5 @@ async def import_from_other(menu, *a):
except Exception as e:
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
await ux_dramatic_pause('Saved.', 3)
menu.update_contents()
# EOF

View File

@ -66,6 +66,8 @@ from utils import call_later_ms
# unsort_ms = (bool) Allow unsorted multisig with BIP-67 disabled
# msas = multisig address show (do not censor multisig addresses)
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
# kttx = (words) Key teleport Tx: last words used (paranoid key)
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)

View File

@ -18,7 +18,7 @@ class QRDisplaySingle(UserInteraction):
# Show a single QR code for (typically) a list of addresses, or a single value.
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None,
is_addrs=False, force_msg=False):
is_addrs=False, force_msg=False, allow_nfc=True):
self.is_alnum = is_alnum
self.idx = 0 # start with first address
self.invert = False # looks better, but neither mode is ideal
@ -29,6 +29,7 @@ class QRDisplaySingle(UserInteraction):
self.msg = msg
self.qr_data = None
self.force_msg = force_msg
self.allow_nfc = allow_nfc
def calc_qr(self, msg):
# Version 2 would be nice, but can't hold what we need, even at min error correction,
@ -95,13 +96,15 @@ class QRDisplaySingle(UserInteraction):
self.redraw()
continue
elif NFC and (ch == '3' or ch == KEY_NFC):
# Share any QR over NFC!
await NFC.share_text(self.addrs[self.idx])
self.redraw()
if not self.allow_nfc:
# not a valid as text over NFC sometimes; treat as cancel
break
else:
# Share any QR over NFC!
await NFC.share_text(self.addrs[self.idx])
self.redraw()
continue
elif ch in 'xy'+KEY_ENTER+KEY_CANCEL:
if dis.has_lcd:
dis.real_clear() # bugfix
break
elif len(self.addrs) == 1:
continue
@ -123,6 +126,10 @@ class QRDisplaySingle(UserInteraction):
self.qr_data = None
self.redraw()
# bugfix
if dis.has_lcd:
dis.real_clear()
async def interact(self):
await self.interact_bare()
the_ux.pop()

View File

@ -473,6 +473,7 @@ async def add_seed_to_vault(encoded, meta=None):
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
is_restore=False, meta=None):
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
if not is_restore:
await add_seed_to_vault(encoded, meta=meta)
dis.fullscreen("Wait...")
@ -637,8 +638,8 @@ def xprv_to_encoded_secret(xprv):
def set_seed_value(words=None, encoded=None, chain=None):
# Save the seed words (or other encoded private key) into secure element,
# and reboot. BIP-39 passphrase is not set at this point (empty string).
# Save the seed words (or other encoded private key) into secure element.
# BIP-39 passphrase is not set at this point (empty string).
if words:
nv = seed_words_to_encoded_secret(words)
else:

340
shared/teleport.py Normal file
View File

@ -0,0 +1,340 @@
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# teleport.py - Magically transport extremely sensitive data between the
# secure environment of two Q's.
#
import utime, uzlib, ngu, aes256ctr, bip39
from utils import problem_file_line, xor
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from glob import settings
from ux import ux_show_story, ux_confirm, show_qr_code
from ux_q1 import show_bbqr_codes, QRScannerInteraction, seed_word_entry, ux_render_words
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from bbqr import int2base36, b32encode
KT_DOMAIN = 'keyteleport.com'
'''
- `w` - 12/18/24 words - 16/24/32 bytes follow
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
- `n` - secure note or password (JSON)
- `e` - full notes export (JSON array)
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
- `p` - binary PSBT to be signed
- `P` - a more-signed binary PSBT being returned back to sender
'''
def short_bbqr(type_code, data):
# Short-circuit basic BBQr encoding here: always Base32, single part: 1 of 1
#hdr = 'B$' + encoding + type_code + int2base36(num_parts) + int2base36(pkt)
hdr = 'B$2' + type_code + int2base36(1) + int2base36(0)
return hdr + b32encode(data)
async def nfc_push_kt(qrdata):
# NFC push to send them to our QR-rendering website
import ndef
url = KT_DOMAIN + '#' + qrdata
n = ndef.ndefMaker()
n.add_url(url, https=True)
from glob import NFC
await NFC.share_loop(n, prompt=KT_DOMAIN, line2="View QR on web")
async def kt_start_rx(*a):
# menu item to "start a receive" operation
rx_key = settings.get("ktrx")
if rx_key:
# Maybe re-use same one? Vaguely risky? Concern is they are confused and
# we don't want to lose the pubkey if they should be scanning not here.
ch = await ux_show_story('''Looks like last attempt wasn't completed. \
You need to do QR scan of data from the sender to move to the next step. \
We will re-use same values as last try, unless you press (R) for new values to be picked.''',
title='Reuse Pubkey?', escape='r'+KEY_QR, hint_icons=KEY_QR)
if ch == KEY_QR:
# help them scan now!
x = QRScannerInteraction()
await x.scan_anything(expect_secret=False, tmp=False)
return
elif ch == 'r':
# wipe and restart; sender's work might be lost
rx_key = None
else:
# keep old keypair -- they might be confused
kp = ngu.secp256k1.keypair(a2b_hex(rx_key))
if not rx_key:
# pick a random key pair, just for this session
kp = ngu.secp256k1.keypair()
settings.set("ktrx", b2a_hex(kp.privkey()))
settings.save()
pubkey = kp.pubkey().to_bytes(False) # compressed format
msg = '''You are starting a teleport of sensitive data from another COLDCARD.\n
It will be double-encrypted with AES-256-CTR using ECDH for one-time key and also \
an optional passphrase.\n
You must show this QR code to the sender somehow. %s to show now''' % KEY_QR
await tk_show_payload('R', pubkey, 'Key Teleport: Receive', 'Show to Sender', msg)
async def tk_show_payload(type_code, pubkey, title, cta=None, msg=None):
# show the QR and/or NFC
from glob import NFC
# XXX proper BBQr for sending data?
# - make easier to pick NFC from QR
qr = short_bbqr(type_code, pubkey)
hints = KEY_QR
if NFC:
hints += KEY_NFC
if msg:
msg += ' or %s to view on your phone' % KEY_NFC
if msg:
msg += '. CANCEL to stop.'
# simply show the QR
while 1:
if msg:
ch = await ux_show_story(msg, title=title, hint_icons=hints)
else:
ch = KEY_QR
if ch == KEY_NFC and NFC:
await nfc_push_kt(qr)
elif ch == KEY_QR or ch == 'y':
await show_qr_code(qr, is_alnum=True, msg=cta, force_msg=True, allow_nfc=False)
elif ch == 'x':
return
async def kt_start_send(rx_pubkey):
# they want to send to this guy, ask them what to send, etc
msg = '''You can now teleport secrets. You can select from seed words, temporary keys,
secure notes and passwords. \
WARNING: \
The other COLDCARD will have full access to all Bitcoin controlled by these keys!
'''
ch = await ux_show_story(msg, title="Key Teleport: Send")
# TODO: pick what to send, somehow ...
body = b'w'+ (b'A'*16)
# Pick and show noid key to sender
noid_key = ngu.random.bytes(16)
msg = "Share passphrase with receiver, via some different channel:"
msg += ux_render_words(bip39.b2a_words(noid_key).split())
await ux_show_story(msg, 'Paranoid Key')
# throw away entropy
my_keypair = ngu.secp256k1.keypair()
words = bip39.b2a_words(noid_key).split(' ')
payload = encode_payload(my_keypair, rx_pubkey, noid_key, body)
await tk_show_payload('S', payload, None, 'Show to Receiver')
async def kt_decode_rx(is_psbt, payload):
# we are getting data back from a sender, decode it.
if not is_psbt:
rx_key = settings.get("ktrx")
if not rx_key:
await ux_show_story("Not expecting any teleports. You need to start over.")
return
his_pubkey = payload[0:33]
body = payload[33:]
pair = ngu.secp256k1.keypair(a2b_hex(rx_key))
else:
randint = payload[0:4]
body = payload[4:]
# may need to iterate over a few wallets?
ses_key, body = await decode_step1(pair, his_pubkey, body)
if not ses_key:
await ux_show_story("QR code is damaged or incorrect.\n\n" + body, title="Decode Fail")
return
while 1:
# ask for noid key
words = await seed_word_entry('Paranoid Key', 12, has_checksum=True)
if not words:
noid_key = b'\x5a' * 16
noid_key = bip39.a2b_words(words)
final = decode_step2(ses_key, noid_key, body)
if final is not None:
break
ch = await ux_show_story("Incorrect Paranoid Key. You can try again or CANCEL to stop.")
if ch == 'x': return
# will ask again
await kt_accept_values(final[0].decode(), final[1:])
async def kt_accept_values(dtype, raw):
# got the secret, decode it more
'''
- `w` - 12/18/24 words - 16/24/32 bytes follow
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
- `n` - one or many notes export (JSON array)
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
- `p` - binary PSBT to be signed
- `P` - a more-signed binary PSBT being returned back to sender
'''
from stash import SecretStash
from chains import current_chain, slip32_deserialize
enc = None
meta = 'Teleported'
if dtype == 'w':
# words.
assert len(raw) in { 16, 24, 32 }
enc = SecretStash.encode(seed_phrase=raw)
elif dtype == 'm':
enc = SecretStash.encode(master_secret=raw)
elif dtype == 'r':
assert len(raw) == 64
enc = b'\x01' + raw
elif dtype == 'x':
# it's an XPRV, but in binary.. some extra data we throw away here; sigh
txt = ngu.codecs.b58_encode(raw)
node, ch, _, _ = slip32_deserialize(txt)
assert ch.name == chains.current_chain.name, 'wrong chain'
enc = SecretStash.encode(node=node)
elif dtype in 'pP':
# raw PSBT -- bigger
from auth import sign_transaction
psbt_len = len(raw)
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
out.write(raw)
# this will take over UX w/ the signing process
sign_transaction(psbt_len, flags=0x0)
return
elif dtype in 'nv':
# all are JSON things
js = loads(raw)
if dtype == 'v':
# one key export from a seed vault
enc = a2b_hex(js[1])
meta = js[2]
elif dtype == 'n':
# secure note(s)
from notes import import_from_json
await import_from_json(dict(coldcard_notes=js))
await ux_dramatic_pause('Imported.', 3)
# TODO: force them into notes submenu so they see result?
return
else:
raise ValueError(dtype)
# key material is arriving; offer to use as main secret or tmp or seed vault
assert enc
#summary = SecretStash.summary(enc[0])
from flow import has_se_secrets, goto_top_menu
from seed import set_ephemeral_seed, set_seed_value
if not has_se_secrets():
# unit has nothing, so this will be the master seed
set_seed_value(encoded=enc)
ok = True
else:
ok = await set_ephemeral_seed(enc, meta=meta)
if ok:
goto_top_menu()
def encode_payload(my_keypair, his_pubkey, noid_key, body):
# do all the encryption
assert len(his_pubkey) == 33
assert len(noid_key) == 16
session_key = my_keypair.ecdh_multiply(his_pubkey)
b1 = aes256ctr.new(session_key).cipher(body)
b1 += ngu.hash.sha256s(b1)[-2:]
b2 = aes256ctr.new(noid_key + session_key[16:]).cipher(b1)
b2 += ngu.hash.sha256s(b2)[-2:]
return my_keypair.pubkey().to_bytes(True) + b2
def decode_step1(my_keypair, his_pubkey, body):
# Do ECDH and get out next layer of encryption
try:
assert len(his_pubkey) == 33
assert len(body) >= 10
session_key = my_keypair.ecdh_multiply(his_pubkey)
body = aes256ctr.new(session_key).cipher(payload[:-2])
chk = sha256s(body)[-2:]
assert chk == payload[-2:], 'first checksum'
except Exception as exc:
ln = problem_file_line(exc)
return None, ln
return session_key, body
def decode_step2(session_key, noid_key, body):
tk = noid_key + session_key[16:]
msg = aes256ctr.new(noid_key + session_key[16:]).cipher(body[:-2])
chk = sha256(msg)[:-2]
return msg if chk == msg[-2:] else None
async def kt_incoming(type_code, payload):
# incoming BBQr was scanned (via main menu, etc)
if type_code == 'R':
# they want to send to this guy
return await kt_start_send(payload)
elif type_code == 'S':
# we are receiving something, let's try to decode
return await kt_decode_rx(False, payload)
elif type_code == 'E':
# incoming PSBT!
return await kt_decode_rx(True, payload)
# EOF

View File

@ -745,4 +745,17 @@ def chunk_checksum(fd, chunk=1024):
return md.digest()
def xor(*args):
# bit-wise xor between all args
vlen = len(args[0])
# all have to be same length
assert all(len(e) == vlen for e in args)
rv = bytearray(vlen)
for i in range(vlen):
for a in args:
rv[i] ^= a[i]
return rv
# EOF

View File

@ -593,7 +593,7 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
from glob import dis
from ux import ux_confirm
assert num_words and prompt and done_cb
assert num_words and prompt
def redraw_words(wrds=None):
if not wrds:
@ -751,7 +751,10 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
else:
err_msg = 'Next key: ' + nextchars
await done_cb(words)
if done_cb:
await done_cb(words)
return words
def ux_dice_rolling():
from glob import dis
@ -987,6 +990,11 @@ class QRScannerInteraction:
await ux_visualize_textqr(txt)
return
if what == 'teleport':
from teleport import kt_incoming
await kt_incoming(*vals)
return
# not reached?
problem = 'Unhandled: ' + what

View File

@ -12,23 +12,10 @@ from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed
from glob import settings
from menu import MenuSystem, MenuItem
from actions import goto_top_menu
from utils import encode_seed_qr, pad_raw_secret
from utils import encode_seed_qr, pad_raw_secret, xor
from charcodes import KEY_QR
from stash import SecretStash, blank_object, SensitiveValues, numwords_to_len, len_to_numwords
def xor(*args):
# bit-wise xor between all args
vlen = len(args[0])
# all have to be same length
assert all(len(e) == vlen for e in args)
rv = bytearray(vlen)
for i in range(vlen):
for a in args:
rv[i] ^= a[i]
return rv
async def xor_split_start(*a):
ch = await ux_show_story('''\