def handle_crypto_setup(self, version, his_pubkey): # pick a one-time key pair for myself, and return the pubkey for that # determine what the session key will be for this connection assert version == 0x1 assert len(his_pubkey) == 64 # pick a random key pair, just for this session pair = ngu.secp256k1.keypair() my_pubkey = pair.pubkey().to_bytes(True) # un-compressed #print('my pubkey = ' + str(b2a_hex(my_pubkey))) #print('his pubkey = ' + str(b2a_hex(his_pubkey))) self.session_key = pair.ecdh_multiply(b'\x04' + his_pubkey) del pair #print("session = " + str(b2a_hex(self.session_key))) # Would be nice to have nonce in addition to the counter, but # harder on the desktop side. ctr = aes256ctr.new(self.session_key) self.encrypt = ctr.cipher self.decrypt = ctr.copy().cipher xfp = settings.get('xfp', 0) xpub = settings.get('xpub', '') #assert my_pubkey[0] == 0x04 return b'mypb' + my_pubkey[1:] + pack('<II', xfp, len(xpub)) + xpub
def generate_generic_export(account_num=0): # Generate data that other programers will use to import Coldcard (single-signer) from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH chain = chains.current_chain() rv = dict(chain=chain.ctype, xpub = settings.get('xpub'), xfp = xfp2str(settings.get('xfp')), account = account_num, ) with stash.SensitiveValues() as sv: # each of these paths would have /{change}/{idx} in usage (not hardened) for name, deriv, fmt, atype in [ ( 'bip44', "m/44'/{ct}'/{acc}'", AF_CLASSIC, 'p2pkh' ), ( 'bip49', "m/49'/{ct}'/{acc}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh' ), # was "p2wpkh-p2sh" ( 'bip84', "m/84'/{ct}'/{acc}'", AF_P2WPKH, 'p2wpkh' ), ]: dd = deriv.format(ct=chain.b44_cointype, acc=account_num) node = sv.derive_path(dd) xfp = xfp2str(swab32(node.my_fp())) xp = chain.serialize_public(node, AF_CLASSIC) zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None # bonus/check: first non-change address: 0/0 node.derive(0, False).derive(0, False) rv[name] = dict(deriv=dd, xpub=xp, xfp=xfp, first=chain.address(node, fmt), name=atype) if zp: rv[name]['_pub'] = zp return rv
def handle_bag_number(self, bag_num): import version, callgate from glob import dis from pincodes import pa if version.is_factory_mode and bag_num: # check state first assert settings.get('tested', False) assert pa.is_blank() assert 8 <= len(bag_num) < 32 # 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 chain_chooser(): # Pick Bitcoin or Testnet3 blockchains from chains import AllChains chain = settings.get('chain', 'BTC') ch = [(i.ctype, i.menu_name or i.name) for i in AllChains] # find index of current choice try: which = [n for n, (k, v) in enumerate(ch) if k == chain][0] except IndexError: which = 0 def set_chain(idx, text): val = ch[idx][0] assert ch[idx][1] == text settings.set('chain', val) try: # update xpub stored in settings import stash with stash.SensitiveValues() as sv: sv.capture_xpub() except ValueError: # no secrets yet, not an error pass return which, [t for _, t in ch], set_chain
def generate_unchained_export(acct_num=0): # They used to rely on our airgapped export file, so this is same style # - for multisig purposes # - BIP-45 style paths for now # - no account numbers (at this level) from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH chain = chains.current_chain() todo = [ ("m/45'", 'p2sh', AF_P2SH), # iff acct_num == 0 ("m/48'/{coin}'/{acct_num}'/1'", 'p2sh_p2wsh', AF_P2WSH_P2SH), ("m/48'/{coin}'/{acct_num}'/2'", 'p2wsh', AF_P2WSH), ] xfp = xfp2str(settings.get('xfp', 0)) rv = dict(account=acct_num, xfp=xfp) with stash.SensitiveValues() as sv: for deriv, name, fmt in todo: if fmt == AF_P2SH and acct_num: continue dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num) node = sv.derive_path(dd) xp = chain.serialize_public(node, fmt) rv['%s_deriv' % name] = dd rv[name] = xp return rv
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)
def current_chain(): # return chain matching current setting from nvstore import settings chain = settings.get('chain', 'BTC') return get_chain(chain)
async def xor_restore_start(*a): # shown on import menu when no seed of any kind yet # - or operational system ch = await ux_show_story('''\ To import a seed split using XOR, you must import all the parts. It does not matter the order (A/B/C or C/A/B) and the Coldcard cannot determine when you have all the parts. You may stop at any time and you will have a valid wallet.''') if ch == 'x': return global import_xor_parts import_xor_parts.clear() from pincodes import pa if not pa.is_secret_blank(): msg = "Since you have a seed already on this Coldcard, the reconstructed XOR seed will be temporary and not saved. Wipe the seed first if you want to commit the new value into the secure element." if settings.get('words', True): msg += '''\n Press (1) to include this Coldcard's seed words into the XOR seed set, or OK to continue without.''' ch = await ux_show_story(msg, escape='1') if ch == 'x': return elif ch == '1': with stash.SensitiveValues() as sv: if sv.mode == 'words': words = bip39.b2a_words(sv.raw).split(' ') import_xor_parts.append(words) return XORWordNestMenu(num_words=24)
def render_value(cls, val, unpad=False): # convert nValue from a transaction into human form. # - always be precise # - return (string, units label) from nvstore import settings rz = settings.get('rz', 8) if rz == 8: # full Bitcoins, for OG's unit = cls.ctype div = 100000000 # caution: don't use 1E8 here, that's a float fmt = '%08d' elif rz == 5: unit = 'm' + cls.ctype # includes mXTN div = 100000 fmt = '%05d' elif rz == 2: unit = 'bits' div = 100 fmt = '%02d' elif rz == 0: return str(val), 'sats' if unpad: # show precise value, but no trailing zeros if (val % div): txt = (('%d.' + fmt) % (val // div, val % div)).rstrip('0') else: # round amount, omit decimal point txt = '%d' % (val // div) else: # all the zeros & fixed with result txt = ('%d.' + fmt) % (val // div, val % div) return txt, unit
def generate_electrum_wallet(addr_type, account_num=0): # Generate line-by-line JSON details about wallet. # # Much reverse enginerring of Electrum here. It's a complex # legacy file format. from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH chain = chains.current_chain() xfp = settings.get('xfp') # Must get the derivation path, and the SLIP32 version bytes right! if addr_type == AF_CLASSIC: mode = 44 elif addr_type == AF_P2WPKH: mode = 84 elif addr_type == AF_P2WPKH_P2SH: mode = 49 else: raise ValueError(addr_type) derive = "m/{mode}'/{coin_type}'/{account}'".format( mode=mode, account=account_num, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: top = chain.serialize_public(sv.derive_path(derive), addr_type) # most values are nicely defaulted, and for max forward compat, don't want to set # anything more than I need to rv = dict(seed_version=17, use_encryption=False, wallet_type='standard') lab = 'Coldcard Import %s' % xfp2str(xfp) if account_num: lab += ' Acct#%d' % account_num # the important stuff. rv['keystore'] = dict(ckcc_xfp=xfp, ckcc_xpub=settings.get('xpub'), hw_type='coldcard', type='hardware', label=lab, derivation=derive, xpub=top) return rv
async def interact(self): # prompt them from nvstore import settings showit = False while 1: if showit: ch = await ux_show_story('''Given:\n\n%s\n\nShould we switch to that wallet now? OK to continue, X to cancel.''' % self._pw, title="Passphrase") else: ch = await ux_show_story('''BIP-39 passphrase (%d chars long) has been provided over USB connection. Should we switch to that wallet now? Press 2 to view the provided passphrase.\n\nOK to continue, X to cancel.''' % len(self._pw), title="Passphrase", escape='2') if ch == '2': showit = True continue break try: if ch != 'y': # they don't want to! self.refused = True await ux_dramatic_pause("Refused.", 1) else: from seed import set_bip39_passphrase # full screen message shown: "Working..." set_bip39_passphrase(self._pw) self.result = settings.get('xpub') except BaseException as exc: self.failed = "Exception" sys.print_exception(exc) finally: self.done() if self.result: new_xfp = settings.get('xfp') await ux_show_story('''Above is the master key fingerprint of the current wallet.''', title="[%s]" % xfp2str(new_xfp))
def delete_inputs_chooser(): # del = (int) 0=normal 1=overwrite+delete input PSBT's, rename outputs del_psbt = settings.get('del', 0) ch = ['Normal', 'Delete PSBTs'] def set_del_psbt(idx, text): settings.set('del', idx) return del_psbt, ch, set_del_psbt
async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from glob import dis import ustruct xfp = xfp2str(settings.get('xfp')) dis.fullscreen('Generating...') # make the data examples = [] imp_multi = [] imp_desc = [] for a, b in generate_bitcoin_core_wallet(account_num, examples): imp_multi.append(a) imp_desc.append(b) imp_multi = ujson.dumps(imp_multi) imp_desc = ujson.dumps(imp_desc) 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: importdescriptors '{imp_desc}' ### Bitcoin Core before v0.21.0 This command can be used on older versions, but it is not as robust and "importdescriptors" should be prefered if possible: importmulti '{imp_multi}' ## Resulting Addresses (first 3) '''.format(imp_multi=imp_multi, imp_desc=imp_desc, 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 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()
def max_fee_chooser(): from psbt import DEFAULT_MAX_FEE_PERCENTAGE limit = settings.get('fee_limit', DEFAULT_MAX_FEE_PERCENTAGE) ch = ['10% (default)', '25%', '50%', 'no limit'] va = [10, 25, 50, -1] try: which = va.index(limit) except ValueError: which = 0 def set(idx, text): settings.set('fee_limit', va[idx]) return which, ch, set
def value_resolution_chooser(): # how to render Bitcoin values ch = ['BTC', 'mBTC', 'bits', 'sats'] va = [8, 5, 2, 0] rz = settings.get('rz', 8) try: which = va.index(rz) except ValueError: which = 0 def doit(idx, text): settings.set('rz', va[idx]) return which, ch, doit
def generate_bitcoin_core_wallet(example_addrs, account_num): # Generate the data for an RPC command to import keys into Bitcoin Core # - yields dicts for json purposes from descriptor import append_checksum import ustruct from public_constants import AF_P2WPKH chain = chains.current_chain() derive = "84'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: prefix = sv.derive_path(derive) xpub = chain.serialize_public(prefix) for i in range(3): sp = '0/%d' % i node = sv.derive_path(sp, master=prefix) a = chain.address(node, AF_P2WPKH) example_addrs.append(('m/%s/%s' % (derive, sp), a)) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp).lower() chain = chains.current_chain() _, vers, _ = version.get_mpy_version() for internal in [False, True]: desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( derive=derive.replace("'", "h"), fingerprint=txt_xfp, coin_type=chain.b44_cointype, account=0, xpub=xpub, change=(1 if internal else 0)) yield { 'desc': append_checksum(desc), 'range': [0, 1000], 'timestamp': 'now', 'internal': internal, 'keypool': True, 'watchonly': True }
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 glob import 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)
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 disable_usb_chooser(): value = settings.get('du', 0) ch = ['Normal', 'Disable USB'] def set_it(idx, text): settings.set('du', idx) import pyb from usb import enable_usb, disable_usb cur = pyb.usb_mode() if cur and idx: # usb enabled, but should not be now disable_usb() elif not cur and not idx: # USB disabled, but now should be enable_usb() return value, ch, set_it
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 make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from glob import dis 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')
def generate_wasabi_wallet(): # Generate the data for a JSON file which Wasabi can open directly as a new wallet. 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(): import glob from nvstore import settings while not glob.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 glob.numpad.last_event_time: continue if now > glob.numpad.last_event_time + timeout: # do a logout now. print("Idle!") from actions import logout_now await logout_now() return # not reached
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) set_bip39_passphrase(pp_sofar) xfp = settings.get('xfp') msg = '''Above is the master key fingerprint of the new wallet. Press X to abort and keep editing passphrase, OK to use the new wallet, or 1 to use and save to MicroSD''' ch = await ux_show_story(msg, title="[%s]" % xfp2str(xfp), escape='1') if ch == 'x': # go back! set_bip39_passphrase(old_pw) return if ch == '1': await PassphraseSaver().append(xfp, pp_sofar) goto_top_menu()
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 ngu, version, uos from glob import numpad from pincodes import pa from nvstore import settings if version.has_fatram: import hsm had_policy = hsm.hsm_policy_available() else: had_policy = False today = ngu.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 ngu.hash.sha256s(result) == 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()
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # load up the simulator w/ indicated encoded secret. could be xprv/words/etc. from sim_settings import sim_defaults import stash, chains from h import b2a_hex from pincodes import pa from nvstore import settings from stash import SecretStash, SensitiveValues from utils import xfp2str settings.current = dict(sim_defaults) settings.overrides.clear() import main raw = main.ENCODED_SECRET pa.change(new_secret=raw) pa.new_main_secret(raw) print("New key in effect: %s" % settings.get('xpub', 'MISSING')) print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # quickly clear all multisig wallets installed from nvstore import settings from ux import restore_menu if settings.get('multisig'): del settings.current['multisig'] settings.save() print("cleared multisigs") restore_menu()
EmptyWallet = [ # xxxxxxxxxxxxxxxx MenuItem('New Wallet', f=pick_new_wallet), MenuItem('Import Existing', menu=ImportWallet), MenuItem('Help', f=virgin_help), MenuItem('Advanced', menu=AdvancedPinnedVirginMenu), MenuItem('Settings', menu=SettingsMenu), ] # In operation, normal system, after a good PIN received. NormalSystem = [ # xxxxxxxxxxxxxxxx MenuItem('Ready To Sign', f=ready2sign), MenuItem('Passphrase', f=start_b39_pw, predicate=lambda: settings.get('words', True)), MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available), MenuItem("Address Explorer", f=address_explore), MenuItem('Secure Logout', f=logout_now), MenuItem('Advanced', menu=AdvancedNormalMenu), MenuItem('Settings', menu=SettingsMenu), ] # Shown until unit is put into a numbered bag FactoryMenu = [ MenuItem('Bag Me Now'), # nice to have NOP at top of menu MenuItem('DFU Upgrade', f=start_dfu), MenuItem('Show Version', f=show_version), MenuItem('Ship W/O Bag', f=ship_wo_bag),
if '--seed' in sys.argv: # --seed "word1 word2 ... word24" => import that seed phrase at start from ustruct import unpack from utils import xfp2str from seed import set_seed_value from main import pa from nvstore import settings words = sys.argv[sys.argv.index('--seed') + 1].split(' ') assert len(words) in {12, 18, 24}, "Expected 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 '--secret' in sys.argv: # --secret 01a1a1a.... Set SE master secret directly. See SecretStash.encode from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex val = sys.argv[sys.argv.index('--secret') + 1] val = a2b_hex(val) assert val[0] in { 0x01, 0x80, 0x81, 0x82} or 16 <= val[0] <= 64, "bad first byte" val += bytes(72 - len(val)) SECRETS.update({ '_pin1_secret': b2a_hex(val), })