def user_images(username, page): per_page = settings.get("images_per_page", 10) images = UserImages.get_gallery_images(page = page,\ per_page = per_page,\ username = username) show_pagination = images.wrapped_count() > per_page images_uploaded = bool(tuple(images)) if not tuple(images) and page != 1: abort(404) thumbnail_size = settings.get("thumbnail_size", "l") pagination = Pagination(page, per_page, images.wrapped_count() ) return render_template("user_images.html",\ images_uploaded = images_uploaded,\ pagination = pagination,\ show_upload_btn = True,\ thumbnail_size = thumbnail_size,\ images = images,\ show_pagination = show_pagination)
def gallery(page): if not get_config().gallery: return redirect(url_for("index")) per_page = settings.get("images_per_page", 10) images = UserImages.get_gallery_images(page = page,\ per_page = per_page,\ gallery = True) if not tuple(images) and page != 1: abort(404) count = images.wrapped_count() show_pagination = count > per_page thumbnail_size = settings.get("thumbnail_size", "l") images_uploaded = count > 0 pagination = Pagination(page, per_page, count) return render_template("gallery.html",\ images_uploaded = images_uploaded,\ pagination = pagination,\ thumbnail_size = thumbnail_size,\ images = images,\ show_pagination = show_pagination)
def article_view(slug): article = Articles.get_article_by_slug(slug) if not article: abort(404) author = article.author next_article = article.get_next_article() previous_article = article.get_previous_article() user_picture = settings.get("portrait", False) related_articles = article.get_similar_articles() show_related_articles = related_articles.wrapped_count(False) > 0 article_series = article.get_article_series() return render_template("article_view.html",\ article = article,\ author = author,\ user_picture = user_picture,\ show_related_articles = show_related_articles,\ related_articles = related_articles,\ next_article = next_article,\ article_series = article_series,\ previous_article = previous_article)
def index(page): articles_per_page = settings.get("articles_per_page") articles = Articles.get_index_articles(page, articles_per_page) count = articles.wrapped_count() show_pagination = count > articles_per_page articles_written = count > 0 if not articles_written and page != 1: abort(404) pagination = Pagination(page, articles_per_page, count) user = Users.get_user(1) images = dict() images['logo'] = settings['logo'] images['portrait'] = settings['portrait'] images['bg'] = settings['bg'] if not user: return redirect(url_for('create_account')) return render_template("index.html",\ pagination = pagination,\ articles = articles,\ images = images,\ articles_written = articles_written,\ show_pagination = show_pagination,\ user = user\ )
async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from main import dis, settings import ustruct xfp = xfp2str(settings.get('xfp')) dis.fullscreen('Generating...') # make the data examples = [] payload = ujson.dumps(list(generate_bitcoin_core_wallet(examples, account_num))) body = '''\ # Bitcoin Core Wallet Import File https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md ## For wallet with master key fingerprint: {xfp} Wallet operates on blockchain: {nb} ## Bitcoin Core RPC The following command can be entered after opening Window -> Console in Bitcoin Core, or using bitcoin-cli: importmulti '{payload}' ## Resulting Addresses (first 3) '''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) body += '\n'.join('%s => %s' % t for t in examples) body += '\n' await write_text_file(fname_pattern, body, 'Bitcoin Core')
async def done_apply(self, *a): # apply the passphrase. # - important to work on empty string here too. from stash import bip39_passphrase old_pw = str(bip39_passphrase) err = set_bip39_passphrase(pp_sofar) if err: # kinda very late: but if not BIP39 based key, ends up here. return await ux_show_story(err, title="Fail") from main import settings xfp = settings.get('xfp') ch = await ux_show_story('''Above is the master key fingerprint of the new wallet. Press X to abort and keep editing passphrase. OK to use the new wallet.''', title="[%s]" % xfp2str(xfp)) if ch == 'x': # go back! set_bip39_passphrase(old_pw) return goto_top_menu()
def handle_bag_number(self, bag_num): import version, callgate from main import dis, pa, is_devmode, settings if version.is_factory_mode() and bag_num: # check state first assert settings.get('tested', False) assert pa.is_blank() assert bag_num[0:2] == b'C0' and len(bag_num) == 8 # do the change failed = callgate.set_bag_number(bag_num) assert not failed callgate.set_rdp_level(2 if not is_devmode else 0) pa.greenlight_firmware() dis.fullscreen(bytes(bag_num).decode()) self.call_after(callgate.show_logout, 1) # always report the existing/new value val = callgate.get_bag_number() or b'' return b'asci' + val
def idle_timeout_chooser(): from ux import DEFAULT_IDLE_TIMEOUT timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT) # in seconds ch = [ ' 2 minutes', ' 5 minutes', '15 minutes', ' 1 hour', ' 4 hours', ' 8 hours', ' Never' ] va = [ 2*60, 5*60, 15*60, 3600, 4*3600, 8*3600, 0 ] try: which = va.index(timeout) except ValueError: which = 0 def set_idle_timeout(idx, text): settings.set('idle_to', va[idx]) return which, ch, set_idle_timeout
async def idle_logout(): import main from main import numpad, settings while not main.hsm_active: await sleep_ms(250) # they may have changed setting recently timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT) * 1000 # ms if timeout == 0: continue now = utime.ticks_ms() if not numpad.last_event_time: continue if now > numpad.last_event_time + timeout: # do a logout now. print("Idle!") from actions import logout_now await logout_now() return # not reached
def generate_wasabi_wallet(): # Generate the data for a JSON file which Wasabi can open directly as a new wallet. from main import settings import ustruct, version # bitcoin (xpub) is used, even for testnet case (ie. no tpub) # - altho, doesn't matter; the wallet operates based on it's own settings for test/mainnet # regardless of the contents of the wallet file btc = chains.BitcoinMain with stash.SensitiveValues() as sv: xpub = btc.serialize_public(sv.derive_path("84'/0'/0'")) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp) chain = chains.current_chain() assert chain.ctype in {'BTC', 'XTN'}, "Only Bitcoin supported" _, vers, _ = version.get_mpy_version() return dict(MasterFingerprint=txt_xfp, ColdCardFirmwareVersion=vers, ExtPubKey=xpub)
async def idle_logout(): from main import numpad, settings while 1: await sleep_ms(250) # they may have changed setting recently timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT) * 1000 # ms if timeout == 0: continue now = utime.ticks_ms() if not numpad.last_event_time: continue if now > numpad.last_event_time + timeout: # do a logout now. print("Idle timeout now!") import callgate callgate.show_logout() return
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)
async def start_login_sequence(): # Boot up login sequence here. # from main import pa, settings, dis, loop, numpad import version if pa.is_blank(): # Blank devices, with no PIN set all, can continue w/o login # Do green-light set immediately after firmware upgrade if version.is_fresh_version(): pa.greenlight_firmware() dis.show() goto_top_menu() return # Allow impatient devs and crazy people to skip the PIN guess = settings.get('_skip_pin', None) if guess is not None: try: dis.fullscreen("(Skip PIN)") pa.setup(guess) pa.login() except: pass # if that didn't work, or no skip defined, force # them to login succefully. while not pa.is_successful(): # always get a PIN and login first await block_until_login() # Must read settings after login settings.set_key() settings.load() # Restore a login preference or two numpad.sensitivity = settings.get('sens', numpad.sensitivity) # Do green-light set immediately after firmware upgrade if not pa.is_secondary: if version.is_fresh_version(): pa.greenlight_firmware() dis.show() # Populate xfp/xpub values, if missing. # - can happen for first-time login of duress wallet # - may indicate lost settings, which we can easily recover from # - these values are important to USB protocol if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank(): try: import stash # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues() as sv: sv.capture_xpub() except Exception as exc: # just in case, keep going; we're not useless and this # is early in boot process print("XFP save failed: %s" % exc) # Allow USB protocol, now that we are auth'ed from usb import enable_usb enable_usb(loop, False) goto_top_menu()
# load up the simulator w/ indicated list of seed words import tcc, main from sim_settings import sim_defaults import stash, chains from h import b2a_hex from main import settings, pa from stash import SecretStash, SensitiveValues from seed import set_seed_value tn = chains.BitcoinTestnet if 1: settings.current = sim_defaults settings.set('chain', 'XTN') set_seed_value(main.WORDS) print("New key in effect: %s" % settings.get('xpub', 'MISSING')) print("Fingerprint: 0x%08x" % settings.get('xfp', 0)) #assert settings.get('xfp', 0) == node.my_fingerprint()
def get(cls): from main import settings rv = settings.get(KEY) return rv or dict()
def load_cache(cls): # first time: read saved value, but rest of time; use what's in memory if not cls._cache_loaded: saved = settings.get(cls.KEY) or [] cls.runtime_cache.extend(saved) cls._cache_loaded = True
] DebugFunctionsMenu = [ # xxxxxxxxxxxxxxxx MenuItem('Debug: assert', f=debug_assert), MenuItem('Debug: except', f=debug_except), MenuItem('Check: BL FW', f=check_firewall_read), MenuItem('Warm Reset', f=reset_self), #MenuItem("Perform Selftest", f=start_selftest), ] DangerZoneMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Debug Functions", menu=DebugFunctionsMenu), # actually harmless MenuItem('Lock Down Seed', f=convert_bip39_to_bip32, predicate=lambda: settings.get('words', True)), MenuItem("Destroy Seed", f=clear_seed), MenuItem("I Am Developer.", menu=maybe_dev_menu), MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label MenuItem('Perform Selftest', f=start_selftest), # little harmful MenuItem("Set High-Water", f=set_highwater), ] BackupStuffMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Backup System", f=backup_everything), MenuItem("Verify Backup", f=verify_backup), MenuItem("Restore Backup", f=restore_everything), # just a redirect really MenuItem("Dump Summary", f=dump_summary), ]
async def handle(self, cmd, args): # Dispatch incoming message, and provide reply. try: cmd = bytes(cmd).decode() except: raise FramingError('decode') if cmd == 'dfu_': # only useful in factory, undocumented. return self.call_after(callgate.enter_dfu) if cmd == 'rebo': import machine return self.call_after(machine.reset) if cmd == 'logo': return self.call_after(callgate.show_logout) if cmd == 'ping': return b'biny' + args if cmd == 'upld': offset, total_size = unpack_from('<II', args) data = memoryview(args)[4 + 4:] return await self.handle_upload(offset, total_size, data) if cmd == 'dwld': offset, length, fileno = unpack_from('<III', args) return await self.handle_download(offset, length, fileno) if cmd == 'ncry': version, his_pubkey = unpack_from('<I64s', args) return self.handle_crypto_setup(version, his_pubkey) if cmd == 'vers': from version import get_mpy_version from callgate import get_bl_version # Returning: date, version(human), bootloader version, full date version # BUT: be ready for additions! rv = list(get_mpy_version()) rv.insert(2, get_bl_version()[0]) return b'asci' + ('\n'.join(rv)).encode() if cmd == 'sha2': return b'biny' + self.file_checksum.digest() if cmd == 'xpub': assert self.encrypted_req, 'must encrypt' return self.handle_xpub(str(args, 'ascii')) if cmd == 'mitm': assert self.encrypted_req, 'must encrypt' return await self.handle_mitm_check() if cmd == 'smsg': # sign message addr_fmt, len_subpath, len_msg = unpack_from('<III', args) subpath = args[12:12 + len_subpath] msg = args[12 + len_subpath:] assert len(msg) == len_msg, "badlen" from auth import sign_msg sign_msg(msg, subpath, addr_fmt) return None if cmd == 'p2sh': # show P2SH (probably multisig) address on screen (also provides it back) # - must provide redeem script, and list of [xfp+path] from auth import start_show_p2sh_address # new multsig goodness, needs mapping from xfp->path and M values addr_fmt, M, N, script_len = unpack_from('<IBBH', args) assert addr_fmt & AFC_SCRIPT assert 1 <= M <= N <= 20 assert 30 <= script_len <= 520 offset = 8 witdeem_script = args[offset:offset + script_len] offset += script_len assert len(witdeem_script) == script_len xfp_paths = [] for i in range(N): ln = args[offset] assert 2 <= ln <= 16, 'badlen' xfp_paths.append(unpack_from('<%dI' % ln, args, offset + 1)) offset += (ln * 4) + 1 assert offset == len(args) return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths, witdeem_script) if cmd == 'show': # simple cases, older code: text subpath from auth import start_show_address addr_fmt, = unpack_from('<I', args) assert not (addr_fmt & AFC_SCRIPT) return b'asci' + start_show_address(addr_fmt, subpath=args[4:]) if cmd == 'enrl': # Enroll new xpubkey to be involved in multisigs. # - text config file must already be uploaded file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 100 < file_len <= (20 * 200), "badlen" # Start an UX interaction, return immediately here from auth import maybe_enroll_xpub maybe_enroll_xpub(sf_len=file_len, ux_reset=True) return None if cmd == 'msck': # Quick check to test if we have a wallet already installed. from multisig import MultisigWallet M, N, xfp_xor = unpack_from('<3I', args) return int(MultisigWallet.quick_check(M, N, xfp_xor)) if cmd == 'stxn': # sign transaction txn_len, finalize, txn_sha = unpack_from('<II32s', args) if txn_sha != self.file_checksum.digest(): return b'err_Checksum' assert 50 < txn_len <= MAX_TXN_LEN, "bad txn len" from auth import sign_transaction sign_transaction(txn_len, bool(finalize)) return None if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok' or cmd == 'enok': # Have we finished (whatever) the transaction, # which needed user approval? If so, provide result. from auth import active_request, UserAuthorizedAction if not active_request: return b'err_No active request' if active_request.refused: UserAuthorizedAction.cleanup() return b'refu' if active_request.failed: rv = b'err_' + active_request.failed.encode() UserAuthorizedAction.cleanup() return rv if not active_request.result: # STILL waiting on user return None if cmd == 'pwok' or cmd == 'enok': # return new root xpub xpub = active_request.result UserAuthorizedAction.cleanup() return b'asci' + bytes(xpub, 'ascii') elif cmd == 'smok': # signed message done: just give them the signature addr, sig = active_request.address, active_request.result UserAuthorizedAction.cleanup() return pack('<4sI', 'smrx', len(addr)) + addr.encode() + sig else: # generic file response resp_len, sha = active_request.result UserAuthorizedAction.cleanup() return pack('<4sI32s', 'strx', resp_len, sha) if cmd == 'pass': # bip39 passphrase provided, maybe use it if authorized assert self.encrypted_req, 'must encrypt' from auth import start_bip39_passphrase from main import settings assert settings.get('words', True), 'no seed' assert len(args) < 400, 'too long' pw = str(args, 'utf8') assert len(pw) < 100, 'too long' return start_bip39_passphrase(pw) if cmd == 'back': # start backup: asks user, takes long time. from auth import start_remote_backup return start_remote_backup() if cmd == 'bagi': return self.handle_bag_number(args) if is_simulator() and cmd[0].isupper(): # special hacky commands to support testing w/ the simulator from sim_usb import do_usb_command return do_usb_command(cmd, args) print("USB garbage: %s +[%d]" % (cmd, len(args))) return b'err_Unknown cmd'
# load up the simulator w/ indicated test master key import tcc, main from sim_settings import sim_defaults import stash, chains from h import b2a_hex from main import settings, pa from stash import SecretStash, SensitiveValues tn = chains.BitcoinTestnet node = tcc.bip32.deserialize(main.TPRV, tn.b32_version_pub, tn.b32_version_priv) assert node if settings.get('xfp') == node.my_fingerprint(): print("right xfp already") else: settings.current = sim_defaults settings.set('chain', 'XTN') raw = SecretStash.encode(xprv=node) pa.change(new_secret=raw) pa.new_main_secret(raw) print("New key in effect: %s" % settings.get('xpub', 'MISSING')) print("Fingerprint: 0x%08x" % settings.get('xfp', 0)) assert settings.get('xfp', 0) == node.my_fingerprint()
# execfile('../../testing/devtest/nvram.py') from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex import tcc, ustruct from main import settings, sf from nvstore import SLOTS # reset whatever's there sf.chip_erase() settings.load() for v in [123, 'hello', 34.56, dict(a=45)]: settings.set('abc', v) assert settings.get('abc') == v a = settings.get('_age', -1) settings.save() assert settings.get('_age') >= a + 1, [settings.get('_age'), a + 1] chk = dict(settings.current) settings.load() # some minor differences in values: bytes vs. strings, so just check keys assert sorted(list(chk)) == sorted(list(settings.current)), \ 'readback fail: \n%r != \n%r' % (chk, settings.current) if 1: # fill it up covered = set()
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # import main from main import settings from ujson import dumps RV.write(dumps(settings.get(main.SKEY)))
def exists(cls): # are there any wallets defined? from main import settings return bool(settings.get('multisig', False))
def from_file(cls, config, name=None): # Given a simple text file, parse contents and create instance (unsaved). # format is: label: value # where label is: # name: nameforwallet # policy: M of N # (8digithex): xpub of cosigner # # quick checks: # - name: 1-20 ascii chars # - M of N line (assume N of N if not spec'd) # - xpub: any bip32 serialization we understand, but be consistent # from main import settings my_xfp = settings.get('xfp') common_prefix = None xpubs = [] path_tops = set() M, N = -1, -1 has_mine = False addr_fmt = AF_P2SH expect_chain = chains.current_chain().ctype lines = config.split('\n') for ln in lines: # remove comments comm = ln.find('#') if comm != -1: ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: # optimization: allow bare xpub if we can calc xfp label = '0' * 8 value = ln else: # complain? if ln: print("no colon: " + ln) continue else: label, value = ln.split(':') label = label.lower() value = value.strip() if label == 'name': name = value elif label == 'policy': try: # accepts: 2 of 3 2/3 2,3 2 3 etc mat = ure.search(r'(\d+)\D*(\d+)', value) assert mat M = int(mat.group(1)) N = int(mat.group(2)) assert 1 <= M <= N <= MAX_SIGNERS except: raise AssertionError('bad policy line') elif label == 'derivation': # reveal the **common** path derivation for all keys try: mat = ure.search(r"(m/)([0123456789/']+)", value) assert mat common_prefix = mat.group(2) assert common_prefix assert 1 <= len(common_prefix) < 30 except: raise AssertionError('bad derivation line') elif label == 'format': # pick segwit vs. classic vs. wrapped version value = value.lower() for fmt_code, fmt_label in cls.FORMAT_NAMES: if value == fmt_label: addr_fmt = fmt_code break else: raise AssertionError('bad format line') elif len(label) == 8: try: xfp = str2xfp(label) except: # complain? #print("Bad xfp: " + ln) continue # deserialize, update list and lots of checks xfp = cls.check_xpub(xfp, value, expect_chain, xpubs, path_tops) if xfp == my_xfp: # not conclusive, but enough for error catching. has_mine = True assert len(xpubs), 'need xpubs' if M == N == -1: # default policy: all keys N = M = len(xpubs) if not name: # provide a default name name = '%d-of-%d' % (M, N) try: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: raise AssertionError('name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N assert addr_fmt & AFC_SCRIPT, 'script style addr fmt' # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway assert has_mine, 'my key not included' if not common_prefix and len(path_tops) == 1: # fill in the common prefix iff we can deduce it from xpubs common_prefix = path_tops.pop() # done. have all the parts return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain, common_prefix=common_prefix)
# quickly clear all multisig wallets installed from main import settings from ux import restore_menu if settings.get('multisig'): del settings.current['multisig'] settings.save() print("cleared multisigs") restore_menu()
async def start_login_sequence(): # Boot up login sequence here. # from main import pa, settings, dis, loop, numpad from ux import idle_logout if pa.is_blank(): # Blank devices, with no PIN set all, can continue w/o login # Do green-light set immediately after firmware upgrade if version.is_fresh_version(): pa.greenlight_firmware() dis.show() goto_top_menu() return # maybe show a nickname before we do anything nickname = settings.get('nick', None) if nickname: try: await show_nickname(nickname) except: pass # Allow impatient devs and crazy people to skip the PIN guess = settings.get('_skip_pin', None) if guess is not None: try: dis.fullscreen("(Skip PIN)") pa.setup(guess) pa.login() except: pass # if that didn't work, or no skip defined, force # them to login succefully. while not pa.is_successful(): # always get a PIN and login first await block_until_login() # Must re-read settings after login settings.set_key() settings.load() # implement "login countdown" feature delay = settings.get('lgto', 0) if delay: pa.reset() await login_countdown(delay) await block_until_login() # implement idle timeout now that we are logged-in loop.create_task(idle_logout()) # Do green-light set immediately after firmware upgrade if not pa.is_secondary: if version.is_fresh_version(): pa.greenlight_firmware() dis.show() # Populate xfp/xpub values, if missing. # - can happen for first-time login of duress wallet # - may indicate lost settings, which we can easily recover from # - these values are important to USB protocol if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank(): try: import stash # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues() as sv: sv.capture_xpub() except Exception as exc: # just in case, keep going; we're not useless and this # is early in boot process print("XFP save failed: %s" % exc) # If HSM policy file is available, offer to start that, # **before** the USB is even enabled. if version.has_fatram: try: import hsm, hsm_ux if hsm.hsm_policy_available(): ar = await hsm_ux.start_hsm_approval(usb_mode=False, startup_mode=True) if ar: await ar.interact() except: pass # Allow USB protocol, now that we are auth'ed from usb import enable_usb enable_usb(loop, False) goto_top_menu()
def from_file(cls, config, name=None): # Given a simple text file, parse contents and create instance (unsaved). # format is: label: value # where label is: # name: nameforwallet # policy: M of N # format: p2sh (+etc) # derivation: m/45'/0 (common prefix) # (8digithex): xpub of cosigner # # quick checks: # - name: 1-20 ascii chars # - M of N line (assume N of N if not spec'd) # - xpub: any bip32 serialization we understand, but be consistent # from main import settings my_xfp = settings.get('xfp') deriv = None xpubs = [] M, N = -1, -1 has_mine = 0 addr_fmt = AF_P2SH expect_chain = chains.current_chain().ctype lines = config.split('\n') for ln in lines: # remove comments comm = ln.find('#') if comm == 0: continue if comm != -1: if not ln[comm+1:comm+2].isdigit(): ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: # pointless optimization: allow bare xpub if we can calc xfp label = '0'*8 value = ln else: # complain? #if ln: print("no colon: " + ln) continue else: label, value = ln.split(':', 1) label = label.lower() value = value.strip() if label == 'name': name = value elif label == 'policy': try: # accepts: 2 of 3 2/3 2,3 2 3 etc mat = ure.search(r'(\d+)\D*(\d+)', value) assert mat M = int(mat.group(1)) N = int(mat.group(2)) assert 1 <= M <= N <= MAX_SIGNERS except: raise AssertionError('bad policy line') elif label == 'derivation': # reveal the path derivation for following key(s) try: assert value, 'blank' deriv = cleanup_deriv_path(value) except BaseException as exc: raise AssertionError('bad derivation line: ' + str(exc)) elif label == 'format': # pick segwit vs. classic vs. wrapped version value = value.lower() for fmt_code, fmt_label in cls.FORMAT_NAMES: if value == fmt_label: addr_fmt = fmt_code break else: raise AssertionError('bad format line') elif len(label) == 8: try: xfp = str2xfp(label) except: # complain? #print("Bad xfp: " + ln) continue # deserialize, update list and lots of checks is_mine = cls.check_xpub(xfp, value, deriv, expect_chain, my_xfp, xpubs) if is_mine: has_mine += 1 assert len(xpubs), 'need xpubs' if M == N == -1: # default policy: all keys N = M = len(xpubs) if not name: # provide a default name name = '%d-of-%d' % (M, N) try: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: raise AssertionError('name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N assert addr_fmt & AFC_SCRIPT, 'script style addr fmt' # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway assert has_mine != 0, 'my key not included' assert has_mine == 1 # 'my key included more than once' # done. have all the parts return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain)
async def test_7z(): # test full 7z round-trip # Altho cleartext mode is not for real, if the code is written, I must test it. from backups import write_complete_backup, restore_complete_doit from sffile import SFFile import tcc, version, uos from main import settings, sf, numpad, pa if version.has_fatram: import hsm had_policy = hsm.hsm_policy_available() else: had_policy = False today = tcc.random.uniform(1000000) import machine machine.reset = lambda: None for chain in ['BTC', 'XTN']: for words in ([], ['abc', 'def']): settings.set('check', today) settings.set('chain', chain) if version.has_608: ls = b'%416d' % today pa.ls_change(ls) ll, sha = await write_complete_backup(words, None, True) result = SFFile(0, ll).read() if words: #open('debug.7z', 'wb').write(result) assert ll > 800 assert len(sha) == 32 assert result[0:6] == b"7z\xbc\xaf'\x1c" assert tcc.sha256(result).digest() == sha assert len(set(result)) >= 240 # encrypted else: sr = str(result, 'ascii') print("Backup contents:\n" + sr) assert sr[0] == '#', result assert 'Coldcard' in sr assert len(set(sr)) < 100 # cleartext, english assert ('chain = "%s"' % chain) in result # test restore # - cant wipe flash, since the backup file is there # - cant wipe all settings becuase PIN and stuff is simulated there del settings.current['check'] if had_policy: from hsm import POLICY_FNAME uos.unlink(POLICY_FNAME) assert not hsm.hsm_policy_available() with SFFile(0, ll) as fd: numpad.inject('y') # for 'success' message await restore_complete_doit(fd, words) assert settings.get('check') == today, \ (settings.get('check'), '!=', today) assert settings.get('chain') == chain, \ (settings.get('chain'), '!=', chain) if version.has_608: assert pa.ls_fetch() == ls if had_policy: assert had_policy == hsm.hsm_policy_available() today += 3 import ux ux.restore_menu()
async def handle(self, cmd, args): # Dispatch incoming message, and provide reply. from main import hsm_active, is_devmode try: cmd = bytes(cmd).decode() except: raise FramingError('decode') if cmd[0].isupper() and (is_simulator() or is_devmode): # special hacky commands to support testing w/ the simulator try: from usb_test_commands import do_usb_command return do_usb_command(cmd, args) except: raise pass if hsm_active: # only a few commands are allowed during HSM mode if cmd not in HSM_WHITELIST: raise HSMDenied if cmd == 'dfu_': # only useful in factory, undocumented. return self.call_after(callgate.enter_dfu) if cmd == 'rebo': import machine return self.call_after(machine.reset) if cmd == 'logo': from utils import clean_shutdown return self.call_after(clean_shutdown) if cmd == 'ping': return b'biny' + args if cmd == 'upld': offset, total_size = unpack_from('<II', args) data = memoryview(args)[4 + 4:] return await self.handle_upload(offset, total_size, data) if cmd == 'dwld': offset, length, fileno = unpack_from('<III', args) return await self.handle_download(offset, length, fileno) if cmd == 'ncry': version, his_pubkey = unpack_from('<I64s', args) return self.handle_crypto_setup(version, his_pubkey) if cmd == 'vers': from version import get_mpy_version, hw_label from callgate import get_bl_version # Returning: date, version(human), bootloader version, full date version # BUT: be ready for additions! rv = list(get_mpy_version()) rv.insert(2, get_bl_version()[0]) rv.append(hw_label) return b'asci' + ('\n'.join(rv)).encode() if cmd == 'sha2': return b'biny' + self.file_checksum.digest() if cmd == 'xpub': assert self.encrypted_req, 'must encrypt' return self.handle_xpub(args) if cmd == 'mitm': assert self.encrypted_req, 'must encrypt' return await self.handle_mitm_check() if cmd == 'smsg': # sign message addr_fmt, len_subpath, len_msg = unpack_from('<III', args) subpath = args[12:12 + len_subpath] msg = args[12 + len_subpath:] assert len(msg) == len_msg, "badlen" from auth import sign_msg sign_msg(msg, subpath, addr_fmt) return None if cmd == 'p2sh': # show P2SH (probably multisig) address on screen (also provides it back) # - must provide redeem script, and list of [xfp+path] from auth import start_show_p2sh_address if hsm_active and not hsm_active.approve_address_share( is_p2sh=True): raise HSMDenied # new multsig goodness, needs mapping from xfp->path and M values addr_fmt, M, N, script_len = unpack_from('<IBBH', args) assert addr_fmt & AFC_SCRIPT assert 1 <= M <= N <= 20 assert 30 <= script_len <= 520 offset = 8 witdeem_script = args[offset:offset + script_len] offset += script_len assert len(witdeem_script) == script_len xfp_paths = [] for i in range(N): ln = args[offset] assert 1 <= ln <= 16, 'badlen' xfp_paths.append(unpack_from('<%dI' % ln, args, offset + 1)) offset += (ln * 4) + 1 assert offset == len(args) return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths, witdeem_script) if cmd == 'show': # simple cases, older code: text subpath from auth import start_show_address addr_fmt, = unpack_from('<I', args) assert not (addr_fmt & AFC_SCRIPT) return b'asci' + start_show_address(addr_fmt, subpath=args[4:]) if cmd == 'enrl': # Enroll new xpubkey to be involved in multisigs. # - text config file must already be uploaded file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 100 < file_len <= (20 * 200), "badlen" # Start an UX interaction, return immediately here from auth import maybe_enroll_xpub maybe_enroll_xpub(sf_len=file_len, ux_reset=True) return None if cmd == 'msck': # Quick check to test if we have a wallet already installed. from multisig import MultisigWallet M, N, xfp_xor = unpack_from('<3I', args) return int(MultisigWallet.quick_check(M, N, xfp_xor)) if cmd == 'stxn': # sign transaction txn_len, flags, txn_sha = unpack_from('<II32s', args) if txn_sha != self.file_checksum.digest(): return b'err_Checksum' assert 50 < txn_len <= MAX_TXN_LEN, "bad txn len" from auth import sign_transaction sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha) return None if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok': # Have we finished (whatever) the transaction, # which needed user approval? If so, provide result. from auth import UserAuthorizedAction req = UserAuthorizedAction.active_request if not req: return b'err_No active request' if req.refused: UserAuthorizedAction.cleanup() return b'refu' if req.failed: rv = b'err_' + req.failed.encode() UserAuthorizedAction.cleanup() return rv if not req.result: # STILL waiting on user return None if cmd == 'pwok': # return new root xpub xpub = req.result UserAuthorizedAction.cleanup() return b'asci' + bytes(xpub, 'ascii') elif cmd == 'smok': # signed message done: just give them the signature addr, sig = req.address, req.result UserAuthorizedAction.cleanup() return pack('<4sI', 'smrx', len(addr)) + addr.encode() + sig else: # generic file response resp_len, sha = req.result UserAuthorizedAction.cleanup() return pack('<4sI32s', 'strx', resp_len, sha) if cmd == 'pass': # bip39 passphrase provided, maybe use it if authorized assert self.encrypted_req, 'must encrypt' from auth import start_bip39_passphrase from main import settings assert settings.get('words', True), 'no seed' assert len(args) < 400, 'too long' pw = str(args, 'utf8') assert len(pw) < 100, 'too long' return start_bip39_passphrase(pw) if cmd == 'back': # start backup: asks user, takes long time. from auth import start_remote_backup return start_remote_backup() if cmd == 'blkc': # report which blockchain we are configured for from chains import current_chain chain = current_chain() return b'asci' + chain.ctype if cmd == 'bagi': return self.handle_bag_number(args) if has_fatram: # HSM and user-related features only larger-memory Mk3 if cmd == 'hsms': # HSM mode "start" -- requires user approval if args: file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 2 <= file_len <= (200 * 1000), "badlen" else: file_len = 0 # Start an UX interaction but return (mostly) immediately here from hsm_ux import start_hsm_approval await start_hsm_approval(sf_len=file_len, usb_mode=True) return None if cmd == 'hsts': # can always query HSM mode from hsm import hsm_status_report import ujson return b'asci' + ujson.dumps(hsm_status_report()) if cmd == 'gslr': # get the value held in the Storage Locker assert hsm_active, 'need hsm' return b'biny' + hsm_active.fetch_storage_locker() # User Mgmt if cmd == 'nwur': # new user from users import Users auth_mode, ul, sl = unpack_from('<BBB', args) username = bytes(args[3:3 + ul]).decode('ascii') secret = bytes(args[3 + ul:3 + ul + sl]) return b'asci' + Users.create(username, auth_mode, secret).encode('ascii') if cmd == 'rmur': # delete user from users import Users ul, = unpack_from('<B', args) username = bytes(args[1:1 + ul]).decode('ascii') return Users.delete(username) if cmd == 'user': # auth user (HSM mode) from users import Users totp_time, ul, tl = unpack_from('<IBB', args) username = bytes(args[6:6 + ul]).decode('ascii') token = bytes(args[6 + ul:6 + ul + tl]) if hsm_active: # just queues these details, can't be checked until PSBT on-hand hsm_active.usb_auth_user(username, token, totp_time) return None else: # dryrun/testing purposes: validate only, doesn't unlock nothing return b'asci' + Users.auth_okay(username, token, totp_time).encode('ascii') print("USB garbage: %s +[%d]" % (cmd, len(args))) return b'err_Unknown cmd'
if '--seed' in sys.argv: # --xfp aabbccdd => pretend we know that key (won't be able to sign) from ustruct import unpack from utils import xfp2str from seed import set_seed_value from main import pa, settings words = sys.argv[sys.argv.index('--seed') + 1].split(' ') assert len(words) == 24, "Expected 24 space-separated words: add some quotes" pa.pin = b'12-12' set_seed_value(words) settings.set('terms_ok', 1) settings.set('_skip_pin', '12-12') settings.set('chain', 'XTN') print("Seed phrase set, resulting XFP: " + xfp2str(settings.get('xfp'))) if '-g' in sys.argv: # do login sim_defaults.pop('_skip_pin', 0) if '--nick' in sys.argv: nick = sys.argv[sys.argv.index('--nick') + 1] sim_defaults['nick'] = nick sim_defaults['terms_ok'] = 1 sim_defaults.pop('_skip_pin', 0) if '--delay' in sys.argv: delay = int(sys.argv[sys.argv.index('--delay') + 1]) sim_defaults['lgto'] = delay
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()
DebugFunctionsMenu = [ # xxxxxxxxxxxxxxxx MenuItem('Debug: assert', f=debug_assert), MenuItem('Debug: except', f=debug_except), MenuItem('Check: BL FW', f=check_firewall_read), MenuItem('Warm Reset', f=reset_self), #MenuItem("Perform Selftest", f=start_selftest), ] DangerZoneMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Debug Functions", menu=DebugFunctionsMenu), # actually harmless MenuItem('Lock Down Seed', f=convert_bip39_to_bip32, predicate=lambda: settings.get('words', True)), MenuItem("Destroy Seed", f=clear_seed), MenuItem("I Am Developer.", menu=maybe_dev_menu), MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label MenuItem('Perform Selftest', f=start_selftest), # little harmful MenuItem("Set High-Water", f=set_highwater), ] BackupStuffMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Backup System", f=backup_everything), MenuItem("Verify Backup", f=verify_backup), MenuItem("Restore Backup", f=restore_everything), # just a redirect really MenuItem("Dump Summary", f=dump_summary), ]
async def ready2sign(*a): # Top menu choice of top menu! Signing! # - check if any signable in SD card, if so do it # - if nothing, then talk about USB connection from public_constants import MAX_TXN_LEN import stash if stash.bip39_passphrase: title = '[%s]' % settings.get('xfp') else: title = None def is_psbt(filename): if '-signed' in filename.lower(): return False with open(filename, 'rb') as fd: taste = fd.read(10) if taste[0:5] == b'psbt\xff': return True if taste[0:10] == b'70736274ff': # hex-encoded return True if taste[0:6] == b'cHNidP': # base64-encoded return True return False # just check if we have candidates, no UI choices = await file_picker(None, suffix='psbt', min_size=50, max_size=MAX_TXN_LEN, taster=is_psbt) if not choices: await ux_show_story("""\ Coldcard is ready to sign spending transactions! Put the proposed transaction onto MicroSD card \ in PSBT format (Partially Signed Bitcoin Transaction) \ or upload a transaction to be signed \ from your wallet software (Electrum) or command line tools. \ You will always be prompted to confirm the details before any signature is performed.\ """, title=title) return if len(choices) == 1: # skip the menu label, path, fn = choices[0] input_psbt = path + '/' + fn else: input_psbt = await file_picker('Choose PSBT file to be signed.', choices=choices, title=title) if not input_psbt: return # start the process from auth import sign_psbt_file await sign_psbt_file(input_psbt)