Rebased
This commit is contained in:
parent
3ebde0ea34
commit
0aa0fc4500
151
docs/key-teleport.md
Normal file
151
docs/key-teleport.md
Normal 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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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. "
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
340
shared/teleport.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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('''\
|
||||
|
||||
Loading…
Reference in New Issue
Block a user