diff --git a/electrum/plugin.py b/electrum/plugin.py index 6e49bbdb3715..fd8bdc0cee79 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -423,6 +423,7 @@ def unpair_id(self, id_): self._close_client(id_) def _close_client(self, id_): + print_error("[DeviceMgr] _close_client: id:"+str(id_)) client = self.client_lookup(id_) self.clients.pop(client, None) if client: @@ -433,6 +434,7 @@ def pair_xpub(self, xpub, id_): self.xpub_ids[xpub] = id_ def client_lookup(self, id_): + print_error("[DeviceMgr] client_lookup: id:"+str(id_))#debugSatochip with self.lock: for client, (path, client_id) in self.clients.items(): if client_id == id_: @@ -508,7 +510,7 @@ def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices=None, include_failing_clients=False): '''Returns a list of DeviceInfo objects: one for each connected, unpaired device accepted by the plugin.''' - print_error("[plugins] DeviceMgr: unpaired_device_infos(): plugin:"+plugin.name+" nb_devices:"+str(len(devices)))#debugSatochip + #print_error("[plugins] DeviceMgr: unpaired_device_infos(): plugin:"+plugin.name+" nb_devices:"+str(len(devices)))#debugSatochip if not plugin.libraries_available: message = plugin.get_library_not_available_message() raise Exception(message) @@ -604,7 +606,7 @@ def _scan_devices_with_hid(self): def scan_devices(self): self.print_error("scanning devices...") - + # First see what's connected that we know about devices = self._scan_devices_with_hid() diff --git a/electrum/plugins/satochip/CardConnector.py b/electrum/plugins/satochip/CardConnector.py index 962ee5166088..0648d6674aed 100644 --- a/electrum/plugins/satochip/CardConnector.py +++ b/electrum/plugins/satochip/CardConnector.py @@ -1,6 +1,8 @@ from smartcard.CardType import AnyCardType from smartcard.CardRequest import CardRequest -from smartcard.CardConnectionObserver import ConsoleCardConnectionObserver +#from smartcard.CardConnectionObserver import ConsoleCardConnectionObserver +from smartcard.CardConnectionObserver import CardConnectionObserver +from smartcard.CardMonitoring import CardMonitor, CardObserver from smartcard.Exceptions import CardConnectionException, CardRequestTimeoutException from smartcard.util import toHexString, toBytes from smartcard.sw.SWExceptions import SWException @@ -12,11 +14,52 @@ from electrum.ecc import ECPubkey, msg_magic from electrum.i18n import _ +import base64 + +# simple observer that will print on the console the card connection events. +class LogCardConnectionObserver( CardConnectionObserver ): + def update( self, cardconnection, ccevent ): + if 'connect'==ccevent.type: + print_error( 'connecting to ' + cardconnection.getReader()) + elif 'disconnect'==ccevent.type: + print_error( 'disconnecting from ' + cardconnection.getReader()) + elif 'command'==ccevent.type: + print_error( '> ', toHexString( ccevent.args[0] )) + elif 'response'==ccevent.type: + if []==ccevent.args[0]: + print_error( '< [] ', "%-2X %-2X" % tuple(ccevent.args[-2:])) + else: + print_error('< ', toHexString(ccevent.args[0]), "%-2X %-2X" % tuple(ccevent.args[-2:])) + +# a simple card observer that detects inserted/removed cards +class RemovalObserver(CardObserver): + """A simple card observer that is notified + when cards are inserted/removed from the system and + prints the list of cards + """ + def __init__(self, parent): + self.parent=parent + + def update(self, observable, actions): + (addedcards, removedcards) = actions + for card in addedcards: + print_error("+Inserted: ", toHexString(card.atr)) + self.parent.client.handler.update_status(True) + for card in removedcards: + print_error("-Removed: ", toHexString(card.atr)) + self.parent.pin= None #reset PIN + self.parent.pin_nbr= None + self.parent.client.handler.update_status(False) + class CardConnector: # Satochip supported version tuple + # v0.4: getBIP32ExtendedKey also returns chaincode + # v0.5: Support for Segwit transaction + # v0.6: bip32 optimization: speed up computation during derivation of non-hardened child + # v0.7: add 2-Factor-Authentication (2FA) support SATOCHIP_PROTOCOL_MAJOR_VERSION=0 - SATOCHIP_PROTOCOL_MINOR_VERSION=6 + SATOCHIP_PROTOCOL_MINOR_VERSION=7 # define the apdus used in this script BYTE_AID= [0x53,0x61,0x74,0x6f,0x43,0x68,0x69,0x70] #SatoChip @@ -31,8 +74,12 @@ def __init__(self, client): self.cardrequest = CardRequest(timeout=10, cardType=self.cardtype) self.cardservice = self.cardrequest.waitforcard() # attach the console tracer - self.observer = ConsoleCardConnectionObserver() + self.observer = LogCardConnectionObserver() #ConsoleCardConnectionObserver() self.cardservice.connection.addObserver(self.observer) + # attach the card removal observer + self.cardmonitor = CardMonitor() + self.cardobserver = RemovalObserver(self) + self.cardmonitor.addObserver(self.cardobserver) # connect to the card and perform a few transmits self.cardservice.connection.connect() # cache PIN @@ -42,7 +89,7 @@ def __init__(self, client): print_error('time-out: no card inserted during last 10s') except Exception as exc: print_error("Error during connection:", exc) - + def card_transmit(self, apdu): try: (response, sw1, sw2) = self.cardservice.connection.transmit(apdu) @@ -56,7 +103,7 @@ def card_transmit(self, apdu): self.cardrequest = CardRequest(timeout=10, cardType=self.cardtype) self.cardservice = self.cardrequest.waitforcard() # attach the console tracer - self.observer = ConsoleCardConnectionObserver() + self.observer = LogCardConnectionObserver()#ConsoleCardConnectionObserver() self.cardservice.connection.addObserver(self.observer) # connect to the card and perform a few transmits self.cardservice.connection.connect() @@ -77,6 +124,7 @@ def get_sw12(self, sw1, sw2): def card_select(self): SELECT = [0x00, 0xA4, 0x04, 0x00, 0x08] apdu = SELECT + CardConnector.BYTE_AID + print_error("[CardConnector] card_select:"+str(apdu)+" obj_type:"+str(type(self)))#debug (response, sw1, sw2) = self.card_transmit(apdu) return (response, sw1, sw2) @@ -138,7 +186,7 @@ def card_setup(self, if option_flags!=0: apdu+=[option_flags>>8, option_flags&0x00ff] apdu+= hmacsha160_key - for i in reverse(range(8)): + for i in reversed(range(8)): apdu+=[(amount_limit>>(8*i))&0xff] # send apdu (contains sensitive data!) @@ -355,6 +403,8 @@ def card_parse_transaction(self, transaction, is_segwit=False): return (response, sw1, sw2) def card_sign_transaction(self, keynbr, txhash, chalresponse): + #if (type(chalresponse)==str): + # chalresponse = list(bytes.fromhex(chalresponse)) cla= JCconstants.CardEdge_CLA ins= JCconstants.INS_SIGN_TRANSACTION p1= keynbr @@ -367,13 +417,99 @@ def card_sign_transaction(self, keynbr, txhash, chalresponse): else: if len(chalresponse)!=20: raise ValueError("Wrong Challenge response length:"+ str(len(chalresponse)) + "(should be 20)") - data= txhash + chalresponse + data= txhash + list(bytes.fromhex("8000")) + chalresponse # 2 middle bytes for 2FA flag lc= len(data) apdu=[cla, ins, p1, p2, lc]+data # send apdu response, sw1, sw2 = self.card_transmit(apdu) return (response, sw1, sw2) + + def card_crypt_transaction_2FA(self, msg, is_encrypt=True): + if (type(msg)==str): + msg = msg.encode('utf8') + msg=list(msg) + msg_out=[] + + # CIPHER_INIT - no data processed + cla= JCconstants.CardEdge_CLA + ins= 0x76 + p2= JCconstants.OP_INIT + blocksize=16 + if is_encrypt: + p1= 0x02 + lc= 0x00 + apdu=[cla, ins, p1, p2, lc] + # for encryption, the data is padded with PKCS#7 + size=len(msg) + padsize= blocksize - (size%blocksize) + msg= msg+ [padsize]*padsize + # send apdu + (response, sw1, sw2) = self.card_transmit(apdu) + # extract IV & id_2FA + IV= response[0:16] + id_2FA= response[16:36] + msg_out=IV + # id_2FA is 20 bytes, should be 32 => use sha256 + from hashlib import sha256 + id_2FA= sha256(bytes(id_2FA)).hexdigest() + else: + p1= 0x01 + lc= 0x10 + apdu=[cla, ins, p1, p2, lc] + # for decryption, the IV must be provided as part of the msg + IV= msg[0:16] + print_error("satochip iv hex"+ bytes(IV).hex()) + msg=msg[16:] + apdu= apdu+IV + if len(msg)%blocksize!=0: + print_error('Padding error!') + # send apdu + (response, sw1, sw2) = self.card_transmit(apdu) + + chunk= 192 # max APDU data=256 => chunk<=255-(4+2) + buffer_offset=0 + buffer_left=len(msg) + # CIPHER PROCESS/UPDATE (optionnal) + while buffer_left>chunk: + p2= JCconstants.OP_PROCESS + le= 2+chunk + apdu=[cla, ins, p1, p2, le] + apdu+=[((chunk>>8) & 0xFF), (chunk & 0xFF)] + apdu+= msg[buffer_offset:(buffer_offset+chunk)] + buffer_offset+=chunk + buffer_left-=chunk + # send apdu + response, sw1, sw2 = self.card_transmit(apdu) + # extract msg + out_size= (response[0]<<8) + response[1] + msg_out+= response[2:2+out_size] + + # CIPHER FINAL/SIGN (last chunk) + chunk= buffer_left #following while condition, buffer_left<=chunk + p2= JCconstants.OP_FINALIZE + le= 2+chunk + apdu=[cla, ins, p1, p2, le] + apdu+=[((chunk>>8) & 0xFF), (chunk & 0xFF)] + apdu+= msg[buffer_offset:(buffer_offset+chunk)] + buffer_offset+=chunk + buffer_left-=chunk + # send apdu + response, sw1, sw2 = self.card_transmit(apdu) + # extract msg + out_size= (response[0]<<8) + response[1] + msg_out+= response[2:2+out_size] + + if is_encrypt: + #convert from list to string + msg_out= base64.b64encode(bytes(msg_out)).decode('ascii') + return (id_2FA, msg_out) + else: + #remove padding + pad= msg_out[-1] + msg_out=msg_out[0:-pad] + msg_out= bytes(msg_out).decode('latin-1')#''.join(chr(i) for i in msg_out) #bytes(msg_out).decode('latin-1') + return (msg_out) def card_create_PIN(self, pin_nbr, pin_tries, pin, ublk): cla= JCconstants.CardEdge_CLA diff --git a/electrum/plugins/satochip/TxParser.py b/electrum/plugins/satochip/TxParser.py index 1703db596c26..0a42afb05570 100644 --- a/electrum/plugins/satochip/TxParser.py +++ b/electrum/plugins/satochip/TxParser.py @@ -53,7 +53,7 @@ def __init__(self, rawTx): self.txChunk=b'' - print_error('[TxParser] TxParser: __init__(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip + #print_error('[TxParser] TxParser: __init__(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip def set_remaining_output(self, nb_outputs): self.txRemainingOutput=nb_outputs @@ -136,9 +136,8 @@ def parse_transaction(self): self.txDigest.update(self.singleHash) self.doubleHash= self.txDigest.digest() - print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip - time.sleep(0.1) #debugSatochip - + #print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip + #time.sleep(0.1) #debugSatochip return self.txChunk def parse_outputs(self): @@ -175,9 +174,8 @@ def parse_outputs(self): self.txDigest.update(self.singleHash) self.doubleHash= self.txDigest.digest() - print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip - time.sleep(0.1) #debugSatochip - + #print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip + #time.sleep(0.1) #debugSatochip return self.txChunk def parse_segwit_transaction(self): @@ -223,16 +221,15 @@ def parse_segwit_transaction(self): self.txDigest.update(self.singleHash) self.doubleHash= self.txDigest.digest() - print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip - time.sleep(0.1) #debugSatochip - + #print_error('[TxParser] TxParser: parse_transaction(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip + #time.sleep(0.1) #debugSatochip return self.txChunk def parse_byte(self, length): self.txChunk+=self.txData[self.txOffset:(self.txOffset+length)] self.txOffset+=length self.txRemaining-=length - print_error('[TxParser] TxParser: parse_byte(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip + #print_error('[TxParser] TxParser: parse_byte(): txOffset='+str(self.txOffset) + " txRemaining="+str(self.txRemaining) + "chunk="+self.txChunk.hex()) #debugSatochip def parse_var_int(self): diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 68716df98d99..5ea45a260d59 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -1,11 +1,13 @@ from struct import pack, unpack from os import urandom import hashlib +import hmac import sys import traceback #electrum from electrum import bitcoin +from electrum import constants from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int from electrum.i18n import _ from electrum.plugin import BasePlugin, Device @@ -18,10 +20,13 @@ from electrum.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey from electrum.mnemonic import Mnemonic from electrum.keystore import bip39_to_seed +from electrum.plugin import run_hook #from electrum.bitcoin import serialize_xpub from electrum.bip32 import serialize_xpub #from electrum.bip32 import BIP32Node +from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog + from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import is_any_tx_output_on_change_branch @@ -40,6 +45,8 @@ SATOCHIP_VID= 0x096E SATOCHIP_PID= 0x0503 +MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. Warning: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") + def bip32path2bytes(bip32path:str) -> (int, bytes): splitPath = bip32path.split('/') splitPath=[x for x in splitPath if x] # removes empty values @@ -80,9 +87,11 @@ def is_pairable(self): return True def close(self): - print_error("[satochip] SatochipClient: close()")#debugSatochip + print_error("[SatochipClient] close()")#debugSatochip self.cc.card_disconnect() - + self.cc.cardmonitor.deleteObserver(self.cc.cardobserver) + print_error("[SatochipClient] Removed cardObserver") + def timeout(self, cutoff): pass @@ -245,11 +254,18 @@ def sign_transaction(self, tx, password): client = self.get_client() segwitTransaction = False + # outputs + txOutputs= ''.join(tx.serialize_output(o) for o in tx.outputs()) + hashOutputs = bh2u(sha256d(bfh(txOutputs))) + txOutputs = var_int(len(tx.outputs()))+txOutputs + print_error('[satochip] sign_transaction(): hashOutputs= '+hashOutputs) #debugSatochip + print_error('[satochip] sign_transaction(): outputs= '+txOutputs) #debugSatochip + # Fetch inputs of the transaction to sign derivations = self.get_tx_derivations(tx) for i,txin in enumerate(tx.inputs()): - #print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forloop: i= '+str(i)) #debugSatochip - #print_error(' [satochip] Satochip_KeyStore: sign_transaction(): txin[type]:'+txin['type']) #debugSatochip + print_error('[satochip] sign_transaction(): input= '+str(i)) #debugSatochip + print_error('[satochip] sign_transaction(): input[type]:'+txin['type']) #debugSatochip if txin['type'] == 'coinbase': self.give_error("Coinbase not supported") # should never happen @@ -268,7 +284,7 @@ def sign_transaction(self, tx, password): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for j, x_pubkey in enumerate(x_pubkeys): - print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forforloop: j= '+str(j)) #debugSatochip + print_error('[satochip] sign_transaction(): forforloop: j= '+str(j)) #debugSatochip if tx.is_txin_complete(txin): break @@ -285,21 +301,58 @@ def sign_transaction(self, tx, password): pre_tx_hex= tx.serialize_preimage(i) pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes pre_hash = sha256d(bfh(pre_tx_hex)) - print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forforloop: pre_hash= '+pre_hash.hex()) #debugSatochip + pre_hash_hex= pre_hash.hex() + print_error('[satochip] sign_transaction(): pre_hash= '+pre_hash_hex) #debugSatochip (response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, segwitTransaction) - print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forforloop: response= '+str(response)) #debugSatochip + #print_error('[satochip] sign_transaction(): response= '+str(response)) #debugSatochip (tx_hash, needs_2fa)= client.parser.parse_parse_transaction(response) - print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forforloop: tx_hash= '+bytearray(tx_hash).hex()) #debugSatochip + print_error('[satochip] sign_transaction(): tx_hash= '+bytearray(tx_hash).hex()) #debugSatochip + # tx_hash should be equal to pre_hash_hex + print_error('[satochip] sign_transaction(): pre_tx_hex= '+pre_tx_hex) #debugSatochip # sign tx keynbr= 0xFF #for extended key if needs_2fa: - #todo: chalenge-response... - chalresponse= b'0'*20 + # format & encrypt msg + import json + coin_type= 1 if constants.net.TESTNET else 0 + if segwitTransaction: + msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction, 'txo':txOutputs, 'ty':txin['type']} + else: + msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction} + msg= json.dumps(msg) + (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True) + d={} + d['msg_encrypt']= msg_out + d['id_2FA']= id_2FA + # self.print_error("encrypted message: "+msg_out) + self.print_error("id_2FA: "+id_2FA) + + #do challenge-response with 2FA device... + client.handler.show_message('2FA request sent! Approve or reject request on your second device.') + run_hook('do_challenge_response', d) + # decrypt and parse reply to extract challenge response + reply_encrypt= d['reply_encrypt'] + if reply_encrypt is None: + #todo: abort tx + break + reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) + print_error("challenge:response= "+ reply_decrypt) + reply_decrypt= reply_decrypt.split(":") + rep_pre_hash_hex= reply_decrypt[0] + if rep_pre_hash_hex!= pre_hash_hex: + #todo: abort tx or retry? + break + chalresponse=reply_decrypt[1] + if chalresponse=="00"*20: + #todo: abort tx? + break + chalresponse= list(bytes.fromhex(chalresponse)) else: chalresponse= None (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, chalresponse) - print_error(' [satochip] Satochip_KeyStore: sign_transaction(): forforloop: sig= '+bytearray(tx_sig).hex()) #debugSatochip + print_error('[satochip] sign_transaction(): sig= '+bytearray(tx_sig).hex()) #debugSatochip + #todo: check sw1sw2 for error (0x9c0b if wrong challenge-response) # enforce low-S signature (BIP 62) tx_sig = bytearray(tx_sig) r,s= get_r_and_s_from_der_sig(tx_sig) @@ -438,16 +491,35 @@ def setup_device(self, device_info, wizard, purpose): create_object_ACL= 0x01 create_key_ACL= 0x01 create_pin_ACL= 0x01 - #option_flags= 0x8000 # activate 2fa with hmac challenge-response - #key= new byte[20] - #amount_limit= 0 + + # Optionnaly setup 2-Factor-Authentication (2FA) + #msg= "Do you want to use 2-Factor-Authentication?" + use_2FA=client.handler.yes_no_question(MSG_USE_2FA) print("[satochip] SatochipPlugin: setup_device(): perform cardSetup:")#debugSatochip - (response, sw1, sw2)=client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, + if (use_2FA): + option_flags= 0x8000 # activate 2fa with hmac challenge-response + secret_2FA= urandom(20) + #secret_2FA=b'\0'*20 #for debug purpose + secret_2FA_hex=secret_2FA.hex() + amount_limit= 0 # always use + (response, sw1, sw2)=client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, + pin_tries_1, ublk_tries_1, pin_1, ublk_1, + secmemsize, memsize, + create_object_ACL, create_key_ACL, create_pin_ACL, + option_flags, list(secret_2FA), amount_limit) + # the secret must be shared with the second factor app (eg on a smartphone) + try: + d = QRDialog(secret_2FA_hex, None, "Secret_2FA", True) + d.exec_() + except Exception as e: + print_error("[satochip] SatochipPlugin: setup_device(): setup 2FA: "+str(e)) + # further communications will require an id and an encryption key (for privacy). + # Both are derived from the secret_2FA using a one-way function inside the Satochip + else: + (response, sw1, sw2)=client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, pin_tries_1, ublk_tries_1, pin_1, ublk_1, secmemsize, memsize, - create_object_ACL, create_key_ACL, create_pin_ACL - #,option_flags, key, amount_limit - ) + create_object_ACL, create_key_ACL, create_pin_ACL) if sw1!=0x90 or sw2!=0x00: print("[satochip] SatochipPlugin: setup_device(): unable to set up applet! sw12="+hex(sw1)+" "+hex(sw2))#debugSatochip raise RuntimeError('Unable to setup the device with error code:'+hex(sw1)+' '+hex(sw2)) @@ -472,7 +544,6 @@ def setup_device(self, device_info, wizard, purpose): hex_authentikey= authentikey.get_public_key_hex(compressed=True) print_error("[satochip] SatochipPlugin: setup_device(): authentikey="+hex_authentikey)#debugSatochip wizard.storage.put('authentikey', hex_authentikey) - #wizard.storage.write() print_error("[satochip] SatochipPlugin: setup_device(): authentikey from storage="+wizard.storage.get('authentikey'))#debugSatochip break diff --git a/electrum/plugins/satochip_2FA/__init__.py b/electrum/plugins/satochip_2FA/__init__.py new file mode 100644 index 000000000000..784aa183a73c --- /dev/null +++ b/electrum/plugins/satochip_2FA/__init__.py @@ -0,0 +1,8 @@ +from electrum.i18n import _ +fullname = _('Satochip 2FA') +description = ' '.join([ + _("This plugin allows the use of a second factor to authorize transactions on a Satochip hardware wallet"), + _("It sends and receives transaction challenge and response"), + _("Data isencrypted and stored on a remote server.") +]) +available_for = ['qt'] diff --git a/electrum/plugins/satochip_2FA/qt.py b/electrum/plugins/satochip_2FA/qt.py new file mode 100644 index 000000000000..0dfec7f513d5 --- /dev/null +++ b/electrum/plugins/satochip_2FA/qt.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# Electrum - lightweight Bitcoin client +# Copyright (C) 2014 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from electrum import util, keystore, ecc, bip32, crypto +from electrum import transaction +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ +from electrum.util import bh2u, bfh + +from electrum.gui.qt.transaction_dialog import show_transaction +from electrum.gui.qt.util import WaitingDialog + +import sys +import traceback +import hashlib +import base64 +import time +from xmlrpc.client import ServerProxy + +server = ServerProxy('https://cosigner.electrum.org/', allow_none=True) + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + + def is_available(self): + return True + + # send the challenge and get the reply + @hook + def do_challenge_response(self, d): + + id_2FA= d['id_2FA'] + msg= d['msg_encrypt'] + replyhash= hashlib.sha256(id_2FA.encode('utf-8')).hexdigest() + + #purge server from old messages then sends message + server.delete(id_2FA) + server.delete(replyhash) + server.put(id_2FA, msg) + + # wait for reply + timeout= 180 + period=10 + reply= None + while timeout>0: + try: + reply = server.get(replyhash) + except Exception as e: + self.print_error("cannot contact server") + continue + if reply: + self.print_error("received response from", replyhash) + self.print_error("response received", reply) + d['reply_encrypt']=base64.b64decode(reply) + server.delete(replyhash) + break + # poll every t seconds + time.sleep(period) + timeout-=period + + if reply is None: + self.print_error("Error: Time-out without server reply...") + d['reply_encrypt']= None #default + \ No newline at end of file