Skip to content

Commit

Permalink
Electrum v3.3.4 - Satochip v0.7: Add 2FA support
Browse files Browse the repository at this point in the history
In CardConnector.py:
- Encrypt/decrypt 2FA challenge/response for privacy
- erase PIN when card is removed

In Satochip.py:
- pairing with 2FA device using QR code
- if 2FA is enabled, tx signing requires response to challenge using hmac-sha1

New plugin in satochip_2FA folder: exchange challenge-response with 2FA device

Minor changes in TxParser.py, plugin.py
  • Loading branch information
Toporin committed May 30, 2019
1 parent bb04379 commit 037fcd0
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 39 deletions.
6 changes: 4 additions & 2 deletions electrum/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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_:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down
150 changes: 143 additions & 7 deletions electrum/plugins/satochip/CardConnector.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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!)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 8 additions & 11 deletions electrum/plugins/satochip/TxParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Expand Down
Loading

0 comments on commit 037fcd0

Please sign in to comment.