Use any QR as BIP-39 passphrase

This commit is contained in:
Peter D. Gray 2023-12-07 08:46:53 -05:00
parent 138cbad573
commit cd00c626c7
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
3 changed files with 65 additions and 14 deletions

View File

@ -68,8 +68,10 @@ def decode_secret(got):
raise ValueError('no idea')
def decode_qr_result(got, expect_secret=False):
def decode_qr_result(got, expect_secret=False, expect_text=False):
# Could be BBQr or text
# - if expect_text, just give us unparsed text back; after BBQr decode
# - otherwise, returns a tuple: (type, (*parsed_data))
if hasattr(got, 'storage'):
# BBQr object
@ -82,6 +84,11 @@ def decode_qr_result(got, expect_secret=False):
if expect_secret and ty in 'PT':
raise QRDecodeExplained('Expected secrets not PSBT/TXN')
if expect_text:
if ty != 'U':
raise QRDecodeExplained('Expected text?')
return got.decode()
if ty == 'P':
# may already be in PSRAM, avoid a copy here
from glob import PSRAM
@ -102,6 +109,11 @@ def decode_qr_result(got, expect_secret=False):
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
raise QRDecodeExplained("Sorry, %s not useful." % msg)
elif expect_text:
# caller just wants text anyway, so we are done
#assert isinstance(got, str)
return got
# First can we decode a master secret of some type?
try:
@ -117,6 +129,7 @@ def decode_qr_result(got, expect_secret=False):
if expect_secret:
raise QRDecodeExplained("Not a secret?")
# try to recognize various bitcoin-related text strings...
return decode_qr_text(got)
def decode_qr_text(got):

View File

@ -24,7 +24,7 @@ from glob import settings, dis
from pincodes import pa
from nvstore import SettingsObject
from files import CardMissingError, needs_microsd, CardSlot
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_CLEAR
# seed words lengths we support: 24=>256 bits, and recommended
@ -287,7 +287,7 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
extra += 'Press (1) to view as QR Code. '
else:
escape = (escape or '') + KEY_QR
extra += 'Press QR button to view as QR Code. '
extra += 'Press '+ KEY_QR + ' to view as QR Code. '
if extra:
msg += '\n\n'
@ -1089,8 +1089,8 @@ class PassphraseMenu(MenuSystem):
if version.has_qwerty:
items = [
MenuItem('Edit Phrase', f=self.view_edit_phrase),
MenuItem('Clear Phrase', f=self.empty_phrase),
MenuItem('Edit Phrase', f=self.view_edit_phrase, shortcut=KEY_QR),
MenuItem('Clear Phrase', f=self.empty_phrase, shortcut=KEY_CLEAR),
MenuItem('APPLY', f=self.done_apply),
MenuItem('CANCEL', f=self.done_cancel),
]
@ -1104,8 +1104,8 @@ class PassphraseMenu(MenuSystem):
MenuItem('APPLY', f=self.done_apply),
MenuItem('CANCEL', f=self.done_cancel),
]
# quick SD card check
# TODO this needs to handle 2 SD cards now ?
# quick SD card check: will use A if both slots are stuffed
if pyb.SDCard().present():
try:
with CardSlot() as card:
@ -1154,7 +1154,7 @@ class PassphraseMenu(MenuSystem):
global pp_sofar
if len(pp_sofar) >= 3:
if not await ux_confirm("Press OK to clear passphrase. X to cancel."):
if not await ux_confirm("Press OK to clear passphrase."):
return
pp_sofar = ''

View File

@ -183,6 +183,7 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100,
# no key-repeat on certain keys
err_msg = last_err = None
press = PressRelease()
exit_armed = False
while 1:
dis.clear_box(x, y, line_len, num_lines)
@ -214,6 +215,9 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100,
ch = await press.wait()
if ch != KEY_CANCEL:
exit_armed = False
if ch == KEY_ENTER:
if len(value) >= min_len:
break
@ -227,12 +231,25 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100,
value = ''
elif ch == KEY_CANCEL:
if confirm_exit:
pp = await ux_show_story(
"OK to leave without any changes? Or CANCEL to avoid leaving.")
if pp == KEY_CANCEL:
continue
value = None
break
if exit_armed:
value = None
break
err_msg = 'Confirm exit w/o change?'
exit_armed = True
continue
else:
value = None
break
elif ch == KEY_QR:
# Insert or replace? I think replace
ss = dis.save_state()
zz = QRScannerInteraction()
got = await zz.scan_text('Scan any QR or Barcode for text.')
if got: # handle cancel, etc
value = got
err_msg = 'Replace w/ data from scan.'
dis.restore_state(ss)
elif b39_complete and ch == KEY_TAB:
# match case and auto-complete BIP-39 word if we can
@ -643,6 +660,27 @@ class QRScannerInteraction:
return data
async def scan_text(self, prompt):
# Scan and return a text string. For things like BIP-39 passphrase
# and perhaps they are re-using a QR from something else. Don't act on contents.
problem = None
while 1:
try:
got = await self.scan(prompt, line2=problem)
if got is None:
return None
# Decode BBQr but not anything more complex
return decode_qr_result(got, expect_text=True)
except QRDecodeExplained as exc:
problem = str(exc)
continue
except Exception as exc:
#import sys; sys.print_exception(exc)
problem = "Unable to decode QR"
continue
async def scan_anything(self, expect_secret=False):
# start a QR scan, and act on what we find, whatever it may be.