electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit f01ef5dd71fc36c83e12d0bea20685576a4cff1b
parent 5ad8c564067d4c2bd418cfdefc8f972306037b43
Author: ThomasV <electrumdev@gmail.com>
Date:   Fri, 21 Aug 2015 00:14:24 +0200

Merge pull request #1391 from keepkey/master

KeepKey Integration
Diffstat:
Micons.qrc | 1+
Aicons/keepkey.png | 0
Mplugins/__init__.py | 10++++++++++
Aplugins/keepkey.py | 669+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 680 insertions(+), 0 deletions(-)

diff --git a/icons.qrc b/icons.qrc @@ -15,6 +15,7 @@ <file>icons/electrum_dark_icon.png</file> <file>icons/file.png</file> <file>icons/hot_seed.png</file> + <file>icons/keepkey.png</file> <file>icons/key.png</file> <file>icons/lock.png</file> <file>icons/microphone.png</file> diff --git a/icons/keepkey.png b/icons/keepkey.png Binary files differ. diff --git a/plugins/__init__.py b/plugins/__init__.py @@ -66,6 +66,16 @@ descriptions = [ 'available_for': ['qt'], }, { + 'name':'keepkey', + 'fullname': 'KeepKey', + 'description': _('Provides support for KeepKey hardware wallet'), + 'available_for': ['qt'], + 'requires': [('keepkeylib','github.com/keepkey/python-keepkey')], + 'requires_wallet_type': ['keepkey'], + 'registers_wallet_type': ('hardware', 'keepkey', _("KeepKey wallet")), + 'available_for': ['qt', 'cmdline'], + }, + { 'name': 'labels', 'fullname': _('LabelSync'), 'description': '\n'.join([ diff --git a/plugins/keepkey.py b/plugins/keepkey.py @@ -0,0 +1,669 @@ +from binascii import unhexlify +from struct import pack +from sys import stderr +from time import sleep +import unicodedata +import threading +import re + +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton +import PyQt4.QtCore as QtCore + +import electrum +from electrum import bitcoin + +from electrum.account import BIP32_Account +from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160, xpub_from_pubkey +from electrum.i18n import _ +from electrum.plugins import BasePlugin, hook, always_hook, run_hook +from electrum.transaction import Transaction, deserialize, is_extended_pubkey, x_to_xpub +from electrum.wallet import BIP32_HD_Wallet +from electrum.util import print_error, print_msg +from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root + +from electrum_gui.qt.util import * +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard + +try: + from keepkeylib.client import types + from keepkeylib.client import proto, BaseClient, ProtocolMixin + from keepkeylib.qt.pinmatrix import PinMatrixWidget + from keepkeylib.transport import ConnectionError + from keepkeylib.transport_hid import HidTransport + KEEPKEY = True +except ImportError: + KEEPKEY = False + +import keepkeylib.ckd_public as ckd_public + +def log(msg): + stderr.write("%s\n" % msg) + stderr.flush() + +def give_error(message): + print_error(message) + raise Exception(message) + + +class Plugin(BasePlugin): + + def __init__(self, config, name): + BasePlugin.__init__(self, config, name) + self._is_available = self._init() + self.wallet = None + self.handler = None + self.client = None + self.transport = None + + def constructor(self, s): + return KeepKeyWallet(s) + + def _init(self): + return KEEPKEY + + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'keepkey': + return False + return True + + def set_enabled(self, enabled): + self.wallet.storage.put('use_' + self.name, enabled) + + def is_enabled(self): + if not self.is_available(): + return False + if self.wallet.has_seed(): + return False + return True + + def compare_version(self, major, minor=0, patch=0): + features = self.get_client().features + v = [features.major_version, features.minor_version, features.patch_version] + self.print_error('firmware version', v) + return cmp(v, [major, minor, patch]) + + def atleast_version(self, major, minor=0, patch=0): + return self.compare_version(major, minor, patch) >= 0 + + def get_client(self): + if not KEEPKEY: + give_error('please install github.com/keepkey/python-keepkey') + + if not self.client or self.client.bad: + d = HidTransport.enumerate() + if not d: + give_error('Could not connect to your KeepKey. Please verify the cable is connected and that no other app is using it.') + self.transport = HidTransport(d[0]) + self.client = QtGuiKeepKeyClient(self.transport) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(1, 0, 0): + self.client = None + give_error('Outdated KeepKey firmware. Please update the firmware from https://www.keepkey.com') + return self.client + + @hook + def close_wallet(self): + print_error("keepkey: clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + self.client = None + self.wallet = None + + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = KeepKeyCmdLineHandler() + + @hook + def load_wallet(self, wallet, window): + self.print_error("load_wallet") + self.wallet = wallet + self.window = window + self.wallet.plugin = self + self.keepkey_button = StatusBarButton(QIcon(":icons/keepkey.png"), _("KeepKey"), self.settings_dialog) + if type(window) is ElectrumWindow: + self.window.statusBar().addPermanentWidget(self.keepkey_button) + if self.handler is None: + self.handler = KeepKeyQtHandler(self.window) + try: + self.get_client().ping('t') + except BaseException as e: + QMessageBox.information(self.window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) + self.wallet.force_watching_only = True + return + if self.wallet.addresses() and not self.wallet.check_proper_device(): + QMessageBox.information(self.window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) + self.wallet.force_watching_only = True + + @hook + def close_wallet(self): + if type(self.window) is ElectrumWindow: + self.window.statusBar().removeWidget(self.keepkey_button) + + @hook + def installwizard_load_wallet(self, wallet, window): + self.load_wallet(wallet, window) + + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'keepkey': + return + seed = wizard.enter_seed_dialog("Enter your KeepKey seed", None, func=lambda x:True) + if not seed: + return + wallet = KeepKeyWallet(storage) + self.wallet = wallet + handler = KeepKeyQtHandler(wizard) + passphrase = handler.get_passphrase(_("Please enter your KeepKey passphrase.") + '\n' + _("Press OK if you do not use one.")) + if passphrase is None: + return + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(seed, 'x/', password, passphrase) + wallet.create_main_account(password) + # disable keepkey plugin + self.set_enabled(False) + return wallet + + @hook + def receive_menu(self, menu, addrs): + if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: + menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) + + def show_address(self, address): + if not self.wallet.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + self.get_client().get_address('Bitcoin', address_n, True) + except Exception, e: + give_error(e) + finally: + self.handler.stop() + + + def settings_dialog(self): + try: + device_id = self.get_client().get_device_id() + except BaseException as e: + self.window.show_message(str(e)) + return + get_label = lambda: self.get_client().features.label + update_label = lambda: current_label_label.setText("Label: %s" % get_label()) + d = QDialog() + layout = QGridLayout(d) + layout.addWidget(QLabel("KeepKey Options"),0,0) + layout.addWidget(QLabel("ID:"),1,0) + layout.addWidget(QLabel(" %s" % device_id),1,1) + + def modify_label(): + response = QInputDialog().getText(None, "Set New KeepKey Label", "New KeepKey Label: (upon submission confirm on KeepKey)") + if not response[1]: + return + new_label = str(response[0]) + self.handler.show_message("Please confirm label change on KeepKey") + status = self.get_client().apply_settings(label=new_label) + self.handler.stop() + update_label() + + current_label_label = QLabel() + update_label() + change_label_button = QPushButton("Modify") + change_label_button.clicked.connect(modify_label) + layout.addWidget(current_label_label,3,0) + layout.addWidget(change_label_button,3,1) + d.exec_() + + + def sign_transaction(self, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client() + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(tx) + #try: + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] + #except Exception, e: + # give_error(e) + #finally: + self.handler.stop() + + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + + def tx_inputs(self, tx, for_sig=False): + inputs = [] + for txin in tx.inputs: + txinputtype = types.TxInputType() + if txin.get('is_coinbase'): + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + else: + def f(x_pubkey): + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(x_pubkey.decode('hex')) + s = [] + node = ckd_public.deserialize(xpub) + return types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = types.TxInputType( + script_type=types.SPENDMULTISIG, + multisig= multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + else: + raise + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = txin['scriptSig'].decode('hex') + txinputtype.script_sig = script_sig + + if 'sequence' in txin: + sequence = txin['sequence'] + txinputtype.sequence = sequence + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, tx): + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + txoutputtype.address_n.extend(address_n) + else: + txoutputtype.address = address + txoutputtype.amount = amount + addrtype, hash_160 = bc_address_to_hash_160(address) + if addrtype == 0: + txoutputtype.script_type = types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = vout['scriptPubKey'].decode('hex') + return t + + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + tx.deserialize() + return self.electrum_tx_to_txtype(tx) + + + + +class KeepKeyWallet(BIP32_HD_Wallet): + wallet_type = 'keepkey' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.mpk = None + self.device_checked = False + self.proper_device = False + self.force_watching_only = False + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_import(self): + return False + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_export(self): + return False + + def can_create_accounts(self): + return True + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self): + return self.plugin.get_client() + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) + + def create_main_account(self, password): + self.create_account('Main account', None) #name, empty password + + def mnemonic_to_seed(self, mnemonic, passphrase): + # keepkey uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + + def derive_xkeys(self, root, derivation, password): + x = self.master_private_keys.get(root) + if x: + root_xprv = pw_decode(x, password) + xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) + return xpub, xprv + else: + derivation = derivation.replace(self.root_name,"44'/0'/") + xpub = self.get_public_key(derivation) + return xpub, None + + def get_public_key(self, bip32_path): + address_n = self.plugin.get_client().expand_path(bip32_path) + node = self.plugin.get_client().get_public_node(address_n).node + xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + raise BaseException( _('Decrypt method is not implemented in KeepKey') ) + #address = public_key_to_bc_address(pubkey.decode('hex')) + #address_path = self.address_id(address) + #address_n = self.get_client().expand_path(address_path) + #try: + # decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) + #except Exception, e: + # give_error(e) + #finally: + # twd.stop() + #return str(decrypted_msg) + + def sign_message(self, address, message, password): + if not self.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.address_id(address) + address_n = self.plugin.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + except Exception, e: + give_error(e) + finally: + self.plugin.handler.stop() + return msg_sig.signature + + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + if not self.check_proper_device(): + give_error('Wrong device or password') + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs: + tx_hash = txin['prevout_hash'] + + ptx = self.transactions.get(tx_hash) + if ptx is None: + ptx = self.network.synchronous_get([('blockchain.transaction.get', [tx_hash])])[0] + ptx = Transaction(ptx) + prev_tx[tx_hash] = ptx + + for x_pubkey in txin['x_pubkeys']: + if not is_extended_pubkey(x_pubkey): + continue + xpub = x_to_xpub(x_pubkey) + for k, v in self.master_public_keys.items(): + if v == xpub: + account_id = re.match("x/(\d+)'", k).group(1) + account_derivation = "44'/0'/%s'"%account_id + xpub_path[xpub] = account_derivation + + self.plugin.sign_transaction(tx, prev_tx, xpub_path) + + def check_proper_device(self): + self.get_client().ping('t') + if not self.device_checked: + address = self.addresses(False)[0] + address_id = self.address_id(address) + n = self.get_client().expand_path(address_id) + device_address = self.get_client().get_address('Bitcoin', n) + self.device_checked = True + + if device_address != address: + self.proper_device = False + else: + self.proper_device = True + + return self.proper_device + + +class KeepKeyGuiMixin(object): + + def __init__(self, *args, **kwargs): + super(KeepKeyGuiMixin, self).__init__(*args, **kwargs) + + def callback_ButtonRequest(self, msg): + if msg.code == 3: + message = "Confirm transaction outputs on KeepKey device to continue" + elif msg.code == 8: + message = "Confirm transaction fee on KeepKey device to continue" + elif msg.code == 7: + message = "Confirm message to sign on KeepKey device to continue" + elif msg.code == 10: + message = "Confirm address on KeepKey device to continue" + else: + message = "Check KeepKey device to continue" + self.handler.show_message(message) + return proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 1: + desc = 'current PIN' + elif msg.type == 2: + desc = 'new PIN' + elif msg.type == 3: + desc = 'new PIN again' + else: + desc = 'PIN' + pin = self.handler.get_pin("Please enter KeepKey %s" % desc) + if not pin: + return proto.Cancel() + return proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + msg = _("Please enter your KeepKey passphrase.") + passphrase = self.handler.get_passphrase(msg) + if passphrase is None: + return proto.Cancel() + return proto.PassphraseAck(passphrase=passphrase) + + def callback_WordRequest(self, msg): + #TODO + log("Enter one word of mnemonic: ") + word = raw_input() + return proto.WordAck(word=word) + + +class KeepKeyCmdLineHandler: + + def get_passphrase(self, msg): + import getpass + print_msg(msg) + return getpass.getpass('') + + def get_pin(self, msg): + t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} + print_msg(msg) + print_msg("a b c\nd e f\ng h i\n-----") + o = raw_input() + return ''.join(map(lambda x: t[x], o)) + + def stop(self): + pass + + def show_message(self, msg): + print_msg(msg) + + +class KeepKeyQtHandler: + + def __init__(self, win): + self.win = win + self.win.connect(win, SIGNAL('keepkey_done'), self.dialog_stop) + self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog) + self.win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) + self.win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) + self.done = threading.Event() + + def stop(self): + self.win.emit(SIGNAL('keepkey_done')) + + def show_message(self, msg): + self.message = msg + self.win.emit(SIGNAL('message_dialog')) + + def get_pin(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('pin_dialog')) + self.done.wait() + return self.response + + def get_passphrase(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('passphrase_dialog')) + self.done.wait() + return self.passphrase + + def pin_dialog(self): + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(_("Enter PIN")) + d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + matrix = PinMatrixWidget() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(self.message)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + if not d.exec_(): + self.response = None + self.response = str(matrix.get_value()) + self.done.set() + + def passphrase_dialog(self): + if type(self.win) is ElectrumWindow: + passphrase = self.win.password_dialog(_("Please enter your KeepKey passphrase")) + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + else: + assert type(self.win) is InstallWizard + from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + d = QDialog() + d.setModal(1) + d.setLayout(make_password_dialog(d, None, self.message, False)) + confirmed, p, passphrase = run_password_dialog(d, None, None) + if not confirmed: + QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) + self.passphrase = None + else: + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + self.done.set() + + def message_dialog(self): + self.d = QDialog() + self.d.setModal(1) + self.d.setWindowTitle('Please Check KeepKey Device') + self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + l = QLabel(self.message) + vbox = QVBoxLayout(self.d) + vbox.addWidget(l) + self.d.show() + + def dialog_stop(self): + self.d.hide() + + +if KEEPKEY: + class QtGuiKeepKeyClient(ProtocolMixin, KeepKeyGuiMixin, BaseClient): + def call_raw(self, msg): + try: + resp = BaseClient.call_raw(self, msg) + except ConnectionError: + self.bad = True + raise + + return resp