async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix == b"getlabel": label = self.get_label() obj = { "title": "Device's label is: %s" % label, } stream = BytesIO(label.encode()) return stream, obj elif prefix == b"setlabel": label = stream.read().strip().decode('ascii') if not label: raise AppError('Device label cannot be empty') scr = Prompt("\n\nSet device label to: %s\n" % label, "Current device label: %s" % self.get_label()) res = await show_screen(scr) if res is False: return None self.set_label(label) obj = { "title": "New device label: %s" % label, } return BytesIO(label.encode()), obj else: raise AppError("Invalid command")
async def menu(self, show_screen): buttons = [(None, "Your wallets")] buttons += [(w, w.name) for w in self.wallets if not w.is_watchonly] if len(buttons) != (len(self.wallets) + 1): buttons += [(None, "Watch only wallets")] buttons += [(w, w.name) for w in self.wallets if w.is_watchonly] menuitem = await show_screen(Menu(buttons, last=(255, None))) if menuitem == 255: # we are done return False else: w = menuitem # pass wallet and network self.show_loader(title="Loading wallet...") cmd = await w.show(self.network, show_screen) if cmd == DELETE: scr = Prompt( "Delete wallet?", 'You are deleting wallet "%s".\n' "Are you sure you want to do it?" % w.name, ) conf = await show_screen(scr) if conf: self.delete_wallet(w) elif cmd == EDIT: scr = InputScreen(title="Enter new wallet name", note="", suggestion=w.name) name = await show_screen(scr) if name is not None and name != w.name and name != "": w.name = name w.save(self.keystore) return True
async def export_mnemonic(self): if await self.show( Prompt( "Warning", "You need to confirm your PIN code " "to export your recovery phrase.\n\n" "Your recovery phrase will be saved " "to the SD card as plain text.\n\n" "Anybody who has access to this SD card " "will be able to read your recovery phrase!\n\n" "Continue?")): self.lock() await self.unlock() filename = "seed-export-%s.txt" % self.mnemonic.split()[0] filepath = "%s/%s" % (self.sdpath, filename) if not platform.is_sd_present(): raise KeyStoreError("SD card is not present") platform.mount_sdcard() with open(filepath, "w") as f: f.write("bip39: ") f.write(self.mnemonic) platform.unmount_sdcard() await self.show( Alert("Success!", "Your seed is exported.\n\nName: %s" % filename, button_text="OK"))
async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix != b"hello": # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) name = stream.read().decode() # ask the user if he really wants it # build a screen scr = Prompt( "Say hello?", "Are you sure you want to say hello to\n\n%s?\n\n" "Saying hello can compromise your security!" % name) # show screen and wait for result res = await show_screen(scr) # check if he confirmed if not res: return obj = { "title": "Hello!", } d = b"Hello " + name.encode() return BytesIO(d), obj
async def menu(self, show_screen): buttons = [(None, "Your wallets")] buttons += [(w, w.name) for w in self.wallets] menuitem = await show_screen(Menu(buttons, last=(255, None))) if menuitem == 255: # we are done return False else: w = menuitem # pass wallet and network scr = WalletScreen(w, self.network, idx=w.unused_recv) cmd = await show_screen(scr) if cmd == DELETE: scr = Prompt( "Delete wallet?", "You are deleting wallet \"%s\".\n" "Are you sure you want to do it?" % w.name) conf = await show_screen(scr) if conf: self.delete_wallet(w) elif cmd == EDIT: scr = InputScreen(title="Enter new wallet name", note="", suggestion=w.name) name = await show_screen(scr) if name is not None and name != w.name and name != "": w.name = name w.save(self.keystore) return True
async def storage_menu(self): """Manage storage""" enabled = self.connection.isCardInserted() buttons = [ # id, text (None, "Smartcard storage"), (0, "Save key to the card", enabled), (1, "Load key from the card", enabled), (2, "Delete key from the card", enabled), (3, "Use a different card", enabled) ] # we stay in this menu until back is pressed while True: note = "Card fingerprint: %s" % self.hexid # wait for menu selection menuitem = await self.show( Menu(buttons, note=note, last=(255, None))) # process the menu button: # back button if menuitem == 255: return elif menuitem == 0: await self.save_mnemonic() await self.show( Alert( "Success!", "Your key is stored on the smartcard now.", button_text="OK", )) elif menuitem == 1: await self.load_mnemonic() await self.show( Alert("Success!", "Your key is loaded.", button_text="OK")) elif menuitem == 2: await self.delete_mnemonic() await self.show( Alert( "Success!", "Your key is deleted from the smartcard.", button_text="OK", )) elif menuitem == 3: if await self.show( Prompt( "Switching the smartcard", "To use a different smartcard you need " "to provide a PIN for current one first!\n\n" "Continue?")): self.lock() await self.unlock() self.lock() self.applet.close_secure_channel() await self.show( Alert("Please swap the card", "Now you can insert another card and set it up.", button_text="Continue")) await self.check_card(check_pin=True) await self.unlock()
async def show_mnemonic(self): if await self.show( Prompt( "Warning", "You need to confirm your PIN code " "to display your recovery phrase.\n\n" "Continue?")): self.lock() await self.unlock() await self.show(MnemonicScreen(self.mnemonic))
async def confirm_new_wallet(self, w, show_screen): keys = w.get_key_dicts(self.network) for k in keys: k["mine"] = self.keystore.owns(k["key"]) if not any([k["mine"] for k in keys]): if not await show_screen( Prompt("Warning!", "None of the keys belong to the device.\n\n" "Are you sure you still want to add the wallet?")): return False return await show_screen(ConfirmWalletScreen(w.name, w.full_policy, keys, w.is_miniscript))
async def confirm_wallets(self, wallets, show_screen): # check if any inputs belong to unknown wallets # wallets is a dict: {wallet: amount} if None not in wallets: return True scr = Prompt( "Warning!", "\nUnknown wallet in inputs!\n\n\n" "The source wallet for some inputs is unknown! This means we can't verify change address.\n\n\n" "Hint:\nYou can cancel this transaction and import the wallet by scanning it's descriptor.\n\n\n" "Proceed to the transaction confirmation?", ) proceed = await show_screen(scr) return proceed
async def process_host_command(self, stream, show_fn): # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix not in self.prefixes: # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) mnemonic = stream.read().strip().decode() if not bip39.mnemonic_is_valid(mnemonic): raise AppError("Invalid mnemonic!") scr = Prompt("Load this mnemonic to memory?", "Mnemonic:") table = MnemonicTable(scr) table.align(scr.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) table.set_mnemonic(mnemonic) if await show_fn(scr): self.keystore.set_mnemonic(mnemonic) return None
async def process_host_command(self, stream, show_screen): if self.keystore.is_locked: raise AppError("Device is locked") # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix == b"slip77": if not await show_screen( Prompt( "Confirm the action", "Send master blinding private key\nto the host?\n\n" "Host is requesting your\nSLIP-77 blinding key.\n\n" "It will be able to watch your funds and unblind transactions." )): return return BytesIO(self.keystore.slip77_key.wif( NETWORKS[self.network])), {} raise AppError("Unknown command")
async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix != b"signmessage": # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) # data format: message to sign<space>derivation_path # read all and delete all crap at the end (if any) # also message should be utf-8 decodable data = stream.read().strip().decode() if " " not in data: raise AppError("Invalid data encoding") arr = data.split(" ") derivation_path = arr[-1] message = " ".join(arr[:-1]) # if we have fingerprint if not derivation_path.startswith("m/"): fingerprint = unhexlify(derivation_path[:8]) if fingerprint != self.keystore.fingerprint: raise AppError("Not my fingerprint") derivation_path = "m" + derivation_path[8:] derivation_path = bip32.parse_path(derivation_path) # ask the user if he really wants to sign this message scr = Prompt( "Sign message with private key at %s?" % bip32.path_to_str(derivation_path), "Message:\n%s" % message) res = await show_screen(scr) if res is False: return None sig = self.sign_message(derivation_path, message.encode()) # for GUI we can also return an object with helpful data xpub = self.keystore.get_xpub(derivation_path) address = script.p2pkh(xpub.key).address() obj = { "title": "Signature for the message:", "note": "using address %s" % address } return BytesIO(sig), obj
async def confirm_sighashes(self, meta, show_screen): """ Checks if custom sighashes are used, warns the user and asks for confirmation. Returns one of the options: - None - sign with provided sighashes - self.DEFAULT_SIGHASH - sign only with default - False - interrupt signing process (user cancel) """ sighash_name = self.get_sighash_info(self.DEFAULT_SIGHASH)["name"] # check if there are any custom sighashes used_custom_sighashes = any([ inp.get("sighash", sighash_name) != sighash_name for inp in meta["inputs"] ]) # no custom sighashes - just continue if not used_custom_sighashes: return None # ask the user if he wants to sign in case of non-default sighashes custom_sighashes = [ ("Input %d: %s" % (i, inp.get("sighash", sighash_name))) for (i, inp) in enumerate(meta["inputs"]) if inp.get("sighash", sighash_name) != sighash_name ] canceltxt = (("Only sign %s" % sighash_name) if len(custom_sighashes) != len(meta["inputs"]) else "Cancel") confirm = await show_screen( Prompt("Warning!", "\nCustom SIGHASH flags are used!\n\n" + "\n".join(custom_sighashes), confirm_text="Proceed anyway", cancel_text=canceltxt)) if confirm: # we set sighash to None # if we want to use whatever sighash is provided in input return None # if we are forced to use default sighash - check # that not all inputs have custom sighashes if len(custom_sighashes) == len(meta["inputs"]): # nothing to sign return False return self.DEFAULT_SIGHASH
async def save_mnemonic(self): if self.is_locked: raise KeyStoreError("Keystore is locked") if self.mnemonic is None: raise KeyStoreError("Recovery phrase is not loaded") path = await self.get_keypath(title="Where to save?", only_if_exist=False, note="Select media") if path is None: return filename = await self.get_input(suggestion=self.mnemonic.split()[0]) if filename is None: return fullpath = "%s/%s.%s" % (path, self.fileprefix(path), filename) if fullpath.startswith(self.sdpath): if not platform.is_sd_present(): raise KeyStoreError("SD card is not present") platform.mount_sdcard() if platform.file_exists(fullpath): scr = Prompt( "\n\nFile already exists: %s\n" % filename, "Would you like to overwrite this file?", ) res = await self.show(scr) if res is False: if fullpath.startswith(self.sdpath): platform.unmount_sdcard() return self.save_aead(fullpath, plaintext=self.mnemonic.encode(), key=self.enc_secret) if fullpath.startswith(self.sdpath): platform.unmount_sdcard() # check it's ok await self.load_mnemonic(fullpath) # return the full file name incl. prefix if saved to SD card, just the name if on flash return fullpath.split("/")[-1] if fullpath.startswith( self.sdpath) else filename
async def save_mnemonic(self): await self.check_card(check_pin=True) encrypt = await self.show( Prompt("Encrypt the secret?", "\nIf you encrypt the secret on the card " "it will only work with this device.\n\n" "Otherwise it will be readable on any device " "after you enter the PIN code.\n\n" "Keep in mind that with encryption enabled " "wiping the device makes the secret unusable!", confirm_text="Yes, encrypt", cancel_text="Keep as plain text")) self.show_loader("Saving secret to the card...") d = self.serialize_data( {"entropy": bip39.mnemonic_to_bytes(self.mnemonic)}, encrypt=encrypt, ) self.applet.save_secret(d) self._is_key_saved = True # check it's ok await self.load_mnemonic()
async def sign_psbt(self, stream, show_screen, encoding=BASE64_STREAM): if encoding == BASE64_STREAM: data = a2b_base64(stream.read()) psbt = PSBT.parse(data) else: psbt = PSBT.read_from(stream) # check if all utxos are there and if there are custom sighashes sighash = SIGHASH.ALL custom_sighashes = [] for i, inp in enumerate(psbt.inputs): if inp.witness_utxo is None: if inp.non_witness_utxo is None: raise WalletError( "Invalid PSBT - missing previous transaction") if inp.sighash_type and inp.sighash_type != SIGHASH.ALL: custom_sighashes.append((i, inp.sighash_type)) if len(custom_sighashes) > 0: txt = [("Input %d: " % i) + SIGHASH_NAMES[sh] for (i, sh) in custom_sighashes] canceltxt = "Only sign ALL" if len(custom_sighashes) != len( psbt.inputs) else "Cancel" confirm = await show_screen( Prompt("Warning!", "\nCustom SIGHASH flags are used!\n\n" + "\n".join(txt), confirm_text="Sign anyway", cancel_text=canceltxt)) if confirm: sighash = None else: if len(custom_sighashes) == len(psbt.inputs): # nothing to sign return wallets, meta = self.parse_psbt(psbt=psbt) # there is an unknown wallet # wallet is a list of tuples: (wallet, amount) if None in [w[0] for w in wallets]: scr = Prompt( "Warning!", "\nUnknown wallet in inputs!\n\n\n" "Wallet for some inpunts is unknown! This means we can't verify change addresses.\n\n\n" "Hint:\nYou can cancel this transaction and import the wallet by scanning it's descriptor.\n\n\n" "Proceed to the transaction confirmation?", ) proceed = await show_screen(scr) if not proceed: return None spends = [] for w, amount in wallets: if w is None: name = "Unknown wallet" else: name = w.name spends.append('%.8f BTC\nfrom "%s"' % (amount / 1e8, name)) title = "Spending:\n" + "\n".join(spends) res = await show_screen(TransactionScreen(title, meta)) if res: self.show_loader(title="Signing transaction...") sigsStart = 0 for i, inp in enumerate(psbt.inputs): sigsStart += len(list(inp.partial_sigs.keys())) for w, _ in wallets: if w is None: continue # fill derivation paths from proprietary fields w.update_gaps(psbt=psbt) w.save(self.keystore) w.fill_psbt(psbt, self.keystore.fingerprint) if w.has_private_keys: w.sign_psbt(psbt, sighash) self.keystore.sign_psbt(psbt, sighash) # remove unnecessary stuff: out_psbt = PSBT(psbt.tx) sigsEnd = 0 for i, inp in enumerate(psbt.inputs): sigsEnd += len(list(inp.partial_sigs.keys())) out_psbt.inputs[i].partial_sigs = inp.partial_sigs del psbt gc.collect() if sigsEnd == sigsStart: raise WalletError( "We didn't add any signatures!\n\nMaybe you forgot to import the wallet?\n\nScan the wallet descriptor to import it." ) if encoding == BASE64_STREAM: txt = b2a_base64(out_psbt.serialize()).decode().strip() else: txt = out_psbt.serialize() return BytesIO(txt)
async def storage_menu(self): """Manage storage""" enabled = self.connection.isCardInserted() buttons = [ # id, text, enabled, color (None, "Smartcard storage"), (0, "Save key to the card", enabled), (1, "Load key from the card", enabled and self.is_key_saved), (2, "Delete key from the card", enabled and self.is_key_saved), (3, "Use a different card", enabled), (4, lv.SYMBOL.SETTINGS + " Get card info", enabled), # (5, lv.SYMBOL.TRASH + " Wipe the card", enabled, 0x951E2D), ] # we stay in this menu until back is pressed while True: # check updated status buttons[2] = (1, "Load key from the card", enabled and self.is_key_saved) buttons[3] = (2, "Delete key from the card", enabled and self.is_key_saved) note = "Card fingerprint: %s" % self.hexid # wait for menu selection menuitem = await self.show( Menu(buttons, note=note, last=(255, None))) # process the menu button: # back button if menuitem == 255: return elif menuitem == 0: await self.save_mnemonic() await self.show( Alert( "Success!", "Your key is stored on the smartcard now.", button_text="OK", )) elif menuitem == 1: await self.load_mnemonic() await self.show( Alert("Success!", "Your key is loaded.", button_text="OK")) elif menuitem == 2: if await self.show( Prompt("Are you sure?", "\n\nDelete the key from the card?")): await self.delete_mnemonic() await self.show( Alert( "Success!", "Your key is deleted from the smartcard.", button_text="OK", )) elif menuitem == 3: if await self.show( Prompt( "Switching the smartcard", "To use a different smartcard you need " "to provide a PIN for current one first!\n\n" "Continue?")): self.lock() await self.unlock() self.lock() self.applet.close_secure_channel() self._userkey = None await self.show( Alert("Please swap the card", "Now you can insert another card and set it up.", button_text="Continue")) await self.check_card(check_pin=True) await self.unlock() elif menuitem == 4: await self.show_card_info() else: raise KeyStoreError("Invalid menu")