def perform_hw1_preflight(self): try: firmwareInfo = self.dongleObject.getFirmwareVersion() firmware = firmwareInfo['version'] self.multiOutputSupported = versiontuple(firmware) >= versiontuple( MULTI_OUTPUT_SUPPORT) self.nativeSegwitSupported = versiontuple( firmware) >= versiontuple(SEGWIT_SUPPORT) self.segwitSupported = self.nativeSegwitSupported or ( firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) if not checkFirmware(firmwareInfo): self.close() raise UserFacingException(MSG_NEEDS_FW_UPDATE_GENERIC) try: self.dongleObject.getOperationMode() except BTChipException as e: if (e.sw == 0x6985): self.close() self.handler.get_setup() # Acquire the new client on the next run else: raise e if self.has_detached_pin_support( self.dongleObject) and not self.is_pin_validated( self.dongleObject): assert self.handler, "no handler for client" remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts( ) if remaining_attempts != 1: msg = "Enter your Ledger PIN - remaining attempts : " + str( remaining_attempts) else: msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." confirmed, p, pin = self.password_dialog(msg) if not confirmed: raise UserFacingException( 'Aborted by user - please unplug the dongle and plug it again before retrying' ) pin = pin.encode() self.dongleObject.verifyPin(pin) self.dongleObject.setAlternateCoinVersion( constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH) except BTChipException as e: if (e.sw == 0x6faa): raise UserFacingException( "Dongle is temporarily locked - please unplug it and replug it again" ) if ((e.sw & 0xFFF0) == 0x63c0): raise UserFacingException( "Invalid PIN - please unplug the dongle and plug it again before retrying" ) if e.sw == 0x6f00 and e.message == 'Invalid channel': # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure raise UserFacingException( "Invalid channel.\n" "Please make sure that 'Browser support' is disabled on your device." ) raise e
def dbb_load_backup(self, show_msg=True): backups = self.hid_send_encrypt(b'{"backup":"list"}') if 'error' in backups: raise UserFacingException(backups['error']['message']) try: f = self.handler.win.query_choice(_("Choose a backup file:"), backups['backup']) except Exception: return False # Back button pushed key = self.backup_password_dialog() if key is None: raise Exception('Canceled by user') key = self.stretch_key(key) if show_msg: self.handler.show_message( _("Loading backup...") + "\n\n" + _("To continue, touch the Digital Bitbox's light for 3 seconds." ) + "\n\n" + _("To cancel, briefly touch the light or wait for the timeout." )) msg = ('{"seed":{"source": "backup", "key": "%s", "filename": "%s"}}' % (key, backups['backup'][f])).encode('utf8') hid_reply = self.hid_send_encrypt(msg) self.handler.finished() if 'error' in hid_reply: raise UserFacingException(hid_reply['error']['message']) return True
def get_xpub(self, bip32_path, xtype): self.checkDevice() # bip32_path is of the form 44'/0'/1' # S-L-O-W - we don't handle the fingerprint directly, so compute # it manually from the previous node # This only happens once so it's bearable #self.get_client() # prompt for the PIN before displaying the dialog if necessary #self.handler.show_message("Computing master public key") if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) bip32_path = bip32.normalize_bip32_derivation(bip32_path) bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) bip32_path = bip32_path[2:] # cut off "m/" if len(bip32_intpath) >= 1: prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:] nodeData = self.dongleObject.getWalletPublicKey(prevPath) publicKey = compress_public_key(nodeData['publicKey']) fingerprint_bytes = hash_160(publicKey)[0:4] childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big") else: fingerprint_bytes = bytes(4) childnum_bytes = bytes(4) nodeData = self.dongleObject.getWalletPublicKey(bip32_path) publicKey = compress_public_key(nodeData['publicKey']) depth = len(bip32_intpath) return BIP32Node(xtype=xtype, eckey=ecc.ECPubkey(bytes(publicKey)), chaincode=nodeData['chainCode'], depth=depth, fingerprint=fingerprint_bytes, child_number=childnum_bytes).to_xpub()
def get_xpub(self, bip32_path, xtype): self.checkDevice() # bip32_path is of the form 44'/0'/1' # S-L-O-W - we don't handle the fingerprint directly, so compute # it manually from the previous node # This only happens once so it's bearable #self.get_client() # prompt for the PIN before displaying the dialog if necessary #self.handler.show_message("Computing master public key") if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh' ] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) splitPath = bip32_path.split('/') if splitPath[0] == 'm': splitPath = splitPath[1:] bip32_path = bip32_path[2:] fingerprint = 0 if len(splitPath) > 1: prevPath = "/".join(splitPath[0:len(splitPath) - 1]) nodeData = self.dongleObject.getWalletPublicKey(prevPath) publicKey = compress_public_key(nodeData['publicKey']) h = hashlib.new('ripemd160') h.update(hashlib.sha256(publicKey).digest()) fingerprint = unpack(">I", h.digest()[0:4])[0] nodeData = self.dongleObject.getWalletPublicKey(bip32_path) publicKey = compress_public_key(nodeData['publicKey']) depth = len(splitPath) lastChild = splitPath[len(splitPath) - 1].split('\'') childnum = int(lastChild[0]) if len( lastChild) == 1 else 0x80000000 | int(lastChild[0]) xpub = serialize_xpub(xtype, nodeData['chainCode'], publicKey, depth, self.i4b(fingerprint), self.i4b(childnum)) return xpub
def validate_op_return_output(output: TxOutput, *, max_size: int = None) -> None: script = output.scriptpubkey if script[0] != opcodes.OP_RETURN: raise UserFacingException(_("Only OP_RETURN scripts are supported.")) if max_size is not None and len(script) > max_size: raise UserFacingException(_("OP_RETURN payload too large." + "\n" + f"(scriptpubkey size {len(script)} > {max_size})")) if output.value != 0: raise UserFacingException(_("Amount for OP_RETURN output must be zero."))
def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: if output.type != TYPE_SCRIPT: raise Exception("Unexpected output type: {}".format(output.type)) script = bfh(output.address) if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported.")) if output.value != 0: raise UserFacingException(_("Amount for OP_RETURN output must be zero.")) return script[2:]
def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: script = output.scriptpubkey if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): raise UserFacingException( _("Only OP_RETURN scripts, with one constant push, are supported.") ) if output.value != 0: raise UserFacingException( _("Amount for OP_RETURN output must be zero.")) return script[2:]
def dbb_erase(self): self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?") + "\n\n" + _("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" + _("To cancel, briefly touch the light or wait for the timeout.")) hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}') self.handler.finished() if 'error' in hid_reply: raise UserFacingException(hid_reply['error']['message']) else: self.password = None raise UserFacingException('Device erased')
def dbb_generate_wallet(self): key = self.stretch_key(self.password) filename = ("Electrum-LTC-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf") msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, to_hexstr(os.urandom(32)))).encode('utf8') reply = self.hid_send_encrypt(msg) if 'error' in reply: raise UserFacingException(reply['error']['message'])
def create_client(self, device, handler): if device.product_key[1] == 2: transport = self._try_webusb(device) else: transport = self._try_hid(device) if not transport: self.logger.info("cannot connect to device") return self.logger.info(f"connected to device at {device.path}") client = self.client_class(transport, handler, self) # Try a ping for device sanity try: client.ping('t') except BaseException as e: self.logger.info(f"ping failed {e}") return None if not client.atleast_version(*self.minimum_firmware): msg = (_('Outdated {} firmware for device labelled {}. Please ' 'download the updated firmware from {}').format( self.device, client.label(), self.firmware_URL)) self.logger.info(msg) if handler: handler.show_error(msg) else: raise UserFacingException(msg) return None return client
def dbb_has_password(self): reply = self.hid_send_plain(b'{"ping":""}') if 'ping' not in reply: raise UserFacingException(_('Device communication error. Please unplug and replug your Digital Bitbox.')) if reply['ping'] == 'password': return True return False
def sign_message(self, keypath: str, message: bytes, xtype: str) -> bytes: if self.bitbox02_device is None: raise Exception( "Need to setup communication first before attempting any BitBox02 calls" ) try: simple_type = { "p2wpkh-p2sh":bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH, "p2wpkh": bitbox02.btc.BTCScriptConfig.P2WPKH, }[xtype] except KeyError: raise UserFacingException("The BitBox02 does not support signing messages for this address type: {}".format(xtype)) _, _, signature = self.bitbox02_device.btc_sign_msg( self._get_coin(), bitbox02.btc.BTCScriptConfigWithKeypath( script_config=bitbox02.btc.BTCScriptConfig( simple_type=simple_type, ), keypath=bip32.convert_bip32_path_to_list_of_uint32(keypath), ), message, ) return signature
def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard)
def catch_exception(self, *args, **kwargs): try: return func(self, *args, **kwargs) except BTChipException as e: if e.sw == 0x6982: raise UserFacingException(_('Your Ledger is locked. Please unlock it.')) else: raise
def checkDevice(self): if not self.preflightDone: try: self.perform_hw1_preflight() except BTChipException as e: if (e.sw == 0x6d00 or e.sw == 0x6700): raise UserFacingException(_("Device not in Litecoin mode")) from e raise e self.preflightDone = True
def give_error(self, message, clear_client=False): _logger.info(message) if not self.signing: self.handler.show_error(message) else: self.signing = False if clear_client: self.client = None raise UserFacingException(message)
def give_error(self, message, clear_client=False): print_error(message) if not self.ux_busy: self.handler.show_error(message) else: self.ux_busy = False if clear_client: self.client = None raise UserFacingException(message)
def give_error(self, message: Exception, clear_client: bool = False): self.logger.info(message) if not self.ux_busy: self.handler.show_error(message) else: self.ux_busy = False if clear_client: self.client = None raise UserFacingException(message)
def scan_and_create_client_for_device(self, *, device_id: str, wizard: 'BaseWizard') -> 'HardwareClientBase': devmgr = self.device_manager() client = wizard.run_task_without_blocking_gui( task=partial(devmgr.client_by_id, device_id)) if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) return client
def manipulate_keystore_dict_during_wizard_setup(self, d: dict): master_xpub = self.dev.master_xpub if master_xpub is not None: try: node = BIP32Node.from_xkey(master_xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException( _('Invalid xpub magic. Make sure your {} device is set to the correct chain.').format(self.device) + ' ' + _('You might have to unplug and plug it in again.') ) from None d['ckcc_xpub'] = master_xpub
def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) if not client.is_uptodate(): msg = (_('Outdated {} firmware for device labelled {}. Please ' 'download the updated firmware from {}') .format(self.device, client.label(), self.firmware_URL)) raise UserFacingException(msg) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) is_creating_wallet = purpose == HWD_SETUP_NEW_WALLET client.get_xpub('m', 'standard', creating=is_creating_wallet) client.used()
def sign_transaction(self, tx, password): if tx.is_complete(): return # previous transactions used as inputs prev_tx = {} for txin in tx.inputs(): tx_hash = txin.prevout.txid.hex() if txin.utxo is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Missing previous tx for legacy input.')) prev_tx[tx_hash] = txin.utxo self.plugin.sign_transaction(self, tx, prev_tx)
def __exit__(self, exc_type, exc_value, traceback): self.end_flow() if exc_value is not None: if issubclass(exc_type, Cancelled): raise UserCancelled from exc_value elif issubclass(exc_type, TrezorFailure): raise RuntimeError(str(exc_value)) from exc_value elif issubclass(exc_type, OutdatedFirmwareError): raise UserFacingException(exc_value) from exc_value else: return False return True
def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: raise UserFacingException( _('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) client.get_xpub( "m/44'/2'", 'standard') # TODO replace by direct derivation once Nano S > 1.1
def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) client.get_xpub('m', 'standard') client.used()
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES _logger.info('Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: node = BIP32Node.from_xkey(xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = node._replace(xtype=xtype).to_xpub() return xpub
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES print_error('[coldcard]', 'Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) return xpub
def btc_multisig_config( self, coin, bip32_path: List[int], wallet: Multisig_Wallet, xtype: str, ): """ Set and get a multisig config with the current device and some other arbitrary xpubs. Registers it on the device if not already registered. xtype: 'p2wsh' | 'p2wsh-p2sh' """ assert xtype in ("p2wsh", "p2wsh-p2sh") if self.bitbox02_device is None: raise Exception( "Need to setup communication first before attempting any BitBox02 calls" ) account_keypath = bip32_path[:-2] xpubs = wallet.get_master_public_keys() our_xpub = self.get_xpub( bip32.convert_bip32_intpath_to_strpath(account_keypath), xtype ) multisig_config = bitbox02.btc.BTCScriptConfig( multisig=bitbox02.btc.BTCScriptConfig.Multisig( threshold=wallet.m, xpubs=[util.parse_xpub(xpub) for xpub in xpubs], our_xpub_index=xpubs.index(our_xpub), script_type={ "p2wsh": bitbox02.btc.BTCScriptConfig.Multisig.P2WSH, "p2wsh-p2sh": bitbox02.btc.BTCScriptConfig.Multisig.P2WSH_P2SH, }[xtype] ) ) is_registered = self.bitbox02_device.btc_is_script_config_registered( coin, multisig_config, account_keypath ) if not is_registered: name = self.handler.name_multisig_account() try: self.bitbox02_device.btc_register_script_config( coin=coin, script_config=multisig_config, keypath=account_keypath, name=name, ) except bitbox02.DuplicateEntryException: raise except: raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02") return multisig_config
def recover_or_erase_dialog(self): msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n" choices = [ (_("Create a wallet using the current seed")), (_("Load a wallet from the micro SD card (the current seed is overwritten)")), (_("Erase the Digital Bitbox")) ] try: reply = self.handler.win.query_choice(msg, choices) except Exception: return # Back button pushed if reply == 2: self.dbb_erase() elif reply == 1: if not self.dbb_load_backup(): return else: if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: raise UserFacingException(_("Full 2FA enabled. This is not supported yet.")) # Use existing seed self.isInitialized = True
def sign_transaction(self, tx, password): if tx.is_complete(): return # previous transactions used as inputs prev_tx = {} # path of the xpubs that are involved xpub_path = {} for txin in tx.inputs(): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): continue xpub, s = parse_xpubkey(x_pubkey) if xpub == self.get_master_public_key(): xpub_path[xpub] = self.get_derivation() self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)