From a4fe14bb82d5853cd26351c7e2ef829debbd1520 Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Tue, 18 Feb 2020 15:31:05 +0100 Subject: [PATCH 01/12] BitBox02 Electrum plugin support This commit adds support for the BitBox02 hardware wallet. It supports both single and multisig for the electrum gui wallet. To use the plugin a local installation of the BitBox02 python library is required. It can be found on PiPy under the name 'bitbox02' and can be installed from the bitbox02-firmware repository in the py/bitbox02 directory. All communication to and from the BitBox02 is noise encrypted, the keys required for this are stored in the wallet config file under the bitbox02 key. The BitBox02 registers a multisig configuration before allowing transaction signing. This multisig configuration includes the threshold, cosigner xpubs, keypath, a variable to indicate for mainnet and testnet, and a name that the user can choose during configuration registration. The user is asked to register the multisig configuration either during address verification or during transaction signing. The check the xpub of the BitBox02 for other hardware wallets, a button is added in the wallet info dialog. The wallet encryption key is fetched in a separate api call, requiring a slightly tweaked override version of the wallet encryption password. --- electrum/bitcoin.py | 15 + electrum/gui/icons/bitbox02.png | Bin 0 -> 1622 bytes electrum/gui/icons/bitbox02_unpaired.png | Bin 0 -> 1629 bytes electrum/gui/qt/main_window.py | 10 +- electrum/gui/qt/util.py | 2 + electrum/plugin.py | 4 + electrum/plugins/bitbox02/__init__.py | 14 + electrum/plugins/bitbox02/bitbox02.py | 620 +++++++++++++++++++++++ electrum/plugins/bitbox02/qt.py | 174 +++++++ electrum/plugins/coldcard/qt.py | 2 +- 10 files changed, 838 insertions(+), 3 deletions(-) create mode 100644 electrum/gui/icons/bitbox02.png create mode 100644 electrum/gui/icons/bitbox02_unpaired.png create mode 100644 electrum/plugins/bitbox02/__init__.py create mode 100644 electrum/plugins/bitbox02/bitbox02.py create mode 100644 electrum/plugins/bitbox02/qt.py diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 16d2fda9c..c1c2d73be 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -432,6 +432,21 @@ def address_to_script(addr: str, *, net=None) -> str: raise BitcoinException(f'unknown address type: {addrtype}') return script +def address_to_hash(addr: str, *, net=None) -> Tuple[int, bytes]: + """Return the pubkey hash / witness program of an address""" + if net is None: net = constants.net + if not is_address(addr, net=net): + raise BitcoinException(f"invalid bitcoin address: {addr}") + witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) + if witprog is not None: + if len(witprog) == 20: + return WIF_SCRIPT_TYPES['p2wpkh'], bytes(witprog) + return WIF_SCRIPT_TYPES['p2wsh'], bytes(witprog) + addrtype, hash_160_ = b58_address_to_hash160(addr) + if addrtype == net.ADDRTYPE_P2PKH: + return WIF_SCRIPT_TYPES['p2pkh'], hash_160_ + return WIF_SCRIPT_TYPES['p2sh'], hash_160_ + def address_to_scripthash(addr: str) -> str: script = address_to_script(addr) return script_to_scripthash(script) diff --git a/electrum/gui/icons/bitbox02.png b/electrum/gui/icons/bitbox02.png new file mode 100644 index 0000000000000000000000000000000000000000..3900c5425ef5059c6ee451759911bb9521838c7c GIT binary patch literal 1622 zcmV-c2C4apP)k=-6$kfA;%NQe4TNaRqW9zr6UfOJSf zf)wIIVEew4T_LY#wKKc3iX_h$2HDo`?9ON3d-LXpmoXzFBO@cDS(0U0aViBWEh;rC z8AC-5T#`zKO4k||VCD=ZF}MtsswF4DEy7BTo03J@mK9X6*5qAu(~^gq232+Da`m&UD5}DKjAu00J;L#VK9@2an0+a>C<0RGcY*X!z#l<}Dzq0G*Ofr>#n*ayI~? zp=hudxF7(X0{1T#i(YYZ`T+upAGk08or2@W32@GggA70e&;T?5jSN5o&;T?5jSN5o z&;T?5jSN5o@QwqVz%_^;rvjj}bV)z9CJdmnGFZ^PQa7goT7T)1eyvg2oKSCrSCsH@ zb*|LSg>h4RsvO`d_6<&hV1YRsB-Reb`@n5Ctw^tMdn{eDh}J}y`jV#=m3xce6=d(o zZ|O2hfJ|-4^VV%g4Y269!vHtz>@WygbOCe&w4BtyEFg?1ZW2b z2mDha*+#g!qK`uYc|28<2^5>Sm5BZx)Z!?n^5O$e1hZo2h#4)76Llg;55~|t2I}pR zhbru9^;1FDC`21Th5Sr8-^2s)Q~Oifde?o|FEalXFcC2jZiRS67QzUk`e2sMAwayG zot?3xqa#+Y*Kfr;0xUeYd(&kvTF=>=k4<*-<@3<@srygZ+QT)r9AEaD)p<>EvHVQ{ z0$4tumySh9H2`}QWYmdrb#=uK4-YxWYPBjn_WbQRyZG?u$ZIw~zTpKRcOLJsh4}^d zA=vxf@1-N}L>y}&H{}hJfp>g-%(B@mTUl9Qi9~|!?d|#IZ2%3c!*$%iZ@%2Hr++>5 zh35wO?A0^Y`L84Tx)OOWj0TlTCEg%3Ow_Xus@Lmr3tC!Q;=l%75Ego$d*6K@xIKV){`L>iSG!sP!c7~E2G6$;ll%lrf<*xaEKEg!yuZKSw^lFcu$<@A zVT9Go&P$OiBO^)1RMQxH+FmKp54tJ9!Bt0DS`fk75i;Bj|NWm+E%mQ1uCi_FR9xB% z-3!Z%5C99Q1}sg+?Se>DZ}Z=#w)&CwY(E|j^^vAAh*>8mC(14ww9|4cOzayD2cLOJ zaHTctkpKiRSe&jYqcDxusWx>e?gU#aK*ToL-f>o>HI3Qb-Q~S;kl|SrTKxWj*7^j) zyPj5ns>!qNcE4LDZisNX<-~I6x)nnUu{g<9 zIW!7pe8Rp?dZxWVew|@yv-YP>Kji_q)DKTs>V9fe{aM*HVd=$p8QV literal 0 HcmV?d00001 diff --git a/electrum/gui/icons/bitbox02_unpaired.png b/electrum/gui/icons/bitbox02_unpaired.png new file mode 100644 index 0000000000000000000000000000000000000000..66fe1109208cf6b644f8e29e1efb397a1b7dd1bb GIT binary patch literal 1629 zcmV-j2BP_iP)6cIFui?jixG^s+;ToZ}A`U<$_q^lqa6}kvlxI*}38Yp5% zk&G09LLi4?E{QYWGJ}@&u6Jg3W?930(v!i4@y<8zZ{ECJLM$vSEG#Up6B><1H<|y3?O#_T58RdY82BfRX|&_H_=QN1I$zv zE7#snN}8!Ewy%|+R58;^rP4S)K5lxyR;xAY^}6sdarw!VLGl*q3$!705@%;;WOsL$ z?Ck82lamuNIXOvsdwVba&Dz=;IXE~V0|NtOaBz@xb#;kc#u1fOD&MA3sj7*2-ZHp2 zb76k~vOpTh1BoI;m&;{G`uqDGGQC85(9IU{S1JqbMqnWJ_xBwVs#SHR*xTE4q^GBc z3=IuAgkA+=B@hz^afgS8+Q!6Kg+hUBZEZOug3OMNj@aguj#KL}QJv&@-2q5kUtf<^ z1JlI95@-pu1X=5ysle)A9(k_lJ>8~49K1ryx!76Z4Skc+hkd9)eID2K?QTCL`sy$Bq60+)p$?B3GhvsOvd+pRgFWx;l#v*Y~TQyOKc06OS}e#MaQwRF{iOoCfaASS(3}; zqIPS6NbFeVsWv|Jekj$U%2r~-{T@#bRDg-8_CDnfyFSz1iva z@d%I7#tFnkKpZqq)J%D@%e9Jko~i~CXxj1jUppo%mE*nhKIzQQIjdIN|C#)?w5VnL+aKKX)53gY4HVJ1ILcl1dcAqUwu|~oVkL~# zL}1ov9*C3zq>1=99l50!ulCB?#)mtAc!J4?MhG-C9x`P z3R_v4S;Dq^GFJ99~V8CL$kRp&~-UV`g z?6)ei*`J=CCgbDdLDgTaz`exHsmDj!#t#^)AngMgi3QdgF-t;jsz#y<;X4lGfhK$; z#zfF^r&TaHNR*w3GiMrkkq3Is7hFWXW1ThYpgrOnBGBF;vgegWt11s7=bztw8EJe} zWqdTysSRqq<)7_TW{F^fk+Z&~{PYXIj~-I;=B3M7ahbPYE~#3|@|yGMv&W%6G2QdFqjdpQ=_)xds2&+7(8s(% zWKPDsaY83i<-w$usXQU)EqB8~L_#C~rLVlmiZlhpb&T9`s?jaRzbkrXY>LQM4;WKL zQ}-!r{M^>~9k|}%#B6~)kGNAs%LYmI9V?oih?FHs1xR3Ih-!jVi!K|#qNsHXZL={K zcz$4{CUMrwy&|Y_Lf@52D3QF6d6q@JN7Qkz8VG9Lu0T^|Qj%@+wXm?Tu&}VOu()~n bUw{Ds?LzhZYyirM00000NkvXXu0mjf)YtAc literal 0 HcmV?d00001 diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 51fee20b7..3125e20c3 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2273,7 +2273,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def show_mpk(index): mpk_text.setText(mpk_list[index]) mpk_text.repaint() # macOS hack for #4777 - + + # declare this value such that the hooks can later figure out what to do + labels_clayout = None # only show the combobox in case multiple accounts are available if len(mpk_list) > 1: # only show the combobox if multiple master keys are defined @@ -2288,6 +2290,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): on_click = lambda clayout: show_mpk(clayout.selected_index()) labels_clayout = ChoicesLayout(_("Master Public Keys"), labels, on_click) vbox.addLayout(labels_clayout.layout()) + labels_clayout.selected_index() else: vbox.addWidget(QLabel(_("Master Public Key"))) @@ -2295,7 +2298,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): vbox.addWidget(mpk_text) vbox.addStretch(1) - btns = run_hook('wallet_info_buttons', self, dialog) or Buttons(CloseButton(dialog)) + btn_export_info = run_hook('wallet_info_buttons', self, dialog) + btn_show_xpub = run_hook('show_xpub_button', self, dialog, labels_clayout) + btn_close = CloseButton(dialog) + btns = Buttons(btn_export_info, btn_show_xpub, btn_close) vbox.addLayout(btns) dialog.setLayout(vbox) dialog.exec_() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 96a7ed08f..1449d344a 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -161,6 +161,8 @@ class Buttons(QHBoxLayout): QHBoxLayout.__init__(self) self.addStretch(1) for b in buttons: + if b is None: + continue self.addWidget(b) class CloseButton(QPushButton): diff --git a/electrum/plugin.py b/electrum/plugin.py index d6136506f..084063cd9 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -650,6 +650,10 @@ class DeviceMgr(ThreadJob): if len(id_) == 0: id_ = str(d['path']) id_ += str(interface_number) + str(usage_page) + # The BitBox02's product_id is not unique per device, thus use the path instead to + # distinguish devices. + if d["product_id"] == 0x2403: + id_ = d['path'] devices.append(Device(path=d['path'], interface_number=interface_number, id_=id_, diff --git a/electrum/plugins/bitbox02/__init__.py b/electrum/plugins/bitbox02/__init__.py new file mode 100644 index 000000000..86812d564 --- /dev/null +++ b/electrum/plugins/bitbox02/__init__.py @@ -0,0 +1,14 @@ +from electrum.i18n import _ + +fullname = "BitBox02" +description = ( + "Provides support for the BitBox02 hardware wallet" +) +requires = [ + ( + "bitbox02", + "https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02", + ) +] +registers_keystore = ("hardware", "bitbox02", _("BitBox02")) +available_for = ["qt"] diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py new file mode 100644 index 000000000..c7098d631 --- /dev/null +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -0,0 +1,620 @@ +# +# BitBox02 Electrum plugin code. +# + +import hid +import hashlib +from typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any + +from electrum import bip32, constants +from electrum.i18n import _ +from electrum.keystore import Hardware_KeyStore, Xpub +from electrum.transaction import PartialTransaction +from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wallet +from electrum.util import bh2u, UserFacingException +from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard +from electrum.logging import get_logger +from electrum.crypto import hmac_oneshot +from electrum.plugin import Device, DeviceInfo +from electrum.simple_config import SimpleConfig +from electrum.json_db import StoredDict +from electrum.storage import get_derivation_used_for_hw_device_encryption + +import electrum.bitcoin as bitcoin +import electrum.ecc as ecc + +from ..hw_wallet import HW_PluginBase, HardwareClientBase +from ..hw_wallet.plugin import LibraryFoundButUnusable + + +try: + from bitbox02 import bitbox02 + from bitbox02 import util + from bitbox02.communication import ( + devices, + HARDENED, + u2fhid, + bitbox_api_protocol, + ) + requirements_ok = True +except ImportError: + requirements_ok = False + + +_logger = get_logger(__name__) + + +class BitBox02Client(HardwareClientBase): + # handler is a BitBox02_Handler, importing it would lead to a circular dependency + def __init__(self, handler: Any, device: Device, config: SimpleConfig): + self.bitbox02_device = None + self.handler = handler + self.device_descriptor = device + self.config = config + self.bitbox_hid_info = None + if self.config.get("bitbox02") is None: + bitbox02_config: dict = { + "remote_static_noise_keys": [], + "noise_privkey": None, + } + self.config.set_key("bitbox02", bitbox02_config) + + bitboxes = devices.get_any_bitbox02s() + for bitbox in bitboxes: + if ( + bitbox["path"] == self.device_descriptor.path + and bitbox["interface_number"] + == self.device_descriptor.interface_number + ): + self.bitbox_hid_info = bitbox + if self.bitbox_hid_info is None: + raise Exception("No BitBox02 detected") + + def label(self) -> str: + return "BitBox02" + + def is_initialized(self) -> bool: + return True + + def close(self): + try: + self.bitbox02_device.close() + except: + pass + + def has_usable_connection_with_device(self) -> bool: + if self.bitbox_hid_info is None: + return False + return True + + def pairing_dialog(self, wizard: bool = True): + def pairing_step(code): + msg = "Please compare and confirm the pairing code on your BitBox02:\n" + choice = [code] + if wizard == True: + return self.handler.win.query_choice(msg, choice) + self.handler.pairing_code_dialog(code) + + def exists_remote_static_pubkey(pubkey: bytes) -> bool: + bitbox02_config = self.config.get("bitbox02") + noise_keys = bitbox02_config.get("remote_static_noise_keys") + if noise_keys is not None: + if pubkey.hex() in [noise_key for noise_key in noise_keys]: + return True + return False + + def set_remote_static_pubkey(pubkey: bytes) -> None: + if not exists_remote_static_pubkey(pubkey): + bitbox02_config = self.config.get("bitbox02") + if bitbox02_config.get("remote_static_noise_keys") is not None: + bitbox02_config["remote_static_noise_keys"].append(pubkey.hex()) + else: + bitbox02_config["remote_static_noise_keys"] = [pubkey.hex()] + self.config.set_key("bitbox02", bitbox02_config) + + def get_noise_privkey() -> Optional[bytes]: + bitbox02_config = self.config.get("bitbox02") + privkey = bitbox02_config.get("noise_privkey") + if privkey is not None: + return bytes.fromhex(privkey) + return None + + def set_noise_privkey(privkey: bytes) -> None: + bitbox02_config = self.config.get("bitbox02") + bitbox02_config["noise_privkey"] = privkey.hex() + self.config.set_key("bitbox02", bitbox02_config) + + def attestation_warning() -> None: + self.handler.attestation_failed_warning( + "The BitBox02 attestation failed.\nTry reconnecting the BitBox02.\nWarning: The device might not be genuine, if the\n problem persists please contact Shift support." + ) + + class NoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig): + """NoiseConfig extends BitBoxNoiseConfig""" + + def show_pairing(self, code: str) -> bool: + choice = [code] + try: + reply = pairing_step(code) + except: + # Close the hid device on exception + hid_device.close() + raise + return True + + def attestation_check(self, result: bool) -> None: + if not result: + attestation_warning() + + def contains_device_static_pubkey(self, pubkey: bytes) -> bool: + return exists_remote_static_pubkey(pubkey) + + def add_device_static_pubkey(self, pubkey: bytes) -> None: + return set_remote_static_pubkey(pubkey) + + def get_app_static_privkey(self) -> Optional[bytes]: + return get_noise_privkey() + + def set_app_static_privkey(self, privkey: bytes) -> None: + return set_noise_privkey(privkey) + + if self.bitbox02_device is None: + hid_device = hid.device() + hid_device.open_path(self.bitbox_hid_info["path"]) + + self.bitbox02_device = bitbox02.BitBox02( + transport=u2fhid.U2FHid(hid_device), + device_info=self.bitbox_hid_info, + noise_config=NoiseConfig(), + ) + + if not self.bitbox02_device.device_info()["initialized"]: + raise Exception( + "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum" + ) + + def check_device_firmware_version(self) -> bool: + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + return self.bitbox02_device.check_firmware_version() + + def coin_network_from_electrum_network(self) -> int: + if constants.net.TESTNET: + return bitbox02.btc.TBTC + return bitbox02.btc.BTC + + def get_password_for_storage_encryption(self) -> str: + derivation = get_derivation_used_for_hw_device_encryption() + derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation) + xpub = self.bitbox02_device.electrum_encryption_key(derivation_list) + node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(()) + return node.eckey.get_public_key_bytes(compressed=True).hex() + + def get_xpub(self, bip32_path: str, xtype: str, display: bool = False) -> str: + if self.bitbox02_device is None: + self.pairing_dialog(wizard=False) + + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + + if not self.bitbox02_device.device_info()["initialized"]: + raise UserFacingException( + "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum" + ) + + xpub_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + coin_network = self.coin_network_from_electrum_network() + + if xtype == "p2wpkh": + if coin_network == bitbox02.btc.BTC: + out_type = bitbox02.btc.BTCPubRequest.ZPUB + else: + out_type = bitbox02.btc.BTCPubRequest.VPUB + elif xtype == "p2wpkh-p2sh": + if coin_network == bitbox02.btc.BTC: + out_type = bitbox02.btc.BTCPubRequest.YPUB + else: + out_type = bitbox02.btc.BTCPubRequest.UPUB + elif xtype == "p2wsh": + if coin_network == bitbox02.btc.BTC: + out_type = bitbox02.btc.BTCPubRequest.CAPITAL_ZPUB + else: + out_type = bitbox02.btc.BTCPubRequest.CAPITAL_VPUB + # The other legacy types are not supported + else: + raise Exception("invalid xtype:{}".format(xtype)) + + return self.bitbox02_device.btc_xpub( + keypath=xpub_keypath, + xpub_type=out_type, + coin=coin_network, + display=display, + ) + + def request_root_fingerprint_from_device(self) -> str: + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + + return self.bitbox02_device.root_fingerprint().hex() + + def is_pairable(self) -> bool: + if self.bitbox_hid_info is None: + return False + return True + + def btc_multisig_config( + self, coin, bip32_path: List[int], wallet: Multisig_Wallet + ): + """ + Set and get a multisig config with the current device and some other arbitrary xpubs. + Registers it on the device if not already registered. + """ + + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + + account_keypath = bip32_path[:4] + xpubs = wallet.get_master_public_keys() + our_xpub = self.get_xpub( + bip32.convert_bip32_intpath_to_strpath(account_keypath), "p2wsh" + ) + + 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), + ) + ) + + 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 show_address( + self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet + ) -> str: + + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + + address_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + coin_network = self.coin_network_from_electrum_network() + + if address_type == "p2wpkh": + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ) + elif address_type == "p2wpkh-p2sh": + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ) + elif address_type == "p2wsh": + if type(wallet) is Multisig_Wallet: + script_config = self.btc_multisig_config( + coin_network, address_keypath, wallet + ) + else: + raise Exception("Can only use p2wsh with multisig wallets") + else: + raise Exception( + "invalid address xtype: {} is not supported by the BitBox02".format( + address_type + ) + ) + + return self.bitbox02_device.btc_address( + keypath=address_keypath, + coin=coin_network, + script_config=script_config, + display=True, + ) + + def sign_transaction( + self, + keystore: Hardware_KeyStore, + tx: PartialTransaction, + wallet: Deterministic_Wallet, + ): + if tx.is_complete(): + return + + if self.bitbox02_device is None: + raise Exception( + "Need to setup communication first before attempting any BitBox02 calls" + ) + + coin = bitbox02.btc.BTC + if constants.net.TESTNET: + coin = bitbox02.btc.TBTC + + tx_script_type = None + + # Build BTCInputType list + inputs = [] + for txin in tx.inputs(): + _, full_path = keystore.find_my_pubkey_in_txinout(txin) + + if full_path is None: + raise Exception( + "A wallet owned pubkey was not found in the transaction input to be signed" + ) + + inputs.append( + { + "prev_out_hash": txin.prevout.txid[::-1], + "prev_out_index": txin.prevout.out_idx, + "prev_out_value": txin.value_sats(), + "sequence": txin.nsequence, + "keypath": full_path, + } + ) + + if tx_script_type == None: + tx_script_type = txin.script_type + elif tx_script_type != txin.script_type: + raise Exception("Cannot mix different input script types") + + if tx_script_type == "p2wpkh": + tx_script_type = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ) + elif tx_script_type == "p2wpkh-p2sh": + tx_script_type = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ) + elif tx_script_type == "p2wsh": + if type(wallet) is Multisig_Wallet: + tx_script_type = self.btc_multisig_config(coin, full_path, wallet) + else: + raise Exception("Can only use p2wsh with multisig wallets") + else: + raise UserFacingException( + "invalid input script type: {} is not supported by the BitBox02".format( + tx_script_type + ) + ) + + # Build BTCOutputType list + outputs = [] + for txout in tx.outputs(): + assert txout.address + # check for change + if txout.is_change: + _, change_pubkey_path = keystore.find_my_pubkey_in_txinout(txout) + outputs.append( + bitbox02.BTCOutputInternal( + keypath=change_pubkey_path, value=txout.value, + ) + ) + else: + addrtype, pubkey_hash = bitcoin.address_to_hash(txout.address) + if addrtype == bitcoin.WIF_SCRIPT_TYPES["p2pkh"]: + output_type = bitbox02.btc.P2PKH + elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2sh"]: + output_type = bitbox02.btc.P2SH + elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2wpkh"]: + output_type = bitbox02.btc.P2WPKH + elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2wsh"]: + output_type = bitbox02.btc.P2WSH + else: + raise UserFacingException( + "Received unsupported output type during transaction signing: {} is not supported by the BitBox02".format( + addrtype + ) + ) + outputs.append( + bitbox02.BTCOutputExternal( + output_type=output_type, + output_hash=pubkey_hash, + value=txout.value, + ) + ) + + if type(wallet) is Standard_Wallet: + keypath_account = full_path[:3] + elif type(wallet) is Multisig_Wallet: + keypath_account = full_path[:4] + else: + raise Exception( + "BitBox02 does not support this wallet type: {}".format(type(wallet)) + ) + + sigs = self.bitbox02_device.btc_sign( + coin, + tx_script_type, + keypath_account=keypath_account, + inputs=inputs, + outputs=outputs, + locktime=tx.locktime, + version=tx.version, + ) + + # Fill signatures + if len(sigs) != len(tx.inputs()): + raise Exception("Incorrect number of inputs signed.") # Should never occur + signatures = [bh2u(ecc.der_sig_from_sig_string(x[1])) + "01" for x in sigs] + tx.update_signatures(signatures) + + +class BitBox02_KeyStore(Hardware_KeyStore): + hw_type = "bitbox02" + device = "BitBox02" + plugin: "BitBox02Plugin" + + def __init__(self, d: StoredDict): + super().__init__(d) + self.force_watching_only = False + self.ux_busy = False + + def get_client(self): + return self.plugin.get_client(self) + + 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 decrypt_message(self, pubkey, message, password): + raise UserFacingException( + _( + "Message encryption, decryption and signing are currently not supported for {}" + ).format(self.device) + ) + + def sign_message(self, sequence, message, password): + raise UserFacingException( + _( + "Message encryption, decryption and signing are currently not supported for {}" + ).format(self.device) + ) + + def sign_transaction(self, tx: PartialTransaction, password: str): + if tx.is_complete(): + return + client = self.get_client() + + try: + try: + self.handler.show_message("Authorize Transaction...") + client.sign_transaction(self, tx, self.handler.win.wallet) + + finally: + self.handler.finished() + + except Exception as e: + self.logger.exception("") + self.give_error(e, True) + return + + def show_address( + self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet + ): + client = self.get_client() + address_path = "{}/{}/{}".format( + self.get_derivation_prefix(), sequence[0], sequence[1] + ) + try: + try: + self.handler.show_message(_("Showing address ...")) + dev_addr = client.show_address(address_path, txin_type, wallet) + finally: + self.handler.finished() + except Exception as e: + self.logger.exception("") + self.handler.show_error(e) + +class BitBox02Plugin(HW_PluginBase): + keystore_class = BitBox02_KeyStore + + DEVICE_IDS = [(0x03EB, 0x2403)] + + SUPPORTED_XTYPES = ("p2wpkh-p2sh", "p2wpkh", "p2wsh") + + def __init__(self, parent: HW_PluginBase, config: SimpleConfig, name: str): + super().__init__(parent, config, name) + + self.libraries_available = self.check_libraries_available() + if not self.libraries_available: + return + self.device_manager().register_devices(self.DEVICE_IDS) + + def get_library_version(self): + try: + from bitbox02 import bitbox02 + version = bitbox02.__version__ + except: + version = "unknown" + if requirements_ok: + return version + else: + raise ImportError() + + + # handler is a BitBox02_Handler + def create_client(self, device: Device, handler: Any) -> BitBox02Client: + if not handler: + self.handler = handler + return BitBox02Client(handler, device, self.config) + + def setup_device( + self, device_info: DeviceInfo, wizard: BaseWizard, purpose: int + ): + 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) + if client.bitbox02_device is None: + client.pairing_dialog() + + def get_xpub( + self, device_id: bytes, derivation: str, xtype: str, wizard: BaseWizard + ): + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported( + _("This type of script is not supported with {}.").format(self.device) + ) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + if client.bitbox02_device is None: + client.handler = self.create_handler(wizard) + client.pairing_dialog() + return client.get_xpub(derivation, xtype) + + def get_client(self, keystore: BitBox02_KeyStore, force_pair: bool = True): + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + + return client + + def show_address( + self, + wallet: Deterministic_Wallet, + address: str, + keystore: BitBox02_KeyStore = None, + ): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + + txin_type = wallet.get_txin_type(address) + sequence = wallet.get_address_index(address) + keystore.show_address(sequence, txin_type, wallet) diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py new file mode 100644 index 000000000..74535b799 --- /dev/null +++ b/electrum/plugins/bitbox02/qt.py @@ -0,0 +1,174 @@ +import time, os +from functools import partial +import copy + +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtWidgets import ( + QPushButton, + QLabel, + QVBoxLayout, + QWidget, + QGridLayout, + QLineEdit, + QHBoxLayout, +) + +from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSignal, pyqtSlot + +from electrum.gui.qt.util import ( + WindowModalDialog, + Buttons, + OkButton, + CancelButton, + get_parent_main_window, +) +from electrum.gui.qt.transaction_dialog import TxDialog + +from electrum.i18n import _ +from electrum.plugin import hook +from electrum.wallet import Multisig_Wallet +from electrum.transaction import PartialTransaction +from electrum import keystore + +from .bitbox02 import BitBox02Plugin +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase +from ..hw_wallet.plugin import only_hook_if_libraries_available, LibraryFoundButUnusable + + +class Plugin(BitBox02Plugin, QtPluginBase): + icon_unpaired = "bitbox02_unpaired.png" + icon_paired = "bitbox02.png" + + def create_handler(self, window): + return BitBox02_Handler(window) + + @only_hook_if_libraries_available + @hook + def receive_menu(self, menu, addrs, wallet): + # Context menu on each address in the Addresses Tab, right click... + if len(addrs) != 1: + return + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class: + + def show_address(keystore=keystore): + keystore.thread.add( + partial(self.show_address, wallet, addrs[0], keystore=keystore) + ) + + device_name = "{} ({})".format(self.device, keystore.label) + menu.addAction(_("Show on {}").format(device_name), show_address) + + @only_hook_if_libraries_available + @hook + def show_xpub_button(self, main_window, dialog, labels_clayout): + # user is about to see the "Wallet Information" dialog + # - add a button to show the xpub on the BitBox02 device + wallet = main_window.wallet + if not any(type(ks) == self.keystore_class for ks in wallet.get_keystores()): + # doesn't involve a BitBox02 wallet, hide feature + return + + btn = QPushButton(_("Show on BitBox02")) + + def on_button_click(): + selected_keystore_index = 0 + if labels_clayout is not None: + selected_keystore_index = labels_clayout.selected_index() + keystores = wallet.get_keystores() + selected_keystore = keystores[selected_keystore_index] + derivation = selected_keystore.get_derivation_prefix() + if type(selected_keystore) != self.keystore_class: + main_window.show_error("Select a BitBox02 xpub") + selected_keystore.get_client().get_xpub( + derivation, keystore.xtype_from_derivation(derivation), True + ) + + btn.clicked.connect(lambda unused: on_button_click()) + return btn + + +class BitBox02_Handler(QtHandlerBase): + setup_signal = pyqtSignal() + + def __init__(self, win): + super(BitBox02_Handler, self).__init__(win, "BitBox02") + self.setup_signal.connect(self.setup_dialog) + + def message_dialog(self, msg): + self.clear_dialog() + self.dialog = dialog = WindowModalDialog( + self.top_level_window(), _("BitBox02 Status") + ) + l = QLabel(msg) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + dialog.show() + + def attestation_failed_warning(self, msg): + self.clear_dialog() + self.dialog = dialog = WindowModalDialog(None, "BitBox02 Attestation Failed") + l = QLabel(msg) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + okButton = OkButton(dialog) + vbox.addWidget(okButton) + dialog.setLayout(vbox) + dialog.exec_() + return + + def pairing_code_dialog(self, code): + self.clear_dialog() + self.dialog = dialog = WindowModalDialog(None, "BitBox02 Pairing Code") + l = QLabel(code) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() + return + + def get_setup(self): + self.done.clear() + self.setup_signal.emit() + self.done.wait() + return + + def name_multisig_account(self): + return QMetaObject.invokeMethod( + self, + "_name_multisig_account", + Qt.BlockingQueuedConnection, + Q_RETURN_ARG(str), + ) + + @pyqtSlot(result=str) + def _name_multisig_account(self): + dialog = WindowModalDialog(None, "Create Multisig Account") + vbox = QVBoxLayout() + label = QLabel( + _( + "Enter a descriptive name for your multisig account.\nYou should later be able to use the name to uniquely identify this multisig account" + ) + ) + hl = QHBoxLayout() + hl.addWidget(label) + name = QLineEdit() + name.setMaxLength(30) + name.resize(200, 40) + he = QHBoxLayout() + he.addWidget(name) + okButton = OkButton(dialog) + hlb = QHBoxLayout() + hlb.addWidget(okButton) + hlb.addStretch(2) + vbox.addLayout(hl) + vbox.addLayout(he) + vbox.addLayout(hlb) + dialog.setLayout(vbox) + dialog.exec_() + return name.text().strip() + + def setup_dialog(self): + self.show_error(_("Please initialize your BitBox02 while connected.")) + return diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py index 358fa59b8..4c590c859 100644 --- a/electrum/plugins/coldcard/qt.py +++ b/electrum/plugins/coldcard/qt.py @@ -57,7 +57,7 @@ class Plugin(ColdcardPlugin, QtPluginBase): btn = QPushButton(_("Export for Coldcard")) btn.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet)) - return Buttons(btn, CloseButton(dialog)) + return btn def export_multisig_setup(self, main_window, wallet): From c0c3627bd2e9d60bc3cd971ac87c18f8077d9b9d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 13:54:17 +0200 Subject: [PATCH 02/12] bitbox02: adapt to updated master --- electrum/plugins/bitbox02/bitbox02.py | 21 +++++---------------- electrum/plugins/hw_wallet/plugin.py | 2 +- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index c7098d631..b56075aa2 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -569,39 +569,28 @@ class BitBox02Plugin(HW_PluginBase): def setup_device( self, device_info: DeviceInfo, wizard: BaseWizard, purpose: int ): - 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 = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) if client.bitbox02_device is None: client.pairing_dialog() + return client def get_xpub( - self, device_id: bytes, derivation: str, xtype: str, wizard: BaseWizard + self, device_id: str, derivation: str, xtype: str, wizard: BaseWizard ): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported( _("This type of script is not supported with {}.").format(self.device) ) - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) + client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) if client.bitbox02_device is None: - client.handler = self.create_handler(wizard) client.pairing_dialog() return client.get_xpub(derivation, xtype) def get_client(self, keystore: BitBox02_KeyStore, force_pair: bool = True): devmgr = self.device_manager() handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) return client def show_address( diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 68335f746..884732dad 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -165,7 +165,7 @@ class HW_PluginBase(BasePlugin): handler: Optional['HardwareHandlerBase']) -> Optional['HardwareClientBase']: raise NotImplementedError() - def get_xpub(self, device_id, derivation: str, xtype, wizard: 'BaseWizard') -> str: + def get_xpub(self, device_id: str, derivation: str, xtype, wizard: 'BaseWizard') -> str: raise NotImplementedError() def create_handler(self, window) -> 'HardwareHandlerBase': From 5f5a1e96abb7a75d6fb2b8eecb151ddfec0744e0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 16:05:33 +0200 Subject: [PATCH 03/12] bitbox02: add udev rules --- contrib/udev/53-hid-bitbox02.rules | 1 + contrib/udev/54-hid-bitbox02.rules | 1 + contrib/udev/README.md | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 contrib/udev/53-hid-bitbox02.rules create mode 100644 contrib/udev/54-hid-bitbox02.rules diff --git a/contrib/udev/53-hid-bitbox02.rules b/contrib/udev/53-hid-bitbox02.rules new file mode 100644 index 000000000..2daffc03b --- /dev/null +++ b/contrib/udev/53-hid-bitbox02.rules @@ -0,0 +1 @@ +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403" diff --git a/contrib/udev/54-hid-bitbox02.rules b/contrib/udev/54-hid-bitbox02.rules new file mode 100644 index 000000000..1b74e4774 --- /dev/null +++ b/contrib/udev/54-hid-bitbox02.rules @@ -0,0 +1 @@ +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n" diff --git a/contrib/udev/README.md b/contrib/udev/README.md index 6ff403a4f..451ef2b2f 100644 --- a/contrib/udev/README.md +++ b/contrib/udev/README.md @@ -6,7 +6,8 @@ These are necessary for the devices to be usable on Linux environments. - `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules - `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules - - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux + - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh + - `53-hid-bitbox02.rules`, `54-hid-bitbox02.rules` (BitBox02): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules - `51-safe-t.rules` (Archos): https://github.com/archos-safe-t/safe-t-common/blob/master/udev/51-safe-t.rules From 15102855c15eae90ca158935dc9e052b24bf88c9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 16:13:13 +0200 Subject: [PATCH 04/12] bitbox02: fix pairing_dialog --- electrum/plugins/bitbox02/bitbox02.py | 56 ++++++++++++++------------- electrum/plugins/bitbox02/qt.py | 11 ------ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index b56075aa2..bcc3ebac7 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -4,7 +4,7 @@ import hid import hashlib -from typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any +from typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any, Callable from electrum import bip32, constants from electrum.i18n import _ @@ -84,16 +84,22 @@ class BitBox02Client(HardwareClientBase): def has_usable_connection_with_device(self) -> bool: if self.bitbox_hid_info is None: - return False + return False return True def pairing_dialog(self, wizard: bool = True): - def pairing_step(code): - msg = "Please compare and confirm the pairing code on your BitBox02:\n" - choice = [code] - if wizard == True: - return self.handler.win.query_choice(msg, choice) - self.handler.pairing_code_dialog(code) + def pairing_step(code: str, device_response: Callable[[], bool]) -> bool: + msg = "Please compare and confirm the pairing code on your BitBox02:\n" + code + self.handler.show_message(msg) + try: + res = device_response() + except: + # Close the hid device on exception + hid_device.close() + raise + finally: + self.handler.finished() + return res def exists_remote_static_pubkey(pubkey: bytes) -> bool: bitbox02_config = self.config.get("bitbox02") @@ -132,15 +138,8 @@ class BitBox02Client(HardwareClientBase): class NoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig): """NoiseConfig extends BitBoxNoiseConfig""" - def show_pairing(self, code: str) -> bool: - choice = [code] - try: - reply = pairing_step(code) - except: - # Close the hid device on exception - hid_device.close() - raise - return True + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + return pairing_step(code, device_response) def attestation_check(self, result: bool) -> None: if not result: @@ -168,6 +167,10 @@ class BitBox02Client(HardwareClientBase): noise_config=NoiseConfig(), ) + self.fail_if_not_initialized() + + def fail_if_not_initialized(self) -> None: + assert self.bitbox02_device if not self.bitbox02_device.device_info()["initialized"]: raise Exception( "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum" @@ -201,10 +204,7 @@ class BitBox02Client(HardwareClientBase): "Need to setup communication first before attempting any BitBox02 calls" ) - if not self.bitbox02_device.device_info()["initialized"]: - raise UserFacingException( - "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum" - ) + self.fail_if_not_initialized() xpub_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) coin_network = self.coin_network_from_electrum_network() @@ -502,11 +502,12 @@ class BitBox02_KeyStore(Hardware_KeyStore): if tx.is_complete(): return client = self.get_client() + assert isinstance(client, BitBox02Client) try: try: self.handler.show_message("Authorize Transaction...") - client.sign_transaction(self, tx, self.handler.win.wallet) + client.sign_transaction(self, tx, self.handler.get_wallet()) finally: self.handler.finished() @@ -535,7 +536,7 @@ class BitBox02_KeyStore(Hardware_KeyStore): class BitBox02Plugin(HW_PluginBase): keystore_class = BitBox02_KeyStore - + minimum_library = (2, 0, 2) DEVICE_IDS = [(0x03EB, 0x2403)] SUPPORTED_XTYPES = ("p2wpkh-p2sh", "p2wpkh", "p2wsh") @@ -571,8 +572,11 @@ class BitBox02Plugin(HW_PluginBase): ): device_id = device_info.device.id_ client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) + assert isinstance(client, BitBox02Client) if client.bitbox02_device is None: - client.pairing_dialog() + wizard.run_task_without_blocking_gui( + task=lambda client=client: client.pairing_dialog()) + client.fail_if_not_initialized() return client def get_xpub( @@ -583,8 +587,8 @@ class BitBox02Plugin(HW_PluginBase): _("This type of script is not supported with {}.").format(self.device) ) client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard) - if client.bitbox02_device is None: - client.pairing_dialog() + assert isinstance(client, BitBox02Client) + assert client.bitbox02_device is not None return client.get_xpub(derivation, xtype) def get_client(self, keystore: BitBox02_KeyStore, force_pair: bool = True): diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index 74535b799..4404ffcdd 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -117,17 +117,6 @@ class BitBox02_Handler(QtHandlerBase): dialog.exec_() return - def pairing_code_dialog(self, code): - self.clear_dialog() - self.dialog = dialog = WindowModalDialog(None, "BitBox02 Pairing Code") - l = QLabel(code) - vbox = QVBoxLayout(dialog) - vbox.addWidget(l) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) - dialog.setLayout(vbox) - dialog.exec_() - return - def get_setup(self): self.done.clear() self.setup_signal.emit() From 0268b63fcb67b9b08e98f109031a81925fc0d901 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 16:21:09 +0200 Subject: [PATCH 05/12] bitbox02: rm some dead code --- electrum/plugins/bitbox02/bitbox02.py | 5 +++-- electrum/plugins/bitbox02/qt.py | 24 ------------------------ electrum/plugins/coldcard/cmdline.py | 6 ------ electrum/plugins/coldcard/qt.py | 16 +--------------- 4 files changed, 4 insertions(+), 47 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index bcc3ebac7..087546dc4 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -131,8 +131,9 @@ class BitBox02Client(HardwareClientBase): self.config.set_key("bitbox02", bitbox02_config) def attestation_warning() -> None: - self.handler.attestation_failed_warning( - "The BitBox02 attestation failed.\nTry reconnecting the BitBox02.\nWarning: The device might not be genuine, if the\n problem persists please contact Shift support." + self.handler.show_error( + "The BitBox02 attestation failed.\nTry reconnecting the BitBox02.\nWarning: The device might not be genuine, if the\n problem persists please contact Shift support.", + blocking=True ) class NoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig): diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index 4404ffcdd..2a63b681f 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -89,11 +89,9 @@ class Plugin(BitBox02Plugin, QtPluginBase): class BitBox02_Handler(QtHandlerBase): - setup_signal = pyqtSignal() def __init__(self, win): super(BitBox02_Handler, self).__init__(win, "BitBox02") - self.setup_signal.connect(self.setup_dialog) def message_dialog(self, msg): self.clear_dialog() @@ -105,24 +103,6 @@ class BitBox02_Handler(QtHandlerBase): vbox.addWidget(l) dialog.show() - def attestation_failed_warning(self, msg): - self.clear_dialog() - self.dialog = dialog = WindowModalDialog(None, "BitBox02 Attestation Failed") - l = QLabel(msg) - vbox = QVBoxLayout(dialog) - vbox.addWidget(l) - okButton = OkButton(dialog) - vbox.addWidget(okButton) - dialog.setLayout(vbox) - dialog.exec_() - return - - def get_setup(self): - self.done.clear() - self.setup_signal.emit() - self.done.wait() - return - def name_multisig_account(self): return QMetaObject.invokeMethod( self, @@ -157,7 +137,3 @@ class BitBox02_Handler(QtHandlerBase): dialog.setLayout(vbox) dialog.exec_() return name.text().strip() - - def setup_dialog(self): - self.show_error(_("Please initialize your BitBox02 while connected.")) - return diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py index ab86f463c..4f246ec77 100644 --- a/electrum/plugins/coldcard/cmdline.py +++ b/electrum/plugins/coldcard/cmdline.py @@ -28,12 +28,6 @@ class ColdcardCmdLineHandler(CmdLineHandler): def stop(self): pass - def show_message(self, msg, on_cancel=None): - print_stderr(msg) - - def show_error(self, msg, blocking=False): - print_stderr(msg) - def update_status(self, b): _logger.info(f'hw device status {b}') diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py index 4c590c859..a9b8ce51a 100644 --- a/electrum/plugins/coldcard/qt.py +++ b/electrum/plugins/coldcard/qt.py @@ -77,15 +77,10 @@ class Plugin(ColdcardPlugin, QtPluginBase): class Coldcard_Handler(QtHandlerBase): - setup_signal = pyqtSignal() - #auth_signal = pyqtSignal(object) def __init__(self, win): super(Coldcard_Handler, self).__init__(win, 'Coldcard') - self.setup_signal.connect(self.setup_dialog) - #self.auth_signal.connect(self.auth_dialog) - def message_dialog(self, msg): self.clear_dialog() self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status")) @@ -93,16 +88,7 @@ class Coldcard_Handler(QtHandlerBase): vbox = QVBoxLayout(dialog) vbox.addWidget(l) dialog.show() - - def get_setup(self): - self.done.clear() - self.setup_signal.emit() - self.done.wait() - return - - def setup_dialog(self): - self.show_error(_('Please initialize your Coldcard while disconnected.')) - return + class CKCCSettingsDialog(WindowModalDialog): From ffe3cef21acd67a4e98e51b49fd13ba1123c7652 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 16:49:11 +0200 Subject: [PATCH 06/12] bitbox02: don't run show_xpub on GUI thread --- electrum/plugins/bitbox02/bitbox02.py | 9 ++++++++- electrum/plugins/bitbox02/qt.py | 7 +++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 087546dc4..5c3f85d45 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -196,7 +196,7 @@ class BitBox02Client(HardwareClientBase): node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(()) return node.eckey.get_public_key_bytes(compressed=True).hex() - def get_xpub(self, bip32_path: str, xtype: str, display: bool = False) -> str: + def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str: if self.bitbox02_device is None: self.pairing_dialog(wizard=False) @@ -612,3 +612,10 @@ class BitBox02Plugin(HW_PluginBase): txin_type = wallet.get_txin_type(address) sequence = wallet.get_address_index(address) keystore.show_address(sequence, txin_type, wallet) + + def show_xpub(self, keystore: BitBox02_KeyStore): + client = keystore.get_client() + assert isinstance(client, BitBox02Client) + derivation = keystore.get_derivation_prefix() + xtype = keystore.get_bip32_node_for_xpub().xtype + client.get_xpub(derivation, xtype, display=True) diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index 2a63b681f..9b1ccc261 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -28,7 +28,6 @@ from electrum.i18n import _ from electrum.plugin import hook from electrum.wallet import Multisig_Wallet from electrum.transaction import PartialTransaction -from electrum import keystore from .bitbox02 import BitBox02Plugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase @@ -77,11 +76,11 @@ class Plugin(BitBox02Plugin, QtPluginBase): selected_keystore_index = labels_clayout.selected_index() keystores = wallet.get_keystores() selected_keystore = keystores[selected_keystore_index] - derivation = selected_keystore.get_derivation_prefix() if type(selected_keystore) != self.keystore_class: main_window.show_error("Select a BitBox02 xpub") - selected_keystore.get_client().get_xpub( - derivation, keystore.xtype_from_derivation(derivation), True + return + selected_keystore.thread.add( + partial(self.show_xpub, keystore=selected_keystore) ) btn.clicked.connect(lambda unused: on_button_click()) From cc4aa1812dfcbe5ff8a569e0672ac60ceb764a9b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 16:51:38 +0200 Subject: [PATCH 07/12] rm some unused imports --- electrum/plugins/bitbox02/bitbox02.py | 5 +---- electrum/plugins/bitbox02/qt.py | 15 ++------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 5c3f85d45..ca99f816c 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -3,18 +3,16 @@ # import hid -import hashlib from typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any, Callable from electrum import bip32, constants from electrum.i18n import _ -from electrum.keystore import Hardware_KeyStore, Xpub +from electrum.keystore import Hardware_KeyStore from electrum.transaction import PartialTransaction from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wallet from electrum.util import bh2u, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard from electrum.logging import get_logger -from electrum.crypto import hmac_oneshot from electrum.plugin import Device, DeviceInfo from electrum.simple_config import SimpleConfig from electrum.json_db import StoredDict @@ -24,7 +22,6 @@ import electrum.bitcoin as bitcoin import electrum.ecc as ecc from ..hw_wallet import HW_PluginBase, HardwareClientBase -from ..hw_wallet.plugin import LibraryFoundButUnusable try: diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index 9b1ccc261..96165f203 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -1,37 +1,26 @@ -import time, os from functools import partial -import copy -from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import ( QPushButton, QLabel, QVBoxLayout, - QWidget, - QGridLayout, QLineEdit, QHBoxLayout, ) -from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSignal, pyqtSlot +from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot from electrum.gui.qt.util import ( WindowModalDialog, - Buttons, OkButton, - CancelButton, - get_parent_main_window, ) -from electrum.gui.qt.transaction_dialog import TxDialog from electrum.i18n import _ from electrum.plugin import hook -from electrum.wallet import Multisig_Wallet -from electrum.transaction import PartialTransaction from .bitbox02 import BitBox02Plugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from ..hw_wallet.plugin import only_hook_if_libraries_available, LibraryFoundButUnusable +from ..hw_wallet.plugin import only_hook_if_libraries_available class Plugin(BitBox02Plugin, QtPluginBase): From 66c264f6130c28f600a26c5d0dc28f0ee79f5298 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 17:23:22 +0200 Subject: [PATCH 08/12] bitcoin.py: change API of address_to_hash --- electrum/bitcoin.py | 34 +++++++++++++++++++++------ electrum/plugins/bitbox02/bitbox02.py | 9 +++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index c1c2d73be..794eb54cd 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -25,7 +25,8 @@ import hashlib from typing import List, Tuple, TYPE_CHECKING, Optional, Union -from enum import IntEnum +import enum +from enum import IntEnum, Enum from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict from . import version @@ -432,20 +433,39 @@ def address_to_script(addr: str, *, net=None) -> str: raise BitcoinException(f'unknown address type: {addrtype}') return script -def address_to_hash(addr: str, *, net=None) -> Tuple[int, bytes]: - """Return the pubkey hash / witness program of an address""" + +class OnchainOutputType(Enum): + """Opaque types of scriptPubKeys. + In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc. + """ + P2PKH = enum.auto() + P2SH = enum.auto() + WITVER0_P2WPKH = enum.auto() + WITVER0_P2WSH = enum.auto() + + +def address_to_hash(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes]: + """Return (type, pubkey hash / witness program) for an address.""" if net is None: net = constants.net if not is_address(addr, net=net): raise BitcoinException(f"invalid bitcoin address: {addr}") witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) if witprog is not None: + if witver != 0: + raise BitcoinException(f"not implemented handling for witver={witver}") if len(witprog) == 20: - return WIF_SCRIPT_TYPES['p2wpkh'], bytes(witprog) - return WIF_SCRIPT_TYPES['p2wsh'], bytes(witprog) + return OnchainOutputType.WITVER0_P2WPKH, bytes(witprog) + elif len(witprog) == 32: + return OnchainOutputType.WITVER0_P2WSH, bytes(witprog) + else: + raise BitcoinException(f"unexpected length for segwit witver=0 witprog: len={len(witprog)}") addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: - return WIF_SCRIPT_TYPES['p2pkh'], hash_160_ - return WIF_SCRIPT_TYPES['p2sh'], hash_160_ + return OnchainOutputType.P2PKH, hash_160_ + elif addrtype == net.ADDRTYPE_P2SH: + return OnchainOutputType.P2SH, hash_160_ + raise BitcoinException(f"unknown address type: {addrtype}") + def address_to_scripthash(addr: str) -> str: script = address_to_script(addr) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index ca99f816c..9d6143583 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -17,6 +17,7 @@ from electrum.plugin import Device, DeviceInfo from electrum.simple_config import SimpleConfig from electrum.json_db import StoredDict from electrum.storage import get_derivation_used_for_hw_device_encryption +from electrum.bitcoin import OnchainOutputType import electrum.bitcoin as bitcoin import electrum.ecc as ecc @@ -411,13 +412,13 @@ class BitBox02Client(HardwareClientBase): ) else: addrtype, pubkey_hash = bitcoin.address_to_hash(txout.address) - if addrtype == bitcoin.WIF_SCRIPT_TYPES["p2pkh"]: + if addrtype == OnchainOutputType.P2PKH: output_type = bitbox02.btc.P2PKH - elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2sh"]: + elif addrtype == OnchainOutputType.P2SH: output_type = bitbox02.btc.P2SH - elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2wpkh"]: + elif addrtype == OnchainOutputType.WITVER0_P2WPKH: output_type = bitbox02.btc.P2WPKH - elif addrtype == bitcoin.WIF_SCRIPT_TYPES["p2wsh"]: + elif addrtype == OnchainOutputType.WITVER0_P2WSH: output_type = bitbox02.btc.P2WSH else: raise UserFacingException( From e830ef309f4b13d0e02587ad2d043eecd88aca74 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Apr 2020 18:20:21 +0200 Subject: [PATCH 09/12] hww: factor out part of hid scan code to HW_PluginBase so that bitbox02 can override it --- electrum/plugin.py | 31 +++++-------------- electrum/plugins/bitbox02/bitbox02.py | 9 +++++- electrum/plugins/coldcard/coldcard.py | 2 +- .../plugins/digitalbitbox/digitalbitbox.py | 2 +- electrum/plugins/hw_wallet/plugin.py | 16 ++++++++++ electrum/plugins/keepkey/keepkey.py | 2 +- electrum/plugins/ledger/ledger.py | 2 +- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index 084063cd9..d86e76706 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -360,9 +360,8 @@ class DeviceMgr(ThreadJob): # A list of clients. The key is the client, the value is # a (path, id_) pair. Needs self.lock. self.clients = {} # type: Dict[HardwareClientBase, Tuple[Union[str, bytes], str]] - # What we recognise. Each entry is a (vendor_id, product_id) - # pair. - self.recognised_hardware = set() + # What we recognise. (vendor_id, product_id) -> Plugin + self._recognised_hardware = {} # type: Dict[Tuple[int, int], HW_PluginBase] # Custom enumerate functions for devices we don't know about. self._enumerate_func = set() # Needs self.lock. # locks: if you need to take multiple ones, acquire them in the order they are defined here! @@ -390,9 +389,9 @@ class DeviceMgr(ThreadJob): for client in clients: client.timeout(cutoff) - def register_devices(self, device_pairs): + def register_devices(self, device_pairs, *, plugin: 'HW_PluginBase'): for pair in device_pairs: - self.recognised_hardware.add(pair) + self._recognised_hardware[pair] = plugin def register_enumerate_func(self, func): with self.lock: @@ -642,24 +641,10 @@ class DeviceMgr(ThreadJob): devices = [] for d in hid_list: product_key = (d['vendor_id'], d['product_id']) - if product_key in self.recognised_hardware: - # Older versions of hid don't provide interface_number - interface_number = d.get('interface_number', -1) - usage_page = d['usage_page'] - id_ = d['serial_number'] - if len(id_) == 0: - id_ = str(d['path']) - id_ += str(interface_number) + str(usage_page) - # The BitBox02's product_id is not unique per device, thus use the path instead to - # distinguish devices. - if d["product_id"] == 0x2403: - id_ = d['path'] - devices.append(Device(path=d['path'], - interface_number=interface_number, - id_=id_, - product_key=product_key, - usage_page=usage_page, - transport_ui_string='hid')) + if product_key in self._recognised_hardware: + plugin = self._recognised_hardware[product_key] + device = plugin.create_device_from_hid_enumeration(d, product_key=product_key) + devices.append(device) return devices @with_scan_lock diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 9d6143583..6a62bee88 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -546,7 +546,7 @@ class BitBox02Plugin(HW_PluginBase): self.libraries_available = self.check_libraries_available() if not self.libraries_available: return - self.device_manager().register_devices(self.DEVICE_IDS) + self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) def get_library_version(self): try: @@ -617,3 +617,10 @@ class BitBox02Plugin(HW_PluginBase): derivation = keystore.get_derivation_prefix() xtype = keystore.get_bip32_node_for_xpub().xtype client.get_xpub(derivation, xtype, display=True) + + def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device': + device = super().create_device_from_hid_enumeration(d, product_key=product_key) + # The BitBox02's product_id is not unique per device, thus use the path instead to + # distinguish devices. + id_ = str(d['path']) + return device._replace(id_=id_) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 810fd91b3..b8cae820c 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -477,7 +477,7 @@ class ColdcardPlugin(HW_PluginBase): if not self.libraries_available: return - self.device_manager().register_devices(self.DEVICE_IDS) + self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) self.device_manager().register_enumerate_func(self.detect_simulator) def get_library_version(self): diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index b14195578..15b91de7c 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -675,7 +675,7 @@ class DigitalBitboxPlugin(HW_PluginBase): 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) + self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) self.digitalbitbox_config = self.config.get('digitalbitbox', {}) diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 884732dad..c7c0c1eef 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -60,6 +60,22 @@ class HW_PluginBase(BasePlugin): def device_manager(self) -> 'DeviceMgr': return self.parent.device_manager + def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device': + # Older versions of hid don't provide interface_number + interface_number = d.get('interface_number', -1) + usage_page = d['usage_page'] + id_ = d['serial_number'] + if len(id_) == 0: + id_ = str(d['path']) + id_ += str(interface_number) + str(usage_page) + device = Device(path=d['path'], + interface_number=interface_number, + id_=id_, + product_key=product_key, + usage_page=usage_page, + transport_ui_string='hid') + return device + @hook def close_wallet(self, wallet: 'Abstract_Wallet'): for keystore in wallet.get_keystores(): diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 1722eaea6..4b0f19a3f 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -88,7 +88,7 @@ class KeepKeyPlugin(HW_PluginBase): self.DEVICE_IDS = (keepkeylib.transport_hid.DEVICE_IDS + keepkeylib.transport_webusb.DEVICE_IDS) # only "register" hid device id: - self.device_manager().register_devices(keepkeylib.transport_hid.DEVICE_IDS) + self.device_manager().register_devices(keepkeylib.transport_hid.DEVICE_IDS, plugin=self) # for webusb transport, use custom enumerate function: self.device_manager().register_enumerate_func(self.enumerate) self.libraries_available = True diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index d5abc5dbe..08cea77a3 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -578,7 +578,7 @@ class LedgerPlugin(HW_PluginBase): self.segwit = config.get("segwit") HW_PluginBase.__init__(self, parent, config, name) if self.libraries_available: - self.device_manager().register_devices(self.DEVICE_IDS) + self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) def get_btchip_device(self, device): ledger = False From dda20583c24e086012885bad079b0e15354c1e27 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Apr 2020 18:38:22 +0200 Subject: [PATCH 10/12] bitbox02: rm BitBox02Client.label override if placeholder anyway, just use base impl (alternatively we should list it in electrum.plugin.PLACEHOLDER_HW_CLIENT_LABELS) --- electrum/plugins/bitbox02/bitbox02.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 6a62bee88..948fc1e16 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -68,9 +68,6 @@ class BitBox02Client(HardwareClientBase): if self.bitbox_hid_info is None: raise Exception("No BitBox02 detected") - def label(self) -> str: - return "BitBox02" - def is_initialized(self) -> bool: return True From 10c358dd38520cc9b17727b94ecdbb72d7c20f88 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Apr 2020 22:18:42 +0200 Subject: [PATCH 11/12] bitbox02: rm plugin.get_client method: just use default impl --- electrum/plugins/bitbox02/bitbox02.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 948fc1e16..3b2892327 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -587,12 +587,6 @@ class BitBox02Plugin(HW_PluginBase): assert client.bitbox02_device is not None return client.get_xpub(derivation, xtype) - def get_client(self, keystore: BitBox02_KeyStore, force_pair: bool = True): - devmgr = self.device_manager() - handler = keystore.handler - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - return client - def show_address( self, wallet: Deterministic_Wallet, From 04dcfe6fd120912a914afff5012cde8b1f969733 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 9 Apr 2020 20:36:07 +0200 Subject: [PATCH 12/12] bitbox02: add to requirements-hw, and include in win/mac binaries --- contrib/build-wine/deterministic.spec | 2 + .../deterministic-build/requirements-hw.txt | 63 +++++++++++++++++++ contrib/osx/osx.spec | 2 + contrib/requirements/requirements-hw.txt | 1 + 4 files changed, 68 insertions(+) diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 2ad11294b..4509c956e 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -23,6 +23,7 @@ hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('ckcc') +hiddenimports += collect_submodules('bitbox02') hiddenimports += ['PyQt5.QtPrintSupport'] # needed by Revealer @@ -48,6 +49,7 @@ datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') +datas += collect_data_files('bitbox02') datas += collect_data_files('jsonrpcserver') datas += collect_data_files('jsonrpcclient') diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 17b38ce36..5b4f853d0 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,8 +1,43 @@ +base58==2.0.0 \ + --hash=sha256:4c7f5687da771b519cf86b3236250e7c3543368c576404c9fe2d992a287666e0 \ + --hash=sha256:c83584a8b917dc52dd634307137f2ad2721a9efb4f1de32fc7eaaaf87844177e +bitbox02==2.0.3 \ + --hash=sha256:1f0164fd9941d3c3a17fb7db3bceddd89458986ef3da6171845e6433c3f66889 \ + --hash=sha256:53d06baafc597a8d14f990e285cd608cdf00be41a6d42ae40c316abad7798bd5 btchip-python==0.1.28 \ --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83 certifi==2020.4.5.1 \ --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \ --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519 +cffi==1.14.0 \ + --hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \ + --hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \ + --hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \ + --hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \ + --hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \ + --hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \ + --hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6 \ + --hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \ + --hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \ + --hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \ + --hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \ + --hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \ + --hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \ + --hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \ + --hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \ + --hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \ + --hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \ + --hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \ + --hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \ + --hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \ + --hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \ + --hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \ + --hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \ + --hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \ + --hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \ + --hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \ + --hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \ + --hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 @@ -14,6 +49,26 @@ click==7.1.1 \ --hash=sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a construct==2.10.56 \ --hash=sha256:97ba13edcd98546f10f7555af41c8ce7ae9d8221525ec4062c03f9adbf940661 +cryptography==2.9 \ + --hash=sha256:0cacd3ef5c604b8e5f59bf2582c076c98a37fe206b31430d0cd08138aff0986e \ + --hash=sha256:192ca04a36852a994ef21df13cca4d822adbbdc9d5009c0f96f1d2929e375d4f \ + --hash=sha256:19ae795137682a9778892fb4390c07811828b173741bce91e30f899424b3934d \ + --hash=sha256:1b9b535d6b55936a79dbe4990b64bb16048f48747c76c29713fea8c50eca2acf \ + --hash=sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02 \ + --hash=sha256:3be7a5722d5bfe69894d3f7bbed15547b17619f3a88a318aab2e37f457524164 \ + --hash=sha256:49870684da168b90110bbaf86140d4681032c5e6a2461adc7afdd93be5634216 \ + --hash=sha256:587f98ce27ac4547177a0c6fe0986b8736058daffe9160dcf5f1bd411b7fbaa1 \ + --hash=sha256:5aca6f00b2f42546b9bdf11a69f248d1881212ce5b9e2618b04935b87f6f82a1 \ + --hash=sha256:6b744039b55988519cc183149cceb573189b3e46e16ccf6f8c46798bb767c9dc \ + --hash=sha256:6b91cab3841b4c7cb70e4db1697c69f036c8bc0a253edc0baa6783154f1301e4 \ + --hash=sha256:7598974f6879a338c785c513e7c5a4329fbc58b9f6b9a6305035fca5b1076552 \ + --hash=sha256:7a279f33a081d436e90e91d1a7c338553c04e464de1c9302311a5e7e4b746088 \ + --hash=sha256:95e1296e0157361fe2f5f0ed307fd31f94b0ca13372e3673fa95095a627636a1 \ + --hash=sha256:9fc9da390e98cb6975eadf251b6e5fa088820141061bf041cd5c72deba1dc526 \ + --hash=sha256:cc20316e3f5a6b582fc3b029d8dc03aabeb645acfcb7fc1d9848841a33265748 \ + --hash=sha256:d1bf5a1a0d60c7f9a78e448adcb99aa101f3f9588b16708044638881be15d6bc \ + --hash=sha256:ed1d0760c7e46436ec90834d6f10477ff09475c692ed1695329d324b2c5cd547 \ + --hash=sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585 Cython==0.29.16 \ --hash=sha256:0542a6c4ff1be839b6479deffdbdff1a330697d7953dd63b6de99c078e3acd5f \ --hash=sha256:0bcf7f87aa0ba8b62d4f3b6e0146e48779eaa4f39f92092d7ff90081ef6133e0 \ @@ -72,6 +127,8 @@ libusb1==1.7.1 \ mnemonic==0.19 \ --hash=sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931 \ --hash=sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6 +noiseprotocol==0.3.1 \ + --hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111 pip==20.0.2 \ --hash=sha256:4ae14a42d8adba3205ebeb38aa68cfc0b6c346e1ae2e699a0b3bad4da19cef5c \ --hash=sha256:7db0c8ea4c7ea51c8049640e8e6e7fde949de672bfa4949920675563a5a6967f @@ -97,12 +154,18 @@ protobuf==3.11.3 \ --hash=sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80 pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f +pycparser==2.20 \ + --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \ + --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 requests==2.23.0 \ --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \ --hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6 safet==0.1.5 \ --hash=sha256:a7fd4b68bb1bc6185298af665c8e8e00e2bb2bcbddbb22844ead929b845c635e \ --hash=sha256:f966a23243312f64d14c7dfe02e8f13f6eeba4c3f51341f2c11ae57831f07de3 +semver==2.9.1 \ + --hash=sha256:095c3cba6d5433f21451101463b22cf831fe6996fcc8a603407fd8bea54f116b \ + --hash=sha256:723be40c74b6468861e0e3dbb80a41fc3b171a2a45bf956c245304773dc06055 setuptools==46.1.3 \ --hash=sha256:4fe404eec2738c20ab5841fa2d791902d2a645f32318a7850ef26f8d7215a8ee \ --hash=sha256:795e0475ba6cd7fa082b1ee6e90d552209995627a2a227a47c6ea93282f4bfb1 diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index f9ba4fec4..4a6a3a947 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -66,6 +66,7 @@ hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('ckcc') +hiddenimports += collect_submodules('bitbox02') hiddenimports += ['PyQt5.QtPrintSupport'] # needed by Revealer datas = [ @@ -81,6 +82,7 @@ datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') +datas += collect_data_files('bitbox02') datas += collect_data_files('jsonrpcserver') datas += collect_data_files('jsonrpcclient') diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index a442d7147..bf9c11f6e 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -13,4 +13,5 @@ safet>=0.1.5 keepkey>=6.3.1 btchip-python>=0.1.26 ckcc-protocol>=0.7.7 +bitbox02>=2.0.2 hidapi