tMerge pull request #2996 from benma/mobile_pairing - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit f1792d1b1309d8ed704beb811a514cc9ed39528e DIR parent 6b9bfddda2d9b8febf88f1264be70f1e0e359396 HTML Author: ThomasV <thomasv@electrum.org> Date: Tue, 17 Oct 2017 08:21:55 +0200 Merge pull request #2996 from benma/mobile_pairing digitalbitbox: import mobile pairing config Diffstat: M plugins/digitalbitbox/digitalbitbo… | 131 +++++++++++++++++++++++++------ M plugins/digitalbitbox/qt.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 23 deletions(-) --- DIR diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py t@@ -5,7 +5,8 @@ try: import electrum - from electrum.bitcoin import TYPE_ADDRESS, var_int, msg_magic, Hash, verify_message, pubkey_from_signature, point_to_ser, public_key_to_p2pkh, EncodeAES, DecodeAES, MyVerifyingKey + from electrum.bitcoin import TYPE_ADDRESS, push_script, var_int, msg_magic, Hash, verify_message, pubkey_from_signature, point_to_ser, public_key_to_p2pkh, EncodeAES, DecodeAES, MyVerifyingKey + from electrum.transaction import Transaction from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from ..hw_wallet import HW_PluginBase t@@ -18,6 +19,10 @@ try: import binascii import struct import hashlib + import requests + import base64 + import os + import sys from ecdsa.ecdsa import generator_secp256k1 from ecdsa.util import sigencode_der from ecdsa.curves import SECP256k1 t@@ -36,7 +41,8 @@ def to_hexstr(s): class DigitalBitbox_Client(): - def __init__(self, hidDevice): + def __init__(self, plugin, hidDevice): + self.plugin = plugin self.dbb_hid = hidDevice self.opened = True self.password = None t@@ -73,13 +79,15 @@ class DigitalBitbox_Client(): def is_paired(self): return self.password is not None + def _get_xpub(self, bip32_path): + if self.check_device_dialog(): + return self.hid_send_encrypt(b'{"xpub": "%s"}' % bip32_path.encode('utf8')) + def get_xpub(self, bip32_path): - if self.check_device_dialog(): - msg = b'{"xpub": "%s"}' % bip32_path.encode('utf8') - reply = self.hid_send_encrypt(msg) + reply = self._get_xpub(bip32_path) + if reply: return reply['xpub'] - return None def dbb_has_password(self): t@@ -165,7 +173,7 @@ class DigitalBitbox_Client(): self.recover_or_erase_dialog() # Already seeded else: self.seed_device_dialog() # Seed if not initialized - + self.mobile_pairing_dialog() return self.isInitialized t@@ -186,7 +194,9 @@ class DigitalBitbox_Client(): if not self.dbb_load_backup(): return else: - pass # Use existing seed + if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: + raise Exception("Full 2FA enabled. This is not supported yet.") + # Use existing seed self.isInitialized = True t@@ -207,6 +217,45 @@ class DigitalBitbox_Client(): return self.isInitialized = True + def mobile_pairing_dialog(self): + dbb_user_dir = None + if sys.platform == 'darwin': + dbb_user_dir = os.path.join(os.environ.get("HOME", ""), "Library", "Application Support", "DBB") + elif sys.platform == 'win32': + dbb_user_dir = os.path.join(os.environ["APPDATA"], "DBB") + else: + dbb_user_dir = os.path.join(os.environ["HOME"], ".dbb") + + if not dbb_user_dir: + return + + try: + with open(os.path.join(dbb_user_dir, "config.dat")) as f: + dbb_config = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return + + if 'encryptionprivkey' not in dbb_config or 'comserverchannelid' not in dbb_config: + return + + choices = [ + _('Do not pair'), + _('Import pairing from the digital bitbox desktop app'), + ] + try: + reply = self.handler.win.query_choice(_('Mobile pairing options'), choices) + except Exception: + return # Back button pushed + + if reply == 0: + if self.plugin.is_mobile_paired(): + del self.plugin.digitalbitbox_config['encryptionprivkey'] + del self.plugin.digitalbitbox_config['comserverchannelid'] + elif reply == 1: + # import pairing from dbb app + self.plugin.digitalbitbox_config['encryptionprivkey'] = dbb_config['encryptionprivkey'] + self.plugin.digitalbitbox_config['comserverchannelid'] = dbb_config['comserverchannelid'] + self.plugin.config.set_key('digitalbitbox', self.plugin.digitalbitbox_config) def dbb_generate_wallet(self): key = self.stretch_key(self.password) t@@ -452,17 +501,28 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): if txinput['type'] != 'p2sh': self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen - # Build pubkeyarray from outputs (unused because echo for smart verification not implemented) - if not p2shTransaction: - for _type, address, amount in tx.outputs(): - assert _type == TYPE_ADDRESS - info = tx.output_info.get(address) - if info is not None: - index, xpubs, m = info - changePath = self.get_derivation() + "/%d/%d" % index - changePubkey = self.derive_pubkey(index[0], index[1]) - pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} - pubkeyarray.append(pubkeyarray_i) + # Build pubkeyarray from outputs + for _type, address, amount in tx.outputs(): + assert _type == TYPE_ADDRESS + info = tx.output_info.get(address) + if info is not None: + index, xpubs, m = info + changePath = self.get_derivation() + "/%d/%d" % index + changePubkey = self.derive_pubkey(index[0], index[1]) + pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} + pubkeyarray.append(pubkeyarray_i) + + # Special serialization of the unsigned transaction for + # the mobile verification app. + class CustomTXSerialization(Transaction): + @classmethod + def input_script(self, txin, estimate_size=False): + if txin['type'] == 'p2pkh': + return Transaction.get_preimage_script(txin) + if txin['type'] == 'p2sh': + return '00' + push_script(Transaction.get_preimage_script(txin)) + raise Exception("unsupported type %s" % txin['type']) + tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize() # Build sign command dbb_signatures = [] t@@ -471,8 +531,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): hashes = hasharray[step * self.maxInputs : (step + 1) * self.maxInputs] msg = ('{"sign": {"meta":"%s", "data":%s, "checkpub":%s} }' % \ - (to_hexstr(Hash(tx.serialize())), json.dumps(hashes), json.dumps(pubkeyarray))).encode('utf8') - + (to_hexstr(Hash(tx_dbb_serialized)), json.dumps(hashes), json.dumps(pubkeyarray))).encode('utf8') dbb_client = self.plugin.get_client(self) if not dbb_client.is_paired(): t@@ -485,6 +544,11 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): if 'echo' not in reply: raise Exception("Could not sign transaction.") + # multisig verification not working correctly yet + if self.plugin.is_mobile_paired() and not p2shTransaction: + reply['tx'] = tx_dbb_serialized + self.plugin.comserver_post_notification(reply) + if steps > 1: self.handler.show_message(_("Signing large transaction. Please be patient ...\r\n\r\n" \ "To continue, touch the Digital Bitbox's blinking light for 3 seconds. " \ t@@ -495,7 +559,8 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): "To continue, touch the Digital Bitbox's blinking light for 3 seconds.\r\n\r\n" \ "To cancel, briefly touch the blinking light or wait for the timeout.")) - reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented) + # Send twice, first returns an echo for smart verification + reply = dbb_client.hid_send_encrypt(msg) self.handler.clear_dialog() if 'error' in reply: t@@ -555,6 +620,8 @@ class DigitalBitboxPlugin(HW_PluginBase): if self.libraries_available: self.device_manager().register_devices(self.DEVICE_IDS) + self.digitalbitbox_config = self.config.get('digitalbitbox', {}) + def get_dbb_device(self, device): dev = hid.device() t@@ -567,7 +634,7 @@ class DigitalBitboxPlugin(HW_PluginBase): self.handler = handler client = self.get_dbb_device(device) if client is not None: - client = DigitalBitbox_Client(client) + client = DigitalBitbox_Client(self, client) return client else: return None t@@ -582,6 +649,24 @@ class DigitalBitboxPlugin(HW_PluginBase): client.get_xpub("m/44'/0'") + def is_mobile_paired(self): + return 'encryptionprivkey' in self.digitalbitbox_config + + + def comserver_post_notification(self, payload): + assert self.is_mobile_paired(), "unexpected mobile pairing error" + url = 'https://digitalbitbox.com/smartverification/index.php' + key_s = base64.b64decode(self.digitalbitbox_config['encryptionprivkey']) + args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % ( + self.digitalbitbox_config['comserverchannelid'], + EncodeAES(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), + ) + try: + requests.post(url, args) + except Exception as e: + self.handler.show_error(str(e)) + + def get_xpub(self, device_id, derivation, wizard): devmgr = self.device_manager() client = devmgr.client_by_id(device_id) DIR diff --git a/plugins/digitalbitbox/qt.py b/plugins/digitalbitbox/qt.py t@@ -2,6 +2,10 @@ from PyQt5.QtWidgets import (QInputDialog, QLineEdit) from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from .digitalbitbox import DigitalBitboxPlugin +from electrum.i18n import _ +from electrum.plugins import hook +from electrum.wallet import Wallet, Standard_Wallet +from electrum.bitcoin import EncodeAES class Plugin(DigitalBitboxPlugin, QtPluginBase): icon_unpaired = ":icons/digitalbitbox_unpaired.png" t@@ -10,6 +14,31 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase): def create_handler(self, window): return DigitalBitbox_Handler(window) + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + + keystore = wallet.get_keystore() + if type(keystore) is not self.keystore_class: + return + + if not self.is_mobile_paired(): + return + + if len(addrs) == 1: + def show_address(): + change, index = wallet.get_address_index(addrs[0]) + keypath = '%s/%d/%d' % (keystore.derivation, change, index) + xpub = self.get_client(keystore)._get_xpub(keypath) + verify_request_payload = { + "type": 'p2pkh', + "echo": xpub['echo'], + } + self.comserver_post_notification(verify_request_payload) + + menu.addAction(_("Show on %s") % self.device, show_address) + class DigitalBitbox_Handler(QtHandlerBase):