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']) f = self.handler.query_choice(_("Choose a backup file:"), backups['backup']) if f is None: return False # user cancelled 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 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 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 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 trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: validate_op_return_output(output) 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.") ) return script[2:]
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 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 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 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 Dash mode")) from e raise e self.preflightDone = True
def dbb_generate_wallet(self): key = self.stretch_key(self.password) filename = ("Dash-Electrum-" + 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 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 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: raise UserFacingException(_('Missing previous tx.')) prev_tx[tx_hash] = txin.utxo self.plugin.sign_transaction(self, tx, prev_tx)
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")) ] reply = self.handler.query_choice(msg, choices) if reply is None: return # user cancelled 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 inputs = [] inputsPaths = [] chipInputs = [] redeemScripts = [] changePath = "" output = None p2shTransaction = False pin = "" client_ledger = self.get_client( ) # prompt for the PIN before displaying the dialog if necessary client_electrum = self.get_client_electrum() assert client_electrum # Fetch inputs of the transaction to sign for txin in tx.inputs(): if txin.is_coinbase_input(): self.give_error( "Coinbase not supported") # should never happen if txin.script_type in ['p2sh']: p2shTransaction = True my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) if not full_path: self.give_error("No matching pubkey for sign_transaction" ) # should never happen full_path = convert_bip32_intpath_to_strpath(full_path)[2:] redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.utxo if txin_prev_tx is None: raise UserFacingException( _('Missing previous tx for legacy input.')) txin_prev_tx_raw = txin_prev_tx.serialize( ) if txin_prev_tx else None txin_prev_tx.deserialize() tx_type = txin_prev_tx.tx_type extra_payload = txin_prev_tx.extra_payload extra_data = b'' if tx_type and extra_payload: extra_payload = extra_payload.serialize() extra_data = bfh(var_int(len(extra_payload))) + extra_payload inputs.append([ txin_prev_tx_raw, txin.prevout.out_idx, redeemScript, txin.prevout.txid.hex(), my_pubkey, txin.nsequence, txin.value_sats(), extra_data ]) inputsPaths.append(full_path) # Sanity check if p2shTransaction: for txin in tx.inputs(): if txin.script_type != 'p2sh': self.give_error( "P2SH / regular input mixed in same transaction not supported" ) # should never happen txOutput = var_int(len(tx.outputs())) for o in tx.outputs(): txOutput += int_to_hex(o.value, 8) script = o.scriptpubkey.hex() txOutput += var_int(len(script) // 2) txOutput += script txOutput = bfh(txOutput) if not client_electrum.supports_multi_output(): if len(tx.outputs()) > 2: self.give_error( "Transaction with more than 2 outputs not supported") for txout in tx.outputs(): if client_electrum.is_hw1( ) and txout.address and not is_b58_address(txout.address): self.give_error( _("This {} device can only send to base58 addresses."). format(self.device)) if not txout.address: if client_electrum.is_hw1(): self.give_error( _("Only address outputs are supported by {}").format( self.device)) # note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26 validate_op_return_output(txout, max_size=190) # Output "change" detection # - only one output and one change is authorized (for hw.1 and nano) # - at most one output can bypass confirmation (~change) (for all) if not p2shTransaction: has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) for txout in tx.outputs(): if txout.is_mine and len(tx.outputs()) > 1 \ and not has_change: # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed if txout.is_change == any_output_on_change_branch: my_pubkey, changePath = self.find_my_pubkey_in_txinout( txout) assert changePath changePath = convert_bip32_intpath_to_strpath( changePath)[2:] has_change = True else: output = txout.address else: output = txout.address if not self.get_client_electrum().canAlternateCoinVersions: v, h = b58_address_to_hash160(output) if v == constants.net.ADDRTYPE_P2PKH: output = hash160_to_b58_address(h, 0) self.handler.show_message( _("Confirm Transaction on your Ledger device...")) try: # Get trusted inputs from the original transactions for utxo in inputs: sequence = int_to_hex(utxo[5], 4) if (not p2shTransaction ) or client_electrum.supports_multi_output(): txtmp = bitcoinTransaction(bfh(utxo[0])) txtmp.extra_data = utxo[7] trustedInput = client_ledger.getTrustedInput( txtmp, utxo[1]) trustedInput['sequence'] = sequence chipInputs.append(trustedInput) if p2shTransaction: redeemScripts.append(bfh(utxo[2])) else: redeemScripts.append(txtmp.outputs[utxo[1]].script) else: tmp = bfh(utxo[3])[::-1] tmp += bfh(int_to_hex(utxo[1], 4)) chipInputs.append({'value': tmp, 'sequence': sequence}) redeemScripts.append(bfh(utxo[2])) # Sign all inputs firstTransaction = True inputIndex = 0 rawTx = tx.serialize_to_network() client_ledger.enableAlternate2fa(False) while inputIndex < len(inputs): client_ledger.startUntrustedTransaction( firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = client_ledger.finalizeInput( b'', 0, 0, changePath, bfh(rawTx)) outputData['outputData'] = txOutput if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() # do the authenticate dialog and get pin: pin = self.handler.get_auth(outputData, client=client_electrum) if not pin: raise UserWarning() self.handler.show_message( _("Confirmed. Signing Transaction...")) else: # Sign input with the provided PIN inputSignature = client_ledger.untrustedHashSign( inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ my_pubkey = inputs[inputIndex][4] tx.add_signature_to_txin(txin_idx=inputIndex, signing_pubkey=my_pubkey.hex(), sig=inputSignature.hex()) inputIndex = inputIndex + 1 firstTransaction = False except UserWarning: self.handler.show_error(_('Cancelled by user')) return except BTChipException as e: if e.sw in (0x6985, 0x6d00): # cancelled by user return elif e.sw == 0x6982: raise # pin lock. decorator will catch it else: self.logger.exception('') self.give_error(e, True) except BaseException as e: self.logger.exception('') self.give_error(e, True) finally: self.handler.finished()
def perform_hw1_preflight(self): try: firmwareInfo = self.dongleObject.getFirmwareVersion() firmware = firmwareInfo['version'] self.multiOutputSupported = versiontuple(firmware) >= versiontuple( MULTI_OUTPUT_SUPPORT) self.canAlternateCoinVersions = ( versiontuple(firmware) >= versiontuple(ALTERNATIVE_COIN_VERSION) and firmwareInfo['specialVersion'] >= 0x20) 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) if self.canAlternateCoinVersions: self.dongleObject.setAlternateCoinVersions( 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 decrypt_message(self, sequence, message, password): raise UserFacingException( _('Encryption and decryption are not implemented by {}').format( self.device))
def decrypt_message(self, pubkey, message, password): raise UserFacingException( _('Encryption and decryption are currently not supported for {}'). format(self.device))