diff --git a/contrib/build-linux/appimage/Dockerfile_ub1604 b/contrib/build-linux/appimage/Dockerfile_ub1604 index c916fd94565a..87794a87a237 100644 --- a/contrib/build-linux/appimage/Dockerfile_ub1604 +++ b/contrib/build-linux/appimage/Dockerfile_ub1604 @@ -12,12 +12,12 @@ RUN apt-get update -q && \ xz-utils=5.1.1alpha+20120614-2ubuntu2 \ libffi-dev=3.2.1-4 \ libncurses5-dev=6.0+20160213-1ubuntu1 \ - libsqlite3-dev=3.11.0-1ubuntu1.4 \ + libsqlite3-dev=3.11.0-1ubuntu1.5 \ libusb-1.0-0-dev=2:1.0.20-1 \ libudev-dev=229-4ubuntu21.28 \ gettext=0.19.7-2ubuntu3.1 \ pkg-config=0.29.1-0ubuntu1 \ - libdbus-1-3=1.10.6-1ubuntu3.4 \ + libdbus-1-3=1.10.6-1ubuntu3.6 \ libpcsclite-dev=1.8.14-1ubuntu1.16.04.1 \ swig=3.0.8-0ubuntu3 \ software-properties-common=0.96.20.9 \ @@ -26,7 +26,7 @@ RUN apt-get update -q && \ add-apt-repository ppa:git-core/ppa && \ apt-get update -q && \ apt-get install -qy \ - git=1:2.26.2-0ppa1~ubuntu16.04.1 \ + git \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ diff --git a/contrib/build-linux/appimage/_build.sh b/contrib/build-linux/appimage/_build.sh index 53b3d1c2e625..0bd09bbc6f4d 100755 --- a/contrib/build-linux/appimage/_build.sh +++ b/contrib/build-linux/appimage/_build.sh @@ -113,7 +113,7 @@ mkdir -p "$CACHEDIR/pip_cache" "$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt" "$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt" "$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt" -"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-satochip.txt" +#"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-satochip.txt" "$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT" "$python" -m pip uninstall -y Cython diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 62112f659abd..d5336e3448cd 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -40,9 +40,9 @@ Cython==0.29.7 \ --hash=sha256:f3f6c09e2c76f2537d61f907702dd921b04d1c3972f01d5530ef1f748f22bd89 \ --hash=sha256:f749287087f67957c020e1de26906e88b8b0c4ea588facb7349c115a63346f67 \ --hash=sha256:f86b96e014732c0d1ded2c1f51444c80176a98c21856d0da533db4e4aef54070 -ecdsa==0.13.2 \ - --hash=sha256:20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c \ - --hash=sha256:5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884 +ecdsa==0.15 \ + --hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \ + --hash=sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277 hidapi==0.7.99.post21 \ --hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \ --hash=sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3 \ @@ -98,6 +98,15 @@ pyblake2==1.1.2 \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 +pysatochip==0.11.2 \ + --hash=sha256:52317953c4d4968a6184c2da7a802d8aa50977ba85f39475ba360e4e78063e17 \ + --hash=sha256:c9b21b2bf9c001e9cb6e78aae3d242f1db72907d8cf7377aa818688d7d436ed1 +pyscard==1.9.9 \ + --hash=sha256:6620a74f58d5fa9076544263bb4e42c946eb20f315c896d14a7e5743d5431469 \ + --hash=sha256:a047738c58d05b4dab15aa9c99fbd92f8d0670900de89c68bec247a422f8d8c7 \ + --hash=sha256:c213a94585a48f8f1ff3c36c06fa3a162d2fd7f2c89240ac632bd38a3fa5df9b \ + --hash=sha256:e6bde541990183858740793806b1c7f4e798670519ae4c96145f35d5d7944c20 \ + --hash=sha256:99d2b450f322f9ed9682fd2a99d95ce781527e371006cded38327efca8158fe7 requests==2.21.0 \ --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b diff --git a/contrib/deterministic-build/requirements-satochip.txt b/contrib/deterministic-build/requirements-satochip.txt index 6eb62b4ad5c7..0a6e7de64f41 100644 --- a/contrib/deterministic-build/requirements-satochip.txt +++ b/contrib/deterministic-build/requirements-satochip.txt @@ -1,4 +1,11 @@ +ecdsa==0.15 \ + --hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \ + --hash=sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277 pyscard==1.9.9 \ --hash=sha256:6620a74f58d5fa9076544263bb4e42c946eb20f315c896d14a7e5743d5431469 \ --hash=sha256:a047738c58d05b4dab15aa9c99fbd92f8d0670900de89c68bec247a422f8d8c7 \ + --hash=sha256:c213a94585a48f8f1ff3c36c06fa3a162d2fd7f2c89240ac632bd38a3fa5df9b \ --hash=sha256:e6bde541990183858740793806b1c7f4e798670519ae4c96145f35d5d7944c20 +pysatochip==0.11.2 \ + --hash=sha256:52317953c4d4968a6184c2da7a802d8aa50977ba85f39475ba360e4e78063e17 \ + --hash=sha256:c9b21b2bf9c001e9cb6e78aae3d242f1db72907d8cf7377aa818688d7d436ed1 \ No newline at end of file diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 55d0558d51bf..b2b8dcd86462 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -3,3 +3,5 @@ trezor[hidapi]>=0.11.0 keepkey>=6.1 btchip-python hidapi +pyscard>=1.9.9 +pysatochip>=0.11.2 diff --git a/plugins/satochip/CardConnector.py b/plugins/satochip/CardConnector.py deleted file mode 100644 index a5df630a499f..000000000000 --- a/plugins/satochip/CardConnector.py +++ /dev/null @@ -1,688 +0,0 @@ -from smartcard.CardType import AnyCardType -from smartcard.CardRequest import CardRequest -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 - -from .JCconstants import JCconstants -from .TxParser import TxParser -from .ecc import ECPubkey, msg_magic - -from electroncash.util import print_error, PrintError -from electroncash.i18n import _ - -import base64 -import traceback - -MSG_WARNING= ("Before you request bitcoins to be sent to addresses in this " - "wallet, ensure you can pair with your device, or that you have " - "its seed (and passphrase, if any). Otherwise all bitcoins you " - "receive will be unspendable.") - -# simple observer that will print on the console the card connection events. -class LogCardConnectionObserver( CardConnectionObserver, PrintError ): - def update( self, cardconnection, ccevent ): - if 'connect'==ccevent.type: - self.print_error( 'connecting to', cardconnection.getReader()) - elif 'disconnect'==ccevent.type: - self.print_error( 'disconnecting from', cardconnection.getReader()) - elif 'command'==ccevent.type: - if (ccevent.args[0][1] in (JCconstants.INS_SETUP, JCconstants.INS_SET_2FA_KEY, - JCconstants.INS_BIP32_IMPORT_SEED, JCconstants.INS_BIP32_RESET_SEED, - JCconstants.INS_CREATE_PIN, JCconstants.INS_VERIFY_PIN, - JCconstants.INS_CHANGE_PIN, JCconstants.INS_UNBLOCK_PIN)): - self.print_error(f"> {toHexString(ccevent.args[0][0:5])}{(len(ccevent.args[0])-5)*' *'}") - else: - self.print_error(f"> {toHexString(ccevent.args[0])}") - elif 'response'==ccevent.type: - if []==ccevent.args[0]: - self.print_error( '< [] ', "%-2X %-2X" % tuple(ccevent.args[-2:])) - else: - self.print_error('< ', toHexString(ccevent.args[0]), "%-2X %-2X" % tuple(ccevent.args[-2:])) - -# a simple card observer that detects inserted/removed cards -class RemovalObserver(CardObserver, PrintError): - """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: - self.print_error("+Inserted: ", toHexString(card.atr)) - if self.parent.client and self.parent.client.handler: - self.parent.client.handler.update_status(True) - for card in removedcards: - self.print_error("-Removed: ", toHexString(card.atr)) - self.parent.pin= None #reset PIN - self.parent.pin_nbr= None - if self.parent.client and self.parent.client.handler: - self.parent.client.handler.update_status(False) - -class CardConnector(PrintError): - - # 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 - # v0.8: support seed reset and pin change - # v0.9: patch message signing for alts - SATOCHIP_PROTOCOL_MAJOR_VERSION=0 - SATOCHIP_PROTOCOL_MINOR_VERSION=9 - - # define the apdus used in this script - BYTE_AID= [0x53,0x61,0x74,0x6f,0x43,0x68,0x69,0x70] #SatoChip - - def __init__(self, client): - # request any card type - #self.print_error("** client", client, " ** handler", client.handler) - self.client=client - self.parser=client.parser - self.cardtype = AnyCardType() - self.needs_2FA = None - try: - # request card insertion - self.cardrequest = CardRequest(timeout=10, cardType=self.cardtype) - self.cardservice = self.cardrequest.waitforcard() - # attach the console tracer - 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 - self.pin_nbr=None - self.pin=None - except CardRequestTimeoutException: - self.print_error('time-out: no card inserted during last 10s') - except Exception as exc: - self.print_error("Error during connection:", repr(exc), traceback.format_exc()) - - def card_transmit(self, apdu): - try: - (response, sw1, sw2) = self.cardservice.connection.transmit(apdu) - if (sw1==0x9C) and (sw2==0x06): - (response, sw1, sw2)= self.card_verify_PIN() - (response, sw1, sw2)= self.cardservice.connection.transmit(apdu) - return (response, sw1, sw2) - except CardConnectionException: - # maybe the card has been removed - try: - self.cardrequest = CardRequest(timeout=10, cardType=self.cardtype) - self.cardservice = self.cardrequest.waitforcard() - # attach the console tracer - self.observer = LogCardConnectionObserver()#ConsoleCardConnectionObserver() - self.cardservice.connection.addObserver(self.observer) - # connect to the card and perform a few transmits - self.cardservice.connection.connect() - # retransmit apdu - (response, sw1, sw2) = self.cardservice.connection.transmit(apdu) - if (sw1==0x9C) and (sw2==0x06): - (response, sw1, sw2)= self.card_verify_PIN() - (response, sw1, sw2)= self.cardservice.connection.transmit(apdu) - return (response, sw1, sw2) - except CardRequestTimeoutException: - self.print_error('time-out: no card inserted during last 10s') - except Exception as exc: - self.print_error("Error during connection:", repr(exc), traceback.format_exc()) - - def card_get_ATR(self): - return self.cardservice.connection.getATR() - - def card_disconnect(self): - self.cardservice.connection.disconnect() - - def get_sw12(self, sw1, sw2): - return 16*sw1+sw2 - - def card_select(self): - SELECT = [0x00, 0xA4, 0x04, 0x00, 0x08] - apdu = SELECT + CardConnector.BYTE_AID - self.print_error("card_select")#debug - (response, sw1, sw2) = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_get_status(self): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_GET_STATUS - p1= 0x00 - p2= 0x00 - le= 0x00 - apdu=[cla, ins, p1, p2, le] - (response, sw1, sw2)= self.card_transmit(apdu) - d={} - if (sw1==0x90) and (sw2==0x00): - d["protocol_major_version"]= response[0] - d["protocol_minor_version"]= response[1] - d["applet_major_version"]= response[2] - d["applet_minor_version"]= response[3] - if len(response) >=8: - d["PIN0_remaining_tries"]= response[4] - d["PUK0_remaining_tries"]= response[5] - d["PIN1_remaining_tries"]= response[6] - d["PUK1_remaining_tries"]= response[7] - self.needs_2FA= d["needs2FA"]= False #default value - if len(response) >=9: - self.needs_2FA= d["needs2FA"]= False if response[8]==0X00 else True - - return (response, sw1, sw2, d) - - def card_setup(self, - pin_tries0, ublk_tries0, pin0, ublk0, - pin_tries1, ublk_tries1, pin1, ublk1, - memsize, memsize2, - create_object_ACL, create_key_ACL, create_pin_ACL, - option_flags=0, hmacsha160_key=None, amount_limit=0): - - # to do: check pin sizes < 256 - pin=[0x4D, 0x75, 0x73, 0x63, 0x6C, 0x65, 0x30, 0x30] # default pin - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_SETUP - p1=0 - p2=0 - apdu=[cla, ins, p1, p2] - - # data=[pin_length(1) | pin | - # pin_tries0(1) | ublk_tries0(1) | pin0_length(1) | pin0 | ublk0_length(1) | ublk0 | - # pin_tries1(1) | ublk_tries1(1) | pin1_length(1) | pin1 | ublk1_length(1) | ublk1 | - # memsize(2) | memsize2(2) | ACL(3) | - # option_flags(2) | hmacsha160_key(20) | amount_limit(8)] - if option_flags==0: - optionsize= 0 - elif option_flags&0x8000==0x8000: - optionsize= 30 - else: - optionsize= 2 - le= 16+len(pin)+len(pin0)+len(pin1)+len(ublk0)+len(ublk1)+optionsize - - apdu+=[le] - apdu+=[len(pin)]+pin - apdu+=[pin_tries0, ublk_tries0, len(pin0)] + pin0 + [len(ublk0)] + ublk0 - apdu+=[pin_tries1, ublk_tries1, len(pin1)] + pin1 + [len(ublk1)] + ublk1 - apdu+=[memsize>>8, memsize&0x00ff, memsize2>>8, memsize2&0x00ff] - apdu+=[create_object_ACL, create_key_ACL, create_pin_ACL] - if option_flags!=0: - apdu+=[option_flags>>8, option_flags&0x00ff] - apdu+= hmacsha160_key - for i in reversed(range(8)): - apdu+=[(amount_limit>>(8*i))&0xff] - - # send apdu (contains sensitive data!) - (response, sw1, sw2) = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_bip32_import_seed(self, seed): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_BIP32_IMPORT_SEED - p1= len(seed) - p2= 0x00 - le= len(seed) - apdu=[cla, ins, p1, p2, le]+seed - - # send apdu (contains sensitive data!) - response, sw1, sw2 = self.card_transmit(apdu) - # compute authentikey pubkey and send to chip for future use - if (sw1==0x90) and (sw2==0x00): - authentikey= self.card_bip32_set_authentikey_pubkey(response) - return authentikey - - def card_reset_seed(self, pin, hmac=[]): - cla= JCconstants.CardEdge_CLA - ins= 0x77 - p1= len(pin) - p2= 0x00 - le= len(pin)+len(hmac) - apdu=[cla, ins, p1, p2, le]+pin+hmac - - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_bip32_get_authentikey(self): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_BIP32_GET_AUTHENTIKEY - p1= 0x00 - p2= 0x00 - le= 0x00 - apdu=[cla, ins, p1, p2, le] - - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - if sw1==0x9c and sw2==0x14: - self.print_error("card_bip32_get_authentikey(): Seed is not initialized => Raising error!") - raise UninitializedSeedError("Satochip seed is not initialized!\n\n "+MSG_WARNING) - if sw1==0x9c and sw2==0x04: - self.print_error("card_bip32_get_authentikey(): Satochip is not initialized => Raising error!") - raise UninitializedSeedError('Satochip is not initialized! You should create a new wallet!\n\n'+MSG_WARNING) - # compute corresponding pubkey and send to chip for future use - if (sw1==0x90) and (sw2==0x00): - authentikey = self.card_bip32_set_authentikey_pubkey(response) - return authentikey - - ''' Allows to compute coordy of authentikey externally to optimize computation time-out - coordy value is verified by the chip before being accepted ''' - def card_bip32_set_authentikey_pubkey(self, response): - cla= JCconstants.CardEdge_CLA - ins= 0x75 - p1= 0x00 - p2= 0x00 - - authentikey= self.parser.parse_bip32_get_authentikey(response) - coordy= authentikey.get_public_key_bytes(compressed=False) - coordy= list(coordy[33:]) - data= response + [len(coordy)&0xFF00, len(coordy)&0x00FF] + coordy - le= len(data) - apdu=[cla, ins, p1, p2, le]+data - - (response, sw1, sw2) = self.card_transmit(apdu) - return authentikey - - def card_bip32_get_extendedkey(self, path): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_BIP32_GET_EXTENDED_KEY - p1= len(path)//4 - p2= 0x40 #option flags: 0x80:erase cache memory - 0x40: optimization for non-hardened child derivation - le= len(path) - apdu=[cla, ins, p1, p2, le] - apdu+= path - - if self.parser.authentikey is None: - self.card_bip32_get_authentikey() - - # send apdu - while (True): - (response, sw1, sw2) = self.card_transmit(apdu) - - # if there is no more memory available, erase cache... - #if self.get_sw12(sw1,sw2)==JCconstants.SW_NO_MEMORY_LEFT: - if (sw1==0x9C) and (sw2==0x01): - self.print_error("card_bip32_get_extendedkey(): Reset memory...")#debugSatochip - apdu[3]=apdu[3]^0x80 - response, sw1, sw2 = self.card_transmit(apdu) - apdu[3]=apdu[3]&0x7f # reset the flag - # other (unexpected) error - if (sw1!=0x90) or (sw2!=0x00): - raise UnexpectedSW12Error('Unexpected error code SW12='+hex(sw1)+" "+hex(sw2)) - # check for non-hardened child derivation optimization - elif ( (response[32]&0x80)== 0x80): - self.print_error("card_bip32_get_extendedkey(): Child Derivation optimization...")#debugSatochip - (pubkey, chaincode)= self.parser.parse_bip32_get_extendedkey(response) - coordy= pubkey.get_public_key_bytes(compressed=False) - coordy= list(coordy[33:]) - authcoordy= self.parser.authentikey.get_public_key_bytes(compressed=False) - authcoordy= list(authcoordy[33:]) - data= response+[len(coordy)&0xFF00, len(coordy)&0x00FF]+coordy - apdu_opt= [cla, 0x74, 0x00, 0x00, len(data)] - apdu_opt= apdu_opt+data - response_opt, sw1_opt, sw2_opt = self.card_transmit(apdu_opt) - #at this point, we have successfully received a response from the card - else: - (key, chaincode)= self.parser.parse_bip32_get_extendedkey(response) - return (key, chaincode) - - def card_sign_message(self, keynbr, message, hmac=b''): - if (type(message)==str): - message = message.encode('utf8') - - # return signature as byte array - # data is cut into chunks, each processed in a different APDU call - chunk= 160 # max APDU data=255 => chunk<=255-(4+2) - buffer_offset=0 - buffer_left=len(message) - - # CIPHER_INIT - no data processed - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_SIGN_MESSAGE - p1= keynbr # 0xff=>BIP32 otherwise STD - p2= JCconstants.OP_INIT - lc= 0x4 - apdu=[cla, ins, p1, p2, lc] - for i in reversed(range(4)): - apdu+= [((buffer_left>>(8*i)) & 0xff)] - - # send apdu - (response, sw1, sw2) = self.card_transmit(apdu) - - # CIPHER PROCESS/UPDATE (optionnal) - while buffer_left>chunk: - #cla= JCconstants.CardEdge_CLA - #ins= INS_COMPUTE_CRYPT - #p1= key_nbr - p2= JCconstants.OP_PROCESS - le= 2+chunk - apdu=[cla, ins, p1, p2, le] - apdu+=[((chunk>>8) & 0xFF), (chunk & 0xFF)] - apdu+= message[buffer_offset:(buffer_offset+chunk)] - buffer_offset+=chunk - buffer_left-=chunk - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - - # CIPHER FINAL/SIGN (last chunk) - chunk= buffer_left #following while condition, buffer_left<=chunk - #cla= JCconstants.CardEdge_CLA - #ins= INS_COMPUTE_CRYPT - #p1= key_nbr - p2= JCconstants.OP_FINALIZE - le= 2+chunk+ len(hmac) - apdu=[cla, ins, p1, p2, le] - apdu+=[((chunk>>8) & 0xFF), (chunk & 0xFF)] - apdu+= message[buffer_offset:(buffer_offset+chunk)]+hmac - buffer_offset+=chunk - buffer_left-=chunk - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_sign_short_message(self, keynbr, message, hmac=b''): - if (type(message)==str): - message = message.encode('utf8') - - # for message less than one chunk in size - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_SIGN_SHORT_MESSAGE - p1= keynbr # oxff=>BIP32 otherwise STD - p2= 0x00 - le= message.length+2+len(hmac) - apdu= [cla, ins, p1, p2, le] - apdu+= [(message.length>>8 & 0xFF), (message.length & 0xFF)] - apdu+= message+ hmac - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_parse_transaction(self, transaction, is_segwit=False): - - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_PARSE_TRANSACTION - p1= JCconstants.OP_INIT - p2= 0X01 if is_segwit else 0x00 - - # init transaction data and context - txparser= TxParser(transaction) - while not txparser.is_parsed(): - - chunk= txparser.parse_segwit_transaction() if is_segwit else txparser.parse_transaction() - lc= len(chunk) - apdu=[cla, ins, p1, p2, lc] - apdu+=chunk - - # log state & send apdu - #if (txparser.is_parsed(): - #le= 86 # [hash(32) | sigsize(2) | sig | nb_input(4) | nb_output(4) | coord_actif_input(4) | amount(8)] - #logCommandAPDU("cardParseTransaction - FINISH",cla, ins, p1, p2, data, le) - #elif p1== JCconstants.OP_INIT: - #logCommandAPDU("cardParseTransaction-INIT",cla, ins, p1, p2, data, le) - #elif p1== JCconstants.OP_PROCESS: - #logCommandAPDU("cardParseTransaction - PROCESS",cla, ins, p1, p2, data, le) - - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - - # switch to process mode after initial call to parse - p1= JCconstants.OP_PROCESS - - 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 - p2= 0x00 - - if len(txhash)!=32: - raise ValueError("Wrong txhash length: " + str(len(txhash)) + "(should be 32)") - elif chalresponse==None: - data= txhash - else: - if len(chalresponse)!=20: - raise ValueError("Wrong Challenge response length:"+ str(len(chalresponse)) + "(should be 20)") - 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_set_2FA_key(self, hmacsha160_key, amount_limit): - cla= JCconstants.CardEdge_CLA - ins= 0x79 - p1= 0x00 - p2= 0x00 - le= 28 # data=[ hmacsha160_key(20) | amount_limit(8) ] - apdu=[cla, ins, p1, p2, le] - - apdu+= hmacsha160_key - for i in reversed(range(8)): - apdu+=[(amount_limit>>(8*i))&0xff] - - # send apdu (contains sensitive data!) - (response, sw1, sw2) = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_reset_2FA_key(self, chalresponse): - cla= JCconstants.CardEdge_CLA - ins= 0x78 - p1= 0x00 - p2= 0x00 - le= 20 # data=[ hmacsha160_key(20) ] - apdu=[cla, ins, p1, p2, le] - apdu+= chalresponse - - # send apdu (contains sensitive data!) - (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] - msg=msg[16:] - apdu= apdu+IV - if len(msg)%blocksize!=0: - self.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 - ins= JCconstants.INS_CREATE_PIN - p1= pin_nbr - p2= pin_tries - lc= 1 + len(pin) + 1 + len(ublk) - apdu=[cla, ins, p1, p2, lc] + [len(pin)] + pin + [len(ublk)] + ublk - - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - #deprecated but used for testcase - def card_verify_PIN_deprecated(self, pin_nbr, pin): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_VERIFY_PIN - p1= pin_nbr - p2= 0x00 - lc= len(pin) - apdu=[cla, ins, p1, p2, lc] + pin - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_verify_PIN(self): - while (True): - (response, sw1, sw2, d)=self.card_get_status() # get number of pin tries remaining - if self.pin is None: - if d.get("PIN0_remaining_tries",-1)==1: - msg = _("Enter the PIN for your Satochip: \n WARNING: ONLY ONE ATTEMPT REMAINING!") - else: - msg = _("Enter the PIN for your Satochip:") - (is_PIN, pin_0, pin_0)= self.client.PIN_dialog(msg) - if pin_0 is None: - raise RuntimeError('Device cannot be unlocked without PIN code!') - pin_0= list(pin_0) - else: - pin_0= self.pin - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_VERIFY_PIN - apdu=[cla, ins, 0x00, 0x00, len(pin_0)] + pin_0 - response, sw1, sw2 = self.cardservice.connection.transmit(apdu) - if sw1==0x90 and sw2==0x00: - self.set_pin(0, pin_0) #cache PIN value - return (response, sw1, sw2) - elif sw1==0x9c and sw2==0x02: - self.set_pin(0, None) #reset cached PIN value - pin_left= d.get("PIN0_remaining_tries",-1)-1 - msg = _("Wrong PIN! {} tries remaining!").format(pin_left) - self.client.handler.show_error(msg) - elif sw1==0x9c and sw2==0x0c: - msg = _("Too many failed attempts! Your Satochip has been blocked! You need your PUK code to unblock it.") - self.client.handler.show_error(msg) - raise RuntimeError('Device blocked with error code:'+hex(sw1)+' '+hex(sw2)) - - def set_pin(self, pin_nbr, pin): - self.pin_nbr=pin_nbr - self.pin=pin - return - - def card_change_PIN(self, pin_nbr, old_pin, new_pin): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_CHANGE_PIN - p1= pin_nbr - p2= 0x00 - lc= 1 + len(old_pin) + 1 + len(new_pin) - apdu=[cla, ins, p1, p2, lc] + [len(old_pin)] + old_pin + [len(new_pin)] + new_pin - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - self.set_pin(0, None) - return (response, sw1, sw2) - - def card_unblock_PIN(self, pin_nbr, ublk): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_UNBLOCK_PIN - p1= pin_nbr - p2= 0x00 - lc= len(ublk) - apdu=[cla, ins, p1, p2, lc] + ublk - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - return (response, sw1, sw2) - - def card_logout_all(self): - cla= JCconstants.CardEdge_CLA - ins= JCconstants.INS_LOGOUT_ALL - p1= 0x00 - p2= 0x00 - lc=0 - apdu=[cla, ins, p1, p2, lc] - # send apdu - response, sw1, sw2 = self.card_transmit(apdu) - self.set_pin(0, None) - return (response, sw1, sw2) - -class AuthenticationError(Exception): - """Raised when the command requires authentication first""" - pass - -class UninitializedSeedError(Exception): - """Raised when the device is not yet seeded""" - pass - -class UnexpectedSW12Error(Exception): - """Raised when the device returns an unexpected error code""" - pass - -if __name__ == "__main__": - - cardconnector= CardConnector() - cardconnector.card_get_ATR() - cardconnector.card_select() - #cardconnector.card_setup() - cardconnector.card_bip32_get_authentikey() - #cardconnector.card_bip32_get_extendedkey() - cardconnector.card_disconnect() diff --git a/plugins/satochip/CardDataParser.py b/plugins/satochip/CardDataParser.py deleted file mode 100644 index 3aec396ca673..000000000000 --- a/plugins/satochip/CardDataParser.py +++ /dev/null @@ -1,236 +0,0 @@ -""" - * Python API for the SatoChip Bitcoin Hardware Wallet - * (c) 2015 by Toporin - 16DMCk4WUaHofchAhpMaQS4UPm4urcy2dN - * Sources available on https://github.com/Toporin - * - * Copyright 2015 by Toporin (https://github.com/Toporin) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -""" -import hashlib -from electroncash.util import to_bytes, print_error -from electroncash.bitcoin import Hash - -from .ecc import ECPubkey, msg_magic, InvalidECPointException - -MSG_WARNING= ("Before you request bitcoins to be sent to addresses in this " - "wallet, ensure you can pair with your device, or that you have " - "its seed (and passphrase, if any). Otherwise all bitcoins you " - "receive will be unspendable.") - -class CardDataParser: - - def __init__(self): - self.authentikey=None - self.authentikey_coordx= None - self.authentikey_from_storage=None - - def parse_bip32_get_authentikey(self,response): - # response= [data_size | data | sig_size | signature] - # where data is coordx - data_size = ((response[0] & 0xff)<<8) + ((response[1] & 0xff)) - data= response[2:(2+data_size)] - msg_size= 2+data_size - msg= response[0:msg_size] - sig_size = ((response[msg_size] & 0xff)<<8) + ((response[msg_size+1] & 0xff)) - signature= response[(msg_size+2):(msg_size+2+sig_size)] - - if sig_size==0: - raise ValueError("Signature missing") - # self-signed - coordx=data - self.authentikey= self.get_pubkey_from_signature(coordx, msg, signature) - self.authentikey_coordx= coordx - - # if already initialized, check that authentikey match value retrieved from storage! - if (self.authentikey_from_storage is not None): - if self.authentikey != self.authentikey_from_storage: - raise ValueError("The seed used to create this wallet file no longer matches the seed of the Satochip device!\n\n"+MSG_WARNING) - - return self.authentikey - - def parse_bip32_import_seed(self,response): - # response= [data_size | data | sig_size | signature] - # where data is coordx - return self.parse_bip32_get_authentikey(response) - - def parse_bip32_get_extendedkey(self, response): - if self.authentikey is None: - raise ValueError("Authentikey not set!") - - # double signature: first is self-signed, second by authentikey - # firs self-signed sig: data= coordx - print_error('[CardDataParser] parse_bip32_get_extendedkey: first signature recovery') - self.chaincode= bytearray(response[0:32]) - data_size = ((response[32] & 0x7f)<<8) + (response[33] & 0xff) # (response[32] & 0x80) is ignored (optimization flag) - data= response[34:(32+2+data_size)] - msg_size= 32+2+data_size - msg= response[0:msg_size] - sig_size = ((response[msg_size] & 0xff)<<8) + (response[msg_size+1] & 0xff) - signature= response[(msg_size+2):(msg_size+2+sig_size)] - if sig_size==0: - raise ValueError("Signature missing") - # self-signed - coordx=data - self.pubkey= self.get_pubkey_from_signature(coordx, msg, signature) - self.pubkey_coordx= coordx - - # second signature by authentikey - print_error('[CardDataParser] parse_bip32_get_extendedkey: second signature recovery') - msg2_size= msg_size+2+sig_size - msg2= response[0:msg2_size] - sig2_size = ((response[msg2_size] & 0xff)<<8) + (response[msg2_size+1] & 0xff) - signature2= response[(msg2_size+2):(msg2_size+2+sig2_size)] - authentikey= self.get_pubkey_from_signature(self.authentikey_coordx, msg2, signature2) - if authentikey != self.authentikey: - raise ValueError("The seed used to create this wallet file no longer matches the seed of the Satochip device!\n\n"+MSG_WARNING) - - return (self.pubkey, self.chaincode) - - ############## - def parse_message_signature(self, response, message, pubkey): - - # Prepend the message for signing as done inside the card!! - message = to_bytes(message, 'utf8') - hash = Hash(msg_magic(message)) - coordx= pubkey.get_public_key_bytes() - - response= bytearray(response) - recid=-1 - for id in range(4): - compsig=self.parse_to_compact_sig(response, id, compressed=True) - # remove header byte - compsig2= compsig[1:] - - try: - pk = ECPubkey.from_sig_string(compsig2, id, hash) - pkbytes= pk.get_public_key_bytes(compressed=True) - except InvalidECPointException: - continue - - if coordx==pkbytes: - recid=id - break - - if recid == -1: - raise ValueError("Unable to recover public key from signature") - - return compsig - - ############## - - def get_pubkey_from_signature(self, coordx, data, sig): - data= bytearray(data) - sig= bytearray(sig) - coordx= bytearray(coordx) - - digest=hashlib.sha256() - digest.update(data) - hash=digest.digest() - - recid=-1 - pubkey=None - for id in range(4): - compsig=self.parse_to_compact_sig(sig, id, compressed=True) - # remove header byte - compsig= compsig[1:] - - try: - pk = ECPubkey.from_sig_string(compsig, id, hash) - pkbytes= pk.get_public_key_bytes(compressed=True) - except InvalidECPointException: - continue - - pkbytes= pkbytes[1:] - - if coordx==pkbytes: - recid=id - pubkey=pk - break - - if recid == -1: - raise ValueError("Unable to recover public key from signature") - - return pubkey - - ###### - - def parse_parse_transaction(self, response): - '''Satochip returns: [(hash_size+2)(2b) | tx_hash(32b) | need2fa(2b) | sig_size(2b) | sig(sig_size) | txcontext]''' - offset=0 - data_size= ((response[offset] & 0xff)<<8) + (response[offset+1] & 0xff) - txhash_size= data_size-2 - offset+=2 - tx_hash= response[offset:(offset+txhash_size)] - offset+=txhash_size - needs_2fa= ((response[offset] & 0xff)<<8) + (response[offset+1] & 0xff) - needs_2fa= False if (needs_2fa==0) else True - offset+=2 - sig_size= ((response[offset] & 0xff)<<8) + (response[offset+1] & 0xff) - sig_data= response[0:data_size+2] # txhash_size+hash+needs_2fa - offset+=2 - if sig_size>0 and self.authentikey_coordx: - sig= response[offset:(offset+sig_size)] - pubkey= self.get_pubkey_from_signature(self.authentikey_coordx, sig_data, sig) - if pubkey != self.authentikey: - raise Exception("signing key is not authentikey!") - #todo: error checking - - return (tx_hash, needs_2fa) - - def parse_to_compact_sig(self, sigin, recid, compressed): - ''' convert a DER encoded signature to compact 65-byte format - input is bytearray in DER format - output is bytearray in compact 65-byteformat - http://bitcoin.stackexchange.com/questions/12554/why-the-signature-is-always-65-13232-bytes-long - https://bitcointalk.org/index.php?topic=215205.0 - ''' - sigout= bytearray(65*[0]) - # parse input - first= sigin[0] - if first!= 0x30: - raise ValueError("Wrong first byte!") - lt= sigin[1] - check= sigin[2] - if check!= 0x02: - raise ValueError("Check byte should be 0x02") - # extract r - lr= sigin[3] - for i in range(32): - tmp= sigin[4+lr-1-i] - if lr>=(i+1): - sigout[32-i]= tmp - else: - sigout[32-i]=0 - # extract s - check= sigin[4+lr]; - if check!= 0x02: - raise ValueError("Second check byte should be 0x02") - ls= sigin[5+lr] - if lt != (lr+ls+4): - raise ValueError("Wrong lt value") - for i in range(32): - tmp= sigin[5+lr+ls-i] - if ls>=(i+1): - sigout[64-i]= tmp; - else: - sigout[32-i]=0; - # 1 byte header - if recid>3 or recid<0: - raise ValueError("Wrong recid value") - if compressed: - sigout[0]= 27 + recid + 4 - else: - sigout[0]= 27 + recid - - return sigout; diff --git a/plugins/satochip/JCconstants.py b/plugins/satochip/JCconstants.py deleted file mode 100644 index fdebbf69cae3..000000000000 --- a/plugins/satochip/JCconstants.py +++ /dev/null @@ -1,264 +0,0 @@ -""" - * Python API for the SatoChip Bitcoin Hardware Wallet - * (c) 2015 by Toporin - 16DMCk4WUaHofchAhpMaQS4UPm4urcy2dN - * Sources available on https:#github.com/Toporin - * - * Copyright 2015 by Toporin (https:#github.com/Toporin) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http:#www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -""" - -class JCconstants: - - #Maximum number of keys handled by the Cardlet - MAX_NUM_KEYS = 0x10; - # Maximum number of PIN codes - MAX_NUM_PINS = 0x8; - # Maximum number of keys allowed for ExtAuth - MAX_NUM_AUTH_KEYS = 0x6; - - # Maximum size for the extended APDU buffer for a 2048 bit key: - # CLA [1 byte] + INS [1 byte] + P1 [1 byte] + P2 [1 byte] + - # LC [3 bytes] + cipher_mode[1 byte] + cipher_direction [1 byte] + - # data_location [1 byte] + data_size [2 bytes] + data [256 bytes] - # = 268 bytes - EXT_APDU_BUFFER_SIZE = 268; - - # Minimum PIN size - PIN_MIN_SIZE = 4; - # Maximum PIN size - PIN_MAX_SIZE = 16; - # PIN[0] initial value... - PIN_INIT_VALUE=[0x4D, 0x75, 0x73, 0x63, 0x6C, 0x65, 0x30, 0x30] #default pin - - # Maximum external authentication tries per key - MAX_KEY_TRIES = 5; - - # Import/Export Object ID - IN_OBJECT_CLA = 0xFFFF; - IN_OBJECT_ID = 0xFFFE; - OUT_OBJECT_CLA = 0xFFFF; - OUT_OBJECT_ID = 0xFFFF; - - KEY_ACL_SIZE = 6; - ACL_READ = 0; - ACL_WRITE = 2; - ACL_USE = 4; - DEFAULT_ACL= [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - - # code of CLA byte in the command APDU header - CardEdge_CLA = 0xB0 - - '''**************************************** - * Instruction codes * - ****************************************''' - - # Applet initialization - INS_SETUP = 0x2A; - - # Keys' use and management - INS_GEN_KEYPAIR = 0x30; - INS_GEN_KEYSYM = 0x31; - INS_IMPORT_KEY = 0x32; - INS_EXPORT_KEY = 0x34; - INS_GET_PUBLIC_FROM_PRIVATE= 0x35; - INS_COMPUTE_CRYPT = 0x36; - INS_COMPUTE_SIGN = 0x37; # added - - # External authentication - INS_CREATE_PIN = 0x40; - INS_VERIFY_PIN = 0x42; - INS_CHANGE_PIN = 0x44; - INS_UNBLOCK_PIN = 0x46; - INS_LOGOUT_ALL = 0x60; - INS_GET_CHALLENGE = 0x62; - INS_EXT_AUTH = 0x38; - - # Objects' use and management - INS_CREATE_OBJ = 0x5A; - INS_DELETE_OBJ = 0x52; - INS_READ_OBJ = 0x56; - INS_WRITE_OBJ = 0x54; - INS_SIZE_OBJ = 0x57; - - # Status information - INS_LIST_OBJECTS = 0x58; - INS_LIST_PINS = 0x48; - INS_LIST_KEYS = 0x3A; - INS_GET_STATUS = 0x3C; - - # HD wallet - INS_COMPUTE_SHA512 = 0x6A; - INS_COMPUTE_HMACSHA512= 0x6B; - INS_BIP32_IMPORT_SEED= 0x6C; - INS_BIP32_RESET_SEED= 0x77; - INS_BIP32_GET_AUTHENTIKEY= 0x73; - INS_BIP32_GET_EXTENDED_KEY= 0x6D; - INS_SIGN_MESSAGE= 0x6E; - INS_SIGN_SHORT_MESSAGE= 0x72; - INS_SIGN_TRANSACTION= 0x6F; - INS_BIP32_SET_EXTENDED_KEY= 0x70; - INS_PARSE_TRANSACTION = 0x71; - - # 2FA - INS_SET_2FA_KEY = 0x79; - - '''**************************************** - * Error codes * - ****************************************''' - #o error! - SW_OK = 0x9000; - # There have been memory problems on the card - SW_NO_MEMORY_LEFT = 0x9c01; - # Entered PIN is not correct */ - SW_AUTH_FAILED = 0x9C02; - # Required operation is not allowed in actual circumstances - SW_OPERATION_NOT_ALLOWED = 0x9C03; - # Required setup is not not done */ - SW_SETUP_NOT_DONE = 0x9C04; - # Required feature is not (yet) supported */ - SW_UNSUPPORTED_FEATURE = 0x9C05; - # Required operation was not authorized because of a lack of privileges */ - SW_UNAUTHORIZED = 0x9C06; - # Required object is missing */ - SW_OBJECT_NOT_FOUND = 0x9C07; - # New object ID already in use */ - SW_OBJECT_EXISTS = 0x9C08; - # Algorithm specified is not correct */ - SW_INCORRECT_ALG = 0x9C09; - - # Incorrect P1 parameter */ - SW_INCORRECT_P1 = 0x9C10; - # Incorrect P2 parameter */ - SW_INCORRECT_P2 = 0x9C11; - # No more data available */ - SW_SEQUENCE_END = 0x9C12; - # Invalid input parameter to command */ - SW_INVALID_PARAMETER = 0x9C0F; - - # Verify operation detected an invalid signature */ - SW_SIGNATURE_INVALID = 0x9C0B; - # Operation has been blocked for security reason */ - SW_IDENTITY_BLOCKED = 0x9C0C; - # Unspecified error */ - SW_UNSPECIFIED_ERROR = 0x9C0D; - # For debugging purposes */ - SW_INTERNAL_ERROR = 0x9CFF; - # For debugging purposes 2*/ - SW_DEBUG_FLAG = 0x9FFF; - # Very low probability error */ - SW_BIP32_DERIVATION_ERROR = 0x9C0E; - # Support only hardened key currently */ - SW_BIP32_HARDENED_KEY_ERROR = 0x9C16; - # Incorrect initialization of method */ - SW_INCORRECT_INITIALIZATION = 0x9C13; - # Bip32 seed is not initialized*/ - SW_BIP32_UNINITIALIZED_SEED = 0x9C14; - # Incorrect transaction hash */ - SW_INCORRECT_TXHASH = 0x9C15; - - '''**************************************** - * Algorithm codes * - ****************************************''' - - # Algorithm Type in APDUs - ALG_RSA = 0x01; #KeyPair.ALG_RSA; - ALG_RSA_CRT = 0x02; #KeyPair.ALG_RSA_CRT; - ALG_EC_FP = 0x05; #KeyPair.ALG_EC_FP; - - # Key Type in Key Blobs - TYPE_RSA_PUBLIC = 4; #KeyBuilder.TYPE_RSA_PUBLIC; - TYPE_RSA_PRIVATE = 5; #KeyBuilder.TYPE_RSA_PRIVATE; - TYPE_RSA_CRT_PRIVATE = 6; #KeyBuilder.TYPE_RSA_CRT_PRIVATE; - TYPE_EC_FP_PUBLIC = 11; #KeyBuilder.TYPE_EC_FP_PUBLIC; - TYPE_EC_FP_PRIVATE = 12; #KeyBuilder.TYPE_EC_FP_PRIVATE; - TYPE_DES = 3; #KeyBuilder.TYPE_DES; - TYPE_AES=15; #KeyBuilder.TYPE_AES; - - # KeyBlob Encoding in Key Blobs - BLOB_ENC_PLAIN = 0x00; - - # Cipher Operations admitted in ComputeCrypt() - OP_INIT = 0x01; - OP_PROCESS = 0x02; - OP_FINALIZE = 0x03; - - # Cipher Directions admitted in ComputeCrypt() - MODE_SIGN = 0x01; #Signature.MODE_SIGN; - MODE_VERIFY = 0x02; #Signature.MODE_VERIFY; - MODE_ENCRYPT = 0x02; #Cipher.MODE_ENCRYPT; - MODE_DECRYPT = 0x01; #Cipher.MODE_DECRYPT; - - # Cipher Modes admitted in ComputeCrypt() - ALG_RSA_NOPAD = 12; #Cipher.ALG_RSA_NOPAD; # 0x00; - ALG_RSA_PKCS1 = 10; #Cipher.ALG_RSA_PKCS1; # 0x01; - ALG_DES_CBC_NOPAD = 1; #Cipher.ALG_DES_CBC_NOPAD; # 0x20; - ALG_DES_ECB_NOPAD = 5; #Cipher.ALG_DES_ECB_NOPAD; # 0x21; - ALG_AES_BLOCK_128_CBC_NOPAD = 13; #Cipher.ALG_AES_BLOCK_128_CBC_NOPAD; - ALG_AES_BLOCK_128_ECB_NOPAD = 14; #Cipher.ALG_AES_BLOCK_128_ECB_NOPAD; - ALG_ECDSA_SHA = 17; #Signature.ALG_ECDSA_SHA;# 0x30; - ALG_ECDSA_SHA_256 = 33; #Bitcoin (Signature.ALG_ECDSA_SHA256==33) https:#javacard.kenai.com/javadocs/classic/javacard/security/Signature.html#ALG_ECDSA_SHA_256 - - DL_APDU = 0x01; - DL_OBJECT = 0x02; - LIST_OPT_RESET = 0x00; - LIST_OPT_NEXT = 0x01; - - OPT_DEFAULT = 0x00; # Use JC defaults - OPT_RSA_PUB_EXP = 0x01; # RSA: provide public exponent - OPT_EC_SECP256k1 = 0x03; # EC: provide P, a, b, G, R, K public key parameters - - # Offsets in buffer[] for key generation - OFFSET_GENKEY_ALG = 0; - OFFSET_GENKEY_SIZE = (OFFSET_GENKEY_ALG + 1); - OFFSET_GENKEY_PRV_ACL = (OFFSET_GENKEY_SIZE + 2); - OFFSET_GENKEY_PUB_ACL = (OFFSET_GENKEY_PRV_ACL + KEY_ACL_SIZE); - OFFSET_GENKEY_OPTIONS = (OFFSET_GENKEY_PUB_ACL + KEY_ACL_SIZE); - OFFSET_GENKEY_RSA_PUB_EXP_LENGTH = (OFFSET_GENKEY_OPTIONS + 1); - OFFSET_GENKEY_RSA_PUB_EXP_VALUE = (OFFSET_GENKEY_RSA_PUB_EXP_LENGTH + 2); - - # JC API 2.2.2 does not define this constant: - ALG_EC_SVDP_DH_PLAIN= 3; #https:#javacard.kenai.com/javadocs/connected/javacard/security/KeyAgreement.html#ALG_EC_SVDP_DH_PLAIN - LENGTH_EC_FP_256= 256; - - #Satochip: default parameters for EC curve secp256k1 - SECP256K1_P =[ - 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFE, 0xFF,0xFF,0xFC,0x2F]; - SECP256K1_a = [ - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00]; - SECP256K1_b = [ - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x07]; - SECP256K1_G = [0x04, #base point, uncompressed form - 0x79,0xBE,0x66,0x7E, 0xF9,0xDC,0xBB,0xAC, - 0x55,0xA0,0x62,0x95, 0xCE,0x87,0x0B,0x07, - 0x02,0x9B,0xFC,0xDB, 0x2D,0xCE,0x28,0xD9, - 0x59,0xF2,0x81,0x5B, 0x16,0xF8,0x17,0x98, - 0x48,0x3A,0xDA,0x77, 0x26,0xA3,0xC4,0x65, - 0x5D,0xA4,0xFB,0xFC, 0x0E,0x11,0x08,0xA8, - 0xFD,0x17,0xB4,0x48, 0xA6,0x85,0x54,0x19, - 0x9C,0x47,0xD0,0x8F, 0xFB,0x10,0xD4,0xB8]; - SECP256K1_R = [ - 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, # order of G - 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFE, - 0xBA,0xAE,0xDC,0xE6, 0xAF,0x48,0xA0,0x3B, - 0xBF,0xD2,0x5E,0x8C, 0xD0,0x36,0x41,0x41]; - SECP256K1_K = 0x01; # cofactor diff --git a/plugins/satochip/README.rst b/plugins/satochip/README.rst index d2c802c06c0b..4fd0939351d5 100644 --- a/plugins/satochip/README.rst +++ b/plugins/satochip/README.rst @@ -14,7 +14,7 @@ Introduction This is a fork of Electron Cash modified for use with the Satochip Hardware Wallet. To use it, you need a device with the Satochip Javacard Applet installed. If the wallet is not intialized yet, Electron Cash will perform the setup (you only need to do this once). During setup, a seed is created: this seed allows you to recover your wallet at anytime, so make sure to BACKUP THE SEED SECURELY! During setup, a PIN code is also created: this PIN allows to unlock th device to access your funds. If you try too many wrong PIN, your device will be locked indefinitely (it is 'bricked'). If you loose your PIN or brick your device, you can only recover your funds with the seed backup. -The Satochip wallet is currently in Beta, use with caution! In this phase, it is recommended to use the software on the Bitcoin testnet only. +The Satochip wallet is currently in Beta, use with caution!You can use the software on the Bitcoin testnet using the --testnet option. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Rem: Electron Cash uses Python 3.x. In case of error, check first that you are not trying to run Electron Cash with Python 2.x or with Python 2.x libraries. @@ -80,7 +80,7 @@ Pyscard is required to connect to the smartcard:: (For alternatives, see https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md for more detailed installation instructions) -To run Electron Cash use:: +To run Electron Cash on the testnet use:: python3 electron-cash -v --testnet diff --git a/plugins/satochip/TxParser.py b/plugins/satochip/TxParser.py deleted file mode 100644 index 22374398230d..000000000000 --- a/plugins/satochip/TxParser.py +++ /dev/null @@ -1,274 +0,0 @@ -''' - * Python API for the SatoChip Bitcoin Hardware Wallet - * (c) 2015 by Toporin - 16DMCk4WUaHofchAhpMaQS4UPm4urcy2dN - * Sources available on https://github.com/Toporin - * - * Copyright 2015 by Toporin (https://github.com/Toporin) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -''' -import hashlib -from enum import Enum, auto -from electroncash.util import print_stderr, print_error -import time #debugSatochip - -class TxState(Enum): - TX_START= auto() - TX_PARSE_INPUT= auto() - TX_PARSE_INPUT_SCRIPT= auto() - TX_PARSE_OUTPUT= auto() - TX_PARSE_OUTPUT_SCRIPT= auto() - TX_PARSE_FINALIZE= auto() - TX_END= auto() - -class TxParser: - CHUNK_SIZE=128 # max chunk size of a script - - def __init__(self, rawTx): - self.txData= rawTx[:] - - self.txRemainingInput=0 - self.txCurrentInput=0 - self.txRemainingOutput=0 - self.txCurrentOutput=0 - self.txAmount=0 - self.txScriptRemaining=0 - self.txOffset=0 - self.txRemaining=len(rawTx) - self.txState= TxState.TX_START - - self.txDigest=hashlib.sha256() - self.singleHash=None - self.doubleHash=None - - self.txChunk=b'' - - #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 - - def is_parsed(self): - return (self.txRemaining==0) - - def get_tx_hash(self): - return self.singleHash - - def get_tx_double_hash(self): - return self.doubleHash - - def parse_transaction(self): - self.txChunk=b'' - if self.txState == TxState.TX_START: - - # max 4+9 bytes accumulated - self.parse_byte(4) # version - self.txRemainingInput= self.parse_var_int() - self.txState= TxState.TX_PARSE_INPUT - - if self.txState == TxState.TX_PARSE_INPUT: - # max 36+9 bytes accumulated - if self.txRemainingInput==0: - self.txRemainingOutput= self.parse_var_int() - self.txState= TxState.TX_PARSE_OUTPUT - #break - else: - self.parse_byte(32); # txOutHash - self.parse_byte(4); # txOutIndex - self.txScriptRemaining= self.parse_var_int(); - self.txState= TxState.TX_PARSE_INPUT_SCRIPT - self.txRemainingInput-=1 - self.txCurrentInput+=1 - #break - - elif self.txState == TxState.TX_PARSE_INPUT_SCRIPT: - # max MAX_CHUNK_SIZE+4 bytes accumulated - chunkSize= self.txScriptRemaining if self.txScriptRemaining int: - curve = curve_secp256k1 - _p = curve.p() - _a = curve.a() - _b = curve.b() - x = x % _p - y2 = (pow(x, 3, _p) + _a * x + _b) % _p - y = msqr.modular_sqrt(y2, _p) - if curve.contains_point(x, y): - if odd == bool(y & 1): - return y - return _p - y - raise InvalidECPointException() - - -def ser_to_point(ser: bytes) -> Tuple[int, int]: - if ser[0] not in (0x02, 0x03, 0x04): - raise ValueError('Unexpected first byte: {}'.format(ser[0])) - if ser[0] == 0x04: - return string_to_number(ser[1:33]), string_to_number(ser[33:]) - x = string_to_number(ser[1:]) - return x, get_y_coord_from_x(x, ser[0] == 0x03) - - -def _ser_to_python_ecdsa_point(ser: bytes) -> ecdsa.ellipticcurve.Point: - x, y = ser_to_point(ser) - try: - return Point(curve_secp256k1, x, y, CURVE_ORDER) - except: - raise InvalidECPointException() - - -class InvalidECPointException(Exception): - """e.g. not on curve, or infinity""" - - -class _MyVerifyingKey(ecdsa.VerifyingKey): - @classmethod - def from_signature(klass, sig, recid, h, curve): # TODO use libsecp?? - """ See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """ - from ecdsa import util, numbertheory - from electroncash import msqr - curveFp = curve.curve - G = curve.generator - order = G.order() - # extract r,s from signature - r, s = util.sigdecode_string(sig, order) - # 1.1 - x = r + (recid//2) * order - # 1.3 - alpha = ( x * x * x + curveFp.a() * x + curveFp.b() ) % curveFp.p() - beta = msqr.modular_sqrt(alpha, curveFp.p()) - y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta - # 1.4 the constructor checks that nR is at infinity - try: - R = Point(curveFp, x, y, order) - except: - raise InvalidECPointException() - # 1.5 compute e from message: - e = string_to_number(h) - minus_e = -e % order - # 1.6 compute Q = r^-1 (sR - eG) - inv_r = numbertheory.inverse_mod(r,order) - try: - Q = inv_r * ( s * R + minus_e * G ) - except: - raise InvalidECPointException() - return klass.from_public_point( Q, curve ) - - -class _MySigningKey(ecdsa.SigningKey): - """Enforce low S values in signatures""" - - def sign_number(self, number, entropy=None, k=None): - r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k) - if s > CURVE_ORDER//2: - s = CURVE_ORDER - s - return r, s - - -class _PubkeyForPointAtInfinity: - point = ecdsa.ellipticcurve.INFINITY - - -class ECPubkey(object): - - def __init__(self, b: bytes): - if b is not None: - assert_bytes(b) - point = _ser_to_python_ecdsa_point(b) - self._pubkey = ecdsa.ecdsa.Public_key(generator_secp256k1, point) - else: - self._pubkey = _PubkeyForPointAtInfinity() - - @classmethod - def from_sig_string(cls, sig_string: bytes, recid: int, msg_hash: bytes): - assert_bytes(sig_string) - if len(sig_string) != 64: - raise Exception('Wrong encoding') - if recid < 0 or recid > 3: - raise ValueError('recid is {}, but should be 0 <= recid <= 3'.format(recid)) - ecdsa_verifying_key = _MyVerifyingKey.from_signature(sig_string, recid, msg_hash, curve=SECP256k1) - ecdsa_point = ecdsa_verifying_key.pubkey.point - return ECPubkey.from_point(ecdsa_point) - - @classmethod - def from_signature65(cls, sig: bytes, msg_hash: bytes): - if len(sig) != 65: - raise Exception("Wrong encoding") - nV = sig[0] - if nV < 27 or nV >= 35: - raise Exception("Bad encoding") - if nV >= 31: - compressed = True - nV -= 4 - else: - compressed = False - recid = nV - 27 - return cls.from_sig_string(sig[1:], recid, msg_hash), compressed - - @classmethod - def from_point(cls, point): - _bytes = point_to_ser(point, compressed=False) # faster than compressed - return ECPubkey(_bytes) - - def get_public_key_bytes(self, compressed=True): - if self.is_at_infinity(): raise Exception('point is at infinity') - return point_to_ser(self.point(), compressed) - - def get_public_key_hex(self, compressed=True): - return bh2u(self.get_public_key_bytes(compressed)) - - def point(self) -> Tuple[int, int]: - return self._pubkey.point.x(), self._pubkey.point.y() - - def __mul__(self, other: int): - if not isinstance(other, int): - raise TypeError('multiplication not defined for ECPubkey and {}'.format(type(other))) - ecdsa_point = self._pubkey.point * other - return self.from_point(ecdsa_point) - - def __rmul__(self, other: int): - return self * other - - def __add__(self, other): - if not isinstance(other, ECPubkey): - raise TypeError('addition not defined for ECPubkey and {}'.format(type(other))) - ecdsa_point = self._pubkey.point + other._pubkey.point - return self.from_point(ecdsa_point) - - def __eq__(self, other): - return self._pubkey.point.x() == other._pubkey.point.x() \ - and self._pubkey.point.y() == other._pubkey.point.y() - - def __ne__(self, other): - return not (self == other) - - def verify_message_for_address(self, sig65: bytes, message: bytes) -> None: - assert_bytes(message) - h = Hash(msg_magic(message)) - public_key, compressed = self.from_signature65(sig65, h) - # check public key - if public_key != self: - raise Exception("Bad signature") - # check message - self.verify_message_hash(sig65[1:], h) - - def verify_message_hash(self, sig_string: bytes, msg_hash: bytes) -> None: - assert_bytes(sig_string) - if len(sig_string) != 64: - raise Exception('Wrong encoding') - ecdsa_point = self._pubkey.point - verifying_key = _MyVerifyingKey.from_public_point(ecdsa_point, curve=SECP256k1) - verifying_key.verify_digest(sig_string, msg_hash, sigdecode=ecdsa.util.sigdecode_string) - - def encrypt_message(self, message: bytes, magic: bytes = b'BIE1'): - """ - ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac - """ - assert_bytes(message) - - randint = ecdsa.util.randrange(CURVE_ORDER) - ephemeral_exponent = number_to_string(randint, CURVE_ORDER) - ephemeral = ECPrivkey(ephemeral_exponent) - ecdh_key = (self * ephemeral.secret_scalar).get_public_key_bytes(compressed=True) - key = hashlib.sha512(ecdh_key).digest() - iv, key_e, key_m = key[0:16], key[16:32], key[32:] - ciphertext = aes_encrypt_with_iv(key_e, iv, message) - ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True) - encrypted = magic + ephemeral_pubkey + ciphertext - mac = hmac_oneshot(key_m, encrypted, hashlib.sha256) - - return base64.b64encode(encrypted + mac) - - @classmethod - def order(cls): - return CURVE_ORDER - - def is_at_infinity(self): - return self == point_at_infinity() - - @classmethod - def is_pubkey_bytes(cls, b: bytes): - try: - ECPubkey(b) - return True - except: - return False - - -def msg_magic(message: bytes) -> bytes: - from electroncash.bitcoin import var_int - length = bfh(var_int(len(message))) - return b"\x18Bitcoin Signed Message:\n" + length + message - - -def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, net=None): - from electroncash.bitcoin import pubkey_to_address - assert_bytes(sig65, message) - if net is None: net = constants.net - try: - h = Hash(msg_magic(message)) - public_key, compressed = ECPubkey.from_signature65(sig65, h) - # check public key using the address - pubkey_hex = public_key.get_public_key_hex(compressed) - for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']: - addr = pubkey_to_address(txin_type, pubkey_hex, net=net) - if address == addr: - break - else: - raise Exception("Bad signature") - # check message - public_key.verify_message_hash(sig65[1:], h) - return True - except Exception as e: - print_error(f"Verification error: {repr(e)}") - return False - - -def is_secret_within_curve_range(secret: Union[int, bytes]) -> bool: - if isinstance(secret, bytes): - secret = string_to_number(secret) - return 0 < secret < CURVE_ORDER - - -class ECPrivkey(ECPubkey): - - def __init__(self, privkey_bytes: bytes): - assert_bytes(privkey_bytes) - if len(privkey_bytes) != 32: - raise Exception('unexpected size for secret. should be 32 bytes, not {}'.format(len(privkey_bytes))) - secret = string_to_number(privkey_bytes) - if not is_secret_within_curve_range(secret): - raise InvalidECPointException('Invalid secret scalar (not within curve order)') - self.secret_scalar = secret - - point = generator_secp256k1 * secret - super().__init__(point_to_ser(point)) - self._privkey = ecdsa.ecdsa.Private_key(self._pubkey, secret) - - @classmethod - def from_secret_scalar(cls, secret_scalar: int): - secret_bytes = number_to_string(secret_scalar, CURVE_ORDER) - return ECPrivkey(secret_bytes) - - @classmethod - def from_arbitrary_size_secret(cls, privkey_bytes: bytes): - """This method is only for legacy reasons. Do not introduce new code that uses it. - Unlike the default constructor, this method does not require len(privkey_bytes) == 32, - and the secret does not need to be within the curve order either. - """ - return ECPrivkey(cls.normalize_secret_bytes(privkey_bytes)) - - @classmethod - def normalize_secret_bytes(cls, privkey_bytes: bytes) -> bytes: - scalar = string_to_number(privkey_bytes) % CURVE_ORDER - if scalar == 0: - raise Exception('invalid EC private key scalar: zero') - privkey_32bytes = number_to_string(scalar, CURVE_ORDER) - return privkey_32bytes - - def sign(self, data: bytes, sigencode=None, sigdecode=None) -> bytes: - if sigencode is None: - sigencode = sig_string_from_r_and_s - if sigdecode is None: - sigdecode = get_r_and_s_from_sig_string - private_key = _MySigningKey.from_secret_exponent(self.secret_scalar, curve=SECP256k1) - sig = private_key.sign_digest_deterministic(data, hashfunc=hashlib.sha256, sigencode=sigencode) - public_key = private_key.get_verifying_key() - if not public_key.verify_digest(sig, data, sigdecode=sigdecode): - raise Exception('Sanity check verifying our own signature failed.') - return sig - - def sign_transaction(self, hashed_preimage: bytes) -> bytes: - return self.sign(hashed_preimage, - sigencode=der_sig_from_r_and_s, - sigdecode=get_r_and_s_from_der_sig) - - def sign_message(self, message: bytes, is_compressed: bool) -> bytes: - def bruteforce_recid(sig_string): - for recid in range(4): - sig65 = construct_sig65(sig_string, recid, is_compressed) - try: - self.verify_message_for_address(sig65, message) - return sig65, recid - except Exception as e: - continue - else: - raise Exception("error: cannot sign message. no recid fits..") - - message = to_bytes(message, 'utf8') - msg_hash = Hash(msg_magic(message)) - sig_string = self.sign(msg_hash, - sigencode=sig_string_from_r_and_s, - sigdecode=get_r_and_s_from_sig_string) - sig65, recid = bruteforce_recid(sig_string) - return sig65 - - def decrypt_message(self, encrypted: Tuple[str, bytes], magic: bytes=b'BIE1') -> bytes: - encrypted = base64.b64decode(encrypted) - if len(encrypted) < 85: - raise Exception('invalid ciphertext: length') - magic_found = encrypted[:4] - ephemeral_pubkey_bytes = encrypted[4:37] - ciphertext = encrypted[37:-32] - mac = encrypted[-32:] - if magic_found != magic: - raise Exception('invalid ciphertext: invalid magic bytes') - try: - ecdsa_point = _ser_to_python_ecdsa_point(ephemeral_pubkey_bytes) - except AssertionError as e: - raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e - if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ecdsa_point.x(), ecdsa_point.y()): - raise Exception('invalid ciphertext: invalid ephemeral pubkey') - ephemeral_pubkey = ECPubkey.from_point(ecdsa_point) - ecdh_key = (ephemeral_pubkey * self.secret_scalar).get_public_key_bytes(compressed=True) - key = hashlib.sha512(ecdh_key).digest() - iv, key_e, key_m = key[0:16], key[16:32], key[32:] - if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256): - raise InvalidPassword() - return aes_decrypt_with_iv(key_e, iv, ciphertext) - - -def construct_sig65(sig_string: bytes, recid: int, is_compressed: bool) -> bytes: - comp = 4 if is_compressed else 0 - return bytes([27 + recid + comp]) + sig_string diff --git a/plugins/satochip/qt.py b/plugins/satochip/qt.py index 32169a12fd9d..571792f48647 100644 --- a/plugins/satochip/qt.py +++ b/plugins/satochip/qt.py @@ -1,18 +1,20 @@ from electroncash.i18n import _ from electroncash.util import print_error from electroncash.plugins import run_hook -from electroncash_gui.qt.util import EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel #from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, WindowModalDialog) +from electroncash_gui.qt.util import EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout, QLineEdit, QCheckBox - from functools import partial from ..hw_wallet.qt import QtHandlerBase, QtPluginBase #satochip from .satochip import SatochipPlugin -from .CardConnector import CardConnector +#pysatochip +from pysatochip.CardConnector import CardConnector +from pysatochip.Satochip2FA import Satochip2FA +from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION class Plugin(SatochipPlugin, QtPluginBase): # icon_unpaired = "satochip_unpaired.png" @@ -59,6 +61,7 @@ def __init__(self, win): class SatochipSettingsDialog(WindowModalDialog): '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten their PIN.''' @@ -97,6 +100,7 @@ def connect_and_doit(): ('sw_version', _("Electrum Support")), ('is_seeded', _("Wallet seeded")), ('needs_2FA', _("Requires 2FA ")), + ('needs_SC', _("Secure Channel")), ] for row_num, (member_name, label) in enumerate(rows): widget = QLabel('') @@ -135,78 +139,69 @@ def _reset_seed(): def show_values(self, client): print_error("Show value!") - v_supported= (CardConnector.SATOCHIP_PROTOCOL_MAJOR_VERSION<<8)+CardConnector.SATOCHIP_PROTOCOL_MINOR_VERSION - sw_rel= hex(v_supported) + sw_rel= 'v' + str(SATOCHIP_PROTOCOL_MAJOR_VERSION) + '.' + str(SATOCHIP_PROTOCOL_MINOR_VERSION) self.sw_version.setText('%s' % sw_rel) (response, sw1, sw2, d)=client.cc.card_get_status() if (sw1==0x90 and sw2==0x00): - v_applet= (d["protocol_major_version"]<<8)+d["protocol_minor_version"] - fw_rel= hex(v_applet) + fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) self.fw_version.setText('%s' % fw_rel) #is_seeded? - try: - client.cc.card_bip32_get_authentikey() - self.is_seeded.setText('%s' % "yes") - except Exception: - self.is_seeded.setText('%s' % "no") + if len(response) >=10: + self.is_seeded.setText('%s' % "yes") if d["is_seeded"] else self.is_seeded.setText('%s' % "no") + else: #for earlier versions + try: + client.cc.card_bip32_get_authentikey() + self.is_seeded.setText('%s' % "yes") + except Exception: + self.is_seeded.setText('%s' % "no") # needs2FA? - if len(response)>=9 and response[8]==0X01: + if d["needs2FA"]: self.needs_2FA.setText('%s' % "yes") - elif len(response)>=9 and response[8]==0X00: + else: self.needs_2FA.setText('%s' % "no") + + # needs secure channel + if d["needs_secure_channel"]: + self.needs_SC.setText('%s' % "yes") else: - self.needs_2FA.setText('%s' % "(unknown)") + self.needs_SC.setText('%s' % "no") else: fw_rel= "(unitialized)" self.fw_version.setText('%s' % fw_rel) self.needs_2FA.setText('%s' % "(unitialized)") self.is_seeded.setText('%s' % "no") + self.needs_SC.setText('%s' % "(unknown)") + def change_pin(self, client): - # old pin - msg = _("Enter the current PIN for your Satochip:") - (is_PIN, oldpin, oldpin)= client.PIN_dialog(msg) - if (not is_PIN): - msg= _("PIN change cancelled!") - client.handler.show_error(msg) + print_error("In change_pin") + msg_oldpin = _("Enter the current PIN for your Satochip:") + msg_newpin = _("Enter a new PIN for your Satochip:") + msg_confirm = _("Please confirm the new PIN for your Satochip:") + msg_error= _("The PIN values do not match! Please type PIN again!") + msg_cancel= _("PIN Change cancelled!") + (is_pin, oldpin, newpin) = client.PIN_change_dialog(msg_oldpin, msg_newpin, msg_confirm, msg_error, msg_cancel) + if (not is_pin): return - # new pin - while (True): - msg = _("Enter a new PIN for your Satochip:") - (is_PIN, newpin, newpin)= client.PIN_dialog(msg) - if (not is_PIN): - msg= _("PIN change cancelled!") - client.handler.show_error(msg) - return - msg = _("Please confirm the new PIN for your Satochip:") - (is_PIN, pin_confirm, pin_confirm)= client.PIN_dialog(msg) - if (not is_PIN): - msg= _("PIN change cancelled!") - client.handler.show_error(msg) - return - if (newpin != pin_confirm): - msg= _("The PIN values do not match! Please type PIN again!") - client.handler.show_error(msg) - else: - break - - oldpin= list(oldpin) - newpin= list(newpin) + + oldpin= list(oldpin) + newpin= list(newpin) (response, sw1, sw2)= client.cc.card_change_PIN(0, oldpin, newpin) if (sw1==0x90 and sw2==0x00): msg= _("PIN changed successfully!") - client.handler.show_error(msg) + client.handler.show_message(msg) else: msg= _("Failed to change PIN!") client.handler.show_error(msg) def reset_seed(self, client): + print_error("In reset_seed") # pin msg = ''.join([ _("WARNING!\n"), @@ -214,10 +209,8 @@ def reset_seed(self, client): _("Please be sure that your wallet is empty and that you have a backup of the seed as a precaution.\n\n"), _("To proceed, enter the PIN for your Satochip:") ]) - (is_password, password, reset_2FA)= self.reset_seed_dialog(msg) - if (not is_password): - msg= _("Seed reset cancelled!") - client.handler.show_error(msg) + (password, reset_2FA)= self.reset_seed_dialog(msg) + if (password is None): return pin = password.encode('utf8') pin= list(pin) @@ -239,11 +232,11 @@ def reset_seed(self, client): d['msg_encrypt']= msg_out d['id_2FA']= id_2FA # print_error("encrypted message: "+msg_out) - print_error("id_2FA: "+id_2FA) + #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) + Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: reply_encrypt= d['reply_encrypt'] @@ -259,7 +252,7 @@ def reset_seed(self, client): (response, sw1, sw2) = client.cc.card_reset_seed(pin, hmac) if (sw1==0x90 and sw2==0x00): msg= _("Seed reset successfully!\nYou should close this wallet and launch the wizard to generate a new wallet.") - client.handler.show_error(msg) + client.handler.show_message(msg) #to do: close client? else: msg= _(f"Failed to reset seed with error code: {hex(sw1)}{hex(sw2)}") @@ -280,7 +273,7 @@ def reset_seed(self, client): #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) + Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: reply_encrypt= d['reply_encrypt'] @@ -297,12 +290,13 @@ def reset_seed(self, client): if (sw1==0x90 and sw2==0x00): msg= _("2FA reset successfully!") client.cc.needs_2FA= False - client.handler.show_error(msg) + client.handler.show_message(msg) else: msg= _(f"Failed to reset 2FA with error code: {hex(sw1)}{hex(sw2)}") client.handler.show_error(msg) def reset_seed_dialog(self, msg): + print_error("In reset_seed_dialog") parent = self.top_level_window() d = WindowModalDialog(parent, _("Enter PIN")) pw = QLineEdit() @@ -319,7 +313,8 @@ def reset_seed_dialog(self, msg): d.setLayout(vbox) passphrase = pw.text() if d.exec_() else None - if passphrase is None: - return (False, None, None) + reset_2FA= cb_reset_2FA.isChecked() - return (True, passphrase, reset_2FA) + return (passphrase, reset_2FA) + + \ No newline at end of file diff --git a/plugins/satochip/satochip.py b/plugins/satochip/satochip.py index 3bc5e138c587..4563603a3e1f 100644 --- a/plugins/satochip/satochip.py +++ b/plugins/satochip/satochip.py @@ -2,6 +2,7 @@ from os import urandom import hashlib import traceback +import logging #electroncash from electroncash import bitcoin @@ -12,7 +13,7 @@ from electroncash.keystore import Hardware_KeyStore from electroncash.transaction import Transaction from electroncash.wallet import Standard_Wallet -from electroncash.util import print_error, bfh, bh2u, versiontuple, PrintError +from electroncash.util import print_error, bfh, bh2u, versiontuple, PrintError, is_verbose from electroncash.bitcoin import hash_160, Hash from electroncash.mnemonic import Mnemonic, Mnemonic_Electrum, seed_type_name, is_seed from electroncash.plugins import run_hook @@ -21,15 +22,16 @@ from ..hw_wallet import HW_PluginBase -#pysatochip -from .ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey - try: - from .CardConnector import CardConnector, UninitializedSeedError - from .CardDataParser import CardDataParser - from .JCconstants import JCconstants - from .TxParser import TxParser - + #pysatochip + from pysatochip.CardConnector import CardConnector, UninitializedSeedError, logger + from pysatochip.JCconstants import JCconstants + from pysatochip.TxParser import TxParser + from pysatochip.Satochip2FA import Satochip2FA + from pysatochip.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey + from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION, SATOCHIP_PROTOCOL_VERSION + + #pyscard from smartcard.sw.SWExceptions import SWException from smartcard.Exceptions import CardConnectionException, CardRequestTimeoutException from smartcard.CardType import AnyCardType @@ -37,10 +39,12 @@ LIBS_AVAILABLE = True except: LIBS_AVAILABLE = False - print_error("[satochip] failed to import requisite libraries, please install the smarcard library 'pyscard'") - print_error("[satochup] satochip will not not be available") + print_error("[satochip] failed to import requisite libraries, please install the 'pyscard' and 'pysatochip' libraries") + print_error("[satochip] satochip will not not be available") raise +logging.basicConfig(level=logging.DEBUG, format='%(levelname)s [%(module)s] %(funcName)s | %(message)s') #debugSatochip + # debug: smartcard reader ids SATOCHIP_VID= 0x096E SATOCHIP_PID= 0x0503 @@ -77,17 +81,14 @@ def __init__(self, plugin, handler): self.print_error("__init__()")#debugSatochip self.device = plugin.device self.handler = handler - self.parser= CardDataParser() - self.cc = CardConnector(self) - - # debug - try: - self.print_error("__init__(): cc.card_get_ATR()")#debugSatochip - self.print_error(self.cc.card_get_ATR()) - response, sw1, sw2 = self.cc.card_select() - except SWException as e: - self.print_error(repr(e)) - + #self.parser= CardDataParser() + #self.cc= CardConnector(self, _logger.getEffectiveLevel()) + self.cc= CardConnector(self) + if is_verbose: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + def __repr__(self): return '' @@ -98,7 +99,7 @@ def close(self): self.print_error("close()")#debugSatochip self.cc.card_disconnect() self.cc.cardmonitor.deleteObserver(self.cc.cardobserver) - self.print_error("Removed cardObserver") + def timeout(self, cutoff): pass @@ -107,17 +108,14 @@ def is_initialized(self): return LIBS_AVAILABLE def label(self): - self.print_error("label(): TODO - currently empty")#debugSatochip + # TODO - currently empty #debugSatochip return "" - def i4b(self, x): - return pack('>I', x) def has_usable_connection_with_device(self): + self.print_error(f"has_usable_connection_with_device()")#debugSatochip try: - #print_error("[satochip] SatochipClient: has_usable_connection_with_device(): cc.card_get_ATR()"+str(self.cc.card_get_ATR()))#debugSatochip - #print_error("[satochip] SatochipClient: has_usable_connection_with_device(): cc.card_select()")#debugSatochip - response, sw1, sw2 = self.cc.card_select() #TODO: something else? + (response, sw1, sw2)=self.cc.card_select() #TODO: something else? except SWException as e: self.print_error(e) return False @@ -130,47 +128,83 @@ def get_xpub(self, bip32_path, xtype): hex_authentikey = self.handler.win.wallet.storage.get('authentikey') self.print_error("get_xpub(): self.handler.win.wallet.storage.authentikey:", hex_authentikey)#debugSatochip if hex_authentikey is not None: - self.parser.authentikey_from_storage= ECPubkey(bytes.fromhex(hex_authentikey)) + self.cc.parser.authentikey_from_storage= ECPubkey(bytes.fromhex(hex_authentikey)) except Exception as e: #attributeError? self.print_error("get_xpub(): exception when getting authentikey from self.handler.win.wallet.storage:", str(e))#debugSatochip - - # bip32_path is of the form 44'/0'/1' - self.print_error("[get_xpub(): bip32_path = ", bip32_path)#debugSatochip - (depth, bytepath)= bip32path2bytes(bip32_path) - (childkey, childchaincode)= self.cc.card_bip32_get_extendedkey(bytepath) - #print_error("[satochip] SatochipClient: get_xpub(): depth="+str(depth))#debugSatochip - if depth == 0: #masterkey - fingerprint= bytes([0,0,0,0]) - child_number= bytes([0,0,0,0]) - else: #get parent info - #print_error("[satochip] SatochipClient: get_xpub(): get xpub for parent")#debugSatochip - (parentkey, parentchaincode)= self.cc.card_bip32_get_extendedkey(bytepath[0:-4]) - fingerprint= hash_160(parentkey.get_public_key_bytes(compressed=True))[0:4] - child_number= bytepath[-4:] - #xpub= serialize_xpub('standard', childchaincode, childkey.get_public_key_bytes(compressed=True), depth, fingerprint, child_number) - xpub= serialize_xpub(xtype, childchaincode, childkey.get_public_key_bytes(compressed=True), depth, fingerprint, child_number) - self.print_error("SatochipClient: get_xpub(): xpub=", xpub)#debugSatochip - return xpub - # return BIP32Node(xtype=xtype, - # eckey=childkey, - # chaincode=childchaincode, - # depth=depth, - # fingerprint=fingerprint, - # child_number=child_number).to_xpub() - + + try: + # needs PIN + self.cc.card_verify_PIN() + + # bip32_path is of the form 44'/0'/1' + self.print_error("[get_xpub(): bip32_path = ", bip32_path)#debugSatochip + (depth, bytepath)= bip32path2bytes(bip32_path) + (childkey, childchaincode)= self.cc.card_bip32_get_extendedkey(bytepath) + + if depth == 0: #masterkey + fingerprint= bytes([0,0,0,0]) + child_number= bytes([0,0,0,0]) + else: #get parent info + (parentkey, parentchaincode)= self.cc.card_bip32_get_extendedkey(bytepath[0:-4]) + fingerprint= hash_160(parentkey.get_public_key_bytes(compressed=True))[0:4] + child_number= bytepath[-4:] + #xpub= serialize_xpub('standard', childchaincode, childkey.get_public_key_bytes(compressed=True), depth, fingerprint, child_number) + xpub= serialize_xpub(xtype, childchaincode, childkey.get_public_key_bytes(compressed=True), depth, fingerprint, child_number) + self.print_error("SatochipClient: get_xpub(): xpub=", xpub)#debugSatochip + return xpub + # return BIP32Node(xtype=xtype, + # eckey=childkey, + # chaincode=childchaincode, + # depth=depth, + # fingerprint=fingerprint, + # child_number=child_number).to_xpub() + except Exception as e: + self.print_error(repr(e)) + return None + def ping_check(self): #check connection is working try: - atr= self.cc.card_get_ATR() + print('ping_check')#debug + #atr= self.cc.card_get_ATR() except Exception as e: self.print_error(repr(e)) raise RuntimeError("Communication issue with Satochip") + def request(self, request_type, *args): + self.print_error('client request: '+ str(request_type)) + + if self.handler is not None: + if (request_type=='update_status'): + reply = self.handler.update_status(*args) + return reply + elif (request_type=='show_error'): + reply = self.handler.show_error(*args) + return reply + elif (request_type=='show_message'): + reply = self.handler.show_message(*args) + return reply + else: + reply = self.handler.show_error('Unknown request: '+str(request_type)) + return reply + else: + self.print_error("self.handler is None! ") + return None + # try: + # method_to_call = getattr(self.handler, request_type) + # print('Type of method_to_call: '+ str(type(method_to_call))) + # print('method_to_call: '+ str(method_to_call)) + # reply = method_to_call(*args) + # return reply + # except Exception as e: + # _logger.exception(f"Exception: {str(e)}") + # raise RuntimeError("GUI exception") + def PIN_dialog(self, msg): while True: password = self.handler.get_passphrase(msg, False) if password is None: - return False, None, None + return False, None if len(password) < 4: msg = _("PIN must have at least 4 characters.") + \ "\n\n" + _("Enter PIN:") @@ -178,9 +212,45 @@ def PIN_dialog(self, msg): msg = _("PIN must have less than 64 characters.") + \ "\n\n" + _("Enter PIN:") else: - self.PIN = password.encode('utf8') - return True, self.PIN, self.PIN - + password = password.encode('utf8') + return True, password + + def PIN_setup_dialog(self, msg, msg_confirm, msg_error): + while(True): + (is_PIN, pin)= self.PIN_dialog(msg) + if not is_PIN: + #return (False, None) + raise RuntimeError(('A PIN code is required to initialize the Satochip!')) + (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm) + if not is_PIN: + #return (False, None) + raise RuntimeError(('A PIN confirmation is required to initialize the Satochip!')) + if (pin != pin_confirm): + self.request('show_error', msg_error) + else: + return (is_PIN, pin) + + def PIN_change_dialog(self, msg_oldpin, msg_newpin, msg_confirm, msg_error, msg_cancel): + #old pin + (is_PIN, oldpin)= self.PIN_dialog(msg_oldpin) + if (not is_PIN): + self.request('show_message', msg_cancel) + return (False, None, None) + + # new pin + while (True): + (is_PIN, newpin)= self.PIN_dialog(msg_newpin) + if (not is_PIN): + self.request('show_message', msg_cancel) + return (False, None, None) + (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm) + if (not is_PIN): + self.request('show_message', msg_cancel) + return (False, None, None) + if (newpin != pin_confirm): + self.request('show_error', msg_error) + else: + return (True, oldpin, newpin) class Satochip_KeyStore(Hardware_KeyStore): hw_type = 'satochip' @@ -188,7 +258,6 @@ class Satochip_KeyStore(Hardware_KeyStore): def __init__(self, d): Hardware_KeyStore.__init__(self, d) - #print_error("[satochip] Satochip_KeyStore: __init__():")#debugSatochip #print_error("[satochip] Satochip_KeyStore: __init__(): xpub:"+str(d.get('xpub')) )#debugSatochip #print_error("[satochip] Satochip_KeyStore: __init__(): derivation"+str(d.get('derivation')))#debugSatochip self.force_watching_only = False @@ -218,8 +287,6 @@ def give_error(self, message, clear_client=False): self.client = None raise Exception(message) - def can_change_password(self): - return True def decrypt_message(self, pubkey, message, password): raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) @@ -244,6 +311,7 @@ def sign_message(self, sequence, message, password): msg= {'action':"sign_msg", 'msg':message} 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 @@ -251,8 +319,8 @@ def sign_message(self, sequence, message, password): 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) + self.handler.show_message('2FA request sent! Approve or reject request on your second device.') + Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: reply_encrypt= d['reply_encrypt'] @@ -270,14 +338,18 @@ def sign_message(self, sequence, message, password): #path= self.get_derivation() + ("/%d/%d" % sequence) keynbr= 0xFF #for extended key (depth, bytepath)= bip32path2bytes(address_path) - (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) - (response2, sw1, sw2) = client.cc.card_sign_message(keynbr, message_byte, hmac) - if (sw1!=0x90 or sw2!=0x00): - self.print_error(f"error during sign_message(): sw12={hex(sw1)} {hex(sw2)}")#debugSatochip - compsig=b'' - client.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action.")) - else: - compsig=client.parser.parse_message_signature(response2, message_byte, key) + (pubkey, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) + #(response2, sw1, sw2) = client.cc.card_sign_message(keynbr, message_byte, hmac) + # if (sw1!=0x90 or sw2!=0x00): + # _logger.info("[satochip] SatochipPlugin: error during sign_message(): sw12="+hex(sw1)+" "+hex(sw2))#debugSatochip + # compsig=b'' + # self.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action.")) + # else: + # compsig=client.parser.parse_message_signature(response2, message_byte, pubkey) + (response2, sw1, sw2, compsig) = client.cc.card_sign_message(keynbr, pubkey, message_byte, hmac) + if (compsig==b''): + self.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action.")) + except Exception as e: self.give_error(e, True) finally: @@ -290,6 +362,7 @@ def sign_transaction(self, tx, password, *, use_cache=False): client = self.get_client() + # outputs txOutputs= ''.join(tx.serialize_output(o) for o in tx.outputs()) hashOutputs = bh2u(Hash(bfh(txOutputs))) @@ -308,6 +381,7 @@ def sign_transaction(self, tx, password, *, use_cache=False): if txin['type'] in ['p2sh']: p2shTransaction = True + pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for j, x_pubkey in enumerate(x_pubkeys): self.print_error('sign_transaction(): forforloop: j=', j) #debugSatochip @@ -330,12 +404,14 @@ def sign_transaction(self, tx, password, *, use_cache=False): pre_hash_hex= pre_hash.hex() self.print_error('sign_transaction(): pre_tx_hex=', pre_tx_hex) #debugSatochip self.print_error('sign_transaction(): pre_hash=', pre_hash_hex) #debugSatochip - (response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since BCH use BIP143 as in Segwit... + #(response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since BCH use BIP143 as in Segwit... #print_error('[satochip] sign_transaction(): response= '+str(response)) #debugSatochip - (tx_hash, needs_2fa) = client.parser.parse_parse_transaction(response) - # tx_hash should be equal to pre_hash_hex - self.print_error('sign_transaction(): tx_hash=', bytes(tx_hash).hex()) #debugSatochip - #todo: assert() + #(tx_hash, needs_2fa) = client.parser.parse_parse_transaction(response) + (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since BCH use BIP143 as in Segwit... + tx_hash_hex= bytearray(tx_hash).hex() + if pre_hash_hex!= tx_hash_hex: + raise RuntimeError(f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}") + # sign tx keynbr= 0xFF #for extended key @@ -355,11 +431,11 @@ def sign_transaction(self, tx, password, *, use_cache=False): #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) + Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: - reply_encrypt = None # init it in case of exc below - reply_encrypt = d['reply_encrypt'] + reply_encrypt= None # init it in case of exc below + reply_encrypt= d['reply_encrypt'] except Exception as e: # Note: give_error here will raise again.. :/ self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True) @@ -384,7 +460,7 @@ def sign_transaction(self, tx, password, *, use_cache=False): else: chalresponse= None (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, chalresponse) - self.print_error('sign_transaction(): sig=', bytes(tx_sig).hex()) #debugSatochip + #self.print_error('sign_transaction(): sig=', bytes(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) @@ -463,9 +539,7 @@ def create_client(self, device, handler): if handler: self.handler = handler - try: - self.print_error('create_client(): try...') rv = SatochipClient(self, handler) return rv except Exception as e: @@ -486,36 +560,44 @@ def setup_device(self, device_info, wizard): client.handler = self.create_handler(wizard) client.cc.parser.authentikey_from_storage=None # https://github.com/simpleledger/Electron-Cash-SLP/pull/101#issuecomment-561238614 - # check applet version - while True: + # check setup + while(True): (response, sw1, sw2, d) = client.cc.card_get_status() - if sw1==0x90 and sw2==0x00: - v_supported= (CardConnector.SATOCHIP_PROTOCOL_MAJOR_VERSION<<8)+CardConnector.SATOCHIP_PROTOCOL_MINOR_VERSION - v_applet= (d["protocol_major_version"]<<8)+d["protocol_minor_version"] - self.print_error(f"Satochip version={hex(v_applet)} Electron Cash supported version= {hex(v_supported)}")#debugSatochip + + # check version + if (client.cc.setup_done): + v_supported= SATOCHIP_PROTOCOL_VERSION + v_applet= d["protocol_version"] + self.print_error(f"[SatochipPlugin] setup_device(): Satochip version={hex(v_applet)} Electrum supported version= {hex(v_supported)}")#debugSatochip if (v_supported0: - 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 diff --git a/setup.py b/setup.py index b6b0d8fa479d..bbeb1a423530 100755 --- a/setup.py +++ b/setup.py @@ -169,7 +169,6 @@ def run(self): 'electroncash_plugins.virtualkeyboard', 'electroncash_plugins.shuffle_deprecated', 'electroncash_plugins.satochip', - 'electroncash_plugins.satochip_2FA', 'electroncash_plugins.fusion', ], package_dir={