add full backup to key-teleport
This commit is contained in:
parent
5c5f8902a1
commit
f23d7f09bf
@ -12,11 +12,14 @@ NFC, passive websites, and QR/BBQr codes.
|
||||
## Steps
|
||||
|
||||
- Receiver picks an EC keypair, stores it in settings, and publishes the pubkey via a QR/NFC
|
||||
- Sender gets that, picks own keypair, and does ECDH to arrive at a shared session key
|
||||
- The pubkey is encrypted by a short 8-digit numeric code, which should be
|
||||
sent by a different channel.
|
||||
- Sender gets QR and numeric code, picks own keypair, and does ECDH to arrive at a
|
||||
shared session key
|
||||
- Sender picks a human-readable secret which is independent of anything else (P key)
|
||||
- The secret data (perhaps a seed phrase, XPRV, secure note, etc) is AES encrypted with P key,
|
||||
then encrypted + MAC added with session key
|
||||
- Data packet is sent to receiver, who can reconstruct the session key via ECDH
|
||||
- The secret data (perhaps a seed phrase, XPRV, secure note, full backup, etc) is
|
||||
AES-256-CTR encrypted with P key, then encrypted + MAC added with session key
|
||||
- Data packet is sent to receiver (via BBQr), 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
|
||||
@ -30,7 +33,7 @@ NFC, passive websites, and QR/BBQr codes.
|
||||
## 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
|
||||
- all transfers are "data typed" and decode only on COLDCARD
|
||||
- Q model is required due to the use of QR codes to ultimately get data into the COLDCARD
|
||||
|
||||
|
||||
@ -45,6 +48,7 @@ The first byte encodes what the package contents (under all the encryption).
|
||||
- `n` - one or many notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes name, source of key)
|
||||
- `p` - binary PSBT to be signed, perhaps multisig but not required.
|
||||
- `b` - complete system backup file (text lines, internal format)
|
||||
|
||||
## QR details
|
||||
|
||||
@ -60,8 +64,10 @@ New type codes for BBQr are defined for the purposes of this application:
|
||||
- `E` for Multisig PSBT: `(randint)(data)` ... randint (4 byte nonce) indicates which
|
||||
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.
|
||||
All the data is encrypted with the exception randint. Keep in mind
|
||||
this is a nonce value picked uniquely for each transfer. The
|
||||
receiver's pubkey is only weakly encrypted by the 8-digit numeric
|
||||
password, but is also a nonce effectively.
|
||||
|
||||
### PSBT Key Selection
|
||||
|
||||
@ -81,7 +87,7 @@ the appropriate pubkeys (and privkey for their side) without communicating
|
||||
more than `randint`. The sending COLDCARD will pick a new random value each time.
|
||||
|
||||
When receiving a multisig PSBT encrypted this way, the receiver does not need
|
||||
to any setup (nor numeric password) and can receive a QR code at any time.
|
||||
to do any setup (nor numeric password) and can receive a QR code at any time.
|
||||
This works because the shared multisig wallet is already setup. Receiver will
|
||||
take the nonce value (randint) and seach all pre-defined multisig wallets for
|
||||
any pubkey that can decrypt the package successfully (based on checksum inside
|
||||
@ -126,7 +132,7 @@ the pubkey QR on another channel. The code is randomly picked, but
|
||||
only represents about 26 bits of entropy and is stretched with
|
||||
a single round of SHA256 before being used as a AES-256-CTR key
|
||||
to decrypt the pubkey. No checksum verifies correct
|
||||
decryption, so any code is accepted, and will with near-100% odds
|
||||
decryption, so any code is accepted, and will with near-50% odds,
|
||||
decrypt to a valid pubkey.
|
||||
|
||||
When the sender is given the receiver's pubkey via QR code, it
|
||||
@ -135,7 +141,7 @@ Thus a MiTM who injects their pubkey will be detected and blocked.
|
||||
|
||||
The "paranoid key" serves the same role in the other direction but
|
||||
it is Base32 character set, so it will not look similar or be
|
||||
confusing.
|
||||
confusing as to its purpose.
|
||||
|
||||
# Web Component
|
||||
|
||||
@ -165,12 +171,16 @@ is optional since the QR can be shown on the Q itself, and 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
|
||||
access to IP addresses or other access 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. All JS resources referenced by the
|
||||
webpage will have content hashes applied to prevent interference,
|
||||
and the site will be served over SSL.
|
||||
|
||||
Visit [keyteleport.com](https://keyteleport.com/), or an
|
||||
[example small QR](https://keyteleport.com/#B$2R0100VHT2AGUUH7KUZUUSTOWOIWHJX3XM7GA2N4BHQOXDFHXLVHVA7K6ZO)
|
||||
and [view source code](https://github.com/coinkite/keyteleport.com).
|
||||
|
||||
# UX Details
|
||||
|
||||
- When the receive process is started by the user, a pubkey is picked
|
||||
@ -185,7 +195,7 @@ and the site will be served over SSL.
|
||||
signed) and the QR is prepared for that receiver. Because we
|
||||
cannot do arbitary conbining, it's best if the next signer continues
|
||||
to teleport the updated PSBT to further signers. In other words,
|
||||
a daisy-chain pattern is prefered to a star patter. The signer
|
||||
a daisy-chain pattern is prefered to a star pattern. The signer
|
||||
who completes the Mth (of N) signature will be able to finalize
|
||||
the transaction, and ideally with PushTx feature, broadcast it.
|
||||
|
||||
@ -197,7 +207,7 @@ We are using 8-character passwords because we want them to be
|
||||
practical to share over non-digital channels such as a voice phone
|
||||
call, or hand-written note.
|
||||
|
||||
It is important to remind users that the passwords should be sent
|
||||
It is very important to remind users that the passwords should be sent
|
||||
by a different channel from the QR itself. Best is to call up your
|
||||
other party and say the letters to them directly.
|
||||
|
||||
|
||||
@ -263,6 +263,25 @@ def restore_from_dict_ll(vals):
|
||||
|
||||
return None, need_ftux
|
||||
|
||||
def text_bk_parser(contents):
|
||||
# given a (binary encoded) text file, decode into a dict of values
|
||||
# - use json rules to decode the "value" sides
|
||||
vals = {}
|
||||
for line in contents.decode().split('\n'):
|
||||
if not line: continue
|
||||
if line[0] == '#': continue
|
||||
|
||||
try:
|
||||
k,v = line.split(' = ', 1)
|
||||
#print("%s = %s" % (k, v))
|
||||
|
||||
vals[k] = ujson.loads(v)
|
||||
except:
|
||||
print("unable to decode line: %r" % line)
|
||||
# but keep going!
|
||||
|
||||
return vals
|
||||
|
||||
async def restore_tmp_from_dict_ll(vals):
|
||||
from glob import dis
|
||||
|
||||
@ -624,19 +643,7 @@ async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary
|
||||
await needs_microsd()
|
||||
return
|
||||
|
||||
vals = {}
|
||||
for line in contents.decode().split('\n'):
|
||||
if not line: continue
|
||||
if line[0] == '#': continue
|
||||
|
||||
try:
|
||||
k,v = line.split(' = ', 1)
|
||||
#print("%s = %s" % (k, v))
|
||||
|
||||
vals[k] = ujson.loads(v)
|
||||
except:
|
||||
print("unable to decode line: %r" % line)
|
||||
# but keep going!
|
||||
vals = text_bk_parser(contents)
|
||||
|
||||
# this leads to reboot if it works, else errors shown, etc.
|
||||
if temporary:
|
||||
|
||||
@ -422,6 +422,7 @@ EmptyWallet = [
|
||||
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
|
||||
MenuItem('Settings', menu=SettingsMenu),
|
||||
ShortcutItem(KEY_QR, predicate=version.has_qr, f=scan_any_qr, arg=(True, False)),
|
||||
]
|
||||
|
||||
# In operation, normal system, after a good PIN received.
|
||||
|
||||
@ -179,11 +179,9 @@ async def kt_start_send(rx_data):
|
||||
|
||||
break
|
||||
|
||||
msg = '''You can now teleport secrets! Select from seed words, seed vault keys, \
|
||||
secure notes or passwords. \
|
||||
|
||||
WARNING: Receiver will have full access to all Bitcoin controlled by these keys!
|
||||
'''
|
||||
msg = '''You can now Key Teleport secrets! Choose what to share on next screen.\
|
||||
\n
|
||||
WARNING: Receiver will have full access to all Bitcoin controlled by these keys!'''
|
||||
|
||||
ch = await ux_show_story(msg, title="Key Teleport: Send")
|
||||
if ch != 'y': return
|
||||
@ -310,6 +308,7 @@ async def kt_accept_values(dtype, raw):
|
||||
- `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
|
||||
- `b` - complete system backup file (text, internal format)
|
||||
'''
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
|
||||
@ -345,6 +344,29 @@ async def kt_accept_values(dtype, raw):
|
||||
sign_transaction(psbt_len, flags=STXN_FINALIZE)
|
||||
return
|
||||
|
||||
elif dtype == 'b':
|
||||
# full system backup, including master: text lines
|
||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict
|
||||
|
||||
vals = text_bk_parser(raw)
|
||||
assert vals # empty?
|
||||
|
||||
from flow import has_secrets
|
||||
|
||||
if has_secrets():
|
||||
# restores as tmp secret and/or offers to save to SeedVault
|
||||
prob = await restore_tmp_from_dict_ll(vals)
|
||||
else:
|
||||
# we have no secret, so... reboot if it works, else errors shown, etc.
|
||||
prob = await restore_from_dict(vals)
|
||||
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
else:
|
||||
# force new rx key because this tfr worked
|
||||
settings.remove_key("ktrx")
|
||||
return
|
||||
|
||||
elif dtype in 'nv':
|
||||
# all are JSON things
|
||||
js = json.loads(raw)
|
||||
@ -502,6 +524,8 @@ class SecretPickerMenu(MenuSystem):
|
||||
msg = 'Master Seed Words' if word_based_seed() else 'Master XPRV'
|
||||
|
||||
m.append( MenuItem(msg, f=self.share_master_secret) )
|
||||
|
||||
m.append( MenuItem("Full COLDCARD Backup", f=self.share_full_backup) )
|
||||
|
||||
super().__init__(m)
|
||||
|
||||
@ -544,6 +568,28 @@ class SecretPickerMenu(MenuSystem):
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 'n', obj=body)
|
||||
|
||||
async def share_full_backup(self, *a):
|
||||
# context, and warn them
|
||||
ch = await ux_show_story("Sending complete backup, including master secret, "
|
||||
"seed vault (if any), multisig wallets, notes/passwords, and all settings! "
|
||||
"The receiving "
|
||||
"COLDCARD must already have the master seed wiped to be able to install "
|
||||
"everything, otherwise only master secret and multisig are saved into a tmp seed. "
|
||||
"OK to proceed?")
|
||||
if ch != 'y': return
|
||||
|
||||
from backups import render_backup_contents
|
||||
|
||||
# renders a text file, with rather a lot of comments; strip them
|
||||
bkup = render_backup_contents(bypass_tmp=True)
|
||||
out = []
|
||||
for ln in bkup.split('\n'):
|
||||
if not ln: continue
|
||||
if ln[0] == '#': continue
|
||||
out.append(ln)
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 'b', raw=b'\n'.join(ln.encode() for ln in out))
|
||||
|
||||
async def share_master_secret(self, _, _2, item):
|
||||
# altho menu items look different we are sharing same thing:
|
||||
# - up to 72 bytes from secure elements
|
||||
|
||||
@ -2207,9 +2207,48 @@ def check_and_decrypt_backup(microsd_path):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def restore_backup_cs(unit_test, pick_menu_item, cap_story, cap_menu,
|
||||
def restore_backup_unpacked(unit_test, pick_menu_item, cap_story, cap_menu,
|
||||
press_select, word_menu_entry, get_setting, is_q1,
|
||||
need_keypress, scan_a_qr, cap_screen, enter_complex):
|
||||
|
||||
# check things are right after unpack & install; FTUX shown
|
||||
def doit(avail_settings=None):
|
||||
|
||||
time.sleep(.3)
|
||||
title, body = cap_story()
|
||||
|
||||
# on simulator Disable USB is always off - so FTUX all the time
|
||||
assert title == 'NO-TITLE' # no Welcome!
|
||||
assert "best security practices" in body
|
||||
assert "USB disabled" in body
|
||||
assert "NFC disabled" in body
|
||||
assert "VirtDisk disabled" in body
|
||||
assert "You can change these under Settings > Hardware On/Off" in body
|
||||
press_select()
|
||||
|
||||
time.sleep(.3)
|
||||
title, body = cap_story()
|
||||
assert title == 'Success!'
|
||||
assert 'has been successfully restored' in body
|
||||
|
||||
if avail_settings:
|
||||
for key in avail_settings:
|
||||
assert get_setting(key)
|
||||
|
||||
# after successful restore - user is in default mode - all OFF
|
||||
# (besides USB on simulator - that is always ON)
|
||||
assert not get_setting("nfc")
|
||||
assert not get_setting("vidsk")
|
||||
|
||||
# avoid simulator reboot; restore normal state
|
||||
unit_test('devtest/abort_ux.py')
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def restore_backup_cs(unit_test, pick_menu_item, cap_story, cap_menu,
|
||||
press_select, word_menu_entry, get_setting, is_q1,
|
||||
need_keypress, scan_a_qr, cap_screen, enter_complex, restore_backup_unpacked):
|
||||
# restore backup with clear seed as first step
|
||||
def doit(fn, passphrase, avail_settings=None, pass_way=None, custom_bkpw=False):
|
||||
unit_test('devtest/clear_seed.py')
|
||||
@ -2244,33 +2283,7 @@ def restore_backup_cs(unit_test, pick_menu_item, cap_story, cap_menu,
|
||||
else:
|
||||
word_menu_entry(passphrase, has_checksum=False)
|
||||
|
||||
time.sleep(.3)
|
||||
title, body = cap_story()
|
||||
# on simulator Disable USB is always off - so FTUX all the time
|
||||
assert title == 'NO-TITLE' # no Welcome!
|
||||
assert "best security practices" in body
|
||||
assert "USB disabled" in body
|
||||
assert "NFC disabled" in body
|
||||
assert "VirtDisk disabled" in body
|
||||
assert "You can change these under Settings > Hardware On/Off" in body
|
||||
press_select()
|
||||
|
||||
time.sleep(.3)
|
||||
title, body = cap_story()
|
||||
assert title == 'Success!'
|
||||
assert 'has been successfully restored' in body
|
||||
|
||||
if avail_settings:
|
||||
for key in avail_settings:
|
||||
assert get_setting(key)
|
||||
|
||||
# after successful restore - user is in default mode - all OFF
|
||||
# (besides USB on simulator - that is always ON)
|
||||
assert not get_setting("nfc")
|
||||
assert not get_setting("vidsk")
|
||||
|
||||
# avoid simulator reboot; restore normal state
|
||||
unit_test('devtest/abort_ux.py')
|
||||
restore_backup_unpacked(avail_settings=avail_settings)
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@ -176,6 +176,31 @@ def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def make_big_notes(settings_set, sim_exec):
|
||||
def doit(count=9):
|
||||
print(">>> Making huge backup file")
|
||||
|
||||
# - to bypass USB msg limit, append as we go
|
||||
notes = []
|
||||
settings_set('notes', [])
|
||||
for n in range(count):
|
||||
v = { fld:('a'*30) if fld != 'misc' else 'b'*1800
|
||||
for fld in ['user', 'password', 'site', 'misc'] }
|
||||
v['title'] = f'Note {n+1}'
|
||||
notes.append(v)
|
||||
rv = sim_exec(cmd := f'settings.current["notes"].append({v!r})')
|
||||
assert 'error' not in rv.lower()
|
||||
|
||||
rv = sim_exec('settings.changed()')
|
||||
assert 'error' not in rv.lower()
|
||||
|
||||
assert len(notes) == count
|
||||
|
||||
return notes
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.mark.qrcode
|
||||
@pytest.mark.parametrize('multisig', [False, 'multisig'])
|
||||
@ -191,7 +216,7 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
|
||||
generate_ephemeral_words, set_bip39_pw, verify_backup_file,
|
||||
check_and_decrypt_backup, restore_backup_cs, clear_ms, seedvault,
|
||||
restore_main_seed, import_ephemeral_xprv, backup_system,
|
||||
press_cancel, sim_exec, pass_way, garbage_collector):
|
||||
press_cancel, sim_exec, pass_way, garbage_collector, make_big_notes):
|
||||
# Make an encrypted 7z backup, verify it, and even restore it!
|
||||
clear_ms()
|
||||
reset_seed_words()
|
||||
@ -201,20 +226,7 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
|
||||
# test larger backup files > 10,000 bytes
|
||||
if multisig == False and st == None and not reuse_pw and not save_pw and not seedvault:
|
||||
# pick just one test case.
|
||||
# - to bypass USB msg limit, append as we go
|
||||
print(">>> Making huge backup file")
|
||||
notes = []
|
||||
settings_set('notes', [])
|
||||
for n in range(9):
|
||||
v = { fld:('a'*30) if fld != 'misc' else 'b'*1800
|
||||
for fld in ['user', 'password', 'site', 'misc'] }
|
||||
v['title'] = f'Note {n+1}'
|
||||
notes.append(v)
|
||||
rv = sim_exec(cmd := f'settings.current["notes"].append({v!r})')
|
||||
print(rv)
|
||||
assert 'error' not in rv.lower()
|
||||
rv = sim_exec(cmd := f'settings.changed()')
|
||||
assert 'error' not in rv.lower()
|
||||
notes = make_big_notes()
|
||||
else:
|
||||
notes = None
|
||||
|
||||
|
||||
@ -5,12 +5,13 @@
|
||||
# - you'll need v1.0.1 of bbqr library for this to work
|
||||
#
|
||||
import pytest, time, re, pdb
|
||||
from helpers import prandom, xfp2str, str2xfp
|
||||
from helpers import prandom, xfp2str, str2xfp, str_to_path
|
||||
from bbqr import join_qrs
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
from base64 import b32encode
|
||||
from constants import *
|
||||
from test_ephemeral import SEEDVAULT_TEST_DATA
|
||||
from test_backup import make_big_notes
|
||||
|
||||
# All tests in this file are exclusively meant for Q
|
||||
#
|
||||
@ -30,6 +31,16 @@ def rx_start(grab_payload, goto_home, pick_menu_item):
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture()
|
||||
def main_do_over(unit_test, settings_get, settings_set):
|
||||
# reset all contents, including master secret ... except ktrx
|
||||
# - so you can test backup-restore onto blank unit
|
||||
def doit():
|
||||
kp = settings_get('ktrx')
|
||||
unit_test('devtest/clear_seed.py')
|
||||
settings_set('ktrx', kp)
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture()
|
||||
def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_story, nfc_block4rf, cap_screen_qr, readback_bbqr):
|
||||
@ -81,14 +92,10 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s
|
||||
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
if tt_code != 'E':
|
||||
qr_data = cap_screen_qr().decode()
|
||||
filetype, qr_raw = join_qrs([qr_data])
|
||||
else:
|
||||
# will be multi-frame BBQr in case of PSBT
|
||||
filetype, qr_raw = readback_bbqr()
|
||||
# this is un-split BBQR which didn't really happen, but useful
|
||||
qr_data = f'B$2{filetype}0100' + b32encode(qr_raw).decode('ascii').rstrip('=')
|
||||
# will be multi-frame BBQr in case of PSBT, other cases usually one frame
|
||||
filetype, qr_raw = readback_bbqr()
|
||||
# this is un-split BBQR which didn't really happen, but useful
|
||||
qr_data = f'B$2{filetype}0100' + b32encode(qr_raw).decode('ascii').rstrip('=')
|
||||
|
||||
assert filetype == tt_code
|
||||
|
||||
@ -163,7 +170,7 @@ def tx_start(press_select, need_keypress, press_cancel, goto_home, pick_menu_ite
|
||||
|
||||
assert title == 'Key Teleport: Send'
|
||||
|
||||
assert 'secure notes' in story
|
||||
assert 'Choose what to share' in story
|
||||
assert 'WARNING' in story
|
||||
press_select()
|
||||
|
||||
@ -224,7 +231,8 @@ def test_tx_quick_note(rx_start, tx_start, cap_menu, enter_complex, pick_menu_it
|
||||
press_select()
|
||||
|
||||
|
||||
def test_tx_master_send(rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select):
|
||||
@pytest.mark.parametrize('testcase', [ 'weak', 'strong'])
|
||||
def test_tx_master_send(testcase, rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select, main_do_over):
|
||||
# Send master secret, but doesn't really work since same as what we have
|
||||
code, rx_pubkey = rx_start()
|
||||
pw = tx_start(rx_pubkey, code)
|
||||
@ -242,15 +250,27 @@ def test_tx_master_send(rx_start, tx_start, cap_menu, enter_complex, pick_menu_i
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data, _ = grab_payload('S')
|
||||
|
||||
if testcase == 'strong':
|
||||
# virginized
|
||||
main_do_over()
|
||||
|
||||
# now, send that back
|
||||
rx_complete(data, pw)
|
||||
|
||||
title, body = cap_story()
|
||||
|
||||
assert title == 'FAILED'
|
||||
assert 'Cannot use master seed as temp' in body
|
||||
assert 'successfully tested' in body
|
||||
if testcase == 'weak':
|
||||
|
||||
assert title == 'FAILED'
|
||||
assert 'Cannot use master seed as temp' in body
|
||||
assert 'successfully tested' in body
|
||||
|
||||
elif testcase == 'strong':
|
||||
# real product would reboot; simulator just quietly goes back to top menu?
|
||||
assert title == ''
|
||||
m = cap_menu()
|
||||
assert m[0] == 'Ready To Sign'
|
||||
|
||||
press_cancel()
|
||||
|
||||
@ -505,7 +525,7 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms,
|
||||
set_master_key,
|
||||
goto_home, press_nfc, nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet):
|
||||
|
||||
# define lots of wallets, do teleport from SD disk
|
||||
# define lots of wallets and do teleport from SD disk
|
||||
|
||||
clear_ms()
|
||||
M, N = 2, 15
|
||||
@ -583,26 +603,109 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms,
|
||||
|
||||
assert title == 'Final TXID'
|
||||
|
||||
'''
|
||||
@pytest.mark.parametrize('N', [14, 20])
|
||||
@pytest.mark.parametrize('M', [2, 14])
|
||||
@pytest.mark.parametrize('incl_xpubs', [ False ])
|
||||
def test_teleport_sd_psbt(M, use_regtest, make_myself_wallet, segwit, dev, clear_ms,
|
||||
fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress,
|
||||
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, ndef_parse_txn_psbt,
|
||||
press_nfc, nfc_read, settings_get, settings_set, open_microsd):
|
||||
|
||||
@pytest.mark.manual
|
||||
def test_teleport_real_ms(dev, fake_ms_txn):
|
||||
#
|
||||
# Do a 2-of-2 w/ USB-attached REAL Q and simulator
|
||||
# - build ms wallet beforehand, both devices (QR); default air-gap settings
|
||||
# - this makes fake txn, sents to (real) device via USB
|
||||
# - do your signature, press (T) to teleport to next
|
||||
# - observe BBQr, but press NFC and capture URL text via keyteleport.com
|
||||
# - get that BBQr string into clipboard, and paste into simulator
|
||||
# - observe working signature on sim side
|
||||
#
|
||||
# py.test test_teleport.py --dev --manual -k test_teleport_real_ms
|
||||
#
|
||||
from bip32 import BIP32Node
|
||||
from struct import unpack
|
||||
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError
|
||||
|
||||
M = N = 2
|
||||
|
||||
#p2wsh
|
||||
deriv = "m/48h/1h/0h/2h"
|
||||
|
||||
# simulator key
|
||||
n = BIP32Node.from_hwif(simulator_fixed_xprv).subkey_for_path(deriv)
|
||||
keys = [ (simulator_fixed_xfp, None, n) ]
|
||||
|
||||
# add device
|
||||
xfp = dev.master_fingerprint
|
||||
xpk = dev.send_recv(CCProtocolPacker.get_xpub(deriv))
|
||||
node = BIP32Node.from_wallet_key(xpk)
|
||||
keys.append((xfp, None, node))
|
||||
|
||||
def p2wsh_mapper(cosigner_idx):
|
||||
# match the default paths created by CC in airgapped MS wallet creation.
|
||||
return str_to_path(deriv)
|
||||
|
||||
psbt = fake_ms_txn(3, 2, M, keys, fee=10000, outvals=None, segwit_in=False,
|
||||
outstyles=['p2pkh'], change_outputs=[], incl_xpubs=False,
|
||||
hack_change_out=False, input_amount=1E8, path_mapper=p2wsh_mapper)
|
||||
|
||||
open('debug/teleport_real_ms.psbt', 'wb').write(psbt)
|
||||
|
||||
ll, sha = dev.upload_file(psbt)
|
||||
dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha))
|
||||
|
||||
print("Follow signing prompts on device, and then do teleport back "
|
||||
"to Simulator via NFC => website => clipboard")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('testcase', [ 'weak', 'partial', 'strong'])
|
||||
def test_send_backup(testcase, rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select, settings_get, settings_set, restore_backup_unpacked, main_do_over, set_encoded_secret, reset_seed_words, make_big_notes):
|
||||
# Send complete backup file.
|
||||
code, rx_pubkey = rx_start()
|
||||
pw = tx_start(rx_pubkey, code)
|
||||
|
||||
keys = import_ms_wallet(M, N, descriptor=descriptor, bip67=bip67)
|
||||
if testcase == 'strong':
|
||||
notes = make_big_notes()
|
||||
|
||||
keys, select_wallet = make_myself_wallet(M, do_import=(not incl_xpubs))
|
||||
'''
|
||||
# other contents require other features to be enabled
|
||||
pick_menu_item('Full COLDCARD Backup')
|
||||
|
||||
title, body = cap_story()
|
||||
|
||||
assert 'Sending complete backup' in body
|
||||
|
||||
press_select()
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data, qr_raw = grab_payload('S')
|
||||
|
||||
if testcase == 'partial':
|
||||
# be on a different master, so backup is restored into seed vault/tmp seed
|
||||
kp = settings_get('ktrx')
|
||||
set_encoded_secret(b'\x20' + prandom(32))
|
||||
settings_set('ktrx', kp)
|
||||
|
||||
if testcase == 'strong':
|
||||
# wipe everything; except we need the keypair
|
||||
main_do_over()
|
||||
|
||||
# now, send that back
|
||||
rx_complete(('S', qr_raw), pw)
|
||||
|
||||
title, body = cap_story()
|
||||
|
||||
if testcase == 'weak':
|
||||
assert title == 'FAILED'
|
||||
assert 'Cannot use master seed as temp' in body
|
||||
assert 'successfully tested' in body
|
||||
press_cancel()
|
||||
|
||||
elif testcase == 'partial':
|
||||
# should be in a tmp seed now
|
||||
assert title == '[0F056943]'
|
||||
assert 'temporary master key is in effect' in body
|
||||
|
||||
reset_seed_words()
|
||||
|
||||
elif testcase == 'strong':
|
||||
restore_backup_unpacked()
|
||||
assert settings_get('notes') == notes
|
||||
settings_set('notes', [])
|
||||
|
||||
# TODO
|
||||
# - send single-sig PSBT
|
||||
# - ms psbt send when lots of unrelated wallets on rx side
|
||||
# - ms psbt from disk file
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user