firmware/shared/tapsigner.py
2025-03-28 15:08:57 -04:00

109 lines
3.9 KiB
Python

# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# tapsigner.py - TAPSIGNER backup file support
#
import ngu
from ubinascii import unhexlify as a2b_hex
from ubinascii import a2b_base64
from ux import ux_show_story, OK, X
from ux import ux_input_text, import_export_prompt
from files import CardSlot, CardMissingError, needs_microsd
from charcodes import KEY_NFC, KEY_QR, KEY_CANCEL
from actions import file_picker, import_extended_key_as_secret
def decrypt_tapsigner_backup(backup_key, data):
try:
backup_key = a2b_hex(backup_key)
# AES-128-CTR (ARM assembly module only supports AES-256-CTR)
decrypt = ngu.aes.CTR(backup_key, bytes(16)) # IV 0
decrypted = decrypt.cipher(data).decode().strip()
# format of TAPSIGNER backup is known in advance
# extended private key is expected at the beginning of the first line
assert decrypted[1:4] == "prv"
except Exception:
raise ValueError("Decryption failed - wrong key?")
return decrypted.split("\n")
async def import_tapsigner_backup_file(_1, _2, item):
from glob import NFC
ephemeral = item.arg
if not ephemeral:
from pincodes import pa
assert pa.is_secret_blank() # "must not have secret"
origin = "from "
label = "TAPSIGNER encrypted backup file"
choice = await import_export_prompt(label, is_import=True)
if choice == KEY_CANCEL:
return
elif choice == KEY_NFC:
data = await NFC.read_tapsigner_b64_backup()
if not data:
# failed to get any data - exit
# error already displayed in nfc.py
return
elif choice == KEY_QR:
# how is binary encoded? who made this QR??!
from ux_q1 import QRScannerInteraction
prob = None
while 1:
data = await QRScannerInteraction.scan(
'Scan TAPSIGNER backup data', prob)
if not data: return # pressed cancel
# guess at serialization between Base64 and Hex
try:
# pure hex, the smarter encoding (when in caps)
data = a2b_hex(data)
except ValueError:
try:
data = a2b_base64(data)
except ValueError:
prob = 'Expected HEX digits or Base64 encoded binary'
continue
break
else:
fn = await file_picker(suffix="aes", min_size=100, max_size=160, **choice)
if not fn: return
origin += (" (%s)" % fn)
try:
with CardSlot(**choice) as card:
with open(fn, 'rb') as fp:
data = fp.read()
except CardMissingError:
await needs_microsd()
return
except Exception as e:
from utils import problem_file_line
await ux_show_story('Failed to read!\n\n\n%s\n%s' % (e, problem_file_line(e)))
return
if await ux_show_story("Make sure to have your TAPSIGNER handy as you will need to provide "
"'Backup Password' from the back of the card in the next step.\n\n"
"Press %s to continue %s to cancel." % (OK, X)) != "y":
return
while True:
backup_key = await ux_input_text("", confirm_exit=False, hex_only=True,
min_len=32, max_len=32,
prompt='Backup Password (32 hex digits)')
if backup_key is None:
return
assert len(backup_key) == 32
try:
extended_key, derivation = decrypt_tapsigner_backup(backup_key, data)
break
except ValueError as e:
await ux_show_story(title="FAILURE", msg=str(e))
continue
await import_extended_key_as_secret(extended_key, ephemeral, origin=origin)
# EOF