async def address_explore(*a): # explore addresses based on derivation path chosen # by proxy external index=0 address while not settings.get('axskip', False): ch = await ux_show_story('''\ The following menu lists the first payment address \ produced by various common wallet systems. Choose the address that your desktop or mobile wallet \ has shown you as the first receive address. WARNING: Please understand that exceeding the gap limit \ of your wallet, or choosing the wrong address on the next screen \ may make it very difficult to recover your funds. Press 4 to start or 6 to hide this message forever.''', escape='46') if ch == '4': break if ch == '6': settings.set('axskip', True) break if ch == 'x': return m = AddressListMenu() await m.render() # slow the_ux.push(m)
async def activate(self, menu, idx): if getattr(self, 'chooser', None): start_chooser(self.chooser) else: # nesting menus, and functions and so on. f = getattr(self, 'next_function', None) if f: rv = await f(menu, idx, self) if isinstance(rv, MenuSystem): # XXX the function should do this itself # go to new menu the_ux.replace(rv) m = getattr(self, 'next_menu', None) if callable(m): m = await m(menu, idx, self) if isinstance(m, list): m = MenuSystem(m) if m: the_ux.push(m)
def drv_entro_start(*a): # UX entry ch = await ux_show_story('''\ Create Entropy for Other Wallets (BIP-85) This feature derives "entropy" based mathematically on this wallet's seed value. \ This will be displayed as a 12 or 24 word seed phrase, \ or formatted in other ways to make it easy to import into \ other wallet systems. You can recreate this value later, based \ only the seed-phrase or backup of this Coldcard. There is no way to reverse the process, should the other wallet system be compromised, \ so the other wallet is effectively segregated from the Coldcard and yet \ still backed-up.''') if ch != 'y': return if stash.bip39_passphrase: if not await ux_confirm('''You have a BIP39 passphrase set right now and so that will become wrapped into the new secret.'''): return choices = [ '12 words', '18 words', '24 words', 'WIF (privkey)', 'XPRV (BIP32)', '32-bytes hex', '64-bytes hex'] m = MenuSystem([MenuItem(c, f=drv_entro_step2) for c in choices]) the_ux.push(m)
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False): # Offer to import (enroll) a new multisig wallet. Allow reject by user. global active_request from multisig import MultisigWallet UserAuthorizedAction.cleanup() if sf_len: with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd: config = fd.read(sf_len).decode() # this call will raise on parsing errors, so let them rise up # and be shown on screen/over usb ms = MultisigWallet.from_file(config, name=name) active_request = NewEnrollRequest(ms) if ux_reset: # for USB case, and import from PSBT # kill any menu stack, and put our thing at the top abort_and_goto(active_request) else: # menu item case: add to stack from ux import the_ux the_ux.push(active_request)
async def start_over(self, *a): # pop everything we've done off the stack self.pop_all() # begin again, empty but same settings self.words = [] the_ux.push(self.__class__(items=None))
async def start_over(self, *a): # pop everything we've done off the stack WordNestMenu.pop_all() # begin again, empty but same settings WordNestMenu.words = [] the_ux.push(WordNestMenu(items=None))
async def start_over(self, *a): # pop everything we've done off the stack self.pop_all() # begin again, empty but same settings self.words = [] the_ux.push(self.__class__(num_words=WordNestMenu.target_words))
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)
async def all_done(new_words): # So we have another part, might be done or not. global import_xor_parts assert len(new_words) == 24 import_xor_parts.append(new_words) XORWordNestMenu.pop_all() num_parts = len(import_xor_parts) seed = xor32(*(bip39.a2b_words(w) for w in import_xor_parts)) msg = "You've entered %d parts so far.\n\n" % num_parts if num_parts >= 2: chk_word = bip39.b2a_words(seed).split(' ')[-1] msg += "If you stop now, the 24th word of the XOR-combined seed phrase\nwill be:\n\n" msg += "24: %s\n\n" % chk_word if all((not x) for x in seed): # zero seeds are never right. msg += "ZERO WARNING\nProvided seed works out to all zeros "\ "right now. You may have doubled a part or made some other mistake.\n\n" msg += "Press (1) to enter next list of words, or (2) if done with all words." ch = await ux_show_story(msg, strict_escape=True, escape='12x', sensitive=True) if ch == 'x': # give up import_xor_parts.clear() # concern: we are contaminated w/ secrets return None elif ch == '1': # do another list of words nxt = XORWordNestMenu(num_words=24) the_ux.push(nxt) elif ch == '2': # done; import on temp basis, or be the main secret from pincodes import pa enc = stash.SecretStash.encode(seed_phrase=seed) if pa.is_secret_blank(): # save it since they have no other secret set_seed_value(encoded=enc) # update menu contents now that wallet defined goto_top_menu() else: pa.tmp_secret(enc) await ux_show_story( "New master key in effect until next power down.") return None
def on_cancel(self): # user pressed cancel on a menu (so he's going upwards) # - if it's a step where we added to the word list, undo that. # - but keep them in our system until: # - when the word list is empty and they cancel, stop words = WordNestMenu.words if self.is_commit and words: words.pop() # replace the menu we are show w/ top-level (a-) menu the_ux.pop() nxt = WordNestMenu(is_commit=True) the_ux.push(nxt) else: the_ux.pop()
async def restore_complete(fname_or_fd): from ux import the_ux with imported('seed') as seed: async def done(words): # remove all pw-picking from menu stack seed.WordNestMenu.pop_all() await restore_complete_doit(fname_or_fd, words) # give them a menu to pick from m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done) the_ux.push(m)
def start_chooser(chooser): # get which one to show as selected, list of choices, and fcn to call after selected, choices, setter = chooser() async def picked(menu, picked, xx_self): menu.chosen = picked menu.show() await sleep_ms(100) # visual feedback that we changed it setter(picked, choices[picked]) the_ux.pop() # make a new menu, just for the choices m = MenuSystem([MenuItem(c, f=picked) for c in choices], chosen=selected) the_ux.push(m)
async def restore_complete(fname_or_fd): from ux import the_ux async def done(words): # remove all pw-picking from menu stack seed.WordNestMenu.pop_all() prob = await restore_complete_doit(fname_or_fd, words) if prob: await ux_show_story(prob, title='FAILED') # give them a menu to pick from, and start picking m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done) the_ux.push(m)
async def activate(self, menu, idx): if self.chooser: # get which one to show as selected, list of choices, and fcn to call after selected, choices, setter = self.chooser() def picked(menu, picked, xx_self): menu.chosen = picked menu.show() await sleep_ms(100) # visual feedback that we changed it setter(picked, choices[picked]) the_ux.pop() # make a new menu, just for the choices m = MenuSystem([MenuItem(c, f=picked) for c in choices], chosen=selected) the_ux.push(m) else: # nesting menus, and functions and so on. f = self.next_function if f: rv = await f(menu, idx, self) if isinstance(rv, MenuSystem): # XXX the function should do this itself # go to new menu the_ux.replace(rv) m = self.next_menu if callable(m): m = await m(menu, idx, self) if isinstance(m, list): m = MenuSystem(m) if m: the_ux.push(m)
async def start_hsm_approval(sf_len=0, usb_mode=False, startup_mode=False): # Show details of the proposed HSM policy (or saved one) # If approved, go into HSM mode and never come back to normal. UserAuthorizedAction.cleanup() is_new = True if sf_len: with SFFile(0, length=sf_len) as fd: json = fd.read(sf_len).decode() else: try: json = open(POLICY_FNAME, 'rt').read() except: raise ValueError("No existing policy") is_new = False # parse as JSON cant_fail = False try: try: js_policy = ujson.loads(json) except: raise ValueError("JSON parse fail") cant_fail = bool(js_policy.get('boot_to_hsm', False)) # parse the policy policy = HSMPolicy() policy.load(js_policy) except BaseException as exc: err = "HSM Policy invalid: %s: %s" % (problem_file_line(exc), str(exc)) if usb_mode: raise ValueError(err) # What to do in a menu case? Shouldn't happen anyway, but # maybe they upgraded the firmware, and so old policy file # isn't suitable anymore. # - or maybe the settings have been f-ed with. print(err) if startup_mode and cant_fail: # die as a brick here, not safe to proceed w/o HSM active import callgate, ux ux.show_fatal_error(err.replace(': ', ':\n ')) callgate.show_logout(1) # die w/ it visible # not reached await ux_show_story("Cannot start HSM.\n\n%s" % err) return # Boot-to-HSM feature: don't ask, just start policy immediately if startup_mode and policy.boot_to_hsm: msg = uio.StringIO() policy.explain(msg) policy.activate(False) the_ux.reset(hsm_ux_obj) return None ar = ApproveHSMPolicy(policy, is_new) UserAuthorizedAction.active_request = ar if startup_mode: return ar if usb_mode: # for USB case, kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) else: # menu item case: add to stack, so we can still back out from ux import the_ux the_ux.push(UserAuthorizedAction.active_request) return ar
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 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
def sign_txt_file(filename): # sign a one-line text file found on a MicroSD card # - not yet clear how to do address types other than 'classic' from files import CardSlot, CardMissingError from sram2 import tmp_buf UserAuthorizedAction.cleanup() addr_fmt = AF_CLASSIC # copy message into memory with CardSlot() as card: with open(filename, 'rt') as fd: text = fd.readline().strip() subpath = fd.readline().strip() if subpath: try: assert subpath[0:1] == 'm' subpath = cleanup_deriv_path(subpath) except: await ux_show_story("Second line of file, if included, must specify a subkey path, like: m/44'/0/0") return # if they are following BIP84 recommended derivation scheme, # then they probably would prefer a segwit/bech32 formatted address if subpath.startswith("m/84'/"): addr_fmt = AF_P2WPKH else: # default: top of wallet. subpath = 'm' try: try: text = str(text, 'ascii') except UnicodeError: raise AssertionError('non-ascii characters') ApproveMessageSign.validate(text) except AssertionError as exc: await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc) return 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': await ux_aborted() return # done. msg = "Created new file:\n\n%s" % out_fn await ux_show_story(msg, title='File Signed') UserAuthorizedAction.check_busy() UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt, approved_cb=done) # do not kill the menu stack! from ux import the_ux the_ux.push(UserAuthorizedAction.active_request)