def done(signature, address): # complete. write out result from ubinascii import b2a_base64 orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out_fn = None sig = b2a_base64(signature).decode('ascii').strip() while 1: # try to put back into same spot # add -signed to end. target_fname = base+'-signed.txt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename( target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: with open(out_full, 'wt') as fd: # save in full RFC style fd.write(RFC_SIGNATURE_TEMPLATE.format(addr=address, msg=text, blockchain='BITCOIN', sig=sig)) # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed message, " "and press OK.", title="Need Card") if ch == 'x': return # done. msg = "Created new file:\n\n%s" % out_fn await ux_show_story(msg, title='File Signed')
class AuditLogger: def __init__(self, dirname, digest, never_log): self.dirname = dirname self.digest = digest self.never_log = never_log def __enter__(self): try: if self.never_log: raise NotImplementedError self.card = CardSlot().__enter__() d = self.card.get_sd_root() + '/' + self.dirname # mkdir if needed try: uos.stat(d) except: uos.mkdir(d) self.fname = d + '/' + b2a_hex( self.digest[-8:]).decode('ascii') + '.log' self.fd = open(self.fname, 'a+t') # append mode except (CardMissingError, OSError, NotImplementedError): # may be fatal or not, depending on configuration self.fname = self.card = None self.fd = sys.stdout return self def __exit__(self, exc_type, exc_value, traceback): if exc_value: self.fd.write('\n\n---- Coldcard Exception ----\n') sys.print_exception(exc_value, self.fd) self.fd.write('\n===\n\n') if self.card: assert self.fd != sys.stdout self.fd.close() self.card.__exit__(exc_type, exc_value, traceback) @property def is_unsaved(self): return not self.card def info(self, msg): print(msg, file=self.fd)
async def import_multisig(*a): # pick text file from SD card, import as multisig setup file def possible(filename): with open(filename, 'rt') as fd: for ln in fd: if 'pub' in ln: return True fn = await file_picker('Pick multisig wallet file to import (.txt)', suffix='.txt', min_size=100, max_size=20 * 200, taster=possible) if not fn: return try: with CardSlot() as card: with open(fn, 'rt') as fp: data = fp.read() except CardMissingError: await needs_microsd() return from auth import maybe_enroll_xpub try: possible_name = (fn.split('/')[-1].split('.'))[0] maybe_enroll_xpub(config=data, name=possible_name) except Exception as e: await ux_show_story('Failed to import.\n\n\n' + str(e))
async def export_wallet_file(self, mode="exported from", extra_msg=None): # create a text file with the details; ready for import to next Coldcard from main import settings my_xfp = xfp2str(settings.get('xfp')) fname_pattern = self.make_fname('export') try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wt') as fp: print("# Coldcard Multisig setup file (%s %s)\n#" % (mode, my_xfp), file=fp) self.render_export(fp) msg = '''Coldcard multisig setup file written:\n\n%s''' % nice if extra_msg: msg += extra_msg await ux_show_story(msg) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return
async def write_text_file(fname_pattern, body, title, total_parts=72): # - total_parts does need not be precise from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: for idx, part in enumerate(body): dis.progress_bar_show(idx / total_parts) fd.write(part.encode()) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''%s file written:\n\n%s''' % (title, nice) await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'): # record **public** values and helpful data into a text file from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd dis.fullscreen('Generating...') # generator function: body = generate_public_contents() # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: for part in body: fd.write(part.encode()) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''Summary file written:\n\n%s''' % nice await ux_show_story(msg)
async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'): # Record **public** values and helpful data into a JSON file from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd dis.fullscreen('Generating...') body = generator() # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wt') as fd: ujson.dump(body, fd) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''%s file written:\n\n%s''' % (label, nice) await ux_show_story(msg)
async def list_files(*A): # list files, don't do anything with them? fn = await file_picker( 'Lists all files on MicroSD. Select one and SHA256(file contents) will be shown.', min_size=0) if not fn: return import tcc from utils import B2A chk = tcc.sha256() try: with CardSlot() as card: with open(fn, 'rb') as fp: while 1: data = fp.read(1024) if not data: break chk.update(data) except CardMissingError: await needs_microsd() return basename = fn.rsplit('/', 1)[-1] ch = await ux_show_story('''SHA256(%s)\n\n%s\n\nPress 6 to delete.''' % (basename, B2A(chk.digest())), escape='6') if ch == '6': from files import securely_blank_file securely_blank_file(fn) return
async def append(self, xfp, bip39pw): # encrypt and save; always appends. from ux import ux_dramatic_pause from glob import dis from actions import needs_microsd while 1: dis.fullscreen('Saving...') try: with CardSlot() as card: self._calc_key(card) data = self._read(card) if self.key else [] data.append(dict(xfp=xfp, pw=bip39pw)) encrypt = ngu.aes.CTR(self.key) msg = encrypt.cipher(ujson.dumps(data)) with open(self.filename(card), 'wb') as fd: fd.write(msg) await ux_dramatic_pause("Saved.", 1) return except CardMissingError: ch = await needs_microsd() if ch == 'x': # undocumented, but needs escape route break
async def more_setup(): # Boot up code; splash screen is being shown # MAYBE: check if we're a brick and die again? Or show msg? try: # Some background "tasks" # from dev_helper import monitor_usb IMPT.start_task('vcp', monitor_usb()) from files import CardSlot CardSlot.setup() # This "pa" object holds some state shared w/ bootloader about the PIN try: from pincodes import pa pa.setup(b'') # just to see where we stand. except RuntimeError as e: print("Problem: %r" % e) if version.is_factory_mode: # in factory mode, turn on USB early to allow debug/setup from usb import enable_usb enable_usb() # always start the self test. if not settings.get('tested', False): from actions import start_selftest await start_selftest() else: # force them to accept terms (unless marked as already done) from actions import accept_terms await accept_terms() # Prompt for PIN and then pick appropriate top-level menu, # based on contents of secure chip (ie. is there # a wallet defined) from actions import start_login_sequence await start_login_sequence() except BaseException as exc: die_with_debug(exc) IMPT.start_task('mainline', mainline())
def __enter__(self): try: if self.never_log: raise NotImplementedError self.card = CardSlot().__enter__() d = self.card.get_sd_root() + '/' + self.dirname # mkdir if needed try: uos.stat(d) except: uos.mkdir(d) self.fname = d + '/' + b2a_hex( self.digest[-8:]).decode('ascii') + '.log' self.fd = open(self.fname, 'a+t') # append mode except (CardMissingError, OSError, NotImplementedError): # may be fatal or not, depending on configuration self.fname = self.card = None self.fd = sys.stdout return self
async def battery_mon(): isRunning = True input = KeyInputHandler(down='udplrxy', up='xy') powermon = Powermon() prev_time = 0 (n1, _) = noise.read() FILENAME = 'battery_mon_test_' + str(n1) + '.txt' while (True): try: with CardSlot() as card: # fname, nice = card.pick_filename(fname_pattern) fname = FILENAME # do actual write with open(fname, 'wb') as fd: print("writing to SD card...") fd.write('Time, Current, Voltage\n') while isRunning: event = await input.get_event() if event != None: key, event_type = event if event_type == 'up': if key == 'x': isRunning = False update(input, utime.ticks_ms(), powermon, fd, prev_time, isRunning) await sleep_ms(1) fd.close() break except Exception as e: # includes CardMissingError import sys sys.print_exception(e) # catch any error ch = await ux_show_story( 'Failed to write! Please insert formated microSD card, ' 'and press OK to try again.\n\nX to cancel.\n\n\n' + str(e)) if ch == 'x': break continue return None
async def clone_write_data(*a): # Write encrypted backup file, for cloning purposes, based on a public key # found on the SD Card. # - input file must already exist on inserted card from files import CardSlot, CardMissingError try: with CardSlot() as card: path = card.get_sd_root() with open(path + '/ccbk-start.json', 'rb') as fd: d = ujson.load(fd) his_pubkey = a2b_hex(d.get('pubkey')) # expect compress pubkey assert len(his_pubkey) == 33 assert 2 <= his_pubkey[0] <= 3 # remove any other clone-files on this card, so no confusion # on receiving end; unlikely they can work anyway since new key each time for path in card.get_paths(): for fn, ftype, *var in uos.ilistdir(path): if fn.endswith('-ccbk.7z'): try: uos.remove(path + '/' + fn) except: pass except (CardMissingError, OSError) as exc: # Standard msg shown if no SD card detected when we need one. await ux_show_story( "Start this process on the other Coldcard, which will write a file onto MicroSD card as the first step.\n\nInsert that card and try again here." ) return # pick our own temp keys for this encryption pair = ngu.secp256k1.keypair() my_pubkey = pair.pubkey().to_bytes(False) session_key = pair.ecdh_multiply(his_pubkey) words = [b2a_hex(session_key).decode()] fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z' await write_complete_backup(words, fname, allow_copies=False) await ux_show_story( "Done.\n\nTake this MicroSD card back to other Coldcard and continue from there." )
async def verify_backup_file(fname_or_fd): # read 7z header, and measure checksums # - no password is wanted/required # - really just checking CRC32, but that's enough against truncated files from files import CardSlot, CardMissingError from actions import needs_microsd prob = None fd = None # filename already picked, open it. try: with CardSlot() as card: prob = 'Unable to open backup file.' fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd prob = 'Unable to read backup file headers. Might be truncated.' compat7z.check_file_headers(fd) prob = 'Unable to verify backup file contents.' zz = compat7z.Builder() files = zz.verify_file_crc(fd, MAX_BACKUP_FILE_SIZE) assert len(files) == 1 fname, fsize = files[0] assert fname == 'ckcc-backup.txt' assert 400 < fsize < MAX_BACKUP_FILE_SIZE, 'size' except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story(prob + '\n\nError: ' + str(e)) return finally: if fd: fd.close() await ux_show_story( "Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test." )
async def make_address_summary_file(path, addr_fmt, fname_pattern='addresses.txt'): # write addresses into a text file on the MicroSD from main import dis from files import CardSlot, CardMissingError from actions import needs_microsd # simple: always set number of addresses. # - takes 60 seconds, to write 250 addresses on actual hardware count = 250 dis.fullscreen('Saving 0-%d' % count) # generator function body = generate_address_csv(path, addr_fmt, count) # pick filename and write try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: for idx, part in enumerate(body): fd.write(part.encode()) if idx % 5 == 0: dis.progress_bar_show(idx / count) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''Address summary file written:\n\n%s''' % nice await ux_show_story(msg)
async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None txid = None from main import settings import os del_after = settings.get('del', 0) while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: if is_comp and del_after: # don't write signed PSBT if we'd just delete it anyway out_fn = None else: with output_encoder(open(out_full, 'wb')) as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename( base+'-final.txn' if not del_after else 'tmp.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'w+t')) as fd: # save transaction, in hex txid = psbt.finalize(fd) if del_after: # rename it now that we know the txid after_full, out2_fn = card.pick_filename( txid+'.txn', out_path, overwrite=True) os.rename(out2_full, after_full) if del_after: # this can do nothing if they swapped SDCard between steps, which is ok, # but if the original file is still there, this blows it away. # - if not yet final, the foo-part.psbt file stays try: securely_blank_file(filename) except: pass # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. if out_fn: msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\n' else: # del_after is probably set msg = '' if out2_fn: msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn if txid and not del_after: msg += '\n\nFinal TXID:\n'+txid await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup()
async def restore_complete_doit(fname_or_fd, words): # Open file, read it, maybe decrypt it; return string if any error # - some errors will be shown, None return in that case # - no return if successful (due to reboot) from main import dis from files import CardSlot, CardMissingError from actions import needs_microsd # build password password = '******'.join(words) prob = None try: with CardSlot() as card: # filename already picked, taste it and maybe consider using its data. try: fd = open(fname_or_fd, 'rb') if isinstance( fname_or_fd, str) else fname_or_fd except: return 'Unable to open backup file.\n\n' + str(fname_or_fd) try: if not words: contents = fd.read() else: try: compat7z.check_file_headers(fd) except Exception as e: return 'Unable to read backup file. Has it been touched?\n\nError: ' \ + str(e) dis.fullscreen("Decrypting...") try: zz = compat7z.Builder() fname, contents = zz.read_file( fd, password, MAX_BACKUP_FILE_SIZE, progress_fcn=dis.progress_bar_show) # simple quick sanity checks assert fname == 'ckcc-backup.txt' assert contents[0:1] == b'#' and contents[-1:] == b'\n' except Exception as e: # assume everything here is "password wrong" errors #print("pw wrong? %s" % e) return ( 'Unable to decrypt backup file. Incorrect password?' '\n\nTried:\n\n' + password) finally: fd.close() except CardMissingError: 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! # this leads to reboot if it works, else errors shown, etc. return await restore_from_dict(vals)
async def write_complete_backup(words, fname_pattern, write_sflash): # Just do the writing from main import dis, pa, settings from files import CardSlot, CardMissingError # Show progress: dis.fullscreen('Encrypting...' if words else 'Generating...') body = render_backup_contents().encode() gc.collect() if words: # NOTE: Takes a few seconds to do the key-streching, but little actual # time to do the encryption. pw = ' '.join(words) zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show) zz.add_data(body) hdr, footer = zz.save('ckcc-backup.txt') filesize = len(body) + MAX_BACKUP_FILE_SIZE del body gc.collect() else: # cleartext dump zz = None filesize = len(body) + 10 if write_sflash: # for use over USB and unit testing: commit file into SPI flash from sffile import SFFile with SFFile(0, max_size=filesize, message='Saving...') as fd: await fd.erase() if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) return fd.tell(), fd.checksum.digest() for copy in range(25): # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) except Exception as e: # includes CardMissingError import sys sys.print_exception(e) # catch any error ch = await ux_show_story( 'Failed to write! Please insert formated MicroSD card, ' 'and press OK to try again.\n\nX to cancel.\n\n\n' + str(e)) if ch == 'x': break continue if copy == 0: while 1: msg = '''Backup file written:\n\n%s\n\n\ To view or restore the file, you must have the full password.\n\n\ Insert another SD card and press 2 to make another copy.''' % (nice) ch = await ux_show_story(msg, escape='2') if ch == 'y': return if ch == '2': break else: ch = await ux_show_story('''File (#%d) written:\n\n%s\n\n\ Press OK for another copy, or press X to stop.''' % (copy + 1, nice), escape='2') if ch == 'x': break
async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: with open(out_full, 'wb') as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename(base+'-final.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'wt')) as fd: # save transaction, in hex psbt.finalize(fd) # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\nFinalized transaction (ready for broadcast):\n\n%s' % out2_fn await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup()
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError from main import dis from sram2 import tmp_buf global active_request UserAuthorizedAction.cleanup() #print("sign: %s" % filename) # copy file into our spiflash # - can't work in-place on the card because we want to support writing out to different card with CardSlot() as card: with open(filename, 'rb') as fd: dis.fullscreen('Reading...') # see how long it is psbt_len = fd.seek(0, 2) fd.seek(0) total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: # blank flash await out.erase() while 1: n = fd.readinto(tmp_buf) if not n: break if n == len(tmp_buf): out.write(tmp_buf) else: out.write(memoryview(tmp_buf)[0:n]) total += n dis.progress_bar_show(total / psbt_len) assert total == psbt_len, repr([total, psbt_len]) async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: with open(out_full, 'wb') as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename(base+'-final.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'wt')) as fd: # save transaction, in hex psbt.finalize(fd) # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\nFinalized transaction (ready for broadcast):\n\n%s' % out2_fn await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() active_request = ApproveTransaction(psbt_len, approved_cb=done) # kill any menu stack, and put our thing at the top abort_and_goto(active_request)
async def file_picker(msg, suffix=None, min_size=1, max_size=1000000, taster=None, choices=None, escape=None): # present a menu w/ a list of files... to be read # - optionally, enforce a max size, and provide a "tasting" function # - if msg==None, don't prompt, just do the search and return list # - if choices is provided; skip search process # - escape: allow these chars to skip picking process from menu import MenuSystem, MenuItem import uos from utils import get_filesize if choices is None: choices = [] try: with CardSlot() as card: sofar = set() for path in card.get_paths(): for fn, ftype, *var in uos.ilistdir(path): if ftype == 0x4000: # ignore subdirs continue if suffix and not fn.lower().endswith(suffix): # wrong suffix continue if fn[0] == '.': continue full_fname = path + '/' + fn # Conside file size # sigh, OS/filesystem variations file_size = var[1] if len(var) == 2 else get_filesize( full_fname) if not (min_size <= file_size <= max_size): continue if taster is not None: try: yummy = taster(full_fname) except IOError: #print("fail: %s" % full_fname) yummy = False if not yummy: continue label = fn while label in sofar: # just the file name isn't unique enough sometimes? # - shouldn't happen anymore now that we dno't support internal FS # - unless we do muliple paths label += path.split('/')[-1] + '/' + fn sofar.add(label) choices.append((label, path, fn)) except CardMissingError: # don't show anything if we're just gathering data if msg is not None: await needs_microsd() return None if msg is None: return choices if not choices: msg = 'Unable to find any suitable files for this operation. ' if suffix: msg += 'The filename must end in "%s". ' % suffix msg += '\n\nMaybe insert (another) SD card and try again?' await ux_show_story(msg) return # tell them they need to pick; can quit here too, but that's obvious. if len(choices) != 1: msg += '\n\nThere are %d files to pick from.' % len(choices) else: msg += '\n\nThere is only one file to pick from.' ch = await ux_show_story(msg, escape=escape) if escape and ch in escape: return ch if ch == 'x': return picked = [] async def clicked(_1, _2, item): picked.append('/'.join(item.arg)) the_ux.pop() items = [ MenuItem(label, f=clicked, arg=(path, fn)) for label, path, fn in choices ] if 0: # don't like; and now showing count on previous page if len(choices) == 1: # if only one choice, we could make the choice for them ... except very confusing items.append(MenuItem(' (one file)', f=None)) else: items.append(MenuItem(' (%d files)' % len(choices), f=None)) menu = MenuSystem(items) the_ux.push(menu) await menu.interact() return picked[0] if picked else None
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH): # collect all xpub- exports on current SD card (must be > 1) # - ask for M value # - create wallet, save and also export # - also create electrum skel to go with that # - only expected to work with our ccxp-foo.json export files. from actions import file_picker import uos, ujson from utils import get_filesize from main import settings chain = chains.current_chain() my_xfp = settings.get('xfp') xpubs = [] files = [] has_mine = False deriv = None try: with CardSlot() as card: for path in card.get_paths(): for fn, ftype, *var in uos.ilistdir(path): if ftype == 0x4000: # ignore subdirs continue if not fn.startswith('ccxp-') or not fn.endswith('.json'): # wrong prefix/suffix: ignore continue full_fname = path + '/' + fn # Conside file size # sigh, OS/filesystem variations file_size = var[1] if len(var) == 2 else get_filesize( full_fname) if not (0 <= file_size <= 1000): # out of range size continue try: with open(full_fname, 'rt') as fp: vals = ujson.load(fp) ln = vals.get(mode) # value in file is BE32, but we want LE32 internally xfp = str2xfp(vals['xfp']) if not deriv: deriv = vals[mode + '_deriv'] else: assert deriv == vals[mode + '_deriv'], "wrong derivation" node, _, _ = import_xpub(ln) if xfp == my_xfp: has_mine = True xpubs.append( (xfp, chain.serialize_public(node, AF_P2SH))) files.append(fn) except CardMissingError: raise except Exception as exc: # show something for coders, but no user feedback sys.print_exception(exc) continue except CardMissingError: await needs_microsd() return # remove dups; easy to happen if you double-tap the export delme = set() for i in range(len(xpubs)): for j in range(len(xpubs)): if j in delme: continue if i == j: continue if xpubs[i] == xpubs[j]: delme.add(j) if delme: xpubs = [x for idx, x in enumerate(xpubs) if idx not in delme] if not xpubs or len(xpubs) == 1 and has_mine: await ux_show_story( "Unable to find any Coldcard exported keys on this card. Must have filename: ccxp-....json" ) return # add myself if not included already if not has_mine: with stash.SensitiveValues() as sv: node = sv.derive_path(deriv) xpubs.append((my_xfp, chain.serialize_public(node, AF_P2SH))) N = len(xpubs) if N > MAX_SIGNERS: await ux_show_story("Too many signers, max is %d." % MAX_SIGNERS) return # pick useful M value to start assert N >= 2 M = (N - 1) if N < 4 else ((N // 2) + 1) while 1: msg = '''How many need to sign?\n %d of %d Press (7 or 9) to change M value, or OK \ to continue. If you expected more or less keys (N=%d #files=%d), \ then check card and file contents. Coldcard multisig setup file and an Electrum wallet file will be created automatically.\ ''' % (M, N, N, len(files)) ch = await ux_show_story(msg, escape='123479') if ch in '1234': M = min(N, int(ch)) # undocumented shortcut elif ch == '9': M = min(N, M + 1) elif ch == '7': M = max(1, M - 1) elif ch == 'x': await ux_dramatic_pause('Aborted.', 2) return elif ch == 'y': break # create appropriate object assert 1 <= M <= N <= MAX_SIGNERS name = 'CC-%d-of-%d' % (M, N) ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, common_prefix=deriv[2:], addr_fmt=addr_fmt) from auth import NewEnrollRequest, active_request active_request = NewEnrollRequest(ms, auto_export=True) # menu item case: add to stack from ux import the_ux the_ux.push(active_request)
async def export_multisig_xpubs(*a): # WAS: Create a single text file with lots of docs, and all possible useful xpub values. # THEN: Just create the one-liner xpub export value they need/want to support BIP45 # NOW: Export JSON with one xpub per useful address type and semi-standard derivation path # # Consumer for this file is supposed to be ourselves, when we build on-device multisig. # from main import settings xfp = xfp2str(settings.get('xfp', 0)) chain = chains.current_chain() fname_pattern = 'ccxp-%s.json' % xfp msg = '''\ This feature creates a small file containing \ the extended public keys (XPUB) you would need to join \ a multisig wallet using the 'Create Airgapped' feature. The public keys exported are: BIP45: m/45' P2WSH-P2SH: m/48'/{coin}'/0'/1' P2WSH: m/48'/{coin}'/0'/2' OK to continue. X to abort. '''.format(coin=chain.b44_cointype) resp = await ux_show_story(msg) if resp != 'y': return try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write: manual JSON here so more human-readable. with open(fname, 'wt') as fp: fp.write('{\n') with stash.SensitiveValues() as sv: for deriv, name, fmt in [ ("m/45'", 'p2sh', AF_P2SH), ("m/48'/{coin}'/0'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), ("m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) ]: dd = deriv.format(coin=chain.b44_cointype) node = sv.derive_path(dd) xp = chain.serialize_public(node, fmt) fp.write(' "%s_deriv": "%s",\n' % (name, dd)) fp.write(' "%s": "%s",\n' % (name, xp)) fp.write(' "xfp": "%s"\n}\n' % xfp) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''BIP45 multisig xpub file written:\n\n%s''' % nice await ux_show_story(msg)
gc.collect() #print("Free mem: %d" % gc.mem_free()) while 1: await the_ux.interact() # Setup to start the splash screen. dis.splash_animate(loop, done_splash2) # Some background "tasks" # from dev_helper import monitor_usb loop.create_task(monitor_usb()) from files import CardSlot CardSlot.setup() # This "pa" object holds some state shared w/ bootloader about the PIN try: from pincodes import PinAttempt pa = PinAttempt() pa.setup(b'') # just to see where we stand. except RuntimeError as e: print("Problem: %r" % e) def go(): # Wrapper for better error handling/recovery at top level. # try: loop.run_forever()
def drv_entro_step2(_1, picked, _2): from main import dis from files import CardSlot, CardMissingError the_ux.pop() index = await ux_enter_number("Index Number?", 9999) if picked in (0,1,2): # BIP39 seed phrases (we only support English) num_words = (12, 18, 24)[picked] width = (16, 24, 32)[picked] # of bytes path = "m/83696968'/39'/0'/{num_words}'/{index}'".format(num_words=num_words, index=index) s_mode = 'words' elif picked == 3: # HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere s_mode = 'wif' path = "m/83696968'/2'/{index}'".format(index=index) width = 32 elif picked == 4: # New XPRV path = "m/83696968'/32'/{index}'".format(index=index) s_mode = 'xprv' width = 64 elif picked in (5, 6): width = 32 if picked == 5 else 64 path = "m/83696968'/128169'/{width}'/{index}'".format(width=width, index=index) s_mode = 'hex' else: raise ValueError(picked) dis.fullscreen("Working...") encoded = None with stash.SensitiveValues() as sv: node = sv.derive_path(path) entropy = hmac.HMAC(b'bip-entropy-from-k', node.private_key(), tcc.sha512).digest() sv.register(entropy) # truncate for this application new_secret = entropy[0:width] # only "new_secret" is interesting past here (node already blanked at this point) del node # Reveal to user! chain = chains.current_chain() if s_mode == 'words': # BIP39 seed phrase, various lengths words = tcc.bip39.from_data(new_secret).split(' ') msg = 'Seed words (%d):\n' % len(words) msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words)) encoded = stash.SecretStash.encode(seed_phrase=new_secret) elif s_mode == 'wif': # for Bitcoin Core: a 32-byte of secret exponent, base58 w/ prefix 0x80 # - always "compressed", so has suffix of 0x01 (inside base58) # - we're not checking it's on curve # - we have no way to represent this internally, since we rely on bip32 # append 0x01 to indicate it's a compressed private key pk = new_secret + b'\x01' msg = 'WIF (privkey):\n' + tcc.codecs.b58_encode(chain.b58_privkey + pk) elif s_mode == 'xprv': # Raw XPRV value. ch, pk = new_secret[0:32], new_secret[32:64] master_node = tcc.bip32.HDNode(chain_code=ch, private_key=pk, child_num=0, depth=0, fingerprint=0) encoded = stash.SecretStash.encode(xprv=master_node) msg = 'Derived XPRV:\n' + chain.serialize_private(master_node) elif s_mode == 'hex': # Random hex number for whatever purpose msg = ('Hex (%d bytes):\n' % width) + str(b2a_hex(new_secret), 'ascii') stash.blank_object(new_secret) new_secret = None # no need to print it again else: raise ValueError(s_mode) msg += '\n\nPath Used (index=%d):\n %s' % (index, path) if new_secret: msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii') print(msg) # XXX debug prompt = '\n\nPress 1 to save to MicroSD card' if encoded is not None: prompt += ', 2 to switch to derived secret.' while 1: ch = await ux_show_story(msg+prompt, sensitive=True, escape='12') if ch == '1': # write to SD card: simple text file try: with CardSlot() as card: fname, out_fn = card.pick_filename('drv-%s-idx%d.txt' % (s_mode, index)) with open(fname, 'wt') as fp: fp.write(msg) fp.write('\n') except CardMissingError: await needs_microsd() continue except Exception as e: await ux_show_story('Failed to write!\n\n\n'+str(e)) continue await ux_show_story("Filename is:\n\n%s" % out_fn, title='Saved') else: break if new_secret is not None: stash.blank_object(new_secret) stash.blank_object(msg) if ch == '2' and (encoded is not None): from main import pa, settings, dis from pincodes import AE_SECRET_LEN # switch over to new secret! dis.fullscreen("Applying...") stash.bip39_passphrase = '' tmp_secret = encoded + bytes(AE_SECRET_LEN - len(encoded)) # monkey-patch to block SE access, and just use new secret pa.fetch = lambda *a, **k: bytearray(tmp_secret) pa.change = lambda *a, **k: None pa.ls_fetch = pa.change pa.ls_change = pa.change # copies system settings to new encrypted-key value, calculates # XFP, XPUB and saves into that, and starts using them. pa.new_main_secret(pa.fetch()) await ux_show_story("New master key in effect until next power down.") if encoded is not None: stash.blank_object(encoded)
def make_menu(self): from menu import MenuItem, MenuSystem from actions import goto_top_menu from ux import ux_show_story from seed import set_bip39_passphrase import pyb # Very quick check for card not present case. if not pyb.SDCard().present(): return None # Read file, decrypt and make a menu to show; OR return None # if any error hit. try: with CardSlot() as card: self._calc_key(card) if not self.key: return None data = self._read(card) if not data: return None except CardMissingError: # not an error: they just aren't using feature return None # We have a list of xfp+pw fields. Make a menu. # Challenge: we need to hint at which is which, but don't want to # show the password on-screen. # - simple algo: # - show either first N or last N chars only # - pick which set which is all-unique, if neither, try N+1 # pws = [] for i in data: p = i.get('pw') if p not in pws: pws.append(p) for N in range(1, 8): parts = [i[0:N] + ('*'*(len(i)-N if len(i) > N else 0)) for i in pws] if len(set(parts)) == len(pws): break parts = [('*'*(len(i)-N if len(i) > N else 0)) + i[-N:] for i in pws] if len(set(parts)) == len(pws): break else: # give up: show it all! parts = pws async def doit(menu, idx, item): # apply the password immediately and drop them at top menu set_bip39_passphrase(data[idx]['pw']) from nvstore import settings from utils import xfp2str xfp = settings.get('xfp') # verification step; I don't see any way for this to go wrong assert xfp == data[idx]['xfp'] # feedback that it worked await ux_show_story("Passphrase restored.", title="[%s]" % xfp2str(xfp)) goto_top_menu() return MenuSystem((MenuItem(label or '(empty)', f=doit) for label in parts))
async def microsd_upgrade(*a): # Upgrade vis MicroSD card # - search for a particular file # - verify it lightly # - erase serial flash # - copy it over (slow) # - reboot into bootloader, which finishes install fn = await file_picker('Pick firmware image to use (.DFU)', suffix='.dfu', min_size=0x7800) if not fn: return failed = None with CardSlot() as card: with open(fn, 'rb') as fp: from main import sf, dis from files import dfu_parse from ustruct import unpack_from offset, size = dfu_parse(fp) # get a copy of special signed heaer at the end of the flash as well from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT hdr = bytearray(FW_HEADER_SIZE) fp.seek(offset + FW_HEADER_OFFSET) # basic checks only: for confused customers, not attackers. try: rv = fp.readinto(hdr) assert rv == FW_HEADER_SIZE magic_value, timestamp, version_string, pk, fw_size = \ unpack_from(FWH_PY_FORMAT, hdr)[0:5] assert magic_value == FW_HEADER_MAGIC assert fw_size == size # TODO: maybe show the version string? Warn them that downgrade doesn't work? except Exception as exc: failed = "Sorry! That does not look like a firmware " \ "file we would want to use.\n\n\n%s" % exc if not failed: # copy binary into serial flash fp.seek(offset) buf = bytearray(256) # must be flash page size pos = 0 dis.fullscreen("Loading...") while pos <= size + FW_HEADER_SIZE: dis.progress_bar_show(pos / size) if pos == size: # save an extra copy of the header (also means we got done) buf = hdr else: here = fp.readinto(buf) if not here: break if pos % 4096 == 0: # erase here sf.sector_erase(pos) while sf.is_busy(): await sleep_ms(10) sf.write(pos, buf) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) pos += here if failed: await ux_show_story(failed, title='Corrupt') return # continue process... import machine machine.reset()
async def doit(self, *a, have_key=None): # make the wallet. from main import dis try: from chains import current_chain import tcc from serializations import hash160 from stash import blank_object if not have_key: # get some random bytes await ux_dramatic_pause("Picking key...", 2) privkey = tcc.secp256k1.generate_secret() else: # caller must range check this already: 0 < privkey < order privkey = have_key # calculate corresponding public key value pubkey = tcc.secp256k1.publickey(privkey, True) # always compressed style dis.fullscreen("Rendering...") # make payment address digest = hash160(pubkey) ch = current_chain() if self.is_segwit: addr = tcc.codecs.bech32_encode(ch.bech32_hrp, 0, digest) else: addr = tcc.codecs.b58_encode(ch.b58_addr + digest) wif = tcc.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01') if self.can_do_qr(): with imported('uqr') as uqr: # make the QR's now, since it's slow is_alnum = self.is_segwit qr_addr = uqr.make( addr if not is_alnum else addr.upper(), min_version=4, max_version=4, encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0)) qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE) else: qr_addr = None qr_wif = None # Use address as filename. clearly will be unique, but perhaps a bit # awkward to work with. basename = addr dis.fullscreen("Saving...") with CardSlot() as card: fname, nice_txt = card.pick_filename( basename + ('-note.txt' if self.template_fn else '.txt')) with open(fname, 'wt') as fp: self.make_txt(fp, addr, wif, privkey, qr_addr, qr_wif) if self.template_fn: fname, nice_pdf = card.pick_filename(basename + '.pdf') with open(fname, 'wb') as fp: self.make_pdf(fp, addr, wif, qr_addr, qr_wif) else: nice_pdf = '' # Half-hearted attempt to cleanup secrets-contaminated memory # - better would be force user to reboot # - and yet, we just output the WIF to SDCard anyway blank_object(privkey) blank_object(wif) del qr_wif except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return await ux_show_story('Done! Created file(s):\n\n%s\n\n%s' % (nice_txt, nice_pdf))
def go(operation='', field='chain', value='BTC'): import common from sram4 import viewfinder_buf print('2: Available RAM = {}'.format(gc.mem_free())) # Avalanche noise source from foundation import Noise common.noise = Noise() # Get the async event loop to pass in where needed common.loop = asyncio.get_event_loop() # System from foundation import System common.system = System() print('2.75: Available RAM = {}'.format(gc.mem_free())) # Initialize the keypad from keypad import Keypad common.keypad = Keypad() print('3: Available RAM = {}'.format(gc.mem_free())) # Initialize SD card from files import CardSlot CardSlot.setup() print('3.5: Available RAM = {}'.format(gc.mem_free())) # External SPI Flash from sflash import SPIFlash common.sf = SPIFlash() # Initialize NV settings from settings import Settings common.settings = Settings(common.loop) print('4: Available RAM = {}'.format(gc.mem_free())) # Initialize the display and show the splash screen from display import Display print("disp 1") common.dis = Display() print("disp 2") common.dis.set_brightness(common.settings.get('screen_brightness', 100)) print("disp 3") common.dis.splash() print('5: Available RAM = {}'.format(gc.mem_free())) # Allocate buffers for camera from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, CAMERA_WIDTH, CAMERA_HEIGHT # QR buf is 1 byte per pixel grayscale import uctypes common.qr_buf = uctypes.bytearray_at(0x20000000, CAMERA_WIDTH * CAMERA_HEIGHT) # common.qr_buf = bytearray(CAMERA_WIDTH * CAMERA_HEIGHT) print('6: Available RAM = {}'.format(gc.mem_free())) # Viewfinder buf 1s 1 bit per pixel and we round the screen width up to 240 # so it's a multiple of 8 bits. The screen height of 303 minus 31 for the # header and 31 for the footer gives 241 pixels, which we round down to 240 # to give one blank (white) line before the footer. common.viewfinder_buf = bytearray( (VIEWFINDER_WIDTH * VIEWFINDER_HEIGHT) // 8) print('7: Available RAM = {}'.format(gc.mem_free())) # Show REPL welcome message print("Passport by Foundation Devices Inc. (C) 2020.\n") print('8: Available RAM = {}'.format(gc.mem_free())) from foundation import SettingsFlash f = SettingsFlash() if operation == 'dump': print('Settings = {}'.format(common.settings.curr_dict)) print('addr = {}'.format(common.settings.addr)) elif operation == 'erase': f.erase() elif operation == 'set': common.settings.set(field, value) elif operation == 'stress': for f in range(35): print("Round {}:".format(f)) print(' Settings = {}'.format(common.settings.curr_dict)) common.settings.set('field_{}'.format(f), f) common.settings.save() print('\nFinal Settings = {}'.format(common.settings.curr_dict)) # This "pa" object holds some state shared w/ bootloader about the PIN try: from pincodes import PinAttempt common.pa = PinAttempt() common.pa.setup(b'') except RuntimeError as e: print("Secure Element Problem: %r" % e) print('9: Available RAM = {}'.format(gc.mem_free())) # Setup the startup task common.loop.create_task(startup()) run_loop()
async def import_xprv(*A): # read an XPRV from a text file and use it. import tcc, chains, ure from main import pa from stash import SecretStash from ubinascii import hexlify as b2a_hex from backups import restore_from_dict assert pa.is_secret_blank() # "must not have secret" def contains_xprv(fname): # just check if likely to be valid; not full check try: with open(fname, 'rt') as fd: for ln in fd: # match tprv and xprv, plus y/zprv etc if 'prv' in ln: return True return False except OSError: # directories? return False # pick a likely-looking file. fn = await file_picker('Select file containing the XPRV to be imported.', min_size=50, max_size=2000, taster=contains_xprv) if not fn: return node, chain, addr_fmt = None, None, None # open file and do it pat = ure.compile(r'.prv[A-Za-z0-9]+') with CardSlot() as card: with open(fn, 'rt') as fd: for ln in fd.readlines(): if 'prv' not in ln: continue found = pat.search(ln) if not found: continue found = found.group(0) for ch in chains.AllChains: for kk in ch.slip132: if found[0] == ch.slip132[kk].hint: try: node = tcc.bip32.deserialize( found, ch.slip132[kk].pub, ch.slip132[kk].priv) chain = ch addr_fmt = kk break except ValueError: pass if node: break if not node: # unable await ux_show_story('''\ Sorry, wasn't able to find an extended private key to import. It should be at \ the start of a line, and probably starts with "xprv".''', title="FAILED") return # encode it in our style d = dict(chain=chain.ctype, raw_secret=b2a_hex(SecretStash.encode(xprv=node))) node.blank() # TODO: capture the address format implied by SLIP32 version bytes #addr_fmt = # restore as if it was a backup (code reuse) await restore_from_dict(d)