remove checksum on rx pubkey

This commit is contained in:
Peter D. Gray 2025-03-24 11:02:47 -04:00
parent fcbe05ed68
commit 749459752f
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
4 changed files with 85 additions and 46 deletions

View File

@ -50,7 +50,7 @@ The first byte encodes what the package contents (under all the encryption).
## 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
they are short enough for a normal QR code. Because the BBQr is
being generated by the COLDCARD embedded firmware, it will not be
compressed and will always be Base32 encoded.
@ -116,10 +116,11 @@ When the teleport process is started, the receiver shares his pubkey
as QR. However, we also show an 8-digit numeric password. The
purpose of this is force the receiver to share this separately from
the pubkey QR on another channel. The code is randomly picked, but
only represents about 26 bits on entropy and is stretched with
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. A 2-byte SHA256 checksum verifies correct
decryption and validates the code was correct.
to decrypt the pubkey. No checksum verifies correct
decryption, so any code is accepted, and will with near-100% odds
decrypt to a valid pubkey.
When the sender is given the receiver's pubkey via QR code, it
prompts for the numeric code and uses it to decrypt the pubkey.
@ -152,7 +153,7 @@ single-frame QR, animated PNG, and "stacked BBQr" (a single tall
PNG with each QR frame stacked).
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
to this site with the data to be transferred "after the hash". This
is optional since the QR can be shown on the Q itself, and would
pass the same data.

View File

@ -101,7 +101,7 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
try:
ty, final_size, got = got.storage.finalize()
except BaseException as exc:
import sys; sys.print_exception(exc)
#import sys; sys.print_exception(exc)
raise QRDecodeExplained("BBQr decode failed: " + str(exc))
if expect_bbqr:
@ -139,6 +139,9 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
elif ty in 'RSE':
# key-teleport related
if ty == 'R' and len(got) != 33:
raise QRDecodeExplained("Truncated KT RX")
return 'teleport', (ty, got)
else:
msg = TYPE_LABELS.get(ty, 'Unknown FileType')

View File

