From 26cb6c7dda02d5bfaf82eb22eef937e5123ca230 Mon Sep 17 00:00:00 2001 From: BTChip Date: Sun, 28 Aug 2016 16:33:15 +0200 Subject: [PATCH 1/3] Add temporary icons --- icons.qrc | 2 ++ icons/ledger.png | Bin 0 -> 1475 bytes icons/ledger_unpaired.png | Bin 0 -> 1473 bytes 3 files changed, 2 insertions(+) create mode 100644 icons/ledger.png create mode 100644 icons/ledger_unpaired.png diff --git a/icons.qrc b/icons.qrc index 2f2f62d1..63ee8626 100644 --- a/icons.qrc +++ b/icons.qrc @@ -17,6 +17,8 @@ icons/keepkey.png icons/keepkey_unpaired.png icons/key.png + icons/ledger.png + icons/ledger_unpaired.png icons/lock.png icons/microphone.png icons/network.png diff --git a/icons/ledger.png b/icons/ledger.png new file mode 100644 index 0000000000000000000000000000000000000000..dadcc1a57c7384aae2684e3faa2bd60ff30faf8b GIT binary patch literal 1475 zcmV;!1w8tRP)F#O|G9$XHQ$5qW+cR5#n8WmR z*L3~5zWTnZuKJ`b%Q6UcEFusHfC8WZC;$oxfC6A|N-nCPEX!J-GE0)A)~6Hz(=-KR z480bQOOgaa2q>l9mDA}+gfWIRP0{NE8A*~rDFq>9`2Z$K0>)T>IRio<%kt6#jN=&n zI((KS35udDofl4$1pPeskYyQ?Bv~4OO=r>XVvOagC*gfEX_~H@*e;Ky5#H9f+QeID znfHlw;^u=O#Jul=rGJ}*Vd$`JYtjF>b*oyeXQc!KrU}E)J8FeIURmbp6Y@Ha@4o*5 z@4Y{civkaFD^L#+qFNi=lPeGq&02auonCG?ZL?Qt+^ZuW2|k%Av{ev z&#>p%@o^kF`r4xKR!SEyUB!`CUdOTHFPn^ZapMJ6Y z0C`NfHs|5^$%)cChZAFGP@D4#i#!NVK|GqAJO|$oN)OQY158ey0Y1F*<@Eev2B$2|kE$a@u2`U2p}s<3w=Z@%%WXn{fkpa3WU3V=c@ z;)TafeS`}aerkK(LpvVCv(M}n0ROEkmw&~%^FOvd@2)%V5GE`n01AKtpa3W&01AKt zpa`U!+P3WhR9vU+d0Vz@TJ*SM+jxBU=(1FL#*icl zEXxumEc!qJPyiHKYA<|iwu&$a+n#3*m~bl#y>%QXn4X^D`mZ%>Zik{Qy!C2r4*tB~ z_IlmWF*N9w3*cLCzl*QGnQVLBe=V@Q_B;Oq%MVYzgLP}~UU2;6*aSZN{HwOtKl1QM zVXt9@2@8OI0#Mb|o~g8^Y2^cW+t6Uo02~@}%Lj1trh9t^V3GG)7{HP3Tj9Eu(!+Dz zO0Mk=%LdRebR0aexAXuH9@yK_#+$AH@@{(?-L(@(4(;!H7x+?$Qi{Vb@5kt_ohY)- zZ3AgZlF(>0xcAXM{p4e~|Gv#Q@xd80aa}u zS+t!G^nD+m=V5R#zn4u@hDM|Dk2T4vszTQnh7pW0)a&zH5Z>9@M2@FH$>T?Joa2&g<)~4&)q7NspnwxBR zCXoq`2~bMmc^+1aJkM*@yAXi7uEX=ZRkLH=^E~LfUJO7&2sBNDq9~Y}n(BLiH*Va3 zqA1Wbtr!z-cE*-vA&Mf*%*^yHyy@xb){7+GsDle2r4*)V!f_nbYBk)vd9$ywcVtF7r!)S#pomi06O&;kc$T*G> z$1##5X^mRS2C)f`QVLm?p(skrTDs|aq@ogWH`9dlLJK5>6xLr$B9nP{5^v=MKmkwy d6aa-f)}P_@PUDh|xsm_?002ovPDHLkV1m$;vAX~O literal 0 HcmV?d00001 diff --git a/icons/ledger_unpaired.png b/icons/ledger_unpaired.png new file mode 100644 index 0000000000000000000000000000000000000000..bac0009f2b91ffd2b2a272bfc30f8a40317591ea GIT binary patch literal 1473 zcmV;y1wQ(TP)x$c?uL3Y(+XE>dXL|1z^Kq!jh+5l_>i@wk62#M8B!un(cLD)RGT?|hntk$>L#M)+& z^@(&6=2ca}fTn>6|2!wx^O_#4+$L#Twm_N4=E1(cU+8PdFG37YJ zo&z&eICAjKmhd)8JGO7dp|{__ftjgTDGiGoCkpWCiN~;Kaw4_;+MdY?JoD7!>kp8{ zg$sEdGy5h}9~|~i??xf7Czg2-mJ6|{QW=M)si_C3X(}p}aprz7(UHilbkE(}dL~6l z@?H}ISSl5I24Ir+nixPyl6nSUlK0A|^f|x{9ycd-q1Rr2i(4QU2gm_(fE*y#hD71% zz0YFj&U@RQ_wCv5@X^PoIKZ2A*S0$`zU#iW=l%NI@7#sCI6w}N1LOd?I6w}N1LTQx zL+8(bkE&s|J@3MWOD&I^mW3})&8Fp7(zXEVtawzQFRiy)Wb1^D45J!r zyTiHxbX^ZekDW+8z@x`bbhPoND}b!qp5{)U!J#9wUGD;43sFjO_?=nIoj!vk+uSyh zuGj0xX0y!uXg~SvOZ@QTMeKimHy*0&+VFjLlv13XU%>JAKg89mKQn(Xo6RB!+GiGR z=L0oOgRbjXUXCAS1%V)&&HiUiau5XYywzz0Aq2VH0Mm7Cd!^<4q@u{kX0y$|!!Se; z1fVqjTxO|Mf@N879H%=AjH0Ny*(PqK3rZunuE*5pL{S9Ow7Rl3&+}V8oVaRivSFD- zCM+&MDTS`<*eud@z1i+W0D7JWUDr3wj&)tv;dx#%0HY{^@B0YD5an{Y?*WdEjv@?0 z_`aWv3pYAr+qNOgGKPkR`WD{s@Nn}*5^L7M1dvh+$8lhqCJKcDMn*>Zs(MEzlR>#$ zhOX-{O%smeFz2$&6RKet!Zb|~LQpIgp{gnt7Z($q+1LolvW&5@F$jVH!!Tf3R;=1F zu?p`HLZB!L^7%Xpg#r{sfo*XNhXBW|)ls0? literal 0 HcmV?d00001 From a88036bc51f15c2babf1fa21bee289acb66a4eb5 Mon Sep 17 00:00:00 2001 From: BTChip Date: Sun, 28 Aug 2016 16:33:34 +0200 Subject: [PATCH 2/3] When no serial number is available, use the path as a temporary one --- lib/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/plugins.py b/lib/plugins.py index a3679d84..46fba581 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -500,8 +500,11 @@ class DeviceMgr(ThreadJob, PrintError): if product_key in self.recognised_hardware: # Older versions of hid don't provide interface_number interface_number = d.get('interface_number', 0) + serial = d['serial_number'] + if len(serial) == 0: + serial = d['path'] devices.append(Device(d['path'], interface_number, - d['serial_number'], product_key)) + serial, product_key)) # Now find out what was disconnected pairs = [(dev.path, dev.id_) for dev in devices] From 3d2de1036cf0560b01b11ec6afacb60f1f36df51 Mon Sep 17 00:00:00 2001 From: BTChip Date: Sun, 28 Aug 2016 16:38:30 +0200 Subject: [PATCH 3/3] Rewrite around new dev manager, rebase to latest Electrum, add P2SH support, add Nano S / Blue support --- plugins/ledger/ledger.py | 477 +++++++++++++++++++++++---------------- plugins/ledger/qt.py | 35 ++- 2 files changed, 311 insertions(+), 201 deletions(-) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index bccb8ebd..648ebe85 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -1,10 +1,10 @@ from binascii import hexlify -from struct import unpack +from struct import pack, unpack import hashlib import time import electrum -from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS +from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS, int_to_hex, var_int from electrum.i18n import _ from electrum.plugins import BasePlugin, hook from electrum.keystore import Hardware_KeyStore @@ -13,9 +13,10 @@ from electrum.util import format_satoshis_plain, print_error try: - from btchip.btchipComm import getDongle, DongleWait + import hid + from btchip.btchipComm import HIDDongleHIDAPI, DongleWait from btchip.btchip import btchip - from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script + from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script from btchip.bitcoinTransaction import bitcoinTransaction from btchip.btchipPersoWizard import StartBTChipPersoDialog from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware @@ -25,8 +26,136 @@ try: except ImportError: BTCHIP = False +class Ledger_Client(): + def __init__(self, hidDevice): + self.dongleObject = btchip(hidDevice) + self.preflightDone = False + + def is_pairable(self): + return True + + def close(self): + self.dongleObject.dongle.close() + + def timeout(self, cutoff): + pass + + def is_initialized(self): + return True + + def label(self): + return "" + + def i4b(self, x): + return pack('>I', x) + + def get_xpub(self, bip32_path): + 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") + try: + 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('\'') + if len(lastChild) == 1: + childnum = int(lastChild[0]) + else: + childnum = 0x80000000 | int(lastChild[0]) + xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) + except Exception, e: + #self.give_error(e, True) + return None + finally: + #self.handler.clear_dialog() + pass + + return EncodeBase58Check(xpub) + + def has_detached_pin_support(self, client): + try: + client.getVerifyPinRemainingAttempts() + return True + except BTChipException, e: + if e.sw == 0x6d00: + return False + raise e + + def is_pin_validated(self, client): + try: + # Invalid SET OPERATION MODE to verify the PIN status + client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB])) + except BTChipException, e: + if (e.sw == 0x6982): + return False + if (e.sw == 0x6A80): + return True + raise e + + def perform_hw1_preflight(self): + try: + firmware = self.dongleObject.getFirmwareVersion()['version'].split(".") + if not checkFirmware(firmware): + self.dongleObject.close() + raise Exception("HW1 firmware version too old. Please update at https://www.ledgerwallet.com") + try: + self.dongleObject.getOperationMode() + except BTChipException, e: + if (e.sw == 0x6985): + self.dongleObject.close() + dialog = StartBTChipPersoDialog() + dialog.exec_() + # 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) and (self.handler <> None): + 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 Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + pin = pin.encode() + self.dongleObject.verifyPin(pin) + except BTChipException, e: + if (e.sw == 0x6faa): + raise Exception("Dongle is temporarily locked - please unplug it and replug it again") + if ((e.sw & 0xFFF0) == 0x63c0): + raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + raise e + + def checkDevice(self): + if not self.preflightDone: + self.perform_hw1_preflight() + self.preflightDone = True + + def password_dialog(self, msg=None): + response = self.handler.get_word(msg) + if response is None: + return False, None, None + return True, response, response + class Ledger_KeyStore(Hardware_KeyStore): + hw_type = 'ledger' device = 'Ledger' def __init__(self, d): @@ -35,16 +164,14 @@ class Ledger_KeyStore(Hardware_KeyStore): # handler. The handler is per-window and preserved across # device reconnects self.force_watching_only = False - self.device_checked = False self.signing = False + def get_derivation(self): + return self.derivation + def get_client(self): - return self.plugin.get_client() - - def init_xpub(self): - client = self.get_client() - self.xpub = self.get_public_key(self.get_derivation()) - + return self.plugin.get_client(self) + def give_error(self, message, clear_client = False): print_error(message) if not self.signing: @@ -53,38 +180,34 @@ class Ledger_KeyStore(Hardware_KeyStore): self.signing = False if clear_client: self.client = None - self.device_checked = False raise Exception(message) - def address_id(self, address): + def address_id_stripped(self, address): # Strip the leading "m/" - return BIP32_HW_Wallet.address_id(self, address)[2:] + change, index = self.get_address_index(address) + derivation = self.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + return address_path[2:] def decrypt_message(self, pubkey, message, password): - self.give_error("Not supported") + raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device) - def sign_message(self, address, message, password): - use2FA = False + def sign_message(self, sequence, message, password): self.signing = True # prompt for the PIN before displaying the dialog if necessary client = self.get_client() - if not self.check_proper_device(): - self.give_error('Wrong device or password') - address_path = self.address_id(address) + address_path = self.get_derivation()[2:] + "/%d/%d"%sequence self.handler.show_message("Signing message ...") try: info = self.get_client().signMessagePrepare(address_path, message) pin = "" if info['confirmationNeeded']: # TODO : handle different confirmation types. For the time being only supports keyboard 2FA - use2FA = True confirmed, p, pin = self.password_dialog() if not confirmed: raise Exception('Aborted by user') pin = pin.encode() - client.bad = True - self.device_checked = False - self.plugin.get_client(self, True, True) + #self.plugin.get_client(self, True, True) signature = self.get_client().signMessageSign(pin) except BTChipException, e: if e.sw == 0x6a80: @@ -95,7 +218,6 @@ class Ledger_KeyStore(Hardware_KeyStore): self.give_error(e, True) finally: self.handler.clear_dialog() - client.bad = use2FA self.signing = False # Parse the ASN.1 signature @@ -122,7 +244,7 @@ class Ledger_KeyStore(Hardware_KeyStore): inputs = [] inputsPaths = [] pubKeys = [] - trustedInputs = [] + chipInputs = [] redeemScripts = [] signatures = [] preparedTrustedInputs = [] @@ -130,53 +252,91 @@ class Ledger_KeyStore(Hardware_KeyStore): changeAmount = None output = None outputAmount = None - use2FA = False + p2shTransaction = False pin = "" + self.get_client() # prompt for the PIN before displaying the dialog if necessary rawTx = tx.serialize() # Fetch inputs of the transaction to sign for txinput in tx.inputs(): if ('is_coinbase' in txinput and txinput['is_coinbase']): self.give_error("Coinbase not supported") # should never happen - inputs.append([ self.transactions[txinput['prevout_hash']].raw, - txinput['prevout_n'] ]) - address = txinput['address'] - inputsPaths.append(self.address_id(address)) - pubKeys.append(self.get_public_keys(address)) + redeemScript = None + signingPos = -1 + hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], txinput['derivation'][0], txinput['derivation'][1]) + if len(txinput['pubkeys']) > 1: + p2shTransaction = True + if 'redeemScript' in txinput: + redeemScript = txinput['redeemScript'] + if p2shTransaction: + chipPublicKey = compress_public_key(self.get_client().getWalletPublicKey(hwAddress)['publicKey']) + for currentIndex, key in enumerate(txinput['pubkeys']): + if chipPublicKey == key.decode('hex'): + signingPos = currentIndex + break + if signingPos == -1: + self.give_error("No matching key for multisignature input") # should never happen + + inputs.append([ txinput['prev_tx'].raw, + txinput['prevout_n'], redeemScript, txinput['prevout_hash'], signingPos ]) + inputsPaths.append(hwAddress) + pubKeys.append(txinput['pubkeys']) + + # Sanity check + if p2shTransaction: + for txinput in tx.inputs(): + if len(txinput['pubkeys']) < 2: + self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen + txOutput = var_int(len(tx.outputs())) + for output in tx.outputs(): + output_type, addr, amount = output + txOutput += int_to_hex(amount, 8) + script = tx.pay_script(output_type, addr) + txOutput += var_int(len(script)/2) + txOutput += script + txOutput = txOutput.decode('hex') # Recognize outputs - only one output and one change is authorized - if len(tx.outputs()) > 2: # should never happen - self.give_error("Transaction with more than 2 outputs not supported") - for type, address, amount in tx.outputs(): - assert type == TYPE_ADDRESS - if self.is_change(address): - changePath = self.address_id(address) - changeAmount = amount - else: - if output <> None: # should never happen - self.give_error("Multiple outputs with no change not supported") - output = address - outputAmount = amount - - self.get_client() # prompt for the PIN before displaying the dialog if necessary - if not self.check_proper_device(): - self.give_error('Wrong device or password') - + if not p2shTransaction: + if len(tx.outputs()) > 2: # should never happen + self.give_error("Transaction with more than 2 outputs not supported") + for i, (_type, address, amount) in enumerate(tx.outputs()): + assert _type == TYPE_ADDRESS + change, index = tx.output_info[i] + if change: + changePath = "%s/%d/%d" % (self.get_derivation()[2:], change, index) + changeAmount = amount + else: + if output <> None: # should never happen + self.give_error("Multiple outputs with no change not supported") + output = address + outputAmount = amount + self.handler.show_message("Signing Transaction ...") try: # Get trusted inputs from the original transactions - for utxo in inputs: - txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) - trustedInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) - # TODO : Support P2SH later - redeemScripts.append(txtmp.outputs[utxo[1]].script) + for utxo in inputs: + if not p2shTransaction: + txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) + chipInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) + redeemScripts.append(txtmp.outputs[utxo[1]].script) + else: + tmp = utxo[3].decode('hex')[::-1].encode('hex') + tmp += int_to_hex(utxo[1], 4) + chipInputs.append({ 'value' : tmp.decode('hex') }) + redeemScripts.append(bytearray(utxo[2].decode('hex'))) + # Sign all inputs firstTransaction = True inputIndex = 0 while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - trustedInputs, redeemScripts[inputIndex]) - outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), - format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex'))) + chipInputs, redeemScripts[inputIndex]) + if not p2shTransaction: + outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), + format_satoshis_plain(tx.get_fee()), changePath, bytearray(rawTx.decode('hex'))) + else: + outputData = self.get_client().finalizeInputFull(txOutput) + outputData['outputData'] = txOutput if firstTransaction: transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: @@ -204,14 +364,11 @@ class Ledger_KeyStore(Hardware_KeyStore): raise Exception('Invalid PIN character') pin = pin2 else: - use2FA = True confirmed, p, pin = self.password_dialog() if not confirmed: raise Exception('Aborted by user') pin = pin.encode() - client.bad = True - self.device_checked = False - self.plugin.get_client(self, True, True) + #self.plugin.get_client(self, True, True) self.handler.show_message("Signing ...") else: # Sign input with the provided PIN @@ -229,35 +386,19 @@ class Ledger_KeyStore(Hardware_KeyStore): # Reformat transaction inputIndex = 0 while inputIndex < len(inputs): - # TODO : Support P2SH later - inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) - preparedTrustedInputs.append([ trustedInputs[inputIndex]['value'], inputScript ]) + if p2shTransaction: + signaturesPack = [signatures[inputIndex]] * len(pubKeys[inputIndex]) + inputScript = get_p2sh_input_script(redeemScripts[inputIndex], signaturesPack) + preparedTrustedInputs.append([ ("\x00" * 4) + chipInputs[inputIndex]['value'], inputScript ]) + else: + inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) + preparedTrustedInputs.append([ chipInputs[inputIndex]['value'], inputScript ]) inputIndex = inputIndex + 1 updatedTransaction = format_transaction(transactionOutput, preparedTrustedInputs) updatedTransaction = hexlify(updatedTransaction) - tx.update(updatedTransaction) - client.bad = use2FA + tx.update_signatures(updatedTransaction) self.signing = False - def check_proper_device(self): - pubKey = DecodeBase58Check(self.xpub)[45:] - if not self.device_checked: - self.handler.show_message("Checking device") - try: - nodeData = self.get_client().getWalletPublicKey("44'/0'/0'") - except Exception, e: - self.give_error(e, True) - finally: - self.handler.clear_dialog() - pubKeyDevice = compress_public_key(nodeData['publicKey']) - self.device_checked = True - if pubKey != pubKeyDevice: - self.proper_device = False - else: - self.proper_device = True - - return self.proper_device - def password_dialog(self, msg=None): if not msg: msg = _("Do not enter your device PIN here !\r\n\r\n" \ @@ -279,121 +420,77 @@ class Ledger_KeyStore(Hardware_KeyStore): class LedgerPlugin(HW_PluginBase): libraries_available = BTCHIP keystore_class = Ledger_KeyStore - hw_type='ledger' client = None + DEVICE_IDS = [ + (0x2581, 0x1807), # HW.1 legacy btchip + (0x2581, 0x2b7c), # HW.1 transitional production + (0x2581, 0x3b7c), # HW.1 ledger production + (0x2581, 0x4b7c), # HW.1 ledger test + (0x2c97, 0x0000), # Blue + (0x2c97, 0x0001) # Nano-S + ] + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + if self.libraries_available: + self.device_manager().register_devices(self.DEVICE_IDS) def btchip_is_connected(self, keystore): try: - self.get_client().getFirmwareVersion() + self.get_client(keystore).getFirmwareVersion() except Exception as e: - self.print_error("get_client", str(e)) return False return True - def get_client(self, force_pair=True, noPin=False): - aborted = False - client = self.client - if not client or client.bad: - try: - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - firmware = client.getFirmwareVersion()['version'].split(".") - if not checkFirmware(firmware): - d.close() - try: - updateFirmware() - except Exception, e: - aborted = True - raise e - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - try: - client.getOperationMode() - except BTChipException, e: - if (e.sw == 0x6985): - d.close() - dialog = StartBTChipPersoDialog() - dialog.exec_() - # Then fetch the reference again as it was invalidated - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - else: - raise e - if not noPin: - # Immediately prompts for the PIN - remaining_attempts = client.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 = wallet.password_dialog(msg) - if not confirmed: - aborted = True - raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') - pin = pin.encode() - client.verifyPin(pin) + def get_btchip_device(self, device): + ledger = False + if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97): + ledger = True + dev = hid.device() + dev.open_path(device.path) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) - except BTChipException, e: - try: - client.dongle.close() - except: - pass - client = None - if (e.sw == 0x6faa): - raise Exception("Dongle is temporarily locked - please unplug it and replug it again") - if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") - raise e - except Exception, e: - try: - client.dongle.close() - except: - pass - client = None - if not aborted: - raise Exception("Could not connect to your Ledger wallet. Please verify access permissions, PIN, or unplug the dongle and plug it again") - else: - raise e - client.bad = False - self.device_checked = False - self.proper_device = False - self.client = client + def verify_btchip_pin(self): + pass + + def create_client(self, device, handler): + self.handler = handler - return self.client + client = self.get_btchip_device(device) + if client <> None: + client = Ledger_Client(client) + return client - def get_public_key(self, bip32_path): - # 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") - try: - 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.get_client().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.get_client().getWalletPublicKey(bip32_path) - publicKey = compress_public_key(nodeData['publicKey']) - depth = len(splitPath) - lastChild = splitPath[len(splitPath) - 1].split('\'') - if len(lastChild) == 1: - childnum = int(lastChild[0]) - else: - childnum = 0x80000000 | int(lastChild[0]) - xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) - except Exception, e: - self.give_error(e, True) - finally: - self.handler.clear_dialog() + def setup_device(self, device_info, wizard): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + #client.handler = wizard + client.handler = self.create_handler(wizard) + client.get_xpub('m') - return EncodeBase58Check(xpub) + def get_xpub(self, device_id, derivation, wizard): + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + #client.handler = wizard + client.handler = self.create_handler(wizard) + client.checkDevice() + xpub = client.get_xpub(derivation) + return xpub + def get_client(self, keystore, force_pair=True): + # All client interaction should not be in the main GUI thread + #assert self.main_thread != threading.current_thread() + devmgr = self.device_manager() + handler = keystore.handler + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + #if client: + # client.used() + if client <> None: + client.checkDevice() + client = client.dongleObject + return client diff --git a/plugins/ledger/qt.py b/plugins/ledger/qt.py index a27a7bf2..b3977a8c 100644 --- a/plugins/ledger/qt.py +++ b/plugins/ledger/qt.py @@ -3,27 +3,35 @@ import threading from PyQt4.Qt import (QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, SIGNAL) import PyQt4.QtCore as QtCore +from electrum_gui.qt.main_window import StatusBarButton from electrum.i18n import _ from electrum.plugins import hook from .ledger import LedgerPlugin, Ledger_KeyStore from ..hw_wallet.qt import QtHandlerBase +from electrum_gui.qt.util import * class Plugin(LedgerPlugin): + icon_unpaired = ":icons/ledger_unpaired.png" + icon_paired = ":icons/ledger.png" @hook def load_wallet(self, wallet, window): - keystore = wallet.get_keystore() - if type(keystore) != self.keystore_class: - return - keystore.handler = BTChipQTHandler(window) - if self.btchip_is_connected(keystore): - if not keystore.check_proper_device(): - window.show_error(_("This wallet does not match your Ledger device")) - wallet.force_watching_only = True - else: - window.show_error(_("Ledger device not detected.\nContinuing in watching-only mode.")) - wallet.force_watching_only = True + for keystore in wallet.get_keystores(): + if type(keystore) != self.keystore_class: + continue + tooltip = self.device + cb = partial(self.show_settings_dialog, window, keystore) + button = StatusBarButton(QIcon(self.icon_unpaired), tooltip, cb) + button.icon_paired = self.icon_paired + button.icon_unpaired = self.icon_unpaired + window.statusBar().addPermanentWidget(button) + handler = BTChipQTHandler(window) + handler.button = button + keystore.handler = handler + keystore.thread = TaskThread(window, window.on_error) + # Trigger a pairing + keystore.thread.add(partial(self.get_client, keystore)) def create_keystore(self, hw_type, derivation, wizard): from electrum.keystore import hardware_keystore @@ -40,6 +48,11 @@ class Plugin(LedgerPlugin): k = hardware_keystore(hw_type, d) return k + def create_handler(self, wizard): + return BTChipQTHandler(wizard) + + def show_settings_dialog(self, window, keystore): + pass class BTChipQTHandler(QtHandlerBase):