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 interact(self): # Prompt user w/ details and get approval from main import dis ch = await ux_show_story(MSG_SIG_TEMPLATE.format(msg=self.text, addr=self.address, subpath=self.subpath)) if ch != 'y': # they don't want to! self.refused = True else: dis.fullscreen('Signing...', percent=.25) # do the signature itself! with stash.SensitiveValues() as sv: dis.progress_bar_show(.50) node = sv.derive_path(self.subpath) pk = node.private_key() sv.register(pk) digest = sv.chain.hash_message(self.text.encode()) dis.progress_bar_show(.75) self.result = tcc.secp256k1.sign(pk, digest) dis.progress_bar_show(1.0) self.done()
async def restore_complete_doit(fname_or_fd, words): from main import dis # build password password = '******'.join(words) # filename already picked, taste it and maybe consider using it's data. try: fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd except: await ux_show_story('Unable to open backup file. \n\n' + str(fname_or_fd)) return try: if not words: contents = fd.read() else: try: compat7z.check_file_headers(fd) except Exception as e: await ux_show_story('Unable to read backup file. Has it been touched?' '\n\nError: ' + str(e)) return dis.fullscreen("Decrypting...") try: zz = compat7z.Builder() fname, contents = zz.read_file(fd, password, progress_fcn=dis.progress_bar_show) assert fname == 'ckcc-backup.txt', "Wrong filename in archive" # simple quick sanity check assert contents[0:1] == b'#' and contents[-1:] == b'\n', "Corrupted after decrypt" except Exception as e: # assume everything here is "password wrong" errors print("pw wrong? %s" % e) await ux_show_story('Unable to decrypt backup file. Incorrect password?' '\n\nTried:\n\n' + password) return finally: fd.close() 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! await restore_from_dict(vals)
async def handle_upload(self, offset, total_size, data): from main import dis, sf, hsm_active from utils import check_firmware_hdr from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' if hsm_active: # additional restrictions in HSM mode assert offset + len(data) <= total_size <= MAX_TXN_LEN, 'psbt' if offset == 0: assert data[0:5] == b'psbt\xff', 'psbt' for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here dis.fullscreen("Receiving...", offset / total_size) sf.sector_erase(pos) while sf.is_busy(): await sleep_ms(10) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] self.file_checksum.update(here) # Very special case for firmware upgrades: intercept and modify # header contents on the fly, and also fail faster if wouldn't work # on this specific hardware. # - workaround: ckcc-protocol upgrade process understates the file # length and appends hdr, but that's kinda a bug, so support both if (pos == (FW_HEADER_OFFSET & ~255) or pos == (total_size - FW_HEADER_SIZE) or pos == total_size): prob = check_firmware_hdr(memoryview(here)[-128:], None, bad_magic_ok=True) if prob: raise ValueError(prob) sf.write(pos, here) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) if offset + len(data) >= total_size and not hsm_active: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
def wipe_flash_filesystem(): # erase and re-format the flash filesystem (/flash/) import ckcc, pyb from main import dis, settings dis.fullscreen('Erasing...') os.umount('/flash') # from extmod/vfs.h BP_IOCTL_SEC_COUNT = (4) BP_IOCTL_SEC_SIZE = (5) # block-level erase fl = pyb.Flash() bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0) assert bsize == 512 bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0) blk = bytearray(bsize) ckcc.rng_bytes(blk) # trickiness: actual flash blocks are offset by 0x100 (FLASH_PART1_START_BLOCK) # so fake MBR can be inserted. Count also inflated by 2X, but not from ioctl above. for n in range(bcount): fl.writeblocks(n + 0x100, blk) ckcc.rng_bytes(blk) dis.progress_bar_show(n * 2 / bcount) # rebuild and mount /flash dis.fullscreen('Rebuilding...') ckcc.wipe_fs() # re-store settings settings.save()
async def try_login(self, retry=True): from main import pa, numpad while retry: self.reset() pin = await self.interact() if pin is None: # Perhaps they are having trouble with touch pad? numpad.sensitivity = 2 continue pa.setup(pin, self.is_secondary) if pa.is_delay_needed() or pa.num_fails: await self.do_delay(pa) # do the actual login attempt now dis.fullscreen("Wait...") try: ok = pa.login() if ok: break except RuntimeError as e: # I'm a brick and other stuff can happen here print("pa.login: %r" % e) await ux_show_story('''\ That's not the right PIN!\n Please check all digits carefully, and that prefix verus suffix break point is correct. Your next attempt will take even longer, so please keep that in mind. ''', title='Wrong PIN')
def wipe_microsd_card(): import ckcc, pyb from main import dis try: os.umount('/sd') except: pass sd = pyb.SDCard() assert sd if not sd.present(): return # power cycle so card details (like size) are re-read from current card sd.power(0) sd.power(1) dis.fullscreen('Part Erase...') cutoff = 1024 # arbitrary blk = bytearray(512) for bnum in range(cutoff): ckcc.rng_bytes(blk) sd.writeblocks(bnum, blk) dis.progress_bar_show(bnum / cutoff) dis.fullscreen('Formating...') # remount, with newfs option os.mount(sd, '/sd', readonly=0, mkfs=1)
async def remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash from main import dis, pa if not stash.bip39_passphrase: if not await ux_confirm( '''You do not have a BIP39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.''' ): return dis.fullscreen('Check...') with stash.SensitiveValues() as sv: if sv.mode != 'words': # not a BIP39 derived secret, so cannot work. await ux_show_story( '''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed') return nv = SecretStash.encode(xprv=sv.node) dis.fullscreen('Saving...') pa.change(new_secret=nv) # re-read settings since key is now different # - also captures xfp, xpub at this point pa.new_main_secret(nv) # check and reload secret pa.reset() pa.login()
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 remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash from main import dis, pa dis.fullscreen('Check...') with stash.SensitiveValues() as sv: if sv.mode != 'words': # not a BIP39 derived secret, so cannot work. await ux_show_story('''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed') return nv = SecretStash.encode(xprv=sv.node) # Important: won't write new XFP to nvram if pw still set stash.bip39_passphrase = '' dis.fullscreen('Saving...') pa.change(new_secret=nv) # re-read settings since key is now different # - also captures xfp, xpub at this point pa.new_main_secret(nv) # check and reload secret pa.reset() pa.login()
async def append(self, xfp, bip39pw): # encrypt and save; always appends. from ux import ux_dramatic_pause from main 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 = tcc.AES(tcc.AES.CTR | tcc.AES.Encrypt, self.key) msg = encrypt.update(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
def activate(self, new_file): # user approved the HSM activation, so apply it. from main import pa, dis import main assert not main.hsm_active main.hsm_active = self self.start_time = utime.ticks_ms() if new_file: dis.fullscreen("Saving...") # save config for next run with open(POLICY_FNAME, 'w+t') as f: ujson.dump(self.save(), f) # that changes the flash, so need to update # the hash stored in SE pa.greenlight_firmware() dis.show() if self.set_sl: self.save_storage_locker() self.reset_period() if self.boot_to_hsm and not new_file: # In boot-to-HSM mode, we cant be sure PIN holder has authority # to spend, so maybe they are rebooting to reset the period. # Assume period has already been used up (conservative model) for r in self.rules: if r.per_period: self.record_spend(r, r.per_period)
def make_msg(start): msg = '' if start == 0: msg = "Press 1 to save to MicroSD." if version.has_fatram: msg += " 4 to view QR Codes." msg += '\n\n' msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) addrs = [] chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: for idx in range(start, start + n): subpath = path.format(account=0, change=0, idx=idx) node = sv.derive_path(subpath, register=False) addr = chain.address(node, addr_fmt) addrs.append(addr) msg += "%s =>\n%s\n\n" % (subpath, addr) dis.progress_bar_show(idx / n) stash.blank_object(node) msg += "Press 9 to see next group, 7 to go back. X to quit." return msg, addrs
def __init__(self, *args): super().__init__() from main import dis dis.fullscreen('Wait...') # this must set self.address and do other slow setup self.setup(*args)
def make_msg(): msg = '' if n > 1: if start == 0: msg = "Press 1 to save to MicroSD." if version.has_fatram and not ms_wallet: msg += " 4 to view QR Codes." msg += '\n\n' msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) else: # single address, from deep path given by user msg += "Showing single address." if version.has_fatram: msg += " Press 4 to view QR Codes." msg += '\n\n' addrs = [] chain = chains.current_chain() dis.fullscreen('Wait...') if ms_wallet: # IMPORTANT safety feature: never show complete address # but show enough they can verify addrs shown elsewhere. # - makes a redeem script # - converts into addr # - assumes 0/0 is first address. for (i, paths, addr, script) in ms_wallet.yield_addresses(start, n): if i == 0 and ms_wallet.N <= 4: msg += '\n'.join(paths) + '\n =>\n' else: msg += '.../0/%d =>\n' % i addrs.append(addr) msg += truncate_address(addr) + '\n\n' dis.progress_bar_show(i/n) else: # single-singer wallets with stash.SensitiveValues() as sv: for idx in range(start, start + n): deriv = path.format(account=self.account_num, change=0, idx=idx) node = sv.derive_path(deriv, register=False) addr = chain.address(node, addr_fmt) addrs.append(addr) msg += "%s =>\n%s\n\n" % (deriv, addr) dis.progress_bar_show(idx/n) stash.blank_object(node) if n > 1: msg += "Press 9 to see next group, 7 to go back. X to quit." return msg, addrs
async def choose_first_address(*a): # Choose from a truncated list of index 0 common addresses, remember # the last address the user selected and use it as the default from main import settings, dis chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: def truncate_address(addr): # Truncates address to width of screen, replacing middle chars middle = "-" leftover = SCREEN_CHAR_WIDTH - len(middle) start = addr[0:(leftover + 1) // 2] end = addr[len(addr) - (leftover // 2):] return start + middle + end # Create list of choices (address_index_0, path, addr_fmt) choices = [] for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) subpath = path.format(account=0, change=0, idx=0) node = sv.derive_path(subpath, register=False) address = chain.address(node, addr_fmt) choices.append((truncate_address(address), path, addr_fmt)) dis.progress_bar_show(len(choices) / len(chains.CommonDerivations)) stash.blank_object(node) picked = None async def clicked(_1, _2, item): if picked is None: picked = item.arg the_ux.pop() items = [ MenuItem(address, f=clicked, arg=i) for i, (address, path, addr_fmt) in enumerate(choices) ] menu = MenuSystem(items) menu.goto_idx(settings.get('axi', 0)) the_ux.push(menu) await menu.interact() if picked is None: return None # update last clicked address settings.put('axi', picked) address, path, addr_fmt = choices[picked] return (path, addr_fmt)
def __init__(self, subpath, addr_fmt): super().__init__() self.subpath = subpath from main import dis dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt)
async def make_summary_file(fname_pattern='public.txt'): from main import dis # record **public** values and helpful data into a text file dis.fullscreen('Generating...') # generator function: body = generate_public_contents() await write_text_file(fname_pattern, body, 'Summary')
async def initial_pin_setup(*a): # First time they select a PIN of any type. from login import LoginUX lll = LoginUX() pin = await lll.get_new_pin( 'Choose PIN', '''\ Pick the main wallet's PIN code now. Be more clever, but an example: 123-4567 It has two parts: prefix (123-) and suffix (-4567). \ Each part must between 2 to 6 digits long. Total length \ can be as long as 12 digits. The prefix part determines the anti-phishing words you will \ see each time you login. Your new PIN protects access to \ this Coldcard device and is not a factor in the wallet's \ seed words or private keys. THERE IS ABSOLUTELY NO WAY TO RECOVER A FORGOTTEN PIN! Write it down. ''') del lll if pin is None: return # A new pin is to be set! from main import pa, dis, settings, loop dis.fullscreen("Saving...") try: assert pa.is_blank() pa.change(new_pin=pin) # check it? kinda, but also get object into normal "logged in" state pa.setup(pin) ok = pa.login() assert ok # must re-read settings after login, because they are encrypted # with a key derived from the main secret. settings.set_key() settings.load() except Exception as e: print("Exception: %s" % e) # Allow USB protocol, now that we are auth'ed from usb import enable_usb enable_usb(loop, False) from menu import MenuSystem from flow import EmptyWallet return MenuSystem(EmptyWallet)
async def ux_dramatic_pause(msg, seconds): from main import dis # show a full-screen msg, with a dramatic pause + progress bar n = seconds * 8 dis.fullscreen(msg) for i in range(n): dis.progress_bar_show(i / n) await sleep_ms(125) ux_clear_keys()
def __init__(self, text, subpath, addr_fmt, approved_cb=None): super().__init__() self.text = text self.subpath = subpath self.approved_cb = approved_cb from main import dis dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt)
def wipe_most(self): # erase everything except settings: takes 5 seconds at least from nvstore import SLOTS end = SLOTS[0] from main import dis dis.fullscreen("Cleanup...") for addr in range(0, end, self.BLOCK_SIZE): self.block_erase(addr) dis.progress_bar_show(addr/end) while self.is_busy(): pass
async def render(self): # Choose from a truncated list of index 0 common addresses, remember # the last address the user selected and use it as the default from main import settings, dis chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: # Create list of choices (address_index_0, path, addr_fmt) choices = [] for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) if self.account_num != 0 and '{account}' not in path: # skip derivations that are not affected by account number continue deriv = path.format(account=self.account_num, change=0, idx=0) node = sv.derive_path(deriv, register=False) address = chain.address(node, addr_fmt) choices.append( (truncate_address(address), path, addr_fmt) ) dis.progress_bar_show(len(choices) / len(chains.CommonDerivations)) stash.blank_object(node) items = [MenuItem(address, f=self.pick_single, arg=(path, addr_fmt)) for i, (address, path, addr_fmt) in enumerate(choices)] # some other choices if self.account_num == 0: items.append(MenuItem("Account Number", f=self.change_account)) items.append(MenuItem("Custom Path", menu=self.make_custom)) # if they have MS wallets, add those next for ms in MultisigWallet.iter_wallets(): if not ms.addr_fmt: continue items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms)) else: items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account)) self.goto_idx(settings.get('axi', 0)) # weak self.replace_items(items)
def set_bip39_passphrase(pw): # apply bip39 passphrase for now (volatile) # takes a bit, so show something from main import dis dis.fullscreen("Working...") # set passphrase import stash stash.bip39_passphrase = pw # capture updated XFP with stash.SensitiveValues() as sv: # can't do it without original seed words (late, but caller has checked) assert sv.mode == 'words' sv.capture_xpub()
def set_bip39_passphrase(pw): # apply bip39 passphrase for now (volatile) # - return None or error msg import stash stash.bip39_passphrase = pw # takes a bit, so show something from main import dis dis.fullscreen("Working...") with stash.SensitiveValues() as sv: if sv.mode != 'words': # can't do it without original seed woods return 'No BIP39 seed words' sv.capture_xpub()
def clear_seed(): from main import dis, pa, settings import utime dis.fullscreen('Clearing...') # clear settings associated with this key, since it will be no more settings.blank() # save a blank secret (all zeros is a special case, detected by bootloader) nv = bytes(AE_SECRET_LEN) pa.change(new_secret=nv) dis.fullscreen('Reboot...') utime.sleep(1) # security: need to reboot to really be sure to clear the secrets from main memory. from machine import reset reset()
async def ship_wo_bag(*a): # Factory command: for dev and test units that have no bag number, and never will. ok = await ux_confirm('''Not recommended! DO NOT USE for units going to paying customers.''') if not ok: return import callgate from main import dis, pa, is_devmode failed = callgate.set_bag_number(b'NOT BAGGED') # 32 chars max if failed: await ux_dramatic_pause('FAILED', 30) else: # lock the bootrom firmware forever callgate.set_rdp_level(2 if not is_devmode else 0) # bag number affects green light status (as does RDP level) pa.greenlight_firmware() dis.fullscreen('No Bag. DONE') callgate.show_logout(1)
def sign_message_digest(digest, subpath, prompt): # do the signature itself! from main import dis if prompt: dis.fullscreen(prompt, percent=.25) with stash.SensitiveValues() as sv: dis.progress_bar_show(.50) node = sv.derive_path(subpath) pk = node.private_key() sv.register(pk) dis.progress_bar_show(.75) rv = tcc.secp256k1.sign(pk, digest) dis.progress_bar_show(1) return rv
async def handle_upload(self, offset, total_size, data): from main import dis, sf # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' rb = bytearray(256) for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here sf.sector_erase(pos) dis.fullscreen("Receiving...") dis.progress_bar_show(offset / total_size) while sf.is_busy(): await sleep_ms(10) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] sf.write(pos, here) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) # use actual read back for verify sf.read(pos, rb) self.file_checksum.update(rb[0:len(here)]) if offset + len(data) >= total_size: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
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)