ledger.py (32370B)
1 from struct import pack, unpack 2 import hashlib 3 import sys 4 import traceback 5 from typing import Optional, Tuple 6 7 from electrum import ecc 8 from electrum import bip32 9 from electrum.crypto import hash_160 10 from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type, is_b58_address 11 from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath 12 from electrum.i18n import _ 13 from electrum.keystore import Hardware_KeyStore 14 from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput 15 from electrum.wallet import Standard_Wallet 16 from electrum.util import bfh, bh2u, versiontuple, UserFacingException 17 from electrum.base_wizard import ScriptTypeNotSupported 18 from electrum.logging import get_logger 19 from electrum.plugin import runs_in_hwd_thread, Device 20 21 from ..hw_wallet import HW_PluginBase, HardwareClientBase 22 from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable 23 24 25 _logger = get_logger(__name__) 26 27 28 try: 29 import hid 30 from btchip.btchipComm import HIDDongleHIDAPI, DongleWait 31 from btchip.btchip import btchip 32 from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script 33 from btchip.bitcoinTransaction import bitcoinTransaction 34 from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware 35 from btchip.btchipException import BTChipException 36 BTCHIP = True 37 BTCHIP_DEBUG = False 38 except ImportError as e: 39 if not (isinstance(e, ModuleNotFoundError) and e.name == 'btchip'): 40 _logger.exception('error importing ledger plugin deps') 41 BTCHIP = False 42 43 MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \ 44 ' https://www.ledgerwallet.com' 45 MSG_NEEDS_FW_UPDATE_SEGWIT = _('Firmware version (or "Bitcoin" app) too old for Segwit support. Please update at') + \ 46 ' https://www.ledgerwallet.com' 47 MULTI_OUTPUT_SUPPORT = '1.1.4' 48 SEGWIT_SUPPORT = '1.1.10' 49 SEGWIT_SUPPORT_SPECIAL = '1.0.4' 50 SEGWIT_TRUSTEDINPUTS = '1.4.0' 51 52 53 def test_pin_unlocked(func): 54 """Function decorator to test the Ledger for being unlocked, and if not, 55 raise a human-readable exception. 56 """ 57 def catch_exception(self, *args, **kwargs): 58 try: 59 return func(self, *args, **kwargs) 60 except BTChipException as e: 61 if e.sw == 0x6982: 62 raise UserFacingException(_('Your Ledger is locked. Please unlock it.')) 63 else: 64 raise 65 return catch_exception 66 67 68 class Ledger_Client(HardwareClientBase): 69 def __init__(self, hidDevice, *, product_key: Tuple[int, int], 70 plugin: HW_PluginBase): 71 HardwareClientBase.__init__(self, plugin=plugin) 72 self.dongleObject = btchip(hidDevice) 73 self.preflightDone = False 74 self._product_key = product_key 75 self._soft_device_id = None 76 77 def is_pairable(self): 78 return True 79 80 @runs_in_hwd_thread 81 def close(self): 82 self.dongleObject.dongle.close() 83 84 def is_initialized(self): 85 return True 86 87 @runs_in_hwd_thread 88 def get_soft_device_id(self): 89 if self._soft_device_id is None: 90 # modern ledger can provide xpub without user interaction 91 # (hw1 would prompt for PIN) 92 if not self.is_hw1(): 93 self._soft_device_id = self.request_root_fingerprint_from_device() 94 return self._soft_device_id 95 96 def is_hw1(self) -> bool: 97 return self._product_key[0] == 0x2581 98 99 def device_model_name(self): 100 return LedgerPlugin.device_name_from_product_key(self._product_key) 101 102 @runs_in_hwd_thread 103 def has_usable_connection_with_device(self): 104 try: 105 self.dongleObject.getFirmwareVersion() 106 except BaseException: 107 return False 108 return True 109 110 @runs_in_hwd_thread 111 @test_pin_unlocked 112 def get_xpub(self, bip32_path, xtype): 113 self.checkDevice() 114 # bip32_path is of the form 44'/0'/1' 115 # S-L-O-W - we don't handle the fingerprint directly, so compute 116 # it manually from the previous node 117 # This only happens once so it's bearable 118 #self.get_client() # prompt for the PIN before displaying the dialog if necessary 119 #self.handler.show_message("Computing master public key") 120 if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): 121 raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) 122 if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): 123 raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) 124 bip32_path = bip32.normalize_bip32_derivation(bip32_path) 125 bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) 126 bip32_path = bip32_path[2:] # cut off "m/" 127 if len(bip32_intpath) >= 1: 128 prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:] 129 nodeData = self.dongleObject.getWalletPublicKey(prevPath) 130 publicKey = compress_public_key(nodeData['publicKey']) 131 fingerprint_bytes = hash_160(publicKey)[0:4] 132 childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big") 133 else: 134 fingerprint_bytes = bytes(4) 135 childnum_bytes = bytes(4) 136 nodeData = self.dongleObject.getWalletPublicKey(bip32_path) 137 publicKey = compress_public_key(nodeData['publicKey']) 138 depth = len(bip32_intpath) 139 return BIP32Node(xtype=xtype, 140 eckey=ecc.ECPubkey(bytes(publicKey)), 141 chaincode=nodeData['chainCode'], 142 depth=depth, 143 fingerprint=fingerprint_bytes, 144 child_number=childnum_bytes).to_xpub() 145 146 def has_detached_pin_support(self, client): 147 try: 148 client.getVerifyPinRemainingAttempts() 149 return True 150 except BTChipException as e: 151 if e.sw == 0x6d00: 152 return False 153 raise e 154 155 def is_pin_validated(self, client): 156 try: 157 # Invalid SET OPERATION MODE to verify the PIN status 158 client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB])) 159 except BTChipException as e: 160 if (e.sw == 0x6982): 161 return False 162 if (e.sw == 0x6A80): 163 return True 164 raise e 165 166 def supports_multi_output(self): 167 return self.multiOutputSupported 168 169 def supports_segwit(self): 170 return self.segwitSupported 171 172 def supports_native_segwit(self): 173 return self.nativeSegwitSupported 174 175 def supports_segwit_trustedInputs(self): 176 return self.segwitTrustedInputs 177 178 @runs_in_hwd_thread 179 def perform_hw1_preflight(self): 180 try: 181 firmwareInfo = self.dongleObject.getFirmwareVersion() 182 firmware = firmwareInfo['version'] 183 self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) 184 self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT) 185 self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) 186 self.segwitTrustedInputs = versiontuple(firmware) >= versiontuple(SEGWIT_TRUSTEDINPUTS) 187 188 if not checkFirmware(firmwareInfo): 189 self.close() 190 raise UserFacingException(MSG_NEEDS_FW_UPDATE_GENERIC) 191 try: 192 self.dongleObject.getOperationMode() 193 except BTChipException as e: 194 if (e.sw == 0x6985): 195 self.close() 196 self.handler.get_setup( ) 197 # Acquire the new client on the next run 198 else: 199 raise e 200 if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject): 201 assert self.handler, "no handler for client" 202 remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() 203 if remaining_attempts != 1: 204 msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) 205 else: 206 msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." 207 confirmed, p, pin = self.password_dialog(msg) 208 if not confirmed: 209 raise UserFacingException('Aborted by user - please unplug the dongle and plug it again before retrying') 210 pin = pin.encode() 211 self.dongleObject.verifyPin(pin) 212 except BTChipException as e: 213 if (e.sw == 0x6faa): 214 raise UserFacingException("Dongle is temporarily locked - please unplug it and replug it again") 215 if ((e.sw & 0xFFF0) == 0x63c0): 216 raise UserFacingException("Invalid PIN - please unplug the dongle and plug it again before retrying") 217 if e.sw == 0x6f00 and e.message == 'Invalid channel': 218 # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure 219 raise UserFacingException("Invalid channel.\n" 220 "Please make sure that 'Browser support' is disabled on your device.") 221 raise e 222 223 @runs_in_hwd_thread 224 def checkDevice(self): 225 if not self.preflightDone: 226 try: 227 self.perform_hw1_preflight() 228 except BTChipException as e: 229 if (e.sw == 0x6d00 or e.sw == 0x6700): 230 raise UserFacingException(_("Device not in Bitcoin mode")) from e 231 raise e 232 self.preflightDone = True 233 234 def password_dialog(self, msg=None): 235 response = self.handler.get_word(msg) 236 if response is None: 237 return False, None, None 238 return True, response, response 239 240 241 class Ledger_KeyStore(Hardware_KeyStore): 242 hw_type = 'ledger' 243 device = 'Ledger' 244 245 plugin: 'LedgerPlugin' 246 247 def __init__(self, d): 248 Hardware_KeyStore.__init__(self, d) 249 # Errors and other user interaction is done through the wallet's 250 # handler. The handler is per-window and preserved across 251 # device reconnects 252 self.force_watching_only = False 253 self.signing = False 254 self.cfg = d.get('cfg', {'mode': 0}) 255 256 def dump(self): 257 obj = Hardware_KeyStore.dump(self) 258 obj['cfg'] = self.cfg 259 return obj 260 261 def get_client(self): 262 return self.plugin.get_client(self).dongleObject 263 264 def get_client_electrum(self) -> Optional[Ledger_Client]: 265 return self.plugin.get_client(self) 266 267 def give_error(self, message, clear_client = False): 268 _logger.info(message) 269 if not self.signing: 270 self.handler.show_error(message) 271 else: 272 self.signing = False 273 if clear_client: 274 self.client = None 275 raise UserFacingException(message) 276 277 def set_and_unset_signing(func): 278 """Function decorator to set and unset self.signing.""" 279 def wrapper(self, *args, **kwargs): 280 try: 281 self.signing = True 282 return func(self, *args, **kwargs) 283 finally: 284 self.signing = False 285 return wrapper 286 287 def decrypt_message(self, pubkey, message, password): 288 raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) 289 290 @runs_in_hwd_thread 291 @test_pin_unlocked 292 @set_and_unset_signing 293 def sign_message(self, sequence, message, password): 294 message = message.encode('utf8') 295 message_hash = hashlib.sha256(message).hexdigest().upper() 296 # prompt for the PIN before displaying the dialog if necessary 297 client_ledger = self.get_client() 298 client_electrum = self.get_client_electrum() 299 address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence 300 self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) 301 try: 302 info = client_ledger.signMessagePrepare(address_path, message) 303 pin = "" 304 if info['confirmationNeeded']: 305 # do the authenticate dialog and get pin: 306 pin = self.handler.get_auth(info, client=client_electrum) 307 if not pin: 308 raise UserWarning(_('Cancelled by user')) 309 pin = str(pin).encode() 310 signature = client_ledger.signMessageSign(pin) 311 except BTChipException as e: 312 if e.sw == 0x6a80: 313 self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") 314 elif e.sw == 0x6985: # cancelled by user 315 return b'' 316 elif e.sw == 0x6982: 317 raise # pin lock. decorator will catch it 318 else: 319 self.give_error(e, True) 320 except UserWarning: 321 self.handler.show_error(_('Cancelled by user')) 322 return b'' 323 except Exception as e: 324 self.give_error(e, True) 325 finally: 326 self.handler.finished() 327 # Parse the ASN.1 signature 328 rLength = signature[3] 329 r = signature[4 : 4 + rLength] 330 sLength = signature[4 + rLength + 1] 331 s = signature[4 + rLength + 2:] 332 if rLength == 33: 333 r = r[1:] 334 if sLength == 33: 335 s = s[1:] 336 # And convert it 337 338 # Pad r and s points with 0x00 bytes when the point is small to get valid signature. 339 r_padded = bytes([0x00]) * (32 - len(r)) + r 340 s_padded = bytes([0x00]) * (32 - len(s)) + s 341 342 return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded 343 344 @runs_in_hwd_thread 345 @test_pin_unlocked 346 @set_and_unset_signing 347 def sign_transaction(self, tx, password): 348 if tx.is_complete(): 349 return 350 inputs = [] 351 inputsPaths = [] 352 chipInputs = [] 353 redeemScripts = [] 354 changePath = "" 355 output = None 356 p2shTransaction = False 357 segwitTransaction = False 358 pin = "" 359 client_ledger = self.get_client() # prompt for the PIN before displaying the dialog if necessary 360 client_electrum = self.get_client_electrum() 361 assert client_electrum 362 363 # Fetch inputs of the transaction to sign 364 for txin in tx.inputs(): 365 if txin.is_coinbase_input(): 366 self.give_error("Coinbase not supported") # should never happen 367 368 if txin.script_type in ['p2sh']: 369 p2shTransaction = True 370 371 if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']: 372 if not client_electrum.supports_segwit(): 373 self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) 374 segwitTransaction = True 375 376 if txin.script_type in ['p2wpkh', 'p2wsh']: 377 if not client_electrum.supports_native_segwit(): 378 self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) 379 segwitTransaction = True 380 381 my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) 382 if not full_path: 383 self.give_error("No matching pubkey for sign_transaction") # should never happen 384 full_path = convert_bip32_intpath_to_strpath(full_path)[2:] 385 386 redeemScript = Transaction.get_preimage_script(txin) 387 txin_prev_tx = txin.utxo 388 if txin_prev_tx is None and not txin.is_segwit(): 389 raise UserFacingException(_('Missing previous tx for legacy input.')) 390 txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None 391 inputs.append([txin_prev_tx_raw, 392 txin.prevout.out_idx, 393 redeemScript, 394 txin.prevout.txid.hex(), 395 my_pubkey, 396 txin.nsequence, 397 txin.value_sats()]) 398 inputsPaths.append(full_path) 399 400 # Sanity check 401 if p2shTransaction: 402 for txin in tx.inputs(): 403 if txin.script_type != 'p2sh': 404 self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen 405 406 txOutput = var_int(len(tx.outputs())) 407 for o in tx.outputs(): 408 txOutput += int_to_hex(o.value, 8) 409 script = o.scriptpubkey.hex() 410 txOutput += var_int(len(script)//2) 411 txOutput += script 412 txOutput = bfh(txOutput) 413 414 if not client_electrum.supports_multi_output(): 415 if len(tx.outputs()) > 2: 416 self.give_error("Transaction with more than 2 outputs not supported") 417 for txout in tx.outputs(): 418 if client_electrum.is_hw1() and txout.address and not is_b58_address(txout.address): 419 self.give_error(_("This {} device can only send to base58 addresses.").format(self.device)) 420 if not txout.address: 421 if client_electrum.is_hw1(): 422 self.give_error(_("Only address outputs are supported by {}").format(self.device)) 423 # note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26 424 validate_op_return_output(txout, max_size=190) 425 426 # Output "change" detection 427 # - only one output and one change is authorized (for hw.1 and nano) 428 # - at most one output can bypass confirmation (~change) (for all) 429 if not p2shTransaction: 430 has_change = False 431 any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) 432 for txout in tx.outputs(): 433 if txout.is_mine and len(tx.outputs()) > 1 \ 434 and not has_change: 435 # prioritise hiding outputs on the 'change' branch from user 436 # because no more than one change address allowed 437 if txout.is_change == any_output_on_change_branch: 438 my_pubkey, changePath = self.find_my_pubkey_in_txinout(txout) 439 assert changePath 440 changePath = convert_bip32_intpath_to_strpath(changePath)[2:] 441 has_change = True 442 else: 443 output = txout.address 444 else: 445 output = txout.address 446 447 self.handler.show_message(_("Confirm Transaction on your Ledger device...")) 448 try: 449 # Get trusted inputs from the original transactions 450 for utxo in inputs: 451 sequence = int_to_hex(utxo[5], 4) 452 if segwitTransaction and not client_electrum.supports_segwit_trustedInputs(): 453 tmp = bfh(utxo[3])[::-1] 454 tmp += bfh(int_to_hex(utxo[1], 4)) 455 tmp += bfh(int_to_hex(utxo[6], 8)) # txin['value'] 456 chipInputs.append({'value' : tmp, 'witness' : True, 'sequence' : sequence}) 457 redeemScripts.append(bfh(utxo[2])) 458 elif (not p2shTransaction) or client_electrum.supports_multi_output(): 459 txtmp = bitcoinTransaction(bfh(utxo[0])) 460 trustedInput = client_ledger.getTrustedInput(txtmp, utxo[1]) 461 trustedInput['sequence'] = sequence 462 if segwitTransaction: 463 trustedInput['witness'] = True 464 chipInputs.append(trustedInput) 465 if p2shTransaction or segwitTransaction: 466 redeemScripts.append(bfh(utxo[2])) 467 else: 468 redeemScripts.append(txtmp.outputs[utxo[1]].script) 469 else: 470 tmp = bfh(utxo[3])[::-1] 471 tmp += bfh(int_to_hex(utxo[1], 4)) 472 chipInputs.append({'value' : tmp, 'sequence' : sequence}) 473 redeemScripts.append(bfh(utxo[2])) 474 475 # Sign all inputs 476 firstTransaction = True 477 inputIndex = 0 478 rawTx = tx.serialize_to_network() 479 client_ledger.enableAlternate2fa(False) 480 if segwitTransaction: 481 client_ledger.startUntrustedTransaction(True, inputIndex, 482 chipInputs, redeemScripts[inputIndex], version=tx.version) 483 # we don't set meaningful outputAddress, amount and fees 484 # as we only care about the alternateEncoding==True branch 485 outputData = client_ledger.finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) 486 outputData['outputData'] = txOutput 487 if outputData['confirmationNeeded']: 488 outputData['address'] = output 489 self.handler.finished() 490 # do the authenticate dialog and get pin: 491 pin = self.handler.get_auth(outputData, client=client_electrum) 492 if not pin: 493 raise UserWarning() 494 self.handler.show_message(_("Confirmed. Signing Transaction...")) 495 while inputIndex < len(inputs): 496 singleInput = [ chipInputs[inputIndex] ] 497 client_ledger.startUntrustedTransaction(False, 0, 498 singleInput, redeemScripts[inputIndex], version=tx.version) 499 inputSignature = client_ledger.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) 500 inputSignature[0] = 0x30 # force for 1.4.9+ 501 my_pubkey = inputs[inputIndex][4] 502 tx.add_signature_to_txin(txin_idx=inputIndex, 503 signing_pubkey=my_pubkey.hex(), 504 sig=inputSignature.hex()) 505 inputIndex = inputIndex + 1 506 else: 507 while inputIndex < len(inputs): 508 client_ledger.startUntrustedTransaction(firstTransaction, inputIndex, 509 chipInputs, redeemScripts[inputIndex], version=tx.version) 510 # we don't set meaningful outputAddress, amount and fees 511 # as we only care about the alternateEncoding==True branch 512 outputData = client_ledger.finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) 513 outputData['outputData'] = txOutput 514 if outputData['confirmationNeeded']: 515 outputData['address'] = output 516 self.handler.finished() 517 # do the authenticate dialog and get pin: 518 pin = self.handler.get_auth(outputData, client=client_electrum) 519 if not pin: 520 raise UserWarning() 521 self.handler.show_message(_("Confirmed. Signing Transaction...")) 522 else: 523 # Sign input with the provided PIN 524 inputSignature = client_ledger.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) 525 inputSignature[0] = 0x30 # force for 1.4.9+ 526 my_pubkey = inputs[inputIndex][4] 527 tx.add_signature_to_txin(txin_idx=inputIndex, 528 signing_pubkey=my_pubkey.hex(), 529 sig=inputSignature.hex()) 530 inputIndex = inputIndex + 1 531 firstTransaction = False 532 except UserWarning: 533 self.handler.show_error(_('Cancelled by user')) 534 return 535 except BTChipException as e: 536 if e.sw in (0x6985, 0x6d00): # cancelled by user 537 return 538 elif e.sw == 0x6982: 539 raise # pin lock. decorator will catch it 540 else: 541 self.logger.exception('') 542 self.give_error(e, True) 543 except BaseException as e: 544 self.logger.exception('') 545 self.give_error(e, True) 546 finally: 547 self.handler.finished() 548 549 @runs_in_hwd_thread 550 @test_pin_unlocked 551 @set_and_unset_signing 552 def show_address(self, sequence, txin_type): 553 client = self.get_client() 554 address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence 555 self.handler.show_message(_("Showing address ...")) 556 segwit = is_segwit_script_type(txin_type) 557 segwitNative = txin_type == 'p2wpkh' 558 try: 559 client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit, segwitNative=segwitNative) 560 except BTChipException as e: 561 if e.sw == 0x6985: # cancelled by user 562 pass 563 elif e.sw == 0x6982: 564 raise # pin lock. decorator will catch it 565 elif e.sw == 0x6b00: # hw.1 raises this 566 self.handler.show_error('{}\n{}\n{}'.format( 567 _('Error showing address') + ':', 568 e, 569 _('Your device might not have support for this functionality.'))) 570 else: 571 self.logger.exception('') 572 self.handler.show_error(e) 573 except BaseException as e: 574 self.logger.exception('') 575 self.handler.show_error(e) 576 finally: 577 self.handler.finished() 578 579 class LedgerPlugin(HW_PluginBase): 580 keystore_class = Ledger_KeyStore 581 minimum_library = (0, 1, 32) 582 client = None 583 DEVICE_IDS = [ 584 (0x2581, 0x1807), # HW.1 legacy btchip 585 (0x2581, 0x2b7c), # HW.1 transitional production 586 (0x2581, 0x3b7c), # HW.1 ledger production 587 (0x2581, 0x4b7c), # HW.1 ledger test 588 (0x2c97, 0x0000), # Blue 589 (0x2c97, 0x0001), # Nano-S 590 (0x2c97, 0x0004), # Nano-X 591 (0x2c97, 0x0005), # RFU 592 (0x2c97, 0x0006), # RFU 593 (0x2c97, 0x0007), # RFU 594 (0x2c97, 0x0008), # RFU 595 (0x2c97, 0x0009), # RFU 596 (0x2c97, 0x000a) # RFU 597 ] 598 VENDOR_IDS = (0x2c97, ) 599 LEDGER_MODEL_IDS = { 600 0x10: "Ledger Nano S", 601 0x40: "Ledger Nano X", 602 } 603 SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') 604 605 def __init__(self, parent, config, name): 606 self.segwit = config.get("segwit") 607 HW_PluginBase.__init__(self, parent, config, name) 608 self.libraries_available = self.check_libraries_available() 609 if not self.libraries_available: 610 return 611 # to support legacy devices and legacy firmwares 612 self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) 613 # to support modern firmware 614 self.device_manager().register_vendor_ids(self.VENDOR_IDS, plugin=self) 615 616 def get_library_version(self): 617 try: 618 import btchip 619 version = btchip.__version__ 620 except ImportError: 621 raise 622 except: 623 version = "unknown" 624 if BTCHIP: 625 return version 626 else: 627 raise LibraryFoundButUnusable(library_version=version) 628 629 @classmethod 630 def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]: 631 """Returns (can_recognize, model_name) tuple.""" 632 # legacy product_keys 633 if product_key in cls.DEVICE_IDS: 634 if product_key[0] == 0x2581: 635 return True, "Ledger HW.1" 636 if product_key == (0x2c97, 0x0000): 637 return True, "Ledger Blue" 638 if product_key == (0x2c97, 0x0001): 639 return True, "Ledger Nano S" 640 if product_key == (0x2c97, 0x0004): 641 return True, "Ledger Nano X" 642 return True, None 643 # modern product_keys 644 if product_key[0] == 0x2c97: 645 product_id = product_key[1] 646 model_id = product_id >> 8 647 if model_id in cls.LEDGER_MODEL_IDS: 648 model_name = cls.LEDGER_MODEL_IDS[model_id] 649 return True, model_name 650 # give up 651 return False, None 652 653 def can_recognize_device(self, device: Device) -> bool: 654 return self._recognize_device(device.product_key)[0] 655 656 @classmethod 657 def device_name_from_product_key(cls, product_key) -> Optional[str]: 658 return cls._recognize_device(product_key)[1] 659 660 def create_device_from_hid_enumeration(self, d, *, product_key): 661 device = super().create_device_from_hid_enumeration(d, product_key=product_key) 662 if not self.can_recognize_device(device): 663 return None 664 return device 665 666 @runs_in_hwd_thread 667 def get_btchip_device(self, device): 668 ledger = False 669 if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c: 670 ledger = True 671 if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c: 672 ledger = True 673 if device.product_key[0] == 0x2c97: 674 if device.interface_number == 0 or device.usage_page == 0xffa0: 675 ledger = True 676 else: 677 return None # non-compatible interface of a Nano S or Blue 678 dev = hid.device() 679 dev.open_path(device.path) 680 dev.set_nonblocking(True) 681 return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) 682 683 @runs_in_hwd_thread 684 def create_client(self, device, handler): 685 if handler: 686 self.handler = handler 687 688 client = self.get_btchip_device(device) 689 if client is not None: 690 client = Ledger_Client(client, product_key=device.product_key, plugin=self) 691 return client 692 693 def setup_device(self, device_info, wizard, purpose): 694 device_id = device_info.device.id_ 695 client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) 696 wizard.run_task_without_blocking_gui( 697 task=lambda: client.get_xpub("m/0'", 'standard')) # TODO replace by direct derivation once Nano S > 1.1 698 return client 699 700 def get_xpub(self, device_id, derivation, xtype, wizard): 701 if xtype not in self.SUPPORTED_XTYPES: 702 raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) 703 client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) 704 client.checkDevice() 705 xpub = client.get_xpub(derivation, xtype) 706 return xpub 707 708 @runs_in_hwd_thread 709 def get_client(self, keystore, force_pair=True, *, 710 devices=None, allow_user_interaction=True): 711 # All client interaction should not be in the main GUI thread 712 client = super().get_client(keystore, force_pair, 713 devices=devices, 714 allow_user_interaction=allow_user_interaction) 715 # returns the client for a given keystore. can use xpub 716 #if client: 717 # client.used() 718 if client is not None: 719 client.checkDevice() 720 return client 721 722 @runs_in_hwd_thread 723 def show_address(self, wallet, address, keystore=None): 724 if keystore is None: 725 keystore = wallet.get_keystore() 726 if not self.show_address_helper(wallet, address, keystore): 727 return 728 if type(wallet) is not Standard_Wallet: 729 keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) 730 return 731 sequence = wallet.get_address_index(address) 732 txin_type = wallet.get_txin_type(address) 733 keystore.show_address(sequence, txin_type)