@ -97,29 +97,34 @@ def generate_rx_code(kp):
# Receiver-side password: given a pubkey (33 bytes, compressed format)
# - construct an 8-digit decimal "password"
# - it's a AES key, but only 26 bits worth
# - add checksum
pubkey = kp.pubkey().to_bytes() # default: compressed format
pubkey = bytearray(kp.pubkey().to_bytes()) # default: compressed format
#assert len(pubkey) == 33
# I want the code to be deterministic, but I also don't want to save it
nk = ngu.hash.sha256s(kp.privkey() + b'COLCARD4EVER')[4:8]
num = '%08d' % (int.from_bytes(nk, 'big') % 1_0000_0000)
# - want the code to be deterministic, but I also don't want to save it
nk = ngu.hash.sha256d(kp.privkey() + b'COLCARD4EVER')
# first byte will be 0x02 or 0x03 (Y coord) -- remove those known 7 bits
pubkey[0] ^= nk[20] & 0xfe
num = '%08d' % (int.from_bytes(nk[4:8], 'big') % 1_0000_0000)
# encryption after baby key stretch
kk = ngu.hash.sha256s(num.encode())
enc = aes256ctr.new(kk).cipher(pubkey)
enc += ngu.hash.sha256s(pubkey)[-2:]
return num, enc
def decrypt_rx_pubkey(code, payload):
# given a 8-digit numeric code, make the key and then decrypt/checksum check
# - every value works, there is no fail.
kk = ngu.hash.sha256s(code.encode())
rx_pubkey = aes256ctr.new(kk).cipher(payload[:-2])
rx_pubkey = bytearray(aes256ctr.new(kk).cipher(payload))
expect = ngu.hash.sha256s(rx_pubkey)[-2:]
# first byte will be 0x02 or 0x03 but other 7 bits are noise
rx_pubkey[0] &= 0x01
rx_pubkey[0] |= 0x02
return rx_pubkey if expect == payload[-2:] else None
return rx_pubkey
async def tk_show_payload(type_code, payload, title, msg, cta=None):
# show the QR and/or NFC
@ -150,24 +155,15 @@ async def tk_show_payload(type_code, payload, title, msg, cta=None):
async def kt_start_send(rx_data):
# a QR was scanned and it held (most of) a pubkey
# - they want to send to this guy
# - ask for a validate the sender's password
# - ask them what to send, etc
# prompt for numeric password
while 1:
code = await ux_input_text('', confirm_exit=False, hex_only=True, max_len=8,
prompt='Teleport Password (number)', min_len=8, b39_complete=False, scan_ok=False,
placeholder='########', funct_keys=None, force_xy=None)
# - ask for the sender's password -- any value will be accepted
code = await ux_input_text('', confirm_exit=False, hex_only=True, max_len=8,
prompt='Teleport Password (number)', min_len=8, b39_complete=False, scan_ok=False,
placeholder='########', funct_keys=None, force_xy=None)
if not code: return
if not code: return
rx_pubkey = decrypt_rx_pubkey(code, rx_data)
if rx_pubkey:
break
ch = await ux_show_story(
"Incorrect Teleport Password.\n\nYou can try again or CANCEL to stop.")
if ch == 'x': return
rx_pubkey = decrypt_rx_pubkey(code, rx_data)
msg = '''You can now teleport secrets! Select from seed words, seed vault keys, \
secure notes or passwords. \
@ -239,13 +235,16 @@ async def kt_decode_rx(is_psbt, payload):
body = payload[4:]
# may need to iterate over a few wallets?
# TODO: multisig
ses_key, body = decode_step1(pair, his_pubkey, body)
if not ses_key:
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key, etc)
await ux_show_story("QR code is damaged, or was sent to a different user. "
"Sender should start again.", title="Teleport Fail")
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key,
# or the numeric code the sender entered was wrong, etc)
await ux_show_story("QR code was damaged, numeric password was wrong, "
"or it was sent to a different user. "
"Sender must start again.", title="Teleport Fail")
return
from glob import dis
@ -281,15 +280,14 @@ async def kt_decode_rx(is_psbt, payload):
async def kt_accept_values(dtype, raw):
# We got some secret, decode it more, and save it.
'''
- `s` - secret, encoded per stash.py
- `m` - (up to 72 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
'''
- `s` - secret, encoded per stash.py
- `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 chains import current_chain, slip32_deserialize
from flow import has_se_secrets, goto_top_menu
@ -396,7 +394,7 @@ def encode_payload(my_keypair, his_pubkey, noid_key, body):
def decode_step1(my_keypair, his_pubkey, body):
# Do ECDH and remove top layer of encryption
try:
assert len(body) >= 10
assert len(body) >= 3
session_key = my_keypair.ecdh_multiply(his_pubkey)
@ -423,8 +421,9 @@ def decode_step2(session_key, noid_key, body):
async def kt_incoming(type_code, payload):
# incoming BBQr was scanned (via main menu, etc)
from exceptions import QRDecodeExplained
if type_code == 'R':
if type_code == 'R':
# they want to send to this guy
return await kt_start_send(payload)

View File

@ -100,13 +100,14 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s
@pytest.fixture()
def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, enter_complex, cap_screen, goto_home):
# finish the teleport by doing QR and getting data
def doit(data, pw):
def doit(data, pw, expect_fail=False):
goto_home()
need_keypress(KEY_QR)
time.sleep(.250) # required
scan_a_qr(data)
time.sleep(.250) # required
if expect_fail: return
scr = cap_screen()
assert 'Teleport Password (text)' in scr
@ -120,7 +121,7 @@ def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr,
def tx_start(press_select, need_keypress, press_cancel, goto_home, pick_menu_item, cap_story, scan_a_qr, enter_complex, cap_screen):
# start the Tx process, capturing password and leaving you are picker menu
def doit(rx_qr, rx_code):
def doit(rx_qr, rx_code, expect_fail=None):
goto_home()
need_keypress(KEY_QR)
time.sleep(.250) # required
@ -128,6 +129,10 @@ def tx_start(press_select, need_keypress, press_cancel, goto_home, pick_menu_ite
time.sleep(.250) # required
scr = cap_screen()
if expect_fail:
assert expect_fail in scr
return
assert 'Teleport Password (number)' in scr
enter_complex(rx_code)
@ -318,5 +323,36 @@ def test_tx_seedvault(data, rx_start, tx_start, cap_menu, enter_complex, pick_me
pick_menu_item('Restore Master')
press_select()
def test_rx_truncated(rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select):
# Truncate the RX Code
code, rx_pubkey = rx_start()
pw = tx_start(rx_pubkey[:-3], code, expect_fail='Truncated KT RX')
def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select):
# simulate wrong numeric code only -- sender doesn't know
right_code, rx_pubkey = rx_start()
code = '00000000'
pw = tx_start(rx_pubkey, code)
# other contents require other features to be enabled
pick_menu_item('Master Seed Words')
time.sleep(.150) # required?
press_select()
time.sleep(.150) # required?
pw, data = grab_payload('S')
# now, send that back
rx_complete(data, pw, expect_fail=True)
title, body = cap_story()
assert title == 'Teleport Fail'
assert 'password was wrong' in body
assert 'start again' in body
press_cancel()
# EOF