electrum-bitcoinprivate/plugins/btchipwallet.py

570 lines
22 KiB
Python
Raw Normal View History

2015-07-05 13:14:53 -07:00
from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL
2014-08-24 10:44:26 -07:00
import PyQt4.QtCore as QtCore
from binascii import unhexlify
from binascii import hexlify
from struct import pack,unpack
from sys import stderr
from time import sleep
from base64 import b64encode, b64decode
import electrum
2014-08-24 10:44:26 -07:00
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
from electrum.account import BIP32_Account
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160
2014-08-24 10:44:26 -07:00
from electrum.i18n import _
2014-08-31 02:42:40 -07:00
from electrum.plugins import BasePlugin, hook
2014-08-24 10:44:26 -07:00
from electrum.transaction import deserialize
2015-05-25 15:09:26 -07:00
from electrum.wallet import BIP32_HD_Wallet, BIP32_Wallet
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
from electrum.util import format_satoshis_plain, print_error, print_msg
2014-08-24 10:44:26 -07:00
import hashlib
2015-07-05 13:14:53 -07:00
import threading
2014-08-24 10:44:26 -07:00
try:
from btchip.btchipComm import getDongle, DongleWait
from btchip.btchip import btchip
from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script
from btchip.bitcoinTransaction import bitcoinTransaction
2014-08-27 14:19:14 -07:00
from btchip.btchipPersoWizard import StartBTChipPersoDialog
2014-09-19 07:02:09 -07:00
from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware
2014-08-27 14:19:14 -07:00
from btchip.btchipException import BTChipException
2014-08-24 10:44:26 -07:00
BTCHIP = True
BTCHIP_DEBUG = False
2014-08-24 10:44:26 -07:00
except ImportError:
BTCHIP = False
class Plugin(BasePlugin):
def __init__(self, gui, name):
BasePlugin.__init__(self, gui, name)
self._is_available = self._init()
self.wallet = None
2015-07-05 13:14:53 -07:00
self.handler = None
2015-05-23 01:38:19 -07:00
2015-05-24 11:37:05 -07:00
def constructor(self, s):
return BTChipWallet(s)
2014-08-24 10:44:26 -07:00
def _init(self):
return BTCHIP
def is_available(self):
if not self._is_available:
return False
if not self.wallet:
return False
if self.wallet.storage.get('wallet_type') != 'btchip':
return False
return True
2014-08-24 10:44:26 -07:00
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
2014-08-24 10:44:26 -07:00
def btchip_is_connected(self):
try:
self.wallet.get_client().getFirmwareVersion()
except:
return False
return True
2015-07-05 13:14:53 -07:00
@hook
def cmdline_load_wallet(self, wallet):
self.wallet = wallet
self.wallet.plugin = self
if self.handler is None:
self.handler = BTChipCmdLineHandler()
2014-08-31 02:42:40 -07:00
@hook
def load_wallet(self, wallet, window):
2015-06-10 17:13:12 -07:00
self.wallet = wallet
2015-07-05 13:14:53 -07:00
self.wallet.plugin = self
2015-06-10 17:13:12 -07:00
self.window = window
2015-07-05 13:14:53 -07:00
if self.handler is None:
self.handler = BTChipQTHandler(self.window.app)
if self.btchip_is_connected():
if not self.wallet.check_proper_device():
QMessageBox.information(self.window, _('Error'), _("This wallet does not match your BTChip device"), _('OK'))
self.wallet.force_watching_only = True
else:
QMessageBox.information(self.window, _('Error'), _("BTChip device not detected.\nContinuing in watching-only mode."), _('OK'))
self.wallet.force_watching_only = True
2014-08-31 02:42:40 -07:00
@hook
2014-08-24 10:44:26 -07:00
def installwizard_restore(self, wizard, storage):
2014-08-31 03:40:57 -07:00
if storage.get('wallet_type') != 'btchip':
return
2014-08-24 10:44:26 -07:00
wallet = BTChipWallet(storage)
try:
wallet.create_main_account(None)
except BaseException as e:
QMessageBox.information(None, _('Error'), str(e), _('OK'))
return
return wallet
2014-08-31 02:42:40 -07:00
@hook
def sign_tx(self, tx):
2014-10-23 22:49:20 -07:00
tx.error = None
2014-08-24 10:44:26 -07:00
try:
2014-10-30 13:10:12 -07:00
self.wallet.sign_transaction(tx, None)
2014-08-24 10:44:26 -07:00
except Exception as e:
tx.error = str(e)
class BTChipWallet(BIP32_HD_Wallet):
2014-08-24 10:44:26 -07:00
wallet_type = 'btchip'
root_derivation = "m/44'/0'"
2014-08-24 10:44:26 -07:00
def __init__(self, storage):
BIP32_HD_Wallet.__init__(self, storage)
2014-08-24 10:44:26 -07:00
self.transport = None
self.client = None
self.mpk = None
self.device_checked = False
2014-09-20 05:27:13 -07:00
self.signing = False
self.force_watching_only = False
2014-08-24 10:44:26 -07:00
def give_error(self, message, clear_client = False):
2015-07-05 13:14:53 -07:00
print_error(message)
if not self.signing:
QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK'))
else:
self.signing = False
if clear_client and self.client is not None:
self.client.bad = True
self.device_checked = False
raise Exception(message)
2014-08-24 10:44:26 -07:00
def get_action(self):
if not self.accounts:
return 'create_accounts'
2015-07-05 09:33:16 -07:00
def can_sign_xpubkey(self, x_pubkey):
xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
return xpub in self.master_public_keys.values()
2014-08-24 10:44:26 -07:00
def can_create_accounts(self):
return False
def synchronize(self):
# synchronize existing accounts
BIP32_Wallet.synchronize(self)
# no further accounts for the moment
2014-08-24 10:44:26 -07:00
def can_change_password(self):
return False
def is_watching_only(self):
return self.force_watching_only
2014-08-24 10:44:26 -07:00
def get_client(self, noPin=False):
if not BTCHIP:
self.give_error('please install github.com/btchip/btchip-python')
2014-08-24 10:44:26 -07:00
aborted = False
if not self.client or self.client.bad:
2015-07-05 13:14:53 -07:00
try:
2014-08-31 09:55:31 -07:00
d = getDongle(BTCHIP_DEBUG)
2014-08-24 10:44:26 -07:00
self.client = btchip(d)
2015-07-05 13:14:53 -07:00
self.client.handler = self.plugin.handler
2014-08-24 10:44:26 -07:00
firmware = self.client.getFirmwareVersion()['version'].split(".")
if not checkFirmware(firmware):
d.close()
try:
updateFirmware()
except Exception, e:
aborted = True
raise e
d = getDongle(BTCHIP_DEBUG)
self.client = btchip(d)
2014-08-27 14:19:14 -07:00
try:
self.client.getOperationMode()
except BTChipException, e:
2014-08-27 14:19:14 -07:00
if (e.sw == 0x6985):
d.close()
dialog = StartBTChipPersoDialog()
2014-08-27 14:19:14 -07:00
dialog.exec_()
# Then fetch the reference again as it was invalidated
2014-08-31 09:55:31 -07:00
d = getDongle(BTCHIP_DEBUG)
2014-08-27 14:19:14 -07:00
self.client = btchip(d)
else:
raise e
if not noPin:
2014-08-24 10:44:26 -07:00
# Immediately prompts for the PIN
remaining_attempts = self.client.getVerifyPinRemainingAttempts()
if remaining_attempts <> 1:
msg = "Enter your BTChip PIN - remaining attempts : " + str(remaining_attempts)
else:
msg = "Enter your BTChip PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped."
confirmed, p, pin = self.password_dialog(msg)
2014-08-24 10:44:26 -07:00
if not confirmed:
aborted = True
raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying')
pin = pin.encode()
2014-08-24 10:44:26 -07:00
self.client.verifyPin(pin)
except BTChipException, e:
try:
self.client.dongle.close()
except:
pass
self.client = None
if (e.sw == 0x6faa):
raise Exception("Dongle is temporarily locked - please unplug it and replug it again")
if ((e.sw & 0xFFF0) == 0x63c0):
raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying")
raise e
2014-08-24 10:44:26 -07:00
except Exception, e:
try:
self.client.dongle.close()
except:
pass
self.client = None
2014-08-24 10:44:26 -07:00
if not aborted:
raise Exception("Could not connect to your BTChip dongle. Please verify access permissions, PIN, or unplug the dongle and plug it again")
2014-08-24 10:44:26 -07:00
else:
raise e
self.client.bad = False
self.device_checked = False
self.proper_device = False
return self.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 derive_xkeys(self, root, derivation, password):
derivation = derivation.replace(self.root_name,"44'/0'/")
xpub = self.get_public_key(derivation)
return xpub, None
def get_private_key(self, address, password):
return []
2014-08-24 10:44:26 -07:00
def get_public_key(self, bip32_path):
# S-L-O-W - we don't handle the fingerprint directly, so compute it manually from the previous node
2014-08-24 10:44:26 -07:00
# This only happens once so it's bearable
self.get_client() # prompt for the PIN before displaying the dialog if necessary
2015-07-05 13:14:53 -07:00
self.plugin.handler.show_message("Computing master public key")
try:
2014-08-24 10:44:26 -07:00
splitPath = bip32_path.split('/')
fingerprint = 0
2014-08-24 10:44:26 -07:00
if len(splitPath) > 1:
prevPath = "/".join(splitPath[0:len(splitPath) - 1])
nodeData = self.get_client().getWalletPublicKey(prevPath)
publicKey = compress_public_key(nodeData['publicKey'])
h = hashlib.new('ripemd160')
h.update(hashlib.sha256(publicKey).digest())
fingerprint = unpack(">I", h.digest()[0:4])[0]
2014-08-24 10:44:26 -07:00
nodeData = self.get_client().getWalletPublicKey(bip32_path)
publicKey = compress_public_key(nodeData['publicKey'])
depth = len(splitPath)
lastChild = splitPath[len(splitPath) - 1].split('\'')
if len(lastChild) == 1:
childnum = int(lastChild[0])
else:
childnum = 0x80000000 | int(lastChild[0])
2014-08-24 10:44:26 -07:00
xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey)
except Exception, e:
self.give_error(e, True)
2014-08-24 10:44:26 -07:00
finally:
2015-07-05 13:14:53 -07:00
self.plugin.handler.stop()
2014-08-24 10:44:26 -07:00
return EncodeBase58Check(xpub)
def get_master_public_key(self):
try:
if not self.mpk:
self.mpk = self.get_public_key("44'/0'")
return self.mpk
except Exception, e:
self.give_error(e, True)
2014-08-24 10:44:26 -07:00
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):
self.give_error("Not supported")
2014-08-24 10:44:26 -07:00
def sign_message(self, address, message, password):
use2FA = False
self.signing = True
2014-08-26 04:04:38 -07:00
self.get_client() # prompt for the PIN before displaying the dialog if necessary
if not self.check_proper_device():
self.give_error('Wrong device or password')
2014-08-24 10:44:26 -07:00
address_path = self.address_id(address)
2015-07-05 13:14:53 -07:00
self.plugin.handler.show_message("Signing message ...")
2014-08-24 10:44:26 -07:00
try:
info = self.get_client().signMessagePrepare(address_path, message)
pin = ""
if info['confirmationNeeded']:
2014-08-24 10:44:26 -07:00
# TODO : handle different confirmation types. For the time being only supports keyboard 2FA
use2FA = True
2014-08-24 10:44:26 -07:00
confirmed, p, pin = self.password_dialog()
if not confirmed:
raise Exception('Aborted by user')
2014-08-24 10:44:26 -07:00
pin = pin.encode()
self.client.bad = True
self.device_checked = False
2014-08-24 10:44:26 -07:00
self.get_client(True)
signature = self.get_client().signMessageSign(pin)
2015-02-23 07:30:33 -08:00
except BTChipException, e:
if e.sw == 0x6a80:
self.give_error("Unfortunately, this message cannot be signed by BTChip. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.")
else:
self.give_error(e, True)
2015-02-23 07:30:33 -08:00
except Exception, e:
self.give_error(e, True)
2014-08-24 10:44:26 -07:00
finally:
2015-07-05 13:14:53 -07:00
self.plugin.handler.stop()
self.client.bad = use2FA
self.signing = False
2014-08-24 10:44:26 -07:00
# Parse the ASN.1 signature
rLength = signature[3]
r = signature[4 : 4 + rLength]
sLength = signature[4 + rLength + 1]
s = signature[4 + rLength + 2:]
if rLength == 33:
r = r[1:]
if sLength == 33:
s = s[1:]
r = str(r)
s = str(s)
# And convert it
return b64encode(chr(27 + 4 + (signature[0] & 0x01)) + r + s)
2014-08-24 10:44:26 -07:00
2014-10-30 13:10:12 -07:00
def sign_transaction(self, tx, password):
if tx.is_complete():
return
2015-07-05 13:14:53 -07:00
#if tx.error:
# raise BaseException(tx.error)
self.signing = True
2014-08-24 10:44:26 -07:00
inputs = []
inputsPaths = []
pubKeys = []
trustedInputs = []
redeemScripts = []
2014-08-24 10:44:26 -07:00
signatures = []
preparedTrustedInputs = []
changePath = ""
2014-08-24 10:44:26 -07:00
changeAmount = None
output = None
outputAmount = None
use2FA = False
2014-08-27 14:19:14 -07:00
pin = ""
rawTx = tx.serialize()
2014-08-24 10:44:26 -07:00
# Fetch inputs of the transaction to sign
for txinput in tx.inputs:
if ('is_coinbase' in txinput and txinput['is_coinbase']):
self.give_error("Coinbase not supported") # should never happen
inputs.append([ self.transactions[txinput['prevout_hash']].raw,
txinput['prevout_n'] ])
2014-08-24 10:44:26 -07:00
address = txinput['address']
inputsPaths.append(self.address_id(address))
pubKeys.append(self.get_public_keys(address))
# Recognize outputs - only one output and one change is authorized
if len(tx.outputs) > 2: # should never happen
self.give_error("Transaction with more than 2 outputs not supported")
for type, address, amount in tx.outputs:
2014-08-24 10:44:26 -07:00
assert type == 'address'
if self.is_change(address):
changePath = self.address_id(address)
changeAmount = amount
else:
if output <> None: # should never happen
self.give_error("Multiple outputs with no change not supported")
2014-08-24 10:44:26 -07:00
output = address
outputAmount = amount
self.get_client() # prompt for the PIN before displaying the dialog if necessary
if not self.check_proper_device():
self.give_error('Wrong device or password')
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
self.plugin.handler.show_message("Signing Transaction ...")
2014-08-24 10:44:26 -07:00
try:
# Get trusted inputs from the original transactions
for utxo in inputs:
txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex')))
2014-08-24 10:44:26 -07:00
trustedInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1]))
# TODO : Support P2SH later
redeemScripts.append(txtmp.outputs[utxo[1]].script)
# Sign all inputs
firstTransaction = True
inputIndex = 0
while inputIndex < len(inputs):
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
2014-08-24 10:44:26 -07:00
trustedInputs, redeemScripts[inputIndex])
outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount),
format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex')))
2014-08-24 10:44:26 -07:00
if firstTransaction:
transactionOutput = outputData['outputData']
2015-07-05 13:14:53 -07:00
if outputData['confirmationNeeded']:
2014-08-24 10:44:26 -07:00
# TODO : handle different confirmation types. For the time being only supports keyboard 2FA
2015-07-05 13:14:53 -07:00
self.plugin.handler.stop()
if 'keycardData' in outputData:
pin2 = ""
for keycardIndex in range(len(outputData['keycardData'])):
msg = "Do not enter your device PIN here !\r\n\r\n" + \
"Your BTChip wants to talk to you and tell you a unique second factor code.\r\n" + \
"For this to work, please match the character between stars of the output address using your security card\r\n\r\n" + \
"Output address : "
for index in range(len(output)):
if index == outputData['keycardData'][keycardIndex]:
msg = msg + "*" + output[index] + "*"
else:
msg = msg + output[index]
msg = msg + "\r\n"
confirmed, p, pin = self.password_dialog(msg)
if not confirmed:
raise Exception('Aborted by user')
try:
pin2 = pin2 + chr(int(pin[0], 16))
except:
raise Exception('Invalid PIN character')
pin = pin2
else:
2015-07-05 13:14:53 -07:00
use2FA = True
confirmed, p, pin = self.password_dialog()
if not confirmed:
raise Exception('Aborted by user')
pin = pin.encode()
self.client.bad = True
self.device_checked = False
self.get_client(True)
2015-07-05 13:14:53 -07:00
self.plugin.handler.show_message("Signing ...")
2014-08-24 10:44:26 -07:00
else:
# Sign input with the provided PIN
2014-08-27 14:19:14 -07:00
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
pin)
inputSignature[0] = 0x30 # force for 1.4.9+
signatures.append(inputSignature)
2014-08-24 10:44:26 -07:00
inputIndex = inputIndex + 1
firstTransaction = False
except Exception, e:
self.give_error(e, True)
2014-08-24 10:44:26 -07:00
finally:
2015-07-05 13:14:53 -07:00
self.plugin.handler.stop()
2014-08-24 10:44:26 -07:00
# Reformat transaction
inputIndex = 0
while inputIndex < len(inputs):
# TODO : Support P2SH later
inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex'))
2014-08-24 10:44:26 -07:00
preparedTrustedInputs.append([ trustedInputs[inputIndex]['value'], inputScript ])
inputIndex = inputIndex + 1
updatedTransaction = format_transaction(transactionOutput, preparedTrustedInputs)
updatedTransaction = hexlify(updatedTransaction)
tx.update(updatedTransaction)
self.client.bad = use2FA
self.signing = False
2014-08-24 10:44:26 -07:00
def check_proper_device(self):
pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:]
if not self.device_checked:
2015-07-05 13:14:53 -07:00
self.plugin.handler.show_message("Checking device")
2014-08-24 10:44:26 -07:00
try:
nodeData = self.get_client().getWalletPublicKey("44'/0'/0'")
except Exception, e:
self.give_error(e, True)
2014-08-24 10:44:26 -07:00
finally:
2015-07-05 13:14:53 -07:00
self.plugin.handler.stop()
2014-08-24 10:44:26 -07:00
pubKeyDevice = compress_public_key(nodeData['publicKey'])
self.device_checked = True
if pubKey != pubKeyDevice:
self.proper_device = False
else:
self.proper_device = True
return self.proper_device
def password_dialog(self, msg=None):
if not msg:
msg = _("Do not enter your device PIN here !\r\n\r\n" \
"Your BTChip wants to talk to you and tell you a unique second factor code.\r\n" \
"For this to work, please open a text editor (on a different computer / device if you believe this computer is compromised) and put your cursor into it, unplug your BTChip and plug it back in.\r\n" \
"It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \
"Check that summary and then enter the second factor code here.\r\n" \
"Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)")
2015-07-05 13:14:53 -07:00
response = self.plugin.handler.prompt_auth(msg)
if response is None:
return False, None, None
return True, response, response
class BTChipQTHandler:
def __init__(self, win):
self.win = win
self.win.connect(win, SIGNAL('btchip_done'), self.dialog_stop)
2015-07-06 06:46:12 -07:00
self.win.connect(win, SIGNAL('btchip_message_dialog'), self.message_dialog)
self.win.connect(win, SIGNAL('btchip_auth_dialog'), self.auth_dialog)
2015-07-05 13:14:53 -07:00
self.done = threading.Event()
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
def stop(self):
self.win.emit(SIGNAL('btchip_done'))
def show_message(self, msg):
self.message = msg
2015-07-06 06:46:12 -07:00
self.win.emit(SIGNAL('btchip_message_dialog'))
2015-07-05 13:14:53 -07:00
def prompt_auth(self, msg):
self.done.clear()
self.message = msg
2015-07-06 06:46:12 -07:00
self.win.emit(SIGNAL('btchip_auth_dialog'))
2015-07-05 13:14:53 -07:00
self.done.wait()
return self.response
def auth_dialog(self):
response = QInputDialog.getText(None, "BTChip Authentication", self.message, QLineEdit.Password)
if not response[1]:
self.response = None
else:
self.response = str(response[0])
self.done.set()
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
def message_dialog(self):
2014-08-24 10:44:26 -07:00
self.d = QDialog()
self.d.setModal(1)
2015-07-05 13:14:53 -07:00
self.d.setWindowTitle('BTChip')
2014-08-24 10:44:26 -07:00
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
2015-07-05 13:14:53 -07:00
l = QLabel(self.message)
2014-08-24 10:44:26 -07:00
vbox = QVBoxLayout(self.d)
vbox.addWidget(l)
self.d.show()
2015-07-05 13:14:53 -07:00
def dialog_stop(self):
if self.d is not None:
self.d.hide()
self.d = None
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
class BTChipCmdLineHandler:
def stop(self):
pass
2014-08-24 10:44:26 -07:00
2015-07-05 13:14:53 -07:00
def show_message(self, msg):
print_msg(msg)
2014-09-02 00:00:20 -07:00
2015-07-05 13:14:53 -07:00
def prompt_auth(self, msg):
import getpass
print_msg(msg)
response = getpass.getpass('')
if len(response) == 0:
return None
return response