From fa82f8267fe94821e252a81c687472e9ced63124 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 20 Mar 2024 09:41:15 +0000 Subject: [PATCH 01/20] Add Satochip hardware wallet support to Electrum v4.5.4 Include new wizard Qt desktop client #8560 WIP TODO: build binaries, adapt qrcodewidget --- electrum/gui/icons/satochip.png | Bin 0 -> 3740 bytes electrum/gui/icons/satochip_unpaired.png | Bin 0 -> 3753 bytes electrum/plugins/satochip/README.rst | 82 ++ electrum/plugins/satochip/__init__.py | 6 + electrum/plugins/satochip/qt.py | 976 +++++++++++++++++++++++ electrum/plugins/satochip/satochip.py | 731 +++++++++++++++++ 6 files changed, 1795 insertions(+) create mode 100644 electrum/gui/icons/satochip.png create mode 100644 electrum/gui/icons/satochip_unpaired.png create mode 100644 electrum/plugins/satochip/README.rst create mode 100644 electrum/plugins/satochip/__init__.py create mode 100644 electrum/plugins/satochip/qt.py create mode 100644 electrum/plugins/satochip/satochip.py diff --git a/electrum/gui/icons/satochip.png b/electrum/gui/icons/satochip.png new file mode 100644 index 0000000000000000000000000000000000000000..ee89895e572dc297a8ba0d381d4434b8e93594c2 GIT binary patch literal 3740 zcmV;N4rB3&P)qG^sjJEP+1TxRVy*KBcd(K(?_9cl4xk=8Q&duxR{p;R)_Fn6^etYd< z?Y;I!&e+YLFq@7 zS6YNu$EiG868>v%?-& z+#W>~9;-M)uU*PUPFWE&8Vd{>Ev#HMu;6?b{e>kTQtGkwh&A?;?I}>>IAT2C@!D|9LD&iyob$l}t^%iUzu@Z9!PWQXLeiiuFE4yjo zsSS`+CPe7X(6oA+yS^w9q1;{pY5ti`7bFWWAGg-lyY^DGLLt{q0Jpn@x})}2xHPCM2P&BXG%mbPQj=8hva z;4AfMaN5vc7mBO1_Ik{R=M}b(pjb;RbZ(X=-Wbf58iNfXIFYui9g8M+EF~COB%<7> zS>O<@Gve}0&t_@Q2;4rL5-f4h_=!A54~ry9ZRNlN-!5m=Ib$f%9Hkt4t(uwy2L|X( z`0`%)o+zKd?2(jUj`1sbZ46a%P57Y0az>Rinv%@XiWA5bHV7PsTcYr3>i@~&MpmKl z-P6dR)EpYq3 z465auVAdJ}W{^{o&`-tVQhI?4k0ECSMVjKF=@nhTAfd!wi}yz^!vjNq+9Qe{AI?9r zlJ-^}MT9OSa7bGvU$<2O^G~_(4BS11oEoF-h^8lSY>ZWV(GYBjQ)(>(7M%5tz=Dcm z`eES`Bw1nuPTq}f(~O;Cp2y6AKUSq2$e`=I9eTCKC*6!Zt%@L!$}1AtVw)2=H1rU$ zbjxnYuE`XtWnSQ$5po$|+R>r^ue7E^KKHJG$Ixz)QZ=dA~}39ND2u;SJq zW9>-yv9ntOU%v1jJ$KGR6W!Ez6 zbGKZ%{Zo@Yf?~~Vz30W?As3mOa&6<_bEkhtZ(Kcqep-_c()?G|E;w-1yQXD`w#*Ybnn zeasLB@=i+>KA!%*N}oHCzrCsexJ zi4-UEWX@M&x-}w+8`Encj>w-TEvt09S-O?v!J%I?(S{-gRM&m?tiO=DjT+e6aTxT=_2dkq2X)$+U z*=<>vdCR@M$HM7_kI<3&t1$Z^bn*mncV#(A!f>MjUpRQ^6j;o&Y=%1C4mGu4_3aDr zZ1R(`BM4VPHv7hzVuP~wb4McULM)j`4evGqr4Ef`>SA4D@vqpeG$nc9Y0~;~TLhc_|iuf04(;wG+7S+5wt-azme6GRPs>n=-s4 zqS9WCWrKc%H^y!Bnz(iXUp)5)ZTxy?p9$Q(Aw_PT8mApmD#G;)-!l^034GYKb#%7R zOdxW}@T43Y>&ooacyjP`Y#Q@z8i^+|x+|-ioWGF%VXuWw-8BWx9}0pkgOfzmwp8Kh zq?a)+0C|F{+y6F|tT4F_aM*P26h$~8LL!z*OOPZ2wI(&G36fUM+6=z+~6AdN|3mb~tLXr?`jB&-qdW{)jYM@a99D*Iy zd?U)aYJ6=i!RHN^P{})>)fiyV=wQ<5p;u|WI)Csti48&vZZm}7%uU-*{?_BSmihgc zn`uwk$2eSj8Ae?QLNrDfsotE3`D!G~w;2`^hoh<4ezUJz5A+qRyZXDXSJ9?_?m@UA3L#p3Alj>4CfkS@WH8}$ zfV*u+BLYrfo3ix$pV6MW(}*)g24v#lNd)Jxp)s>aF`E?ez>ckTwB$HVEqWYgQz$~z z{_{q*&MUW-gSt6-45ZsD+FEUfr0ktX7oFAUueJDW8;fhpS}JjH^zV^6IB@qtdYnW) zyncZG^V9~Ewlw1L!D)Ea4>4kI%*^{}DgCYf9AbSuLZ}mLcsP18{y1@cP@F+^U*Thv zS9un(#t5)nkg;J&*gx$(MZi3InO8DaAJH|680!WAOkctKa0*-YZ2<<49%jXQfv1q7rpfWe?3>_mln{hV{2S_Fl?)5 zfWd-O9rBg^rKl%4p}FHI#1QV$>|8I{uq18mVC!}aWDn*RK0*cc-@v5Nq1jP` z<{-6l)UJu7(}uX6kxRK+NSP=440)Ym;li>4nto&z;*DWogS2?l1wc;+S8vNdQ@ozG zmK37jO_vyXNkD%XaB+6${5)^<_^V#0)(qb8?$t#dGbf)5%dLp|Mm@OCIq9@P)#dTf3)=>jTkh-xtcPu4#xNOLFTdM3; z5axf@r^s`=ou$bcMX{zB)bLj1ru+)CZ%tPmw90iaFRpz#!){V$#oU9fcdt_%OK)Ah z3!vA}ze&Hkn2j)<31MmpmuBTF4mx7TI5O!Y+Ed#~xf)0vZngJjl#)Z=RrqYsB(;&CnGV@1J- z=Z+zBYiL>RY{r{+EW@HZvJ}N2hdj4qU|U7D&Gfmo4t9~lhM^1bRPqw#3~0UX$|jFW zTNT6y`rhQx##4`^sI!MttT`U#Y~O8x`>RfJTgsDiZSPEA=ey6`G7GCmJ*`~hc>=Vz zW6eX?_R;jCYY}aVK!~Kqm5dJpZphpJ_+NVB{5uHKnq6-;qMP1q>6rf>RFMN0+%~ zSOpt?JNQAY9JMT9MyYbfQk*FQ^^Q7leRS#4ksH}WBCKnppLY)HV#dwoyhdFSFg^ys;_T(A8M zR)#@uM0NMvV*;6=l>O;+&fb7fogVe}db~C4X*}`+&#wqDSu0MiqTgLP=pwJoS_&*U z=M^Sz3GlU^*I<8Lyp~@4IuC5)fxS*{px%skQ&!4;-R8-P!<2Pm9V$ctVHy)EY-K2# z@(LyfAPv`7l6tD|HGyqH6Lw9d8p(-Joe7P+9g$i!mJZ6m>f!E}9~;iSMw`FhjZ%R_ zl)j(qzJ@{K(J(Ja2U$L{>ob8&|J&ztXyt|1(FhS1t;zMeVu$F2C^=b{wF@H4uGXk^ zFluzrt8_PRKROz*=6m;JT{myU@X>`IdERp!x;5m5W2MWg50ix5d1m27>=d1lx)=~qtN`PK%!SMbAFg|WVKy?O) zr*6gmTN$G?d9p*fU;z97qVqcs^s%MBBk(%{ePqDL>i+^UEMT06fW`p;0000k5t7&n zL{LnYl)3_el@K8a3Iu4vvfzja1|P9nW`JS1@AtXq-t+4^XC5%jojd28d+(W9UEa0k zpE>*S`+d%D@BQ1q{X3gs5dLB_rc& zszx@DD(K!hzvow!{pGJ=ePSU@zIA5KFu9FP`c@dyIkjn%497xCk zA`hZ0jE&d8H2WS{X59-@WA8RA30N0^*+Xwp=HJ(X|HTe4&ITCGplk#+7YTLi$iMXq zicftAoZqiVZxRU*1W+ao@F_5h0hT56VOzQchM`wgT)&ETkXo~n(tr0apcD^gcLO#n z7}wFI7`On(j{k)9?wLJg$ZXw8iLX5Z&Zi9E<&-HW0;wd#OcciPjj;dd`>;%!UNJo@ z(s1%$|B=$mz6Tf`U@j+^(a_79D4Buj&gbkP%UUDPppRT6FyeGlAhJaZuYV-jmk}%Dh0@udZF9Ljf zC&j1Ct_Z*`;lxM;Ai(p{F&IZ(SrUViDo3Z@LBj6CVE!T9i-Q`1lovtK^YG`g-Ew3P z?4#JkuY&W}sA)=dS!q~{k#nfp@DteQ&(q97bH$+FPl$_PZl`MDFRdW9x553%J1~u& zAj={&AExLHlfc!~mej72%F;YcfpaiTxeM++Z)jqnx>7#)zZCXg3FfQ?vznF2S+tB8 ziNdzxDO4?6(E;%H?^AT#pU4xwUOzD!0q3nob=&8vd8n!&occXQ#*PE$sRuLnc1^S> zEV&xoNI$&CPsp<5PJc#`5o5vC4Aat_<~q`R%zRE%x2S`t7@%%!k4hs`i2N)t41s_o^5O zz`6Tp-R{f<7|2lDpf9~HS0@^Wp=ks>?;cacMp2=l%||es!7QcEc?BKaSOVtx55T#m z!#5j9fKvZrFbwn8?}TgPwto5aTOc&zYN9|0tjbm~i=b>C%$`H_**0080ON>BQ$^># zxndm`X*l107LM2c4Yo)Ax+tTf4`cT|NZB`bf%AGQu0y`P6*b7q^2jcdXj8nSDJGmW z$1)95cE?upG^)>j4ny4)eRAsa0l)tM#U@UKp>}x1;dKm~036$1gZ187EHti>s5Js+ zF_qM}49`fQ9Mc9dD*${FzI0SQz<}9J%uzU!Qj%7ERHk?W=5Nk~^Q8?eddKaQYyL>} z*0A9Gs?h?w7r0Oa)};@_v2t}mhz0+dT)B$U%l-+R&sPan1>2P8K}QbNt!*qe`vDSm z9;oQNt5mVBs8MyO1%~Q|Uc3qdya0|m{JnnH7^QM3Dwl7U!doIsJpDJsuh}Ta5(mRi z3J4n-hrx5Cw`Fux*_(U89=84tMlB~rHC9} z3Z94e+;N2Kt{Zd!DV2fo)+V@j@9hmRz3Ej-KJt{VD{mnSkhW3I-@K0a9difO5SB-X zNXQ3HqxO>H*lv>H@#9GRJXEp$uF~NmM&qbj_Z*T7zYEUmR$8rM<>YhcY!;MGgM>It zH;jen-4AnESl#N(iB6sFs6f%VD9 zV7+%v|G|Xlos%ec_#~L!s+X)q`yoCLL!%q{Q!y~JhDd-)Eu@!5(J%lW*5{VOvEq5f zJBW_(k+k~y2R3|ksm}5c>I|c3iKHmyCtF-33}zBxU$Y9f`Ncz5h1zAgmQN}2m1}f8 z#8IN4)-aT^wh4$QyS#mt`PCLm%$z^yTm~f|w3Ly87GmTKERW8C z>(z?~`dZqO>afI$<-|X~cF=6xwJM^do*0gRxr}fh{SAy){;_7OyRBb^A-9-6M&X(<*cd$U~-X3-)6uA!VfaSLG z=Lxb>A=3*cO@yr>}A3j6w zz4sx;l3<(}Fj~NDHo#;WkWkp=ASfCKcZDC`k9vlmN(I(y6_)-X96Lg$2{&n;8?r|Z zk?{5b2-~+qIC>1sPz6@y0L&ImhdHGxDdpQ3n?}LY+^ktAr7~2cgUsfQlv%SD!ta{F zS-pUxN;j*NEYEPwNO+pxtB7s|El??KwdL~hHcCDF9q=#x6r8;Va8&MaUY0GH1{>~% z_oGjBfhYTcYN2Q?e|q{Me+n8*aBPZ0$eWtzyTEAJ4~kQF2?&+S%8YdEfdC*vocsEaVMx%W|a^V`}38N7g$6=*nrRu_2-0!*&_ z9%YulARlE?hKKRSF>vqG2cRTS=$s?jTkt9{F^%{KK()Y zG>&)y+*iEtp162uqGD>(Tr^;^kesxxVs&QW2$D~I3k^SbVZh+Yb6`GvhQi*Fa!#@o zwLt1Td}&@+p|VH!Q}o8kP@LAA%OjnNp+2Lp%TjO%OaBZQ%)~rHc3&Sdc3Z}iSafH zI)|a(yc|A@RGkM6r%vfAU!Pup^dPhCbxO=$D9?jpD2%FCR>3xJk?sPV!ML2$#qT}!A`+zIMgC9K{ z!`?kQDB$(j%0;J5qTHdAfW_SF1Hf{FD%M`G%0aMr3$MuSXOV9{gqrLBbnpS>_wJVf7z;!(@ zC{$+G7D`Ne2+UIp7&Gt%0=fzxut+aZ+FCLo7({-<3#gg5Kvx+`0w`6M!%QbyflLzO zs%5BIx=c3(x<7Xv#EVB!3+#F*?M@8U4G==D7;@PKIZEG<+H$!sPEq8F@$znjr0Yrk zeldLCRi5yuw!c!#7qu$>*PAJJ_gomAieS*Q2*@mia-=TJVKFiw}7jbhVOa!#fum6l=4z4SywTLpvObVS-}z) zz^{8A!xlfSxUNzKLZe0!1*99-f^ckJ2kUI@Hwr23v($2`AOvkTg2@9GyMW}k7i0Jj z*QlyrOxVOTk5lGHo4{;#aHG8N9v>V5)BXUf1`w=r6UYKClSE>|)fire45-wPS5flu zW#D|nIv0+fSK{@j#6Uh*4Nwv`+&G5vAD;tfwIQ1ffc<1GhJ0(Wrncfs9;M9cjo|#l z{Hw2!_|(&|Br|}; z0TT0|91n;&5IXOI1_y9PFp~u^SwXz?^k1_FrMKFb2ItJBc_J(OO( z9^CdfU}gq@Gs)289}!3tKq3#rh= 3.6) + Homepage: https://github.com/Toporin/electrum-satochip + +Introduction +============ + +This plugin allows to integrate the Satochip Hardware Wallet with Electrum. To use it, you need a device with the Satochip javacard applet installed (see https://github.com/Toporin/SatochipApplet). +If the wallet is not intialized yet, Electrum 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!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: Electrum uses Python 3.x. In case of error, check first that you are not trying to run Electrum with Python 2.x or with Python 2.x libraries. + +Development version (Windows 64bits) +===================================== + +Install the latest python 3.6 release from https://www.python.org (https://www.python.org/downloads/release/python-368/) +(Caution: installing another release than 3.6 may cause incompatibility issues with pyscard) + +Clone or download the code from GitHub. + +Open a PowerShell command line in the electrum folder + +In PowerShell, install the electrum dependencies:: + + python -m pip install . + +You may also ned to install Python3-pyqt5:: + + python -m pip install pyqt5 + +Install pyscard from https://pyscard.sourceforge.io/ +Pyscard is required to connect to the smartcard:: + + python -m pip install pyscard + + +In PowerShell, run electrum on the testnet (-v allows for verbose output):: + + python .\run_electrum -v --testnet + + +Development version (Ubuntu) +============================== +(Electrum requires Python 3.6, which should be installed by default on Ubuntu) +(If necessary, install pip: sudo apt-get install python3-pip) + +Electrum is a pure python application. To use the +Qt interface, install the Qt dependencies:: + + sudo apt-get install python3-pyqt5 + +Check out the code from GitHub:: + + git clone git://github.com/Toporin/electrum.git + cd electrum + +In the electrum folder: + +Run install (this should install dependencies):: + + python3 -m pip install . + +Install pyscard (https://pyscard.sourceforge.io/) +Pyscard is required to connect to the smartcard:: + sudo apt-get install pcscd + sudo apt-get install python3-pyscard +(For alternatives, see https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md for more detailed installation instructions) + + +To run Electrum use:: + python3 electrum -v --testnet + + diff --git a/electrum/plugins/satochip/__init__.py b/electrum/plugins/satochip/__init__.py new file mode 100644 index 000000000000..78a3d14c3217 --- /dev/null +++ b/electrum/plugins/satochip/__init__.py @@ -0,0 +1,6 @@ + +fullname = 'Satochip Wallet' +description = 'Provides support for Satochip hardware wallet' +requires = [('satochip', 'github.com/Toporin/pysatochip')] +registers_keystore = ('hardware', 'satochip', "Satochip wallet") +available_for = ['qt'] \ No newline at end of file diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py new file mode 100644 index 000000000000..7886314532ba --- /dev/null +++ b/electrum/plugins/satochip/qt.py @@ -0,0 +1,976 @@ +from electrum.i18n import _ +from electrum.logging import get_logger +from electrum.keystore import bip39_is_checksum_valid +from electrum.simple_config import SimpleConfig +from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) +from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub, WalletWizardComponent +from electrum.plugin import hook +from PyQt5.QtCore import Qt, pyqtSignal, QRegExp +from PyQt5.QtGui import QRegExpValidator +from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) + +from functools import partial +from os import urandom +import textwrap +import threading + +#satochip +from .satochip import SatochipPlugin +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase + +#pysatochip +from pysatochip.CardConnector import CardConnector, UnexpectedSW12Error, CardError, CardNotPresentError, WrongPinError +from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST +from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION + +# Seed import wizard msg +PASSPHRASE_HELP_SHORT =_( + "A passphrase is an optional feature that allows you to extend your seed with additional entropy. " + "A passphrase is not a PIN.") +PASSPHRASE_NOT_PIN = _( + "If set, you will need your passphrase " + "along with your BIP39 seed to restore your wallet from a backup. " + "If you are not sure, leave this field empty!") + +_logger = get_logger(__name__) + +MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") + +class Plugin(SatochipPlugin, QtPluginBase): + icon_unpaired = "satochip_unpaired.png" + icon_paired = "satochip.png" + + def create_handler(self, window): + return Satochip_Handler(window) + + def requires_settings(self): + # Return True to add a Settings button. + return True + + def settings_widget(self, window): + # Return a button that when pressed presents a settings dialog. + return EnterButton(_('Settings'), partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + # Return a settings dialog. + d = WindowModalDialog(window, _("Email settings")) + vbox = QVBoxLayout(d) + + d.setMinimumSize(500, 200) + vbox.addStretch() + vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) + d.show() + + def show_settings_dialog(self, window, keystore): + # When they click on the icon for Satochip we come here. + def connect(): + device_id = self.choose_device(window, keystore) + return device_id + def show_dialog(device_id): + if device_id: + SatochipSettingsDialog(window, self, keystore, device_id).exec_() + keystore.thread.add(connect, on_success=show_dialog) + + @hook + def init_wallet_wizard(self, wizard: 'QENewWalletWizard'): + self.extend_wizard(wizard) + + # insert satochip pages in new wallet wizard + def extend_wizard(self, wizard: 'QENewWalletWizard'): + super().extend_wizard(wizard) + views = { + 'satochip_start': {'gui': WCScriptAndDerivation}, + 'satochip_xpub': {'gui': WCHWXPub}, + 'satochip_not_setup': {'gui': WCSatochipSetupParams}, + 'satochip_do_setup': {'gui': WCSatochipSetup}, + 'satochip_not_seeded': {'gui': WCSatochipImportSeedParams}, + 'satochip_import_seed': {'gui': WCSatochipImportSeed}, + 'satochip_unlock': {'gui': WCHWUnlock} + } + wizard.navmap_merge(views) + +class Satochip_Handler(QtHandlerBase): + + def __init__(self, win): + super(Satochip_Handler, self).__init__(win, 'Satochip') + +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.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(SatochipSettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + self.config = devmgr.config + handler = keystore.handler + self.thread = thread = keystore.thread + self.window = window + + def connect_and_doit(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + return client + + body = QWidget() + body_layout = QVBoxLayout(body) + grid = QGridLayout() + grid.setColumnStretch(3, 1) + + # see + title = QLabel('''
+Satochip Wallet +
satochip.io''') + title.setTextInteractionFlags(Qt.LinksAccessibleByMouse) + + grid.addWidget(title, 0, 0, 1, 2, Qt.AlignHCenter) + y = 3 + + rows = [ + ('fw_version', _("Firmware Version:")), + ('sw_version', _("Electrum Support:")), + ('is_seeded', _("Wallet seeded:")), + ('needs_2FA', _("Requires 2FA:")), + ('needs_SC', _("Secure Channel:")), + ('card_label', _("Card label:")), + ] + for row_num, (member_name, label) in enumerate(rows): + widget = QLabel('') + widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) + + grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight) + grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft) + setattr(self, member_name, widget) + y += 1 + + body_layout.addLayout(grid) + + pin_btn = QPushButton('Change PIN') + def _change_pin(): + thread.add(connect_and_doit, on_success=self.change_pin) + pin_btn.clicked.connect(_change_pin) + + seed_btn = QPushButton('Reset seed') + def _reset_seed(): + thread.add(connect_and_doit, on_success=self.reset_seed) + thread.add(connect_and_doit, on_success=self.show_values) + seed_btn.clicked.connect(_reset_seed) + + set_2FA_btn = QPushButton('Enable 2FA') + def _set_2FA(): + thread.add(connect_and_doit, on_success=self.set_2FA) + thread.add(connect_and_doit, on_success=self.show_values) + set_2FA_btn.clicked.connect(_set_2FA) + + reset_2FA_btn = QPushButton('Disable 2FA') + def _reset_2FA(): + thread.add(connect_and_doit, on_success=self.reset_2FA) + thread.add(connect_and_doit, on_success=self.show_values) + reset_2FA_btn.clicked.connect(_reset_2FA) + + change_2FA_server_btn = QPushButton('Select 2FA server') + def _change_2FA_server(): + thread.add(connect_and_doit, on_success=self.change_2FA_server) + change_2FA_server_btn.clicked.connect(_change_2FA_server) + + verify_card_btn = QPushButton('Verify card') + def _verify_card(): + thread.add(connect_and_doit, on_success=self.verify_card) + verify_card_btn.clicked.connect(_verify_card) + + change_card_label_btn = QPushButton('Change label') + def _change_card_label(): + thread.add(connect_and_doit, on_success=self.change_card_label) + change_card_label_btn.clicked.connect(_change_card_label) + + + y += 3 + grid.addWidget(pin_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(seed_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(set_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(reset_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(change_2FA_server_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(verify_card_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(change_card_label_btn, y, 0, 1, 2, Qt.AlignHCenter) + y += 2 + grid.addWidget(CloseButton(self), y, 0, 1, 2, Qt.AlignHCenter) + + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(body) + + # Fetch values and show them + thread.add(connect_and_doit, on_success=self.show_values) + + + def show_values(self, client): + _logger.info("Show value!") + is_ok= client.verify_PIN() + if not is_ok: + msg= f"action cancelled by user" + self.window.show_error(msg) + return + + 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): + #fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) + fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) +'-'+ str(d["applet_major_version"]) +'.'+ str(d["applet_minor_version"]) + self.fw_version.setText('%s' % fw_rel) + + #is_seeded? + 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 d["needs2FA"]: + self.needs_2FA.setText('%s' % "yes") + else: + self.needs_2FA.setText('%s' % "no") + + # needs secure channel + if d["needs_secure_channel"]: + self.needs_SC.setText('%s' % "yes") + else: + self.needs_SC.setText('%s' % "no") + + # card label + (response, sw1, sw2, label)= client.cc.card_get_label() + if (label==""): + label= "(none)" + self.card_label.setText('%s' % label) + + 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)") + self.card_label.setText('%s' % "(none)") + + + def change_pin(self, client): + _logger.info("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 + + oldpin= list(oldpin) + newpin= list(newpin) + try: + (response, sw1, sw2)= client.cc.card_change_PIN(0, oldpin, newpin) + if (sw1==0x90 and sw2==0x00): + msg= _("PIN changed successfully!") + self.window.show_message(msg) + else: + msg= _("Failed to change PIN!") + self.window.show_error(msg) + except WrongPinError as ex: + msg= (f"Failed to change PIN. Wrong PIN! {ex.pin_left} tries remaining!") + self.window.show_error(msg) + except Exception as ex: + self.window.show_error(str(ex)) + + def reset_seed(self, client): + _logger.info("In reset_seed") + # is_ok= client.verify_PIN() + # if not is_ok: + # msg= f"action cancelled by user" + # self.window.show_error(msg) + # return + + # pin + msg = ''.join([ + _("WARNING!\n"), + _("You are about to reset the seed of your Satochip. This process is irreversible!\n"), + _("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:") + ]) + password = self.reset_seed_dialog(msg) + if (password is None): + return + pin = password.encode('utf8') + pin= list(pin) + + # if 2FA is enabled, get challenge-response + hmac=[] + if (client.cc.needs_2FA is None): + (response, sw1, sw2, d)=client.cc.card_get_status() + if client.cc.needs_2FA: + # challenge based on authentikey + authentikeyx= bytearray(client.cc.parser.authentikey_coordx).hex() + + # format & encrypt msg + import json + msg= {'action':"reset_seed", 'authentikeyx':authentikeyx} + 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 + + #do challenge-response with 2FA device... + self.window.show_message('2FA request sent! Approve or reject request on your second device.') + server_2FA = self.config.get("satochip_2FA_server", default= SERVER_LIST[0]) + Satochip2FA.do_challenge_response(d, server_name= server_2FA) + # decrypt and parse reply to extract challenge response + try: + reply_encrypt= d['reply_encrypt'] + except Exception as e: + self.give_error("No response received from 2FA", True) + reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) + _logger.info("challenge:response= "+ reply_decrypt) + reply_decrypt= reply_decrypt.split(":") + chalresponse=reply_decrypt[1] + hmac= list(bytes.fromhex(chalresponse)) + + # send request + (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.") + self.window.show_message(msg) + #to do: close client? + elif (sw1==0x9c and sw2==0x0b): + msg= _(f"Failed to reset seed: request rejected by 2FA device (error code: {hex(256*sw1+sw2)})") + self.window.show_message(msg) + #to do: close client? + else: + msg= _(f"Failed to reset seed with error code: {hex(256*sw1+sw2)}") + self.window.show_error(msg) + + def reset_seed_dialog(self, msg): + _logger.info("In reset_seed_dialog") + parent = self.top_level_window() + d = WindowModalDialog(parent, _("Enter PIN")) + pw = QLineEdit() + pw.setEchoMode(2) + pw.setMinimumWidth(200) + + vbox = QVBoxLayout() + vbox.addWidget(WWLabel(msg)) + vbox.addWidget(pw) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + + passphrase = pw.text() if d.exec_() else None + return passphrase + + def set_2FA(self, client): + if not client.cc.needs_2FA: + use_2FA=client.handler.yes_no_question(MSG_USE_2FA) + if (use_2FA): + # verify PIN + is_ok= client.verify_PIN() + if not is_ok: + msg= f"action cancelled by user" + self.window.show_error(msg) + return + + secret_2FA= urandom(20) + secret_2FA_hex=secret_2FA.hex() + # the secret must be shared with the second factor app (eg on a smartphone) + try: + help_txt="Scan the QR-code with your Satochip-2FA app and make a backup of the following secret: "+ secret_2FA_hex + d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, help_text=help_txt, show_copy_text_btn=True, show_cancel_btn=True, config=self.config) + result=d.exec_() # result should be 0 or 1 + if (result==1): + # further communications will require an id and an encryption key (for privacy). + # Both are derived from the secret_2FA using a one-way function inside the Satochip + amount_limit= 0 # i.e. always use + (response, sw1, sw2)=client.cc.card_set_2FA_key(secret_2FA, amount_limit) + if sw1!=0x90 or sw2!=0x00: + _logger.info(f"Unable to set 2FA with error code:= {hex(256*sw1+sw2)}") + self.window.show_error(f'Unable to setup 2FA with error code: {hex(256*sw1+sw2)}') + else: + self.window.show_message("2FA enabled successfully!") + else: + self.window.show_message("2FA cancelled by user!") + return + except Exception as e: + _logger.info(f"SatochipPlugin: setup 2FA error: {e}") + self.window.show_error(f'Unable to setup 2FA with error code: {e}') + return + + def reset_2FA(self, client): + if client.cc.needs_2FA: + # verify pin + is_ok= client.verify_PIN() + if not is_ok: + msg= f"action cancelled by user" + self.window.show_error(msg) + return + + # challenge based on ID_2FA + # format & encrypt msg + import json + msg= {'action':"reset_2FA"} + 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 + + #do challenge-response with 2FA device... + self.window.show_message('2FA request sent! Approve or reject request on your second device.') + server_2FA = self.config.get("satochip_2FA_server", default= SERVER_LIST[0]) + Satochip2FA.do_challenge_response(d, server_name= server_2FA) + # decrypt and parse reply to extract challenge response + try: + reply_encrypt= d['reply_encrypt'] + except Exception as e: + self.give_error("No response received from 2FA!", True) + reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) + _logger.info("challenge:response= "+ reply_decrypt) + reply_decrypt= reply_decrypt.split(":") + chalresponse=reply_decrypt[1] + hmac= list(bytes.fromhex(chalresponse)) + + # send request + (response, sw1, sw2) = client.cc.card_reset_2FA_key(hmac) + if (sw1==0x90 and sw2==0x00): + msg= _("2FA reset successfully!") + client.cc.needs_2FA= False + self.window.show_message(msg) + elif (sw1==0x9c and sw2==0x17): + msg= _(f"Failed to reset 2FA: \nyou must reset the seed first (error code {hex(256*sw1+sw2)})") + self.window.show_error(msg) + else: + msg= _(f"Failed to reset 2FA with error code: {hex(256*sw1+sw2)}") + self.window.show_error(msg) + else: + msg= _(f"2FA is already disabled!") + self.window.show_error(msg) + + def change_2FA_server(self, client): + _logger.info("in change_2FA_server") + help_txt="Select 2FA server in the list:" + option_name= "satochip_2FA_server" + options= SERVER_LIST #["server1", "server2", "server3"] + title= "Select 2FA server" + d = SelectOptionsDialog(option_name = option_name, options = options, parent=None, title=title, help_text=help_txt, config=self.config) + result=d.exec_() # result should be 0 or 1 + + def verify_card(self, client): + # verify pin + is_ok= client.verify_PIN() + if not is_ok: + return + + # verify authenticity + is_authentic, txt_ca, txt_subca, txt_device, txt_error = self.card_verify_authenticity(client) + + # wrap data for better display + tmp = "" + for line in txt_ca.splitlines(): + tmp += textwrap.fill(line, 120, subsequent_indent="\t") + "\n" + txt_ca = tmp + tmp = "" + for line in txt_subca.splitlines(): + tmp += textwrap.fill(line, 120, subsequent_indent="\t") + "\n" + txt_subca = tmp + tmp = "" + for line in txt_device.splitlines(): + tmp += textwrap.fill(line, 120, subsequent_indent="\t") + "\n" + txt_device = tmp + + if is_authentic: + txt_result= 'Device authenticated successfully!' + else: + txt_result= ''.join(['Error: could not authenticate the issuer of this card! \n', + 'Reason: ', txt_error , '\n\n', + 'If you did not load the card yourself, be extremely careful! \n', + 'Contact support(at)satochip.io to report a suspicious device.']) + d = DeviceCertificateDialog( + parent=None, + title= "Satochip certificate chain", + is_authentic = is_authentic, + txt_summary = txt_result, + txt_ca = txt_ca, + txt_subca = txt_subca, + txt_device = txt_device, + ) + result=d.exec_() + + + def card_verify_authenticity(self, client): #todo: add this function in pysatochip + + cert_pem=txt_error="" + try: + cert_pem=client.cc.card_export_perso_certificate() + _logger.info('Cert PEM: '+ str(cert_pem)) + except CardError as ex: + txt_error= ''.join(["Unable to get device certificate: feature unsupported! \n", + "Authenticity validation is only available starting with Satochip v0.12 and higher"]) + except CardNotPresentError as ex: + txt_error= "No card found! Please insert card." + except UnexpectedSW12Error as ex: + txt_error= "Exception during device certificate export: " + str(ex) + + if cert_pem=="(empty)": + txt_error= "Device certificate is empty: the card has not been personalized!" + + if txt_error!="": + return False, "(empty)", "(empty)", "(empty)", txt_error + + # check the certificate chain from root CA to device + from pysatochip.certificate_validator import CertificateValidator + validator= CertificateValidator() + is_valid_chain, device_pubkey, txt_ca, txt_subca, txt_device, txt_error= validator.validate_certificate_chain(cert_pem, client.cc.card_type) + if not is_valid_chain: + return False, txt_ca, txt_subca, txt_device, txt_error + + # perform challenge-response with the card to ensure that the key is correctly loaded in the device + is_valid_chalresp, txt_error = client.cc.card_challenge_response_pki(device_pubkey) + + return is_valid_chalresp, txt_ca, txt_subca, txt_device, txt_error + + def change_card_label(self, client): + msg = ''.join([ + _("You can optionaly add a label to your Satochip.\n"), + _("This label must be less than 64 chars long."), + ]) + + # verify pin + is_ok= client.verify_PIN() + if not is_ok: + # msg= f"action cancelled by user" + # self.window.show_error(msg) + return + + # label dialog + label = self.change_card_label_dialog(client, msg) + if label is None: + self.window.show_message(_("Operation aborted by user!")) + return + + # set new label + (response, sw1, sw2)= client.cc.card_set_label(label) + if (sw1==0x90 and sw2==0x00): + self.window.show_message(_("Card label changed successfully!")) + elif (sw1==0x6D and sw2==0x00): + self.window.show_error(_("Error: card does not support label!")) # starts with satochip v0.12 + else: + self.window.show_error(f"Error while changing label: sw12={hex(sw1)} {hex(sw2)}") + + def change_card_label_dialog(self, client, msg): + _logger.info("In change_card_label_dialog") + while (True): + parent = self.top_level_window() + d = WindowModalDialog(parent, _("Enter Label")) + pw = QLineEdit() + pw.setEchoMode(0) + pw.setMinimumWidth(200) + + vbox = QVBoxLayout() + vbox.addWidget(WWLabel(msg)) + vbox.addWidget(pw) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + + label = pw.text() if d.exec_() else None + if label is None or len(label.encode('utf-8'))<=64: + return label + else: + self.window.show_error(_("Card label should not be longer than 64 chars!")) + + +class SelectOptionsDialog(WindowModalDialog): + + def __init__( + self, + *, + option_name, + options=None, + parent=None, + title="", + help_text=None, + config: SimpleConfig, + ): + WindowModalDialog.__init__(self, parent, title) + self.config = config + + vbox = QVBoxLayout() + if help_text: + text_label = WWLabel() + text_label.setText(help_text) + vbox.addWidget(text_label) + + def set_option(): + _logger.info(f"New 2FA server: {options_combo.currentText()}") + # save in config + config.set_key(option_name, options_combo.currentText(), save=True) + _logger.info("config changed!") + + default= config.get(option_name, default= SERVER_LIST[0]) + options_combo = QComboBox() + options_combo.addItems(options) + options_combo.setCurrentText(default) + options_combo.currentIndexChanged.connect(set_option) + vbox.addWidget(options_combo) + + hbox = QHBoxLayout() + hbox.addStretch(1) + + b = QPushButton(_("Ok")) + hbox.addWidget(b) + b.clicked.connect(self.accept) + b.setDefault(True) + + vbox.addLayout(hbox) + self.setLayout(vbox) + + # note: the word-wrap on the text_label is causing layout sizing issues. + # see https://stackoverflow.com/a/25661985 and https://bugreports.qt.io/browse/QTBUG-37673 + # workaround: + self.setMinimumSize(self.sizeHint()) + +class DeviceCertificateDialog(WindowModalDialog): + + def __init__( + self, + *, + parent=None, + title="", + is_authentic, + txt_summary = "", + txt_ca = "", + txt_subca = "", + txt_device = "", + ): + WindowModalDialog.__init__(self, parent, title) + + + #super(QWidget, self).__init__(parent) + self.layout = QVBoxLayout(self) + + # add summary text + self.summary = QLabel(txt_summary) + if is_authentic: + self.summary.setStyleSheet('color: green') + else: + self.summary.setStyleSheet('color: red') + self.summary.setWordWrap(True) + self.layout.addWidget(self.summary) + + # Initialize tab screen + self.tabs = QTabWidget() + self.tab1 = QWidget() + self.tab2 = QWidget() + self.tab3 = QWidget() + self.tabs.resize(300,200) + + # Add tabs + self.tabs.addTab(self.tab1,"RootCA") + self.tabs.addTab(self.tab2,"SubCA") + self.tabs.addTab(self.tab3,"Device") + + # Create first tab + self.tab1.layout = QVBoxLayout(self) + self.cert1 = QLabel(txt_ca) + self.cert1.setWordWrap(True) + self.tab1.layout.addWidget(self.cert1) + self.tab1.setLayout(self.tab1.layout) + + # Create second tab + self.tab2.layout = QVBoxLayout(self) + self.cert2 = QLabel(txt_subca) + self.cert2.setWordWrap(True) + self.tab2.layout.addWidget(self.cert2) + self.tab2.setLayout(self.tab2.layout) + + # Create third tab + self.tab3.layout = QVBoxLayout(self) + self.cert3 = QLabel(txt_device) + self.cert3.setWordWrap(True) + self.tab3.layout.addWidget(self.cert3) + self.tab3.setLayout(self.tab3.layout) + + # Add tabs to widget + self.layout.addWidget(self.tabs) + self.setLayout(self.layout) + + +########################## +# Setup PIN wizard # +########################## + +def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + +class SatochipSetupLayout(QVBoxLayout): + validChanged = pyqtSignal([bool], arguments=['valid']) + + def __init__(self, device): + _logger.info("[SatochipSetupLayout] __init__()") + QVBoxLayout.__init__(self) + + vbox = QVBoxLayout() + + # intro + msg_setup = WWLabel(_("Please take a moment to set up your Satochip. This must be done only once.")) + vbox.addWidget(msg_setup) + + self.pw = PasswordLineEdit() + self.pw.setMinimumWidth(32) + #self.pw.setMaximumWidth(32) + #self.pw.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) + vbox.addWidget(WWLabel("Enter new PIN:")) + vbox.addWidget(self.pw) + self.addLayout(vbox) + + self.pw2 = PasswordLineEdit() + self.pw2.setMinimumWidth(32) + #self.pw2.setMaximumWidth(32) + #self.pw2.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) + vbox2 = QVBoxLayout() + vbox2.addWidget(WWLabel("Confirm new PIN:")) + vbox2.addWidget(self.pw2) + self.addLayout(vbox2) + + # PIN validation + if (self.pw.text()=="" or self.pw.text()==None): + self.validChanged.emit(False) + + def set_enabled(): + is_valid= True + if self.pw.text() != self.pw2.text(): + is_valid= False + + pw_bytes = self.pw.text().encode("utf-8") + if len(pw_bytes)<4 or len(pw_bytes)>16: + is_valid= False + + pw2_bytes = self.pw2.text().encode("utf-8") + if len(pw2_bytes)<4 or len(pw2_bytes)>16: + is_valid= False + + self.validChanged.emit(is_valid) + + self.pw2.textChanged.connect(set_enabled) + self.pw.textChanged.connect(set_enabled) + + + def get_settings(self): + _logger.info("[SatochipSetupLayout] get_settings()") + return self.pw.text() + +class WCSatochipSetupParams(WalletWizardComponent): + def __init__(self, parent, wizard): + _logger.info("[WCSatochipSetupParams] __init__()") + WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + self.plugins = wizard.plugins + self._busy = True + + def on_ready(self): + _logger.info("[WCSatochipSetupParams] on_ready()") + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + _name, _info = current_cosigner['hardware_device'] + self.settings_layout = SatochipSetupLayout(_info.device.id_) + self.settings_layout.validChanged.connect(self.on_settings_valid_changed) + self.layout().addLayout(self.settings_layout) + self.layout().addStretch(1) + + self.valid = True # debug + self.busy = False + + def on_settings_valid_changed(self, is_valid: bool): + _logger.info(f"[WCSatochipSetupParams] on_settings_valid_changed() is_valid: {is_valid}") + self.valid = is_valid + + def apply(self): + _logger.info("[WCSatochipSetupParams] apply()") + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + current_cosigner['satochip_setup_settings'] = self.settings_layout.get_settings() + + +class WCSatochipSetup(WalletWizardComponent): + def __init__(self, parent, wizard): + WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + _logger.info('[WCSatochipSetup] __init__()') # debugsatochip + + self.plugins = wizard.plugins + self.plugin = self.plugins.get_plugin('satochip') + + self.layout().addWidget(WWLabel('Done')) + + self._busy = True + + def on_ready(self): + _logger.info('[WCSatochipSetup] on_ready()') # debugsatochip + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + settings = current_cosigner['satochip_setup_settings'] + #method = current_cosigner['satochip_init'] + _name, _info = current_cosigner['hardware_device'] + device_id = _info.device.id_ + _logger.info(f'[WCSatochipSetup] on_ready() device_id: {device_id}') # debugsatochip + + + client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + client.handler = self.plugin.create_handler(self.wizard) + + def initialize_device_task(settings, device_id, client): + try: + #self.plugin._initialize_device(settings, method, device_id, handler) + self.plugin._setup_device(settings, device_id, client) + _logger.info('[WCSatochipSetup] initialize_device_task() Done initialize device') + self.valid = True + self.wizard.requestNext.emit() # triggers Next GUI thread from event loop + except Exception as e: + self.valid = False + self.error = repr(e) + _logger.exception(repr(e)) + finally: + self.busy = False + + t = threading.Thread( + target=initialize_device_task, + args=(settings, device_id, client), + daemon=True) + t.start() + + def apply(self): + pass + +########################## +# Import seed wizard # +########################## + +class SatochipSeedLayout(QVBoxLayout): + validChanged = pyqtSignal([bool], arguments=['valid']) + + def __init__(self, device): + QVBoxLayout.__init__(self) + + label = QLabel(_("Enter a label to name your device:")) + self.label_e = QLineEdit() + hl = QHBoxLayout() + hl.addWidget(label) + hl.addWidget(self.label_e) + hl.addStretch(1) + self.addLayout(hl) + + self.text_e = QTextEdit() + self.text_e.setMaximumHeight(60) + msg = _("Enter your BIP39 mnemonic:") + + # TODO: validation? + def set_enabled(): + item = ' '.join(str(clean_text(self.text_e)).split()) + (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(item) + self.validChanged.emit(is_checksum_valid and is_wordlist_valid) + self.text_e.textChanged.connect(set_enabled) + + self.addWidget(QLabel(msg)) + self.addWidget(self.text_e) + + passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + self.addWidget(passphrase_msg) + self.addWidget(passphrase_warning) + #self.cb_phrase = QCheckBox(_('Enable passphrases')) + #self.cb_phrase.setChecked(False) + #self.addWidget(self.cb_phrase) + + self.passphrase_e = QLineEdit() + self.passphrase_e.setMinimumWidth(100) + passphrase_label = _("Enter your BIP39 passphrase (optional):") + # TODO: validation? + + self.addWidget(QLabel(passphrase_label)) + self.addWidget(self.passphrase_e) + + def get_settings(self): + item = ' '.join(str(clean_text(self.text_e)).split()) + return self.label_e.text(), item, self.passphrase_e.text() + +class WCSatochipImportSeedParams(WalletWizardComponent): + def __init__(self, parent, wizard): + WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + self.plugins = wizard.plugins + self._busy = True + + def on_ready(self): + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + _name, _info = current_cosigner['hardware_device'] + self.settings_layout = SatochipSeedLayout(_info.device.id_) + self.settings_layout.validChanged.connect(self.on_settings_valid_changed) + self.layout().addLayout(self.settings_layout) + self.layout().addStretch(1) + + self.valid = True # TODO #current_cosigner['satochip_init'] != TIM_PRIVKEY # TODO: only privkey is validated + self.busy = False + + def on_settings_valid_changed(self, is_valid: bool): + self.valid = is_valid + + def apply(self): + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + current_cosigner['satochip_seed_settings'] = self.settings_layout.get_settings() + + +class WCSatochipImportSeed(WalletWizardComponent): + def __init__(self, parent, wizard): + WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + self.plugins = wizard.plugins + self.plugin = self.plugins.get_plugin('satochip') + + self.layout().addWidget(WWLabel('Done')) + + self._busy = True + + def on_ready(self): + current_cosigner = self.wizard.current_cosigner(self.wizard_data) + settings = current_cosigner['satochip_seed_settings'] + #method = current_cosigner['satochip_init'] + _name, _info = current_cosigner['hardware_device'] + device_id = _info.device.id_ + client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + client.handler = self.plugin.create_handler(self.wizard) + + def initialize_device_task(settings, device_id, handler): + try: + self.plugin._import_seed(settings, device_id, handler) + _logger.info('[WCSatochipImportSeed] initialize_device_task() Done initialize device') + self.valid = True + self.wizard.requestNext.emit() # triggers Next GUI thread from event loop + except Exception as e: + self.valid = False + self.error = repr(e) + _logger.exception(repr(e)) + finally: + self.busy = False + + t = threading.Thread( + target=initialize_device_task, + args=(settings, device_id, client.handler), + daemon=True) + t.start() + + def apply(self): + pass \ No newline at end of file diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py new file mode 100644 index 000000000000..f4060d906daf --- /dev/null +++ b/electrum/plugins/satochip/satochip.py @@ -0,0 +1,731 @@ +from os import urandom +import hashlib +import time + +#electrum +from electrum import mnemonic +from electrum import constants +from electrum import descriptor +from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int +from electrum.i18n import _ +from electrum.plugin import Device, runs_in_hwd_thread +from electrum.keystore import Hardware_KeyStore, bip39_to_seed, bip39_is_checksum_valid +from electrum.transaction import Transaction +from electrum.wallet import Standard_Wallet +from electrum.util import bfh, versiontuple, UserFacingException +from electrum.crypto import hash_160, sha256d +from electrum.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey +from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath +from electrum.logging import get_logger +from electrum.simple_config import SimpleConfig +from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog + +from ..hw_wallet import HW_PluginBase, HardwareClientBase + +#pysatochip +from pysatochip.CardConnector import CardConnector +from pysatochip.CardConnector import UninitializedSeedError, CardNotPresentError, UnexpectedSW12Error, WrongPinError, PinBlockedError, PinRequiredError +from pysatochip.JCconstants import JCconstants +from pysatochip.TxParser import TxParser +from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST +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 +from smartcard.CardRequest import CardRequest + +_logger = get_logger(__name__) + +# version history for the plugin +SATOCHIP_PLUGIN_REVISION= 'lib0.11.a-plugin0.1' + +# debug: smartcard reader ids +SATOCHIP_VID= 0 #0x096E +SATOCHIP_PID= 0 #0x0503 + +MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") + +def bip32path2bytes(bip32path:str) -> (int, bytes): + intPath= convert_bip32_strpath_to_intpath(bip32path) + depth= len(intPath) + bytePath=b'' + for index in intPath: + bytePath+= index.to_bytes(4, byteorder='big', signed=False) + return (depth, bytePath) + +class SatochipClient(HardwareClientBase): + def __init__(self, plugin: HW_PluginBase, handler): + HardwareClientBase.__init__(self, plugin=plugin) + _logger.info(f"[SatochipClient] __init__()") + self._soft_device_id = None + self.device = plugin.device + self.handler = handler + #self.parser= CardDataParser() + self.cc= CardConnector(self, _logger.getEffectiveLevel()) + self.ux_busy= False + + def show_error(self, message, clear_client=False): + _logger.error(f"[SatochipClient] show_error() {message}") + if not self.ux_busy: + self.handler.show_error(message) + else: + self.ux_busy = False + if clear_client: + self.client = None + #raise UserFacingException(message) + + def __repr__(self): + return '' + + def is_pairable(self): + return True + + def close(self): + _logger.info(f"close()") + self.cc.card_disconnect() + self.cc.cardmonitor.deleteObserver(self.cc.cardobserver) + + def timeout(self, cutoff): + pass + + def is_initialized(self): + _logger.info(f"SATOCHIP is_initialized()") + + time.sleep(0.3) # let some time to setup communication channel + (response, sw1, sw2, d)=self.cc.card_get_status() + + # if setup is not done, we return None + if (not self.cc.setup_done): + _logger.info(f"SATOCHIP is_initialized() None (no setup)") + return None + # if not seeded, return False + if (self.cc.setup_done and not self.cc.is_seeded): + _logger.info(f"SATOCHIP is_initialized() False (PIN set but card not seeded)") + return False + # initialized if pin is set and device is seeded + if (self.cc.setup_done and self.cc.is_seeded): + _logger.info(f"SATOCHIP is_initialized() True (PIN set and card seeded)") + return True + + def get_soft_device_id(self): + return self._soft_device_id + + def label(self): + # TODO - currently empty + return "" + + def device_model_name(self): + return "Satochip" + + def has_usable_connection_with_device(self): + _logger.info(f"has_usable_connection_with_device()") + try: + atr= self.cc.card_get_ATR() # (response, sw1, sw2)= self.cc.card_select() #TODO: something else? get ATR? + _logger.info("Card ATR: " + bytes(atr).hex() ) + except Exception as e: #except SWException as e: + _logger.exception(f"Exception in has_usable_connection_with_device: {str(e)}") + return False + return True + + def verify_PIN(self, pin=None): + while(True): + try: + print("DEBUG verify pin") + #when pin is None, pysatochip use a cached pin if available + (response, sw1, sw2)= self.cc.card_verify_PIN_simple(pin) + return True + + # recoverable errors + except CardNotPresentError: + msg = f"No card found! \nPlease insert card, then enter your PIN:" + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN == False: + return False + except PinRequiredError as ex: + # no pin value cached in pysatochip + msg = f'Enter the PIN for your card:' + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN == False: + return False + except WrongPinError as ex: + pin= None # reset pin + msg = f"Wrong PIN! {ex.pin_left} tries remaining! \n Enter the PIN for your card:" + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN == False: + return False + + # unrecoverable errors + except PinBlockedError as ex: + raise UserFacingException(f"Too many failed attempts! Your device has been blocked! \n\nYou need to factory reset your card (error code 0x9C0C)") + except UnexpectedSW12Error as ex: + raise UserFacingException(f"Unexpected error during PIN verification: {ex}") + except Exception as ex: + raise UserFacingException(f"Unexpected error during PIN verification: {ex}") + + + def get_xpub(self, bip32_path, xtype): + assert xtype in SatochipPlugin.SUPPORTED_XTYPES + + # needs PIN + is_ok = self.verify_PIN() + + # bip32_path is of the form 44'/0'/1' + _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}") + (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= BIP32Node(xtype=xtype, + eckey=childkey, + chaincode=childchaincode, + depth=depth, + fingerprint=fingerprint, + child_number=child_number).to_xpub() + _logger.info(f"[SatochipClient] get_xpub(): xpub={str(xpub)}") + return xpub + + def ping_check(self): + #check connection is working + try: + print('ping_check')#debug + #atr= self.cc.card_get_ATR() + except Exception as e: + _logger.exception(f"Exception: {str(e)}") + raise RuntimeError("Communication issue with Satochip") + + def request(self, request_type, *args): + _logger.info('[SatochipClient] 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: + _logger.info('[SatochipClient] self.handler is None! ') + return None + + def PIN_dialog(self, msg): + while self.ux_busy: + sleep(1) + + while True: + password = self.handler.get_passphrase(msg, False) + if password is None: + return False, None + if len(password) < 4: + msg = _("PIN must have at least 4 characters.") + \ + "\n\n" + _("Enter PIN:") + elif len(password) > 16: + msg = _("PIN must have less than 16 characters.") + \ + "\n\n" + _("Enter PIN:") + else: + 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' + device = 'Satochip' + plugin: 'SatochipPlugin' + + def __init__(self, d): + Hardware_KeyStore.__init__(self, d) + self.force_watching_only = False + self.ux_busy = False + + def dump(self): + # our additions to the stored data about keystore -- only during creation? + d = Hardware_KeyStore.dump(self) + return d + + def get_derivation(self): + return self.derivation + + def give_error(self, message, clear_client=False): + _logger.error(f"[Satochip_KeyStore] give_error() {message}") + if not self.ux_busy: + self.handler.show_error(message) + else: + self.ux_busy = False + if clear_client: + self.client = None + raise UserFacingException(message) + + def decrypt_message(self, pubkey, message, password): + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + + def sign_message(self, sequence, message, password, *, script_type=None): + message_byte = message.encode('utf8') + message_hash = hashlib.sha256(message_byte).hexdigest().upper() + client = self.get_client() + is_ok= client.verify_PIN() + if not is_ok: + return b'' + + address_path = self.get_derivation_prefix() + "/%d/%d"%sequence #self.get_derivation()[2:] + "/%d/%d"%sequence + _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}") + self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) + # check if 2FA is required + hmac=b'' + if (client.cc.needs_2FA is None): + (response, sw1, sw2, d)=client.cc.card_get_status() + if client.cc.needs_2FA: + # challenge based on sha256(btcheader+msg) + # format & encrypt msg + import json + msg= {'action':"sign_msg", 'msg':message} + msg= json.dumps(msg) + #do challenge-response with 2FA device... + hmac= self.do_challenge_response(msg) + hmac= bytes.fromhex(hmac) + try: + keynbr= 0xFF #for extended key + (depth, bytepath)= bip32path2bytes(address_path) + (pubkey, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) + (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.")) + return compsig + + except Exception as e: + #self.give_error(e, True) + _logger.info(f"[Satochip_KeyStore] sign_message: Exception {e}") + #self.handler.show_error(e) + return b'' + finally: + _logger.info(f"[Satochip_KeyStore] sign_message: finally") + self.handler.finished() + + + def sign_transaction(self, tx, password): + _logger.info(f"In sign_transaction(): tx: {str(tx)}") + client = self.get_client() + is_ok = client.verify_PIN() + segwitTransaction = False + + # outputs + txOutputs = var_int(len(tx.outputs())) + for o in tx.outputs(): + txOutputs += int_to_hex(o.value, 8) + script = o.scriptpubkey.hex() + txOutputs += var_int(len(script)//2) + txOutputs += script + #txOutputs = bfh(txOutputs) + hashOutputs = sha256d(bfh(txOutputs)).hex() + _logger.info(f"In sign_transaction(): hashOutputs= {hashOutputs}") + _logger.info(f"In sign_transaction(): outputs= {txOutputs}") + + # Fetch inputs of the transaction to sign + for i,txin in enumerate(tx.inputs()): + + if tx.is_complete(): + break + + desc = txin.script_descriptor + assert desc + script_type = desc.to_legacy_electrum_script_type() + + _logger.info(f"In sign_transaction(): input= {str(i)} - input[type]: {script_type}") + if txin.is_coinbase_input(): + self.give_error("Coinbase not supported") # should never happen + + if script_type in ['p2sh']: + p2shTransaction = True + + if script_type in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: + segwitTransaction = True + + my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin) + if not inputPath: + self.give_error("No matching pubkey for sign_transaction") # should never happen + inputPath = convert_bip32_intpath_to_strpath(inputPath) #[2:] + inputHash = sha256d(bfh(tx.serialize_preimage(i))) + + # get corresponing extended key + (depth, bytepath)= bip32path2bytes(inputPath) + (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) + + # parse tx + pre_tx_hex= tx.serialize_preimage(i) + pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes + pre_hash = sha256d(bfh(pre_tx_hex)) + pre_hash_hex= pre_hash.hex() + _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx_hex= {pre_tx_hex}") + _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash_hex}") + (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction) + tx_hash_hex= bytearray(tx_hash).hex() + if pre_hash_hex!= tx_hash_hex: + raise RuntimeError("[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}") + + #2FA + keynbr= 0xFF #for extended key + if needs_2fa: + # format & encrypt msg + import json + coin_type= 1 if constants.net.TESTNET else 0 + if segwitTransaction: + msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction, 'txo':txOutputs, 'ty':script_type} + else: + msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction} + msg= json.dumps(msg) + + #do challenge-response with 2FA device... + hmac= self.do_challenge_response(msg) + hmac= list(bytes.fromhex(hmac)) + else: + hmac= None + + # sign tx + (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, hmac) + # check sw1sw2 for error (0x9c0b if wrong challenge-response) + if sw1 != 0x90 or sw2 != 0x00: + self.give_error(f"Satochip failed to sign transaction with code {hex(256*sw1+sw2)}") + + # enforce low-S signature (BIP 62) + tx_sig = bytes(tx_sig) #bytearray(tx_sig) + r,s= get_r_and_s_from_der_sig(tx_sig) + if s > CURVE_ORDER//2: + s = CURVE_ORDER - s + tx_sig=der_sig_from_r_and_s(r, s) + #update tx with signature + tx_sig = tx_sig.hex()+'01' + #tx.add_signature_to_txin(i,j,tx_sig) + tx.add_signature_to_txin(txin_idx=i, + signing_pubkey=my_pubkey.hex(), + sig=tx_sig) + # end of for loop + + _logger.info(f"Tx is complete: {str(tx.is_complete())}") + tx.raw = tx.serialize() + return + + def show_address(self, sequence, txin_type): + _logger.info(f'[Satochip_KeyStore] show_address(): todo!') + return + + def do_challenge_response(self, msg): + client = self.get_client() + (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True) + d={} + d['msg_encrypt']= msg_out + d['id_2FA']= id_2FA + _logger.info("id_2FA: "+id_2FA) + + reply_encrypt= None + hmac= 20*"00" # default response (reject) + status_msg="" + + # get server_2FA from config from existing object + server_2FA = self.plugin.config.get("satochip_2FA_server", default= SERVER_LIST[0]) + status_msg += f"2FA request sent to '{server_2FA}' \nApprove or reject request on your second device." + self.handler.show_message(status_msg) + try: + Satochip2FA.do_challenge_response(d, server_name= server_2FA) + # decrypt and parse reply to extract challenge response + reply_encrypt= d['reply_encrypt'] + except Exception as e: + status_msg += f"\nFailed to contact cosigner! \n=> Select another 2FA server in Satochip settings\n\n" + self.handler.show_message(status_msg) + if reply_encrypt is not None: + reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) + _logger.info("challenge:response= "+ reply_decrypt) + reply_decrypt= reply_decrypt.split(":") + hmac= reply_decrypt[1] + return hmac # return a hexstring + + +class SatochipPlugin(HW_PluginBase): + libraries_available= True + minimum_library = (0, 0, 0) + keystore_class= Satochip_KeyStore + DEVICE_IDS= [ + (SATOCHIP_VID, SATOCHIP_PID) + ] + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + + def __init__(self, parent, config, name): + _logger.info(f"[SatochipPlugin] init()") + HW_PluginBase.__init__(self, parent, config, name) + self.device_manager().register_enumerate_func(self.detect_smartcard_reader) + + def get_library_version(self): + return '0.0.1' + + def detect_smartcard_reader(self): + _logger.info(f"[SatochipPlugin] detect_smartcard_reader") + self.cardtype = AnyCardType() + try: + cardrequest = CardRequest(timeout=0.1, cardType=self.cardtype) + cardservice = cardrequest.waitforcard() + return [Device(path="/satochip", + interface_number=-1, + id_="/satochip", + product_key=(SATOCHIP_VID,SATOCHIP_PID), + usage_page=0, + transport_ui_string='ccid')] + except CardRequestTimeoutException: + _logger.info(f'time-out: no card found') + return [] + except Exception as exc: + _logger.info(f"Error during connection:{str(exc)}") + return [] + return [] + + + def create_client(self, device, handler): + _logger.info(f"[SatochipPlugin] create_client()") + + if handler: + self.handler = handler + + try: + rv = SatochipClient(self, handler) + return rv + except Exception as e: + _logger.exception(f"[SatochipPlugin] create_client() exception: {str(e)}") + return None + + def get_xpub(self, device_id, derivation, xtype, wizard): + # this seems to be part of the pairing process only, not during normal ops? + # base_wizard:on_hw_derivation + _logger.info(f"[SatochipPlugin] get_xpub()") + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = self.create_handler(wizard) + client.ping_check() + + xpub = client.get_xpub(derivation, xtype) + return xpub + + def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_interaction=True): + # All client interaction should not be in the main GUI thread + devmgr = self.device_manager() + handler = keystore.handler + client = devmgr.client_for_keystore(self, handler, keystore, force_pair, + devices=devices, + allow_user_interaction=allow_user_interaction) + # returns the client for a given keystore. can use xpub + if client is not None: + client.ping_check() + return client + + def _setup_device(self, settings, device_id, handler): + _logger.info(f"[SatochipPlugin] _setup_device()") + + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + if not client: + raise Exception(_("The device was disconnected.")) + + # check that card is indeed a Satochip + if (client.cc.card_type != "Satochip"): + raise Exception(_('Failed to create a client for this device.') + '\n' + + _('Inserted card is not a Satochip!')) + + pin_0 = settings + pin_0 = list(pin_0.encode("utf-8")) + client.cc.set_pin(0, pin_0) #cache PIN value in client + pin_tries_0= 0x05 + # PUK code can be used when PIN is unknown and the card is locked + # We use a random value as the PUK is not used currently in the electrum GUI + ublk_tries_0= 0x01 + ublk_0= list(urandom(16)) + #the second pin is not used currently, use random values + pin_tries_1= 0x01 + ublk_tries_1= 0x01 + pin_1= list(urandom(16)) + ublk_1= list(urandom(16)) + secmemsize= 32 # number of slot reserved in memory cache + memsize= 0x0000 # RFU + create_object_ACL= 0x01 # RFU + create_key_ACL= 0x01 # RFU + create_pin_ACL= 0x01 # RFU + + # setup + try: + (response, sw1, sw2)=client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, + pin_tries_1, ublk_tries_1, pin_1, ublk_1, + secmemsize, memsize, + create_object_ACL, create_key_ACL, create_pin_ACL) + if sw1==0x90 and sw2==0x00: + _logger.info(f"[SatochipPlugin] _setup_device(): setup applet successfully!") + client.handler.show_message(f"Satochip setup performed successfully!") + elif sw1==0x9c and sw2==0x07: + _logger.error(f"[SatochipPlugin] _setup_device(): error applet setup already done (code {hex(sw1*256+sw2)})") + client.handler.show_error(f"Satochip error: applet setup already done (code {hex(sw1*256+sw2)})") + else: + _logger.error(f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") + client.handler.show_error(f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") + except Exception as ex: + _logger.error(f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") + client.handler.show_error(f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") + + # verify pin: + client.verify_PIN() + + def _import_seed(self, settings, device_id, handler): + _logger.info(f"[SatochipPlugin] _import_seed()") + + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + if not client: + raise Exception(_("The device was disconnected.")) + + label, seed, passphrase = settings + + # check seed validity + (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(seed) + if is_checksum_valid and is_wordlist_valid: + _logger.info(f"[SatochipPlugin] _import_seed() seed format is valid!") + masterseed_bytes=bip39_to_seed(seed, passphrase=passphrase) + masterseed_list= list(masterseed_bytes) + else: + _logger.error(f"[SatochipPlugin] _import_seed() wrong seed format!") + raise Exception('Wrong BIP39 mnemonic format!') + + # verify pin: + is_ok = client.verify_PIN() + + # import seed + try: + authentikey= client.cc.card_bip32_import_seed(masterseed_list) + _logger.info(f"[SatochipPlugin] _import_seed(): seed imported successfully!") + client.handler.show_message(f"seed imported successfully!") + hex_authentikey= authentikey.get_public_key_hex(compressed=True) + _logger.info(f"[SatochipPlugin] _import_seed(): authentikey={hex_authentikey}") + except Exception as ex: + _logger.error(f"[SatochipPlugin] _import_seed(): exception during seed import: {ex}") + client.handler.show_error(f"Exception during seed import: {ex}") + + # import label + (response, sw1, sw2)= client.cc.card_set_label(label) + if (sw1==0x90 and sw2==0x00): + _logger.info(f"[SatochipPlugin] _import_seed(): card label changed successfully") + #client.handler.show_message(_("Card label changed successfully!")) + elif (sw1==0x6D and sw2==0x00): + _logger.info(f"[SatochipPlugin] _import_seed(): failed to set label: card does not support label (code {hex(sw1*256+sw2)})") + client.handler.show_error(_("Error: card does not support label!")) # starts with satochip v0.12 + else: + _logger.info(f"[SatochipPlugin] _import_seed(): unknown error while setting label (code {hex(sw1*256+sw2)})") + client.handler.show_error(f"Error while setting card label (code {hex(sw1*256+sw2)})") + + def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str: + _logger.info(f"[SatochipPlugin] wizard_entry_for_device()") + _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_info: {device_info}") + _logger.info(f"[SatochipPlugin] wizard_entry_for_device() new_wallet: {new_wallet}") + + device_state = device_info.initialized # can be None, False or True. + # None is used to distinguish a completely new card from a card where the seed has been reset, but the PIN is still set. + _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_state: {device_state}") + if new_wallet: + if device_state == None: + return 'satochip_not_setup' + elif device_state == False: + return 'satochip_not_seeded' + else: + return 'satochip_start' + else: + # todo: assert is_setup & is_seeded + if device_state is not True: + # This can happen if you reset the seed of the Satochip for an existing wallet, then try to open that wallet file. + _logger.error(f"[SatochipPlugin] wizard_entry_for_device() existing wallet with non-seeded Satochip!") + return 'satochip_unlock' + + # insert satochip pages in new wallet wizard + def extend_wizard(self, wizard: 'NewWalletWizard'): + _logger.info(f"[SatochipPlugin] extend_wizard()") + views = { + 'satochip_start': { + 'next': 'satochip_xpub', + }, + 'satochip_xpub': { + 'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore', + 'accept': wizard.maybe_master_pubkey, + 'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) + }, + 'satochip_not_setup': { + 'next': 'satochip_do_setup', + }, + 'satochip_do_setup': { + 'next': 'satochip_not_seeded', + }, + 'satochip_not_seeded': { + 'next': 'satochip_import_seed', + }, + 'satochip_import_seed': { + 'next': 'satochip_start', + }, + 'satochip_unlock': { + 'last': True + }, + } + wizard.navmap_merge(views) + + def show_address(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + + # Standard_Wallet => not multisig, must be bip32 + if type(wallet) is not Standard_Wallet: + keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + return + + sequence = wallet.get_address_index(address) + txin_type = wallet.get_txin_type(address) + keystore.show_address(sequence, txin_type) From 20be6e3236c0df2fd744758c1f5ae3ef274419e7 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 20 Mar 2024 09:54:02 +0000 Subject: [PATCH 02/20] Refactor class QRDialog: add optional 'cancel' button In satochip plugin, a user can setup a 2FA using a qrcode. The cancel button allows the user to cancel this action. --- electrum/gui/qt/qrcodewidget.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py index 80c1f4364887..aee8868eb1d4 100644 --- a/electrum/gui/qt/qrcodewidget.py +++ b/electrum/gui/qt/qrcodewidget.py @@ -128,6 +128,7 @@ def __init__( show_text=False, help_text=None, show_copy_text_btn=False, + show_cancel_btn=False, config: SimpleConfig, ): WindowModalDialog.__init__(self, parent, title) @@ -182,10 +183,21 @@ def copy_text_to_clipboard(): hbox.addWidget(b) b.clicked.connect(print_qr) - b = QPushButton(_("Close")) - hbox.addWidget(b) - b.clicked.connect(self.accept) - b.setDefault(True) + if show_cancel_btn: + b = QPushButton(_("Ok")) + hbox.addWidget(b) + b.clicked.connect(self.accept) + b.setDefault(True) + + b = QPushButton(_("Cancel")) + hbox.addWidget(b) + b.clicked.connect(self.reject) + b.setDefault(True) + else: + b = QPushButton(_("Close")) + hbox.addWidget(b) + b.clicked.connect(self.accept) + b.setDefault(True) vbox.addLayout(hbox) self.setLayout(vbox) From 7a8d6ca9bd9c104b29323e0238c1c0c720d39d4a Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 20 Mar 2024 20:15:23 +0000 Subject: [PATCH 03/20] Satochip: update build scripts for Linux, Windows & MacOS --- contrib/build-linux/appimage/Dockerfile | 2 ++ contrib/build-wine/build-electrum-git.sh | 2 +- contrib/build-wine/deterministic.spec | 1 + contrib/deterministic-build/requirements-hw.txt | 16 ++++++++++++++++ contrib/osx/make_osx.sh | 2 +- contrib/osx/osx.spec | 1 + contrib/requirements/requirements-hw.txt | 4 ++++ 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile index 30cfd0fb648f..80ad480d1429 100644 --- a/contrib/build-linux/appimage/Dockerfile +++ b/contrib/build-linux/appimage/Dockerfile @@ -34,6 +34,8 @@ RUN apt-get update -q && \ libffi-dev \ libncurses5-dev \ libncurses5 \ + libpcsclite-dev \ + swig \ libtinfo-dev \ libtinfo5 \ libsqlite3-dev \ diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index 15731e02ac75..1ece4715da67 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -43,7 +43,7 @@ $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-scr --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-binaries.txt info "Installing hardware wallet requirements..." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ - --no-binary :all: --only-binary cffi,cryptography,hidapi \ + --no-binary :all: --only-binary cffi,cryptography,hidapi,pyscard \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 013c55a97eb5..5010bf4c60d0 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -43,6 +43,7 @@ datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') +datas += collect_data_files('pysatochip') # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 6bc3f120e158..1acaaba995e7 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -334,6 +334,22 @@ pyaes==1.6.1 \ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pysatochip==0.12.6 \ + --hash=sha256:f751ae93ea784dc3ef77508da56df6222a195d8f7ead53f87d29ea84c7bc8f90 \ + --hash=sha256:d1255c5126c0c76b86f5eb1289b1cdc0b14a6f1265c82a63ce07074f6ccb2903 +pyscard==2.0.7 \ + --hash=sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf \ + --hash=sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed \ + --hash=sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646 \ + --hash=sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab \ + --hash=sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046 \ + --hash=sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9 \ + --hash=sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2 \ + --hash=sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc \ + --hash=sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43 \ + --hash=sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c \ + --hash=sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b \ + --hash=sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a pyserial==3.5 \ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 161998991dad..93f380f0e53b 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -84,7 +84,7 @@ python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: || fail "Could not install build dependencies (mac)" info "Installing some build-time deps for compilation..." -brew install autoconf automake libtool gettext coreutils pkgconfig +brew install autoconf automake libtool gettext coreutils pkgconfig swig info "Building PyInstaller." PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index bf15dce3c1ca..af93c660ab99 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -46,6 +46,7 @@ datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') +datas += collect_data_files('pysatochip') # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index a77e273febe4..39ec59605cd7 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -23,6 +23,10 @@ ckcc-protocol>=0.7.7 # device plugin: bitbox02 bitbox02>=6.2.0 +# device plugin: satochip +pyscard>=1.9.9 +pysatochip==0.12.6 + # device plugin: jade cbor>=1.0.0,<2.0.0 pyserial>=3.5.0,<4.0.0 From 7193d88ee9d1aa2ef755d63eb4159e86493186bb Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 20 Mar 2024 21:25:12 +0000 Subject: [PATCH 04/20] Satochip: clean code using flake8 --- electrum/plugins/satochip/qt.py | 16 ++++---- electrum/plugins/satochip/satochip.py | 54 ++++++++++----------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 7886314532ba..06fc34149b3d 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -4,7 +4,7 @@ from electrum.simple_config import SimpleConfig from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub, WalletWizardComponent +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub, WalletWizardComponent, QENewWalletWizard from electrum.plugin import hook from PyQt5.QtCore import Qt, pyqtSignal, QRegExp from PyQt5.QtGui import QRegExpValidator @@ -728,7 +728,7 @@ class SatochipSetupLayout(QVBoxLayout): def __init__(self, device): _logger.info("[SatochipSetupLayout] __init__()") QVBoxLayout.__init__(self) - + vbox = QVBoxLayout() # intro @@ -753,7 +753,7 @@ def __init__(self, device): self.addLayout(vbox2) # PIN validation - if (self.pw.text()=="" or self.pw.text()==None): + if (self.pw.text()=="" or self.pw.text() is None): self.validChanged.emit(False) def set_enabled(): @@ -801,7 +801,7 @@ def on_ready(self): def on_settings_valid_changed(self, is_valid: bool): _logger.info(f"[WCSatochipSetupParams] on_settings_valid_changed() is_valid: {is_valid}") self.valid = is_valid - + def apply(self): _logger.info("[WCSatochipSetupParams] apply()") current_cosigner = self.wizard.current_cosigner(self.wizard_data) @@ -812,7 +812,7 @@ class WCSatochipSetup(WalletWizardComponent): def __init__(self, parent, wizard): WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) _logger.info('[WCSatochipSetup] __init__()') # debugsatochip - + self.plugins = wizard.plugins self.plugin = self.plugins.get_plugin('satochip') @@ -865,7 +865,7 @@ class SatochipSeedLayout(QVBoxLayout): def __init__(self, device): QVBoxLayout.__init__(self) - + label = QLabel(_("Enter a label to name your device:")) self.label_e = QLineEdit() hl = QHBoxLayout() @@ -877,7 +877,7 @@ def __init__(self, device): self.text_e = QTextEdit() self.text_e.setMaximumHeight(60) msg = _("Enter your BIP39 mnemonic:") - + # TODO: validation? def set_enabled(): item = ' '.join(str(clean_text(self.text_e)).split()) @@ -973,4 +973,4 @@ def initialize_device_task(settings, device_id, handler): t.start() def apply(self): - pass \ No newline at end of file + pass diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index f4060d906daf..062366068f92 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -8,10 +8,11 @@ from electrum import descriptor from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int from electrum.i18n import _ -from electrum.plugin import Device, runs_in_hwd_thread -from electrum.keystore import Hardware_KeyStore, bip39_to_seed, bip39_is_checksum_valid +from electrum.plugin import Device, DeviceInfo +from electrum.keystore import Hardware_KeyStore, bip39_to_seed, bip39_is_checksum_valid, ScriptTypeNotSupported from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet +from electrum.wizard import NewWalletWizard from electrum.util import bfh, versiontuple, UserFacingException from electrum.crypto import hash_160, sha256d from electrum.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey @@ -64,17 +65,6 @@ def __init__(self, plugin: HW_PluginBase, handler): self.handler = handler #self.parser= CardDataParser() self.cc= CardConnector(self, _logger.getEffectiveLevel()) - self.ux_busy= False - - def show_error(self, message, clear_client=False): - _logger.error(f"[SatochipClient] show_error() {message}") - if not self.ux_busy: - self.handler.show_error(message) - else: - self.ux_busy = False - if clear_client: - self.client = None - #raise UserFacingException(message) def __repr__(self): return '' @@ -132,7 +122,6 @@ def has_usable_connection_with_device(self): def verify_PIN(self, pin=None): while(True): try: - print("DEBUG verify pin") #when pin is None, pysatochip use a cached pin if available (response, sw1, sw2)= self.cc.card_verify_PIN_simple(pin) return True @@ -140,20 +129,20 @@ def verify_PIN(self, pin=None): # recoverable errors except CardNotPresentError: msg = f"No card found! \nPlease insert card, then enter your PIN:" - (is_PIN, pin)= self.PIN_dialog(msg) - if is_PIN == False: + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN is False: return False except PinRequiredError as ex: # no pin value cached in pysatochip msg = f'Enter the PIN for your card:' - (is_PIN, pin)= self.PIN_dialog(msg) - if is_PIN == False: + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN is False: return False except WrongPinError as ex: pin= None # reset pin msg = f"Wrong PIN! {ex.pin_left} tries remaining! \n Enter the PIN for your card:" - (is_PIN, pin)= self.PIN_dialog(msg) - if is_PIN == False: + (is_PIN, pin)= self.PIN_dialog(msg) + if is_PIN is False: return False # unrecoverable errors @@ -194,7 +183,7 @@ def get_xpub(self, bip32_path, xtype): def ping_check(self): #check connection is working try: - print('ping_check')#debug + _logger.info('[SatochipClient] ping_check()')#debug #atr= self.cc.card_get_ATR() except Exception as e: _logger.exception(f"Exception: {str(e)}") @@ -221,9 +210,6 @@ def request(self, request_type, *args): return None def PIN_dialog(self, msg): - while self.ux_busy: - sleep(1) - while True: password = self.handler.get_passphrase(msg, False) if password is None: @@ -347,10 +333,10 @@ def sign_message(self, sequence, message, password, *, script_type=None): finally: _logger.info(f"[Satochip_KeyStore] sign_message: finally") self.handler.finished() - + def sign_transaction(self, tx, password): - _logger.info(f"In sign_transaction(): tx: {str(tx)}") + _logger.info(f"In sign_transaction(): tx: {str(tx)}") client = self.get_client() is_ok = client.verify_PIN() segwitTransaction = False @@ -364,8 +350,8 @@ def sign_transaction(self, tx, password): txOutputs += script #txOutputs = bfh(txOutputs) hashOutputs = sha256d(bfh(txOutputs)).hex() - _logger.info(f"In sign_transaction(): hashOutputs= {hashOutputs}") - _logger.info(f"In sign_transaction(): outputs= {txOutputs}") + _logger.info(f"In sign_transaction(): hashOutputs= {hashOutputs}") + _logger.info(f"In sign_transaction(): outputs= {txOutputs}") # Fetch inputs of the transaction to sign for i,txin in enumerate(tx.inputs()): @@ -624,9 +610,9 @@ def _import_seed(self, settings, device_id, handler): client = devmgr.client_by_id(device_id) if not client: raise Exception(_("The device was disconnected.")) - + label, seed, passphrase = settings - + # check seed validity (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(seed) if is_checksum_valid and is_wordlist_valid: @@ -636,7 +622,7 @@ def _import_seed(self, settings, device_id, handler): else: _logger.error(f"[SatochipPlugin] _import_seed() wrong seed format!") raise Exception('Wrong BIP39 mnemonic format!') - + # verify pin: is_ok = client.verify_PIN() @@ -668,13 +654,13 @@ def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_info: {device_info}") _logger.info(f"[SatochipPlugin] wizard_entry_for_device() new_wallet: {new_wallet}") - device_state = device_info.initialized # can be None, False or True. + device_state = device_info.initialized # can be None, False or True. # None is used to distinguish a completely new card from a card where the seed has been reset, but the PIN is still set. _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_state: {device_state}") if new_wallet: - if device_state == None: + if device_state is None: return 'satochip_not_setup' - elif device_state == False: + elif device_state is False: return 'satochip_not_seeded' else: return 'satochip_start' From ed537de7f859190f8ecaea636d9889f36f28a06f Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 21 Mar 2024 09:28:42 +0000 Subject: [PATCH 05/20] Satochip: correct flake8 issue in __init__.py flake8 . --count --select="$ELECTRUM_LINTERS" --ignore="$ELECTRUM_LINTERS_IGNORE" --show-source --statistics --exclude "*_pb2.py,electrum/_vendor/" ./electrum/plugins/satochip/__init__.py:6:23: W292 no newline at end of file available_for = ['qt'] ^ 1 W292 no newline at end of file --- electrum/plugins/satochip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/satochip/__init__.py b/electrum/plugins/satochip/__init__.py index 78a3d14c3217..75b91583a857 100644 --- a/electrum/plugins/satochip/__init__.py +++ b/electrum/plugins/satochip/__init__.py @@ -3,4 +3,4 @@ description = 'Provides support for Satochip hardware wallet' requires = [('satochip', 'github.com/Toporin/pysatochip')] registers_keystore = ('hardware', 'satochip', "Satochip wallet") -available_for = ['qt'] \ No newline at end of file +available_for = ['qt'] From 040d0ca43ac248e595b14ed340c601d613e969b5 Mon Sep 17 00:00:00 2001 From: Toporin Date: Sun, 9 Jun 2024 20:09:23 +0100 Subject: [PATCH 06/20] Satochip: adapt bitcoin.py/transaction.py calls bitcoin.py/transaction.py: API changes: rm most hex usage See also https://github.com/spesmilo/electrum/commit/2f1095510c2f50bfe2c2ebc0c7159e9b8f945aca --- electrum/plugins/satochip/satochip.py | 63 +++++++++++++-------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 062366068f92..f2d834eaa6d7 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -6,16 +6,16 @@ from electrum import mnemonic from electrum import constants from electrum import descriptor -from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int +from electrum.bitcoin import TYPE_ADDRESS, var_int from electrum.i18n import _ from electrum.plugin import Device, DeviceInfo from electrum.keystore import Hardware_KeyStore, bip39_to_seed, bip39_is_checksum_valid, ScriptTypeNotSupported -from electrum.transaction import Transaction +from electrum.transaction import Transaction, Sighash from electrum.wallet import Standard_Wallet from electrum.wizard import NewWalletWizard from electrum.util import bfh, versiontuple, UserFacingException from electrum.crypto import hash_160, sha256d -from electrum.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey +from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig, ECPubkey from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath from electrum.logging import get_logger from electrum.simple_config import SimpleConfig @@ -341,17 +341,18 @@ def sign_transaction(self, tx, password): is_ok = client.verify_PIN() segwitTransaction = False - # outputs - txOutputs = var_int(len(tx.outputs())) + # outputs (bytes format) + txOutputs = bytearray() + txOutputs += var_int(len(tx.outputs())) for o in tx.outputs(): - txOutputs += int_to_hex(o.value, 8) - script = o.scriptpubkey.hex() - txOutputs += var_int(len(script)//2) + txOutputs += int.to_bytes(o.value, length=8, byteorder="little", signed=False) + script = o.scriptpubkey + txOutputs += var_int(len(script)) txOutputs += script - #txOutputs = bfh(txOutputs) - hashOutputs = sha256d(bfh(txOutputs)).hex() + txOutputs = bytes(txOutputs) + hashOutputs = sha256d(txOutputs).hex() _logger.info(f"In sign_transaction(): hashOutputs= {hashOutputs}") - _logger.info(f"In sign_transaction(): outputs= {txOutputs}") + _logger.info(f"In sign_transaction(): outputs= {txOutputs.hex()}") # Fetch inputs of the transaction to sign for i,txin in enumerate(tx.inputs()): @@ -377,23 +378,21 @@ def sign_transaction(self, tx, password): if not inputPath: self.give_error("No matching pubkey for sign_transaction") # should never happen inputPath = convert_bip32_intpath_to_strpath(inputPath) #[2:] - inputHash = sha256d(bfh(tx.serialize_preimage(i))) + inputHash = sha256d(tx.serialize_preimage(i)) # get corresponing extended key (depth, bytepath)= bip32path2bytes(inputPath) (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) - # parse tx - pre_tx_hex= tx.serialize_preimage(i) - pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes - pre_hash = sha256d(bfh(pre_tx_hex)) - pre_hash_hex= pre_hash.hex() - _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx_hex= {pre_tx_hex}") - _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash_hex}") - (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction) - tx_hash_hex= bytearray(tx_hash).hex() - if pre_hash_hex!= tx_hash_hex: - raise RuntimeError("[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}") + # parse tx (bytes format) + pre_tx= tx.serialize_preimage(i) + pre_hash = sha256d(pre_tx) + _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx= {pre_tx.hex()}") + _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash.hex()}") + (response, sw1, sw2, tx_hash_list, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction) + tx_hash= bytearray(tx_hash_list) + if pre_hash!= tx_hash: + raise RuntimeError(f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash.hex()} vs {tx_hash.hex()}") #2FA keynbr= 0xFF #for extended key @@ -402,9 +401,9 @@ def sign_transaction(self, tx, password): import json coin_type= 1 if constants.net.TESTNET else 0 if segwitTransaction: - msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction, 'txo':txOutputs, 'ty':script_type} + msg= {'tx':pre_tx.hex(), 'ct':coin_type, 'sw':segwitTransaction, 'txo':txOutputs.hex(), 'ty':script_type} else: - msg= {'tx':pre_tx_hex, 'ct':coin_type, 'sw':segwitTransaction} + msg= {'tx':pre_tx.hex(), 'ct':coin_type, 'sw':segwitTransaction} msg= json.dumps(msg) #do challenge-response with 2FA device... @@ -414,25 +413,23 @@ def sign_transaction(self, tx, password): hmac= None # sign tx - (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, hmac) + (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash_list, hmac) # check sw1sw2 for error (0x9c0b if wrong challenge-response) if sw1 != 0x90 or sw2 != 0x00: self.give_error(f"Satochip failed to sign transaction with code {hex(256*sw1+sw2)}") # enforce low-S signature (BIP 62) tx_sig = bytes(tx_sig) #bytearray(tx_sig) - r,s= get_r_and_s_from_der_sig(tx_sig) + r,s= get_r_and_s_from_ecdsa_der_sig(tx_sig) if s > CURVE_ORDER//2: s = CURVE_ORDER - s - tx_sig=der_sig_from_r_and_s(r, s) + tx_sig = ecdsa_der_sig_from_r_and_s(r, s) #update tx with signature - tx_sig = tx_sig.hex()+'01' - #tx.add_signature_to_txin(i,j,tx_sig) - tx.add_signature_to_txin(txin_idx=i, - signing_pubkey=my_pubkey.hex(), - sig=tx_sig) + tx_sig = tx_sig + Sighash.to_sigbytes(Sighash.ALL) + tx.add_signature_to_txin(txin_idx=i, signing_pubkey=my_pubkey, sig=tx_sig) # end of for loop + _logger.info(f"Tx is complete: {str(tx.is_complete())}") tx.raw = tx.serialize() return From a41fe39363d3fd0e728b366f21f0ff13e46e0d23 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 20 Jun 2024 09:43:46 +0100 Subject: [PATCH 07/20] Clean Satochip plugin code using Flake8 & autopep8 Using: $ ELECTRUM_LINTERS='E,F,W,C90,B' --- electrum/plugins/satochip/qt.py | 459 +++++++++++++++----------- electrum/plugins/satochip/satochip.py | 451 ++++++++++++++----------- 2 files changed, 514 insertions(+), 396 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 06fc34149b3d..14cbd1eb979e 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -2,30 +2,32 @@ from electrum.logging import get_logger from electrum.keystore import bip39_is_checksum_valid from electrum.simple_config import SimpleConfig -from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) +from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, + OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub, WalletWizardComponent, QENewWalletWizard from electrum.plugin import hook from PyQt5.QtCore import Qt, pyqtSignal, QRegExp from PyQt5.QtGui import QRegExpValidator -from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) +from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, + QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) from functools import partial from os import urandom import textwrap import threading -#satochip +# satochip from .satochip import SatochipPlugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -#pysatochip +# pysatochip from pysatochip.CardConnector import CardConnector, UnexpectedSW12Error, CardError, CardNotPresentError, WrongPinError from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION # Seed import wizard msg -PASSPHRASE_HELP_SHORT =_( +PASSPHRASE_HELP_SHORT = _( "A passphrase is an optional feature that allows you to extend your seed with additional entropy. " "A passphrase is not a PIN.") PASSPHRASE_NOT_PIN = _( @@ -35,7 +37,8 @@ _logger = get_logger(__name__) -MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") +MSG_USE_2FA = _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") + class Plugin(SatochipPlugin, QtPluginBase): icon_unpaired = "satochip_unpaired.png" @@ -67,9 +70,11 @@ def show_settings_dialog(self, window, keystore): def connect(): device_id = self.choose_device(window, keystore) return device_id + def show_dialog(device_id): if device_id: - SatochipSettingsDialog(window, self, keystore, device_id).exec_() + SatochipSettingsDialog( + window, self, keystore, device_id).exec_() keystore.thread.add(connect, on_success=show_dialog) @hook @@ -90,11 +95,13 @@ def extend_wizard(self, wizard: 'QENewWalletWizard'): } wizard.navmap_merge(views) + class Satochip_Handler(QtHandlerBase): def __init__(self, win): super(Satochip_Handler, self).__init__(win, 'Satochip') + class SatochipSettingsDialog(WindowModalDialog): '''This dialog doesn't require a device be paired with a wallet. @@ -142,9 +149,10 @@ def connect_and_doit(): ] for row_num, (member_name, label) in enumerate(rows): widget = QLabel('') - widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) + widget.setTextInteractionFlags( + Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) - grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight) + grid.addWidget(QLabel(label), y, 0, 1, 1, Qt.AlignRight) grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft) setattr(self, member_name, widget) y += 1 @@ -152,44 +160,50 @@ def connect_and_doit(): body_layout.addLayout(grid) pin_btn = QPushButton('Change PIN') + def _change_pin(): thread.add(connect_and_doit, on_success=self.change_pin) pin_btn.clicked.connect(_change_pin) seed_btn = QPushButton('Reset seed') + def _reset_seed(): thread.add(connect_and_doit, on_success=self.reset_seed) thread.add(connect_and_doit, on_success=self.show_values) seed_btn.clicked.connect(_reset_seed) set_2FA_btn = QPushButton('Enable 2FA') + def _set_2FA(): thread.add(connect_and_doit, on_success=self.set_2FA) thread.add(connect_and_doit, on_success=self.show_values) set_2FA_btn.clicked.connect(_set_2FA) reset_2FA_btn = QPushButton('Disable 2FA') + def _reset_2FA(): thread.add(connect_and_doit, on_success=self.reset_2FA) thread.add(connect_and_doit, on_success=self.show_values) reset_2FA_btn.clicked.connect(_reset_2FA) change_2FA_server_btn = QPushButton('Select 2FA server') + def _change_2FA_server(): thread.add(connect_and_doit, on_success=self.change_2FA_server) change_2FA_server_btn.clicked.connect(_change_2FA_server) verify_card_btn = QPushButton('Verify card') + def _verify_card(): thread.add(connect_and_doit, on_success=self.verify_card) verify_card_btn.clicked.connect(_verify_card) change_card_label_btn = QPushButton('Change label') + def _change_card_label(): thread.add(connect_and_doit, on_success=self.change_card_label) change_card_label_btn.clicked.connect(_change_card_label) - y += 3 grid.addWidget(pin_btn, y, 0, 1, 2, Qt.AlignHCenter) y += 2 @@ -213,28 +227,31 @@ def _change_card_label(): # Fetch values and show them thread.add(connect_and_doit, on_success=self.show_values) - def show_values(self, client): _logger.info("Show value!") - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: - msg= f"action cancelled by user" + msg = f"action cancelled by user" self.window.show_error(msg) return - sw_rel= 'v' + str(SATOCHIP_PROTOCOL_MAJOR_VERSION) + '.' + str(SATOCHIP_PROTOCOL_MINOR_VERSION) + 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): - #fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) - fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) +'-'+ str(d["applet_major_version"]) +'.'+ str(d["applet_minor_version"]) + (response, sw1, sw2, d) = client.cc.card_get_status() + if (sw1 == 0x90 and sw2 == 0x00): + # fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) + fw_rel = 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) + \ + '-' + str(d["applet_major_version"]) + '.' + \ + str(d["applet_minor_version"]) self.fw_version.setText('%s' % fw_rel) - #is_seeded? - 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 + # is_seeded? + 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") @@ -254,43 +271,44 @@ def show_values(self, client): self.needs_SC.setText('%s' % "no") # card label - (response, sw1, sw2, label)= client.cc.card_get_label() - if (label==""): - label= "(none)" + (response, sw1, sw2, label) = client.cc.card_get_label() + if (label == ""): + label = "(none)" self.card_label.setText('%s' % label) else: - fw_rel= "(unitialized)" + 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)") self.card_label.setText('%s' % "(none)") - def change_pin(self, client): _logger.info("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) + 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 - oldpin= list(oldpin) - newpin= list(newpin) + oldpin = list(oldpin) + newpin = list(newpin) try: - (response, sw1, sw2)= client.cc.card_change_PIN(0, oldpin, newpin) - if (sw1==0x90 and sw2==0x00): - msg= _("PIN changed successfully!") + (response, sw1, sw2) = client.cc.card_change_PIN(0, oldpin, newpin) + if (sw1 == 0x90 and sw2 == 0x00): + msg = _("PIN changed successfully!") self.window.show_message(msg) else: - msg= _("Failed to change PIN!") + msg = _("Failed to change PIN!") self.window.show_error(msg) except WrongPinError as ex: - msg= (f"Failed to change PIN. Wrong PIN! {ex.pin_left} tries remaining!") + msg = ( + f"Failed to change PIN. Wrong PIN! {ex.pin_left} tries remaining!") self.window.show_error(msg) except Exception as ex: self.window.show_error(str(ex)) @@ -314,52 +332,58 @@ def reset_seed(self, client): if (password is None): return pin = password.encode('utf8') - pin= list(pin) + pin = list(pin) # if 2FA is enabled, get challenge-response - hmac=[] + hmac = [] if (client.cc.needs_2FA is None): - (response, sw1, sw2, d)=client.cc.card_get_status() + (response, sw1, sw2, d) = client.cc.card_get_status() if client.cc.needs_2FA: # challenge based on authentikey - authentikeyx= bytearray(client.cc.parser.authentikey_coordx).hex() + authentikeyx = bytearray(client.cc.parser.authentikey_coordx).hex() # format & encrypt msg import json - msg= {'action':"reset_seed", 'authentikeyx':authentikeyx} - 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 - - #do challenge-response with 2FA device... - self.window.show_message('2FA request sent! Approve or reject request on your second device.') - server_2FA = self.config.get("satochip_2FA_server", default= SERVER_LIST[0]) - Satochip2FA.do_challenge_response(d, server_name= server_2FA) + msg = {'action': "reset_seed", 'authentikeyx': authentikeyx} + 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 + + # do challenge-response with 2FA device... + self.window.show_message( + '2FA request sent! Approve or reject request on your second device.') + server_2FA = self.config.get( + "satochip_2FA_server", default=SERVER_LIST[0]) + Satochip2FA.do_challenge_response(d, server_name=server_2FA) # decrypt and parse reply to extract challenge response try: - reply_encrypt= d['reply_encrypt'] + reply_encrypt = d['reply_encrypt'] except Exception as e: self.give_error("No response received from 2FA", True) - reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) - _logger.info("challenge:response= "+ reply_decrypt) - reply_decrypt= reply_decrypt.split(":") - chalresponse=reply_decrypt[1] - hmac= list(bytes.fromhex(chalresponse)) + reply_decrypt = client.cc.card_crypt_transaction_2FA( + reply_encrypt, False) + _logger.info("challenge:response= " + reply_decrypt) + reply_decrypt = reply_decrypt.split(":") + chalresponse = reply_decrypt[1] + hmac = list(bytes.fromhex(chalresponse)) # send request (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.") + if (sw1 == 0x90 and sw2 == 0x00): + msg = _( + "Seed reset successfully!\nYou should close this wallet and launch the wizard to generate a new wallet.") self.window.show_message(msg) - #to do: close client? - elif (sw1==0x9c and sw2==0x0b): - msg= _(f"Failed to reset seed: request rejected by 2FA device (error code: {hex(256*sw1+sw2)})") + # to do: close client? + elif (sw1 == 0x9c and sw2 == 0x0b): + msg = _( + f"Failed to reset seed: request rejected by 2FA device (error code: {hex(256*sw1+sw2)})") self.window.show_message(msg) - #to do: close client? + # to do: close client? else: - msg= _(f"Failed to reset seed with error code: {hex(256*sw1+sw2)}") + msg = _( + f"Failed to reset seed with error code: {hex(256*sw1+sw2)}") self.window.show_error(msg) def reset_seed_dialog(self, msg): @@ -381,107 +405,120 @@ def reset_seed_dialog(self, msg): def set_2FA(self, client): if not client.cc.needs_2FA: - use_2FA=client.handler.yes_no_question(MSG_USE_2FA) + use_2FA = client.handler.yes_no_question(MSG_USE_2FA) if (use_2FA): # verify PIN - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: - msg= f"action cancelled by user" + msg = f"action cancelled by user" self.window.show_error(msg) return - secret_2FA= urandom(20) - secret_2FA_hex=secret_2FA.hex() + secret_2FA = urandom(20) + secret_2FA_hex = secret_2FA.hex() # the secret must be shared with the second factor app (eg on a smartphone) try: - help_txt="Scan the QR-code with your Satochip-2FA app and make a backup of the following secret: "+ secret_2FA_hex - d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, help_text=help_txt, show_copy_text_btn=True, show_cancel_btn=True, config=self.config) - result=d.exec_() # result should be 0 or 1 - if (result==1): + help_txt = "Scan the QR-code with your Satochip-2FA app and make a backup of the following secret: " + secret_2FA_hex + d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, + help_text=help_txt, show_copy_text_btn=True, show_cancel_btn=True, config=self.config) + result = d.exec_() # result should be 0 or 1 + if (result == 1): # further communications will require an id and an encryption key (for privacy). # Both are derived from the secret_2FA using a one-way function inside the Satochip - amount_limit= 0 # i.e. always use - (response, sw1, sw2)=client.cc.card_set_2FA_key(secret_2FA, amount_limit) - if sw1!=0x90 or sw2!=0x00: - _logger.info(f"Unable to set 2FA with error code:= {hex(256*sw1+sw2)}") - self.window.show_error(f'Unable to setup 2FA with error code: {hex(256*sw1+sw2)}') + amount_limit = 0 # i.e. always use + (response, sw1, sw2) = client.cc.card_set_2FA_key( + secret_2FA, amount_limit) + if sw1 != 0x90 or sw2 != 0x00: + _logger.info( + f"Unable to set 2FA with error code:= {hex(256*sw1+sw2)}") + self.window.show_error( + f'Unable to setup 2FA with error code: {hex(256*sw1+sw2)}') else: - self.window.show_message("2FA enabled successfully!") + self.window.show_message( + "2FA enabled successfully!") else: self.window.show_message("2FA cancelled by user!") return except Exception as e: _logger.info(f"SatochipPlugin: setup 2FA error: {e}") - self.window.show_error(f'Unable to setup 2FA with error code: {e}') + self.window.show_error( + f'Unable to setup 2FA with error code: {e}') return def reset_2FA(self, client): if client.cc.needs_2FA: # verify pin - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: - msg= f"action cancelled by user" + msg = f"action cancelled by user" self.window.show_error(msg) return # challenge based on ID_2FA # format & encrypt msg import json - msg= {'action':"reset_2FA"} - 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 - - #do challenge-response with 2FA device... - self.window.show_message('2FA request sent! Approve or reject request on your second device.') - server_2FA = self.config.get("satochip_2FA_server", default= SERVER_LIST[0]) - Satochip2FA.do_challenge_response(d, server_name= server_2FA) + msg = {'action': "reset_2FA"} + 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 + + # do challenge-response with 2FA device... + self.window.show_message( + '2FA request sent! Approve or reject request on your second device.') + server_2FA = self.config.get( + "satochip_2FA_server", default=SERVER_LIST[0]) + Satochip2FA.do_challenge_response(d, server_name=server_2FA) # decrypt and parse reply to extract challenge response try: - reply_encrypt= d['reply_encrypt'] + reply_encrypt = d['reply_encrypt'] except Exception as e: self.give_error("No response received from 2FA!", True) - reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) - _logger.info("challenge:response= "+ reply_decrypt) - reply_decrypt= reply_decrypt.split(":") - chalresponse=reply_decrypt[1] - hmac= list(bytes.fromhex(chalresponse)) + reply_decrypt = client.cc.card_crypt_transaction_2FA( + reply_encrypt, False) + _logger.info("challenge:response= " + reply_decrypt) + reply_decrypt = reply_decrypt.split(":") + chalresponse = reply_decrypt[1] + hmac = list(bytes.fromhex(chalresponse)) # send request (response, sw1, sw2) = client.cc.card_reset_2FA_key(hmac) - if (sw1==0x90 and sw2==0x00): - msg= _("2FA reset successfully!") - client.cc.needs_2FA= False + if (sw1 == 0x90 and sw2 == 0x00): + msg = _("2FA reset successfully!") + client.cc.needs_2FA = False self.window.show_message(msg) - elif (sw1==0x9c and sw2==0x17): - msg= _(f"Failed to reset 2FA: \nyou must reset the seed first (error code {hex(256*sw1+sw2)})") + elif (sw1 == 0x9c and sw2 == 0x17): + msg = _( + f"Failed to reset 2FA: \nyou must reset the seed first (error code {hex(256*sw1+sw2)})") self.window.show_error(msg) else: - msg= _(f"Failed to reset 2FA with error code: {hex(256*sw1+sw2)}") + msg = _( + f"Failed to reset 2FA with error code: {hex(256*sw1+sw2)}") self.window.show_error(msg) else: - msg= _(f"2FA is already disabled!") + msg = _(f"2FA is already disabled!") self.window.show_error(msg) def change_2FA_server(self, client): _logger.info("in change_2FA_server") - help_txt="Select 2FA server in the list:" - option_name= "satochip_2FA_server" - options= SERVER_LIST #["server1", "server2", "server3"] - title= "Select 2FA server" - d = SelectOptionsDialog(option_name = option_name, options = options, parent=None, title=title, help_text=help_txt, config=self.config) - result=d.exec_() # result should be 0 or 1 + help_txt = "Select 2FA server in the list:" + option_name = "satochip_2FA_server" + options = SERVER_LIST # ["server1", "server2", "server3"] + title = "Select 2FA server" + d = SelectOptionsDialog(option_name=option_name, options=options, + parent=None, title=title, help_text=help_txt, config=self.config) + result = d.exec_() # result should be 0 or 1 def verify_card(self, client): # verify pin - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: return # verify authenticity - is_authentic, txt_ca, txt_subca, txt_device, txt_error = self.card_verify_authenticity(client) + is_authentic, txt_ca, txt_subca, txt_device, txt_error = self.card_verify_authenticity( + client) # wrap data for better display tmp = "" @@ -498,53 +535,56 @@ def verify_card(self, client): txt_device = tmp if is_authentic: - txt_result= 'Device authenticated successfully!' + txt_result = 'Device authenticated successfully!' else: - txt_result= ''.join(['Error: could not authenticate the issuer of this card! \n', - 'Reason: ', txt_error , '\n\n', - 'If you did not load the card yourself, be extremely careful! \n', - 'Contact support(at)satochip.io to report a suspicious device.']) + txt_result = ''.join(['Error: could not authenticate the issuer of this card! \n', + 'Reason: ', txt_error, '\n\n', + 'If you did not load the card yourself, be extremely careful! \n', + 'Contact support(at)satochip.io to report a suspicious device.']) d = DeviceCertificateDialog( - parent=None, - title= "Satochip certificate chain", - is_authentic = is_authentic, - txt_summary = txt_result, - txt_ca = txt_ca, - txt_subca = txt_subca, - txt_device = txt_device, + parent=None, + title="Satochip certificate chain", + is_authentic=is_authentic, + txt_summary=txt_result, + txt_ca=txt_ca, + txt_subca=txt_subca, + txt_device=txt_device, ) - result=d.exec_() + result = d.exec_() + # todo: add this function in pysatochip + def card_verify_authenticity(self, client): - def card_verify_authenticity(self, client): #todo: add this function in pysatochip - - cert_pem=txt_error="" + cert_pem = txt_error = "" try: - cert_pem=client.cc.card_export_perso_certificate() - _logger.info('Cert PEM: '+ str(cert_pem)) + cert_pem = client.cc.card_export_perso_certificate() + _logger.info('Cert PEM: ' + str(cert_pem)) except CardError as ex: - txt_error= ''.join(["Unable to get device certificate: feature unsupported! \n", + txt_error = ''.join(["Unable to get device certificate: feature unsupported! \n", "Authenticity validation is only available starting with Satochip v0.12 and higher"]) except CardNotPresentError as ex: - txt_error= "No card found! Please insert card." + txt_error = "No card found! Please insert card." except UnexpectedSW12Error as ex: - txt_error= "Exception during device certificate export: " + str(ex) + txt_error = "Exception during device certificate export: " + \ + str(ex) - if cert_pem=="(empty)": - txt_error= "Device certificate is empty: the card has not been personalized!" + if cert_pem == "(empty)": + txt_error = "Device certificate is empty: the card has not been personalized!" - if txt_error!="": + if txt_error != "": return False, "(empty)", "(empty)", "(empty)", txt_error # check the certificate chain from root CA to device from pysatochip.certificate_validator import CertificateValidator - validator= CertificateValidator() - is_valid_chain, device_pubkey, txt_ca, txt_subca, txt_device, txt_error= validator.validate_certificate_chain(cert_pem, client.cc.card_type) + validator = CertificateValidator() + is_valid_chain, device_pubkey, txt_ca, txt_subca, txt_device, txt_error = validator.validate_certificate_chain( + cert_pem, client.cc.card_type) if not is_valid_chain: return False, txt_ca, txt_subca, txt_device, txt_error # perform challenge-response with the card to ensure that the key is correctly loaded in the device - is_valid_chalresp, txt_error = client.cc.card_challenge_response_pki(device_pubkey) + is_valid_chalresp, txt_error = client.cc.card_challenge_response_pki( + device_pubkey) return is_valid_chalresp, txt_ca, txt_subca, txt_device, txt_error @@ -555,7 +595,7 @@ def change_card_label(self, client): ]) # verify pin - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: # msg= f"action cancelled by user" # self.window.show_error(msg) @@ -568,13 +608,15 @@ def change_card_label(self, client): return # set new label - (response, sw1, sw2)= client.cc.card_set_label(label) - if (sw1==0x90 and sw2==0x00): + (response, sw1, sw2) = client.cc.card_set_label(label) + if (sw1 == 0x90 and sw2 == 0x00): self.window.show_message(_("Card label changed successfully!")) - elif (sw1==0x6D and sw2==0x00): - self.window.show_error(_("Error: card does not support label!")) # starts with satochip v0.12 + elif (sw1 == 0x6D and sw2 == 0x00): + # starts with satochip v0.12 + self.window.show_error(_("Error: card does not support label!")) else: - self.window.show_error(f"Error while changing label: sw12={hex(sw1)} {hex(sw2)}") + self.window.show_error( + f"Error while changing label: sw12={hex(sw1)} {hex(sw2)}") def change_card_label_dialog(self, client, msg): _logger.info("In change_card_label_dialog") @@ -592,10 +634,11 @@ def change_card_label_dialog(self, client, msg): d.setLayout(vbox) label = pw.text() if d.exec_() else None - if label is None or len(label.encode('utf-8'))<=64: + if label is None or len(label.encode('utf-8')) <= 64: return label else: - self.window.show_error(_("Card label should not be longer than 64 chars!")) + self.window.show_error( + _("Card label should not be longer than 64 chars!")) class SelectOptionsDialog(WindowModalDialog): @@ -625,7 +668,7 @@ def set_option(): config.set_key(option_name, options_combo.currentText(), save=True) _logger.info("config changed!") - default= config.get(option_name, default= SERVER_LIST[0]) + default = config.get(option_name, default=SERVER_LIST[0]) options_combo = QComboBox() options_combo.addItems(options) options_combo.setCurrentText(default) @@ -648,6 +691,7 @@ def set_option(): # workaround: self.setMinimumSize(self.sizeHint()) + class DeviceCertificateDialog(WindowModalDialog): def __init__( @@ -656,15 +700,14 @@ def __init__( parent=None, title="", is_authentic, - txt_summary = "", - txt_ca = "", - txt_subca = "", - txt_device = "", + txt_summary="", + txt_ca="", + txt_subca="", + txt_device="", ): WindowModalDialog.__init__(self, parent, title) - - #super(QWidget, self).__init__(parent) + # super(QWidget, self).__init__(parent) self.layout = QVBoxLayout(self) # add summary text @@ -681,12 +724,12 @@ def __init__( self.tab1 = QWidget() self.tab2 = QWidget() self.tab3 = QWidget() - self.tabs.resize(300,200) + self.tabs.resize(300, 200) # Add tabs - self.tabs.addTab(self.tab1,"RootCA") - self.tabs.addTab(self.tab2,"SubCA") - self.tabs.addTab(self.tab3,"Device") + self.tabs.addTab(self.tab1, "RootCA") + self.tabs.addTab(self.tab2, "SubCA") + self.tabs.addTab(self.tab3, "Device") # Create first tab self.tab1.layout = QVBoxLayout(self) @@ -722,6 +765,7 @@ def clean_text(widget): text = widget.toPlainText().strip() return ' '.join(text.split()) + class SatochipSetupLayout(QVBoxLayout): validChanged = pyqtSignal([bool], arguments=['valid']) @@ -732,57 +776,59 @@ def __init__(self, device): vbox = QVBoxLayout() # intro - msg_setup = WWLabel(_("Please take a moment to set up your Satochip. This must be done only once.")) + msg_setup = WWLabel( + _("Please take a moment to set up your Satochip. This must be done only once.")) vbox.addWidget(msg_setup) self.pw = PasswordLineEdit() self.pw.setMinimumWidth(32) - #self.pw.setMaximumWidth(32) - #self.pw.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) + # self.pw.setMaximumWidth(32) + # self.pw.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) vbox.addWidget(WWLabel("Enter new PIN:")) vbox.addWidget(self.pw) self.addLayout(vbox) self.pw2 = PasswordLineEdit() self.pw2.setMinimumWidth(32) - #self.pw2.setMaximumWidth(32) - #self.pw2.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) + # self.pw2.setMaximumWidth(32) + # self.pw2.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) vbox2 = QVBoxLayout() vbox2.addWidget(WWLabel("Confirm new PIN:")) vbox2.addWidget(self.pw2) self.addLayout(vbox2) # PIN validation - if (self.pw.text()=="" or self.pw.text() is None): + if (self.pw.text() == "" or self.pw.text() is None): self.validChanged.emit(False) def set_enabled(): - is_valid= True + is_valid = True if self.pw.text() != self.pw2.text(): - is_valid= False + is_valid = False pw_bytes = self.pw.text().encode("utf-8") - if len(pw_bytes)<4 or len(pw_bytes)>16: - is_valid= False + if len(pw_bytes) < 4 or len(pw_bytes) > 16: + is_valid = False pw2_bytes = self.pw2.text().encode("utf-8") - if len(pw2_bytes)<4 or len(pw2_bytes)>16: - is_valid= False + if len(pw2_bytes) < 4 or len(pw2_bytes) > 16: + is_valid = False self.validChanged.emit(is_valid) self.pw2.textChanged.connect(set_enabled) self.pw.textChanged.connect(set_enabled) - def get_settings(self): _logger.info("[SatochipSetupLayout] get_settings()") return self.pw.text() + class WCSatochipSetupParams(WalletWizardComponent): def __init__(self, parent, wizard): _logger.info("[WCSatochipSetupParams] __init__()") - WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + WalletWizardComponent.__init__( + self, parent, wizard, title=_('Satochip Setup')) self.plugins = wizard.plugins self._busy = True @@ -791,27 +837,31 @@ def on_ready(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) _name, _info = current_cosigner['hardware_device'] self.settings_layout = SatochipSetupLayout(_info.device.id_) - self.settings_layout.validChanged.connect(self.on_settings_valid_changed) + self.settings_layout.validChanged.connect( + self.on_settings_valid_changed) self.layout().addLayout(self.settings_layout) self.layout().addStretch(1) - self.valid = True # debug + self.valid = True # debug self.busy = False def on_settings_valid_changed(self, is_valid: bool): - _logger.info(f"[WCSatochipSetupParams] on_settings_valid_changed() is_valid: {is_valid}") + _logger.info( + f"[WCSatochipSetupParams] on_settings_valid_changed() is_valid: {is_valid}") self.valid = is_valid def apply(self): _logger.info("[WCSatochipSetupParams] apply()") current_cosigner = self.wizard.current_cosigner(self.wizard_data) - current_cosigner['satochip_setup_settings'] = self.settings_layout.get_settings() + current_cosigner['satochip_setup_settings'] = self.settings_layout.get_settings( + ) class WCSatochipSetup(WalletWizardComponent): def __init__(self, parent, wizard): - WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) - _logger.info('[WCSatochipSetup] __init__()') # debugsatochip + WalletWizardComponent.__init__( + self, parent, wizard, title=_('Satochip Setup')) + _logger.info('[WCSatochipSetup] __init__()') # debugsatochip self.plugins = wizard.plugins self.plugin = self.plugins.get_plugin('satochip') @@ -821,23 +871,25 @@ def __init__(self, parent, wizard): self._busy = True def on_ready(self): - _logger.info('[WCSatochipSetup] on_ready()') # debugsatochip + _logger.info('[WCSatochipSetup] on_ready()') # debugsatochip current_cosigner = self.wizard.current_cosigner(self.wizard_data) settings = current_cosigner['satochip_setup_settings'] - #method = current_cosigner['satochip_init'] + # method = current_cosigner['satochip_init'] _name, _info = current_cosigner['hardware_device'] device_id = _info.device.id_ - _logger.info(f'[WCSatochipSetup] on_ready() device_id: {device_id}') # debugsatochip - + # debugsatochip + _logger.info(f'[WCSatochipSetup] on_ready() device_id: {device_id}') - client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + client = self.plugins.device_manager.client_by_id( + device_id, scan_now=False) client.handler = self.plugin.create_handler(self.wizard) def initialize_device_task(settings, device_id, client): try: - #self.plugin._initialize_device(settings, method, device_id, handler) + # self.plugin._initialize_device(settings, method, device_id, handler) self.plugin._setup_device(settings, device_id, client) - _logger.info('[WCSatochipSetup] initialize_device_task() Done initialize device') + _logger.info( + '[WCSatochipSetup] initialize_device_task() Done initialize device') self.valid = True self.wizard.requestNext.emit() # triggers Next GUI thread from event loop except Exception as e: @@ -860,6 +912,7 @@ def apply(self): # Import seed wizard # ########################## + class SatochipSeedLayout(QVBoxLayout): validChanged = pyqtSignal([bool], arguments=['valid']) @@ -893,9 +946,9 @@ def set_enabled(): passphrase_warning.setStyleSheet("color: red") self.addWidget(passphrase_msg) self.addWidget(passphrase_warning) - #self.cb_phrase = QCheckBox(_('Enable passphrases')) - #self.cb_phrase.setChecked(False) - #self.addWidget(self.cb_phrase) + # self.cb_phrase = QCheckBox(_('Enable passphrases')) + # self.cb_phrase.setChecked(False) + # self.addWidget(self.cb_phrase) self.passphrase_e = QLineEdit() self.passphrase_e.setMinimumWidth(100) @@ -909,9 +962,11 @@ def get_settings(self): item = ' '.join(str(clean_text(self.text_e)).split()) return self.label_e.text(), item, self.passphrase_e.text() + class WCSatochipImportSeedParams(WalletWizardComponent): def __init__(self, parent, wizard): - WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + WalletWizardComponent.__init__( + self, parent, wizard, title=_('Satochip Setup')) self.plugins = wizard.plugins self._busy = True @@ -919,11 +974,13 @@ def on_ready(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) _name, _info = current_cosigner['hardware_device'] self.settings_layout = SatochipSeedLayout(_info.device.id_) - self.settings_layout.validChanged.connect(self.on_settings_valid_changed) + self.settings_layout.validChanged.connect( + self.on_settings_valid_changed) self.layout().addLayout(self.settings_layout) self.layout().addStretch(1) - self.valid = True # TODO #current_cosigner['satochip_init'] != TIM_PRIVKEY # TODO: only privkey is validated + # TODO #current_cosigner['satochip_init'] != TIM_PRIVKEY # TODO: only privkey is validated + self.valid = True self.busy = False def on_settings_valid_changed(self, is_valid: bool): @@ -931,12 +988,14 @@ def on_settings_valid_changed(self, is_valid: bool): def apply(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) - current_cosigner['satochip_seed_settings'] = self.settings_layout.get_settings() + current_cosigner['satochip_seed_settings'] = self.settings_layout.get_settings( + ) class WCSatochipImportSeed(WalletWizardComponent): def __init__(self, parent, wizard): - WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip Setup')) + WalletWizardComponent.__init__( + self, parent, wizard, title=_('Satochip Setup')) self.plugins = wizard.plugins self.plugin = self.plugins.get_plugin('satochip') @@ -947,16 +1006,18 @@ def __init__(self, parent, wizard): def on_ready(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) settings = current_cosigner['satochip_seed_settings'] - #method = current_cosigner['satochip_init'] + # method = current_cosigner['satochip_init'] _name, _info = current_cosigner['hardware_device'] device_id = _info.device.id_ - client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + client = self.plugins.device_manager.client_by_id( + device_id, scan_now=False) client.handler = self.plugin.create_handler(self.wizard) def initialize_device_task(settings, device_id, handler): try: self.plugin._import_seed(settings, device_id, handler) - _logger.info('[WCSatochipImportSeed] initialize_device_task() Done initialize device') + _logger.info( + '[WCSatochipImportSeed] initialize_device_task() Done initialize device') self.valid = True self.wizard.requestNext.emit() # triggers Next GUI thread from event loop except Exception as e: diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index f2d834eaa6d7..d0992127242c 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -2,7 +2,7 @@ import hashlib import time -#electrum +# electrum from electrum import mnemonic from electrum import constants from electrum import descriptor @@ -23,7 +23,7 @@ from ..hw_wallet import HW_PluginBase, HardwareClientBase -#pysatochip +# pysatochip from pysatochip.CardConnector import CardConnector from pysatochip.CardConnector import UninitializedSeedError, CardNotPresentError, UnexpectedSW12Error, WrongPinError, PinBlockedError, PinRequiredError from pysatochip.JCconstants import JCconstants @@ -31,7 +31,7 @@ from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION, SATOCHIP_PROTOCOL_VERSION -#pyscard +# pyscard from smartcard.sw.SWExceptions import SWException from smartcard.Exceptions import CardConnectionException, CardRequestTimeoutException from smartcard.CardType import AnyCardType @@ -40,22 +40,24 @@ _logger = get_logger(__name__) # version history for the plugin -SATOCHIP_PLUGIN_REVISION= 'lib0.11.a-plugin0.1' +SATOCHIP_PLUGIN_REVISION = 'lib0.11.a-plugin0.1' # debug: smartcard reader ids -SATOCHIP_VID= 0 #0x096E -SATOCHIP_PID= 0 #0x0503 +SATOCHIP_VID = 0 # 0x096E +SATOCHIP_PID = 0 # 0x0503 -MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") +MSG_USE_2FA = _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") -def bip32path2bytes(bip32path:str) -> (int, bytes): - intPath= convert_bip32_strpath_to_intpath(bip32path) - depth= len(intPath) - bytePath=b'' + +def bip32path2bytes(bip32path: str) -> (int, bytes): + intPath = convert_bip32_strpath_to_intpath(bip32path) + depth = len(intPath) + bytePath = b'' for index in intPath: - bytePath+= index.to_bytes(4, byteorder='big', signed=False) + bytePath += index.to_bytes(4, byteorder='big', signed=False) return (depth, bytePath) + class SatochipClient(HardwareClientBase): def __init__(self, plugin: HW_PluginBase, handler): HardwareClientBase.__init__(self, plugin=plugin) @@ -63,8 +65,8 @@ def __init__(self, plugin: HW_PluginBase, handler): self._soft_device_id = None self.device = plugin.device self.handler = handler - #self.parser= CardDataParser() - self.cc= CardConnector(self, _logger.getEffectiveLevel()) + # self.parser= CardDataParser() + self.cc = CardConnector(self, _logger.getEffectiveLevel()) def __repr__(self): return '' @@ -83,8 +85,8 @@ def timeout(self, cutoff): def is_initialized(self): _logger.info(f"SATOCHIP is_initialized()") - time.sleep(0.3) # let some time to setup communication channel - (response, sw1, sw2, d)=self.cc.card_get_status() + time.sleep(0.3) # let some time to setup communication channel + (response, sw1, sw2, d) = self.cc.card_get_status() # if setup is not done, we return None if (not self.cc.setup_done): @@ -92,11 +94,13 @@ def is_initialized(self): return None # if not seeded, return False if (self.cc.setup_done and not self.cc.is_seeded): - _logger.info(f"SATOCHIP is_initialized() False (PIN set but card not seeded)") + _logger.info( + f"SATOCHIP is_initialized() False (PIN set but card not seeded)") return False # initialized if pin is set and device is seeded if (self.cc.setup_done and self.cc.is_seeded): - _logger.info(f"SATOCHIP is_initialized() True (PIN set and card seeded)") + _logger.info( + f"SATOCHIP is_initialized() True (PIN set and card seeded)") return True def get_soft_device_id(self): @@ -112,47 +116,51 @@ def device_model_name(self): def has_usable_connection_with_device(self): _logger.info(f"has_usable_connection_with_device()") try: - atr= self.cc.card_get_ATR() # (response, sw1, sw2)= self.cc.card_select() #TODO: something else? get ATR? - _logger.info("Card ATR: " + bytes(atr).hex() ) - except Exception as e: #except SWException as e: - _logger.exception(f"Exception in has_usable_connection_with_device: {str(e)}") + # (response, sw1, sw2)= self.cc.card_select() #TODO: something else? get ATR? + atr = self.cc.card_get_ATR() + _logger.info("Card ATR: " + bytes(atr).hex()) + except Exception as e: # except SWException as e: + _logger.exception( + f"Exception in has_usable_connection_with_device: {str(e)}") return False return True def verify_PIN(self, pin=None): - while(True): + while (True): try: - #when pin is None, pysatochip use a cached pin if available - (response, sw1, sw2)= self.cc.card_verify_PIN_simple(pin) + # when pin is None, pysatochip use a cached pin if available + (response, sw1, sw2) = self.cc.card_verify_PIN_simple(pin) return True # recoverable errors except CardNotPresentError: msg = f"No card found! \nPlease insert card, then enter your PIN:" - (is_PIN, pin)= self.PIN_dialog(msg) + (is_PIN, pin) = self.PIN_dialog(msg) if is_PIN is False: return False except PinRequiredError as ex: # no pin value cached in pysatochip msg = f'Enter the PIN for your card:' - (is_PIN, pin)= self.PIN_dialog(msg) + (is_PIN, pin) = self.PIN_dialog(msg) if is_PIN is False: return False except WrongPinError as ex: - pin= None # reset pin + pin = None # reset pin msg = f"Wrong PIN! {ex.pin_left} tries remaining! \n Enter the PIN for your card:" - (is_PIN, pin)= self.PIN_dialog(msg) + (is_PIN, pin) = self.PIN_dialog(msg) if is_PIN is False: return False # unrecoverable errors except PinBlockedError as ex: - raise UserFacingException(f"Too many failed attempts! Your device has been blocked! \n\nYou need to factory reset your card (error code 0x9C0C)") + raise UserFacingException( + f"Too many failed attempts! Your device has been blocked! \n\nYou need to factory reset your card (error code 0x9C0C)") except UnexpectedSW12Error as ex: - raise UserFacingException(f"Unexpected error during PIN verification: {ex}") + raise UserFacingException( + f"Unexpected error during PIN verification: {ex}") except Exception as ex: - raise UserFacingException(f"Unexpected error during PIN verification: {ex}") - + raise UserFacingException( + f"Unexpected error during PIN verification: {ex}") def get_xpub(self, bip32_path, xtype): assert xtype in SatochipPlugin.SUPPORTED_XTYPES @@ -162,16 +170,18 @@ def get_xpub(self, bip32_path, xtype): # bip32_path is of the form 44'/0'/1' _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}") - (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= BIP32Node(xtype=xtype, + (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 = BIP32Node(xtype=xtype, eckey=childkey, chaincode=childchaincode, depth=depth, @@ -181,29 +191,30 @@ def get_xpub(self, bip32_path, xtype): return xpub def ping_check(self): - #check connection is working + # check connection is working try: - _logger.info('[SatochipClient] ping_check()')#debug - #atr= self.cc.card_get_ATR() + _logger.info('[SatochipClient] ping_check()') # debug + # atr= self.cc.card_get_ATR() except Exception as e: _logger.exception(f"Exception: {str(e)}") raise RuntimeError("Communication issue with Satochip") def request(self, request_type, *args): - _logger.info('[SatochipClient] client request: '+ str(request_type)) + _logger.info('[SatochipClient] client request: ' + str(request_type)) if self.handler is not None: - if (request_type=='update_status'): + if (request_type == 'update_status'): reply = self.handler.update_status(*args) return reply - elif (request_type=='show_error'): + elif (request_type == 'show_error'): reply = self.handler.show_error(*args) return reply - elif (request_type=='show_message'): + elif (request_type == 'show_message'): reply = self.handler.show_message(*args) return reply else: - reply = self.handler.show_error('Unknown request: '+str(request_type)) + reply = self.handler.show_error( + 'Unknown request: ' + str(request_type)) return reply else: _logger.info('[SatochipClient] self.handler is None! ') @@ -216,43 +227,45 @@ def PIN_dialog(self, msg): return False, None if len(password) < 4: msg = _("PIN must have at least 4 characters.") + \ - "\n\n" + _("Enter PIN:") + "\n\n" + _("Enter PIN:") elif len(password) > 16: msg = _("PIN must have less than 16 characters.") + \ - "\n\n" + _("Enter PIN:") + "\n\n" + _("Enter PIN:") else: 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) + 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) + # 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!')) + # 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) + # 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) + (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) + (is_PIN, pin_confirm) = self.PIN_dialog(msg_confirm) if (not is_PIN): self.request('show_message', msg_cancel) return (False, None, None) @@ -261,6 +274,7 @@ def PIN_change_dialog(self, msg_oldpin, msg_newpin, msg_confirm, msg_error, msg_ else: return (True, oldpin, newpin) + class Satochip_KeyStore(Hardware_KeyStore): hw_type = 'satochip' device = 'Satochip' @@ -290,51 +304,55 @@ def give_error(self, message, clear_client=False): raise UserFacingException(message) def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + raise RuntimeError( + _('Encryption and decryption are currently not supported for {}').format(self.device)) def sign_message(self, sequence, message, password, *, script_type=None): message_byte = message.encode('utf8') message_hash = hashlib.sha256(message_byte).hexdigest().upper() client = self.get_client() - is_ok= client.verify_PIN() + is_ok = client.verify_PIN() if not is_ok: return b'' - address_path = self.get_derivation_prefix() + "/%d/%d"%sequence #self.get_derivation()[2:] + "/%d/%d"%sequence + # self.get_derivation()[2:] + "/%d/%d"%sequence + address_path = self.get_derivation_prefix() + "/%d/%d" % sequence _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}") - self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) - # check if 2FA is required - hmac=b'' + self.handler.show_message( + "Signing message ...\r\nMessage hash: " + message_hash) + # check if 2FA is required + hmac = b'' if (client.cc.needs_2FA is None): - (response, sw1, sw2, d)=client.cc.card_get_status() + (response, sw1, sw2, d) = client.cc.card_get_status() if client.cc.needs_2FA: # challenge based on sha256(btcheader+msg) # format & encrypt msg import json - msg= {'action':"sign_msg", 'msg':message} - msg= json.dumps(msg) - #do challenge-response with 2FA device... - hmac= self.do_challenge_response(msg) - hmac= bytes.fromhex(hmac) + msg = {'action': "sign_msg", 'msg': message} + msg = json.dumps(msg) + # do challenge-response with 2FA device... + hmac = self.do_challenge_response(msg) + hmac = bytes.fromhex(hmac) try: - keynbr= 0xFF #for extended key - (depth, bytepath)= bip32path2bytes(address_path) - (pubkey, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) - (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.")) + keynbr = 0xFF # for extended key + (depth, bytepath) = bip32path2bytes(address_path) + (pubkey, chaincode) = client.cc.card_bip32_get_extendedkey(bytepath) + (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.")) return compsig except Exception as e: - #self.give_error(e, True) + # self.give_error(e, True) _logger.info(f"[Satochip_KeyStore] sign_message: Exception {e}") - #self.handler.show_error(e) + # self.handler.show_error(e) return b'' finally: _logger.info(f"[Satochip_KeyStore] sign_message: finally") self.handler.finished() - def sign_transaction(self, tx, password): _logger.info(f"In sign_transaction(): tx: {str(tx)}") client = self.get_client() @@ -345,7 +363,8 @@ def sign_transaction(self, tx, password): txOutputs = bytearray() txOutputs += var_int(len(tx.outputs())) for o in tx.outputs(): - txOutputs += int.to_bytes(o.value, length=8, byteorder="little", signed=False) + txOutputs += int.to_bytes(o.value, length=8, + byteorder="little", signed=False) script = o.scriptpubkey txOutputs += var_int(len(script)) txOutputs += script @@ -355,7 +374,7 @@ def sign_transaction(self, tx, password): _logger.info(f"In sign_transaction(): outputs= {txOutputs.hex()}") # Fetch inputs of the transaction to sign - for i,txin in enumerate(tx.inputs()): + for i, txin in enumerate(tx.inputs()): if tx.is_complete(): break @@ -364,9 +383,11 @@ def sign_transaction(self, tx, password): assert desc script_type = desc.to_legacy_electrum_script_type() - _logger.info(f"In sign_transaction(): input= {str(i)} - input[type]: {script_type}") + _logger.info( + f"In sign_transaction(): input= {str(i)} - input[type]: {script_type}") if txin.is_coinbase_input(): - self.give_error("Coinbase not supported") # should never happen + # should never happen + self.give_error("Coinbase not supported") if script_type in ['p2sh']: p2shTransaction = True @@ -376,60 +397,69 @@ def sign_transaction(self, tx, password): my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin) if not inputPath: - self.give_error("No matching pubkey for sign_transaction") # should never happen - inputPath = convert_bip32_intpath_to_strpath(inputPath) #[2:] + # should never happen + self.give_error("No matching pubkey for sign_transaction") + inputPath = convert_bip32_intpath_to_strpath(inputPath) # [2:] inputHash = sha256d(tx.serialize_preimage(i)) # get corresponing extended key - (depth, bytepath)= bip32path2bytes(inputPath) - (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) + (depth, bytepath) = bip32path2bytes(inputPath) + (key, chaincode) = client.cc.card_bip32_get_extendedkey(bytepath) # parse tx (bytes format) - pre_tx= tx.serialize_preimage(i) + pre_tx = tx.serialize_preimage(i) pre_hash = sha256d(pre_tx) - _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx= {pre_tx.hex()}") - _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash.hex()}") - (response, sw1, sw2, tx_hash_list, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction) - tx_hash= bytearray(tx_hash_list) - if pre_hash!= tx_hash: - raise RuntimeError(f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash.hex()} vs {tx_hash.hex()}") - - #2FA - keynbr= 0xFF #for extended key + _logger.info( + f"[Satochip_KeyStore] sign_transaction(): pre_tx= {pre_tx.hex()}") + _logger.info( + f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash.hex()}") + (response, sw1, sw2, tx_hash_list, needs_2fa) = client.cc.card_parse_transaction( + pre_tx, segwitTransaction) + tx_hash = bytearray(tx_hash_list) + if pre_hash != tx_hash: + raise RuntimeError( + f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash.hex()} vs {tx_hash.hex()}") + + # 2FA + keynbr = 0xFF # for extended key if needs_2fa: # format & encrypt msg import json - coin_type= 1 if constants.net.TESTNET else 0 + coin_type = 1 if constants.net.TESTNET else 0 if segwitTransaction: - msg= {'tx':pre_tx.hex(), 'ct':coin_type, 'sw':segwitTransaction, 'txo':txOutputs.hex(), 'ty':script_type} + msg = {'tx': pre_tx.hex(), 'ct': coin_type, 'sw': segwitTransaction, + 'txo': txOutputs.hex(), 'ty': script_type} else: - msg= {'tx':pre_tx.hex(), 'ct':coin_type, 'sw':segwitTransaction} - msg= json.dumps(msg) + msg = {'tx': pre_tx.hex(), 'ct': coin_type, + 'sw': segwitTransaction} + msg = json.dumps(msg) - #do challenge-response with 2FA device... - hmac= self.do_challenge_response(msg) - hmac= list(bytes.fromhex(hmac)) + # do challenge-response with 2FA device... + hmac = self.do_challenge_response(msg) + hmac = list(bytes.fromhex(hmac)) else: - hmac= None + hmac = None # sign tx - (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash_list, hmac) + (tx_sig, sw1, sw2) = client.cc.card_sign_transaction( + keynbr, tx_hash_list, hmac) # check sw1sw2 for error (0x9c0b if wrong challenge-response) if sw1 != 0x90 or sw2 != 0x00: - self.give_error(f"Satochip failed to sign transaction with code {hex(256*sw1+sw2)}") + self.give_error( + f"Satochip failed to sign transaction with code {hex(256*sw1+sw2)}") # enforce low-S signature (BIP 62) - tx_sig = bytes(tx_sig) #bytearray(tx_sig) - r,s= get_r_and_s_from_ecdsa_der_sig(tx_sig) - if s > CURVE_ORDER//2: + tx_sig = bytes(tx_sig) # bytearray(tx_sig) + r, s = get_r_and_s_from_ecdsa_der_sig(tx_sig) + if s > CURVE_ORDER // 2: s = CURVE_ORDER - s tx_sig = ecdsa_der_sig_from_r_and_s(r, s) - #update tx with signature + # update tx with signature tx_sig = tx_sig + Sighash.to_sigbytes(Sighash.ALL) - tx.add_signature_to_txin(txin_idx=i, signing_pubkey=my_pubkey, sig=tx_sig) + tx.add_signature_to_txin( + txin_idx=i, signing_pubkey=my_pubkey, sig=tx_sig) # end of for loop - _logger.info(f"Tx is complete: {str(tx.is_complete())}") tx.raw = tx.serialize() return @@ -440,43 +470,46 @@ def show_address(self, sequence, txin_type): def do_challenge_response(self, msg): client = self.get_client() - (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True) - d={} - d['msg_encrypt']= msg_out - d['id_2FA']= id_2FA - _logger.info("id_2FA: "+id_2FA) + (id_2FA, msg_out) = client.cc.card_crypt_transaction_2FA(msg, True) + d = {} + d['msg_encrypt'] = msg_out + d['id_2FA'] = id_2FA + _logger.info("id_2FA: " + id_2FA) - reply_encrypt= None - hmac= 20*"00" # default response (reject) - status_msg="" + reply_encrypt = None + hmac = 20 * "00" # default response (reject) + status_msg = "" # get server_2FA from config from existing object - server_2FA = self.plugin.config.get("satochip_2FA_server", default= SERVER_LIST[0]) + server_2FA = self.plugin.config.get( + "satochip_2FA_server", default=SERVER_LIST[0]) status_msg += f"2FA request sent to '{server_2FA}' \nApprove or reject request on your second device." self.handler.show_message(status_msg) try: - Satochip2FA.do_challenge_response(d, server_name= server_2FA) + Satochip2FA.do_challenge_response(d, server_name=server_2FA) # decrypt and parse reply to extract challenge response - reply_encrypt= d['reply_encrypt'] + reply_encrypt = d['reply_encrypt'] except Exception as e: status_msg += f"\nFailed to contact cosigner! \n=> Select another 2FA server in Satochip settings\n\n" self.handler.show_message(status_msg) if reply_encrypt is not None: - reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) - _logger.info("challenge:response= "+ reply_decrypt) - reply_decrypt= reply_decrypt.split(":") - hmac= reply_decrypt[1] - return hmac # return a hexstring + reply_decrypt = client.cc.card_crypt_transaction_2FA( + reply_encrypt, False) + _logger.info("challenge:response= " + reply_decrypt) + reply_decrypt = reply_decrypt.split(":") + hmac = reply_decrypt[1] + return hmac # return a hexstring class SatochipPlugin(HW_PluginBase): - libraries_available= True + libraries_available = True minimum_library = (0, 0, 0) - keystore_class= Satochip_KeyStore - DEVICE_IDS= [ - (SATOCHIP_VID, SATOCHIP_PID) + keystore_class = Satochip_KeyStore + DEVICE_IDS = [ + (SATOCHIP_VID, SATOCHIP_PID) ] - SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', + 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') def __init__(self, parent, config, name): _logger.info(f"[SatochipPlugin] init()") @@ -495,7 +528,7 @@ def detect_smartcard_reader(self): return [Device(path="/satochip", interface_number=-1, id_="/satochip", - product_key=(SATOCHIP_VID,SATOCHIP_PID), + product_key=(SATOCHIP_VID, SATOCHIP_PID), usage_page=0, transport_ui_string='ccid')] except CardRequestTimeoutException: @@ -506,7 +539,6 @@ def detect_smartcard_reader(self): return [] return [] - def create_client(self, device, handler): _logger.info(f"[SatochipPlugin] create_client()") @@ -517,7 +549,8 @@ def create_client(self, device, handler): rv = SatochipClient(self, handler) return rv except Exception as e: - _logger.exception(f"[SatochipPlugin] create_client() exception: {str(e)}") + _logger.exception( + f"[SatochipPlugin] create_client() exception: {str(e)}") return None def get_xpub(self, device_id, derivation, xtype, wizard): @@ -525,7 +558,8 @@ def get_xpub(self, device_id, derivation, xtype, wizard): # base_wizard:on_hw_derivation _logger.info(f"[SatochipPlugin] get_xpub()") if xtype not in self.SUPPORTED_XTYPES: - raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + raise ScriptTypeNotSupported( + _('This type of script is not supported with {}.').format(self.device)) devmgr = self.device_manager() client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) @@ -557,45 +591,53 @@ def _setup_device(self, settings, device_id, handler): # check that card is indeed a Satochip if (client.cc.card_type != "Satochip"): raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Inserted card is not a Satochip!')) + _('Inserted card is not a Satochip!')) pin_0 = settings pin_0 = list(pin_0.encode("utf-8")) - client.cc.set_pin(0, pin_0) #cache PIN value in client - pin_tries_0= 0x05 + client.cc.set_pin(0, pin_0) # cache PIN value in client + pin_tries_0 = 0x05 # PUK code can be used when PIN is unknown and the card is locked # We use a random value as the PUK is not used currently in the electrum GUI - ublk_tries_0= 0x01 - ublk_0= list(urandom(16)) - #the second pin is not used currently, use random values - pin_tries_1= 0x01 - ublk_tries_1= 0x01 - pin_1= list(urandom(16)) - ublk_1= list(urandom(16)) - secmemsize= 32 # number of slot reserved in memory cache - memsize= 0x0000 # RFU - create_object_ACL= 0x01 # RFU - create_key_ACL= 0x01 # RFU - create_pin_ACL= 0x01 # RFU + ublk_tries_0 = 0x01 + ublk_0 = list(urandom(16)) + # the second pin is not used currently, use random values + pin_tries_1 = 0x01 + ublk_tries_1 = 0x01 + pin_1 = list(urandom(16)) + ublk_1 = list(urandom(16)) + secmemsize = 32 # number of slot reserved in memory cache + memsize = 0x0000 # RFU + create_object_ACL = 0x01 # RFU + create_key_ACL = 0x01 # RFU + create_pin_ACL = 0x01 # RFU # setup try: - (response, sw1, sw2)=client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, - pin_tries_1, ublk_tries_1, pin_1, ublk_1, - secmemsize, memsize, - create_object_ACL, create_key_ACL, create_pin_ACL) - if sw1==0x90 and sw2==0x00: - _logger.info(f"[SatochipPlugin] _setup_device(): setup applet successfully!") - client.handler.show_message(f"Satochip setup performed successfully!") - elif sw1==0x9c and sw2==0x07: - _logger.error(f"[SatochipPlugin] _setup_device(): error applet setup already done (code {hex(sw1*256+sw2)})") - client.handler.show_error(f"Satochip error: applet setup already done (code {hex(sw1*256+sw2)})") + (response, sw1, sw2) = client.cc.card_setup(pin_tries_0, ublk_tries_0, pin_0, ublk_0, + pin_tries_1, ublk_tries_1, pin_1, ublk_1, + secmemsize, memsize, + create_object_ACL, create_key_ACL, create_pin_ACL) + if sw1 == 0x90 and sw2 == 0x00: + _logger.info( + f"[SatochipPlugin] _setup_device(): setup applet successfully!") + client.handler.show_message( + f"Satochip setup performed successfully!") + elif sw1 == 0x9c and sw2 == 0x07: + _logger.error( + f"[SatochipPlugin] _setup_device(): error applet setup already done (code {hex(sw1*256+sw2)})") + client.handler.show_error( + f"Satochip error: applet setup already done (code {hex(sw1*256+sw2)})") else: - _logger.error(f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") - client.handler.show_error(f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") + _logger.error( + f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") + client.handler.show_error( + f"[SatochipPlugin] _setup_device(): unable to set up applet! sw12={hex(sw1)} {hex(sw2)}") except Exception as ex: - _logger.error(f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") - client.handler.show_error(f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") + _logger.error( + f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") + client.handler.show_error( + f"[SatochipPlugin] _setup_device(): exception during setup: {ex}") # verify pin: client.verify_PIN() @@ -613,11 +655,13 @@ def _import_seed(self, settings, device_id, handler): # check seed validity (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(seed) if is_checksum_valid and is_wordlist_valid: - _logger.info(f"[SatochipPlugin] _import_seed() seed format is valid!") - masterseed_bytes=bip39_to_seed(seed, passphrase=passphrase) - masterseed_list= list(masterseed_bytes) + _logger.info( + f"[SatochipPlugin] _import_seed() seed format is valid!") + masterseed_bytes = bip39_to_seed(seed, passphrase=passphrase) + masterseed_list = list(masterseed_bytes) else: - _logger.error(f"[SatochipPlugin] _import_seed() wrong seed format!") + _logger.error( + f"[SatochipPlugin] _import_seed() wrong seed format!") raise Exception('Wrong BIP39 mnemonic format!') # verify pin: @@ -625,35 +669,46 @@ def _import_seed(self, settings, device_id, handler): # import seed try: - authentikey= client.cc.card_bip32_import_seed(masterseed_list) - _logger.info(f"[SatochipPlugin] _import_seed(): seed imported successfully!") + authentikey = client.cc.card_bip32_import_seed(masterseed_list) + _logger.info( + f"[SatochipPlugin] _import_seed(): seed imported successfully!") client.handler.show_message(f"seed imported successfully!") - hex_authentikey= authentikey.get_public_key_hex(compressed=True) - _logger.info(f"[SatochipPlugin] _import_seed(): authentikey={hex_authentikey}") + hex_authentikey = authentikey.get_public_key_hex(compressed=True) + _logger.info( + f"[SatochipPlugin] _import_seed(): authentikey={hex_authentikey}") except Exception as ex: - _logger.error(f"[SatochipPlugin] _import_seed(): exception during seed import: {ex}") + _logger.error( + f"[SatochipPlugin] _import_seed(): exception during seed import: {ex}") client.handler.show_error(f"Exception during seed import: {ex}") # import label - (response, sw1, sw2)= client.cc.card_set_label(label) - if (sw1==0x90 and sw2==0x00): - _logger.info(f"[SatochipPlugin] _import_seed(): card label changed successfully") - #client.handler.show_message(_("Card label changed successfully!")) - elif (sw1==0x6D and sw2==0x00): - _logger.info(f"[SatochipPlugin] _import_seed(): failed to set label: card does not support label (code {hex(sw1*256+sw2)})") - client.handler.show_error(_("Error: card does not support label!")) # starts with satochip v0.12 + (response, sw1, sw2) = client.cc.card_set_label(label) + if (sw1 == 0x90 and sw2 == 0x00): + _logger.info( + f"[SatochipPlugin] _import_seed(): card label changed successfully") + # client.handler.show_message(_("Card label changed successfully!")) + elif (sw1 == 0x6D and sw2 == 0x00): + _logger.info( + f"[SatochipPlugin] _import_seed(): failed to set label: card does not support label (code {hex(sw1*256+sw2)})") + # starts with satochip v0.12 + client.handler.show_error(_("Error: card does not support label!")) else: - _logger.info(f"[SatochipPlugin] _import_seed(): unknown error while setting label (code {hex(sw1*256+sw2)})") - client.handler.show_error(f"Error while setting card label (code {hex(sw1*256+sw2)})") + _logger.info( + f"[SatochipPlugin] _import_seed(): unknown error while setting label (code {hex(sw1*256+sw2)})") + client.handler.show_error( + f"Error while setting card label (code {hex(sw1*256+sw2)})") def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str: _logger.info(f"[SatochipPlugin] wizard_entry_for_device()") - _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_info: {device_info}") - _logger.info(f"[SatochipPlugin] wizard_entry_for_device() new_wallet: {new_wallet}") + _logger.info( + f"[SatochipPlugin] wizard_entry_for_device() device_info: {device_info}") + _logger.info( + f"[SatochipPlugin] wizard_entry_for_device() new_wallet: {new_wallet}") - device_state = device_info.initialized # can be None, False or True. + device_state = device_info.initialized # can be None, False or True. # None is used to distinguish a completely new card from a card where the seed has been reset, but the PIN is still set. - _logger.info(f"[SatochipPlugin] wizard_entry_for_device() device_state: {device_state}") + _logger.info( + f"[SatochipPlugin] wizard_entry_for_device() device_state: {device_state}") if new_wallet: if device_state is None: return 'satochip_not_setup' @@ -665,7 +720,8 @@ def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) # todo: assert is_setup & is_seeded if device_state is not True: # This can happen if you reset the seed of the Satochip for an existing wallet, then try to open that wallet file. - _logger.error(f"[SatochipPlugin] wizard_entry_for_device() existing wallet with non-seeded Satochip!") + _logger.error( + f"[SatochipPlugin] wizard_entry_for_device() existing wallet with non-seeded Satochip!") return 'satochip_unlock' # insert satochip pages in new wallet wizard @@ -706,7 +762,8 @@ def show_address(self, wallet, address, keystore=None): # Standard_Wallet => not multisig, must be bip32 if type(wallet) is not Standard_Wallet: - keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + keystore.handler.show_error( + _('This function is only available for standard wallets when using {}.').format(self.device)) return sequence = wallet.get_address_index(address) From fcaacce6c907f502c79fa450aeb2321309ec5a64 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 20 Jun 2024 11:16:16 +0100 Subject: [PATCH 08/20] Satochip plugin: clean code Remove unused imports, variables & code --- electrum/plugins/satochip/qt.py | 23 +++++------- electrum/plugins/satochip/satochip.py | 53 ++++++--------------------- 2 files changed, 21 insertions(+), 55 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 14cbd1eb979e..628a879ca3f1 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -4,11 +4,10 @@ from electrum.simple_config import SimpleConfig from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) -from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub, WalletWizardComponent, QENewWalletWizard +from electrum.gui.qt.qrcodewidget import QRDialog +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard from electrum.plugin import hook from PyQt5.QtCore import Qt, pyqtSignal, QRegExp -from PyQt5.QtGui import QRegExpValidator from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) @@ -22,7 +21,7 @@ from ..hw_wallet.qt import QtHandlerBase, QtPluginBase # pysatochip -from pysatochip.CardConnector import CardConnector, UnexpectedSW12Error, CardError, CardNotPresentError, WrongPinError +from pysatochip.CardConnector import UnexpectedSW12Error, CardError, CardNotPresentError, WrongPinError from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION @@ -103,10 +102,10 @@ def __init__(self, win): class SatochipSettingsDialog(WindowModalDialog): - '''This dialog doesn't require a device be paired with a wallet. + """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.''' + their PIN.""" def __init__(self, window, plugin, keystore, device_id): title = _("{} Settings").format(plugin.device) @@ -360,7 +359,7 @@ def reset_seed(self, client): # decrypt and parse reply to extract challenge response try: reply_encrypt = d['reply_encrypt'] - except Exception as e: + except Exception: self.give_error("No response received from 2FA", True) reply_decrypt = client.cc.card_crypt_transaction_2FA( reply_encrypt, False) @@ -473,7 +472,7 @@ def reset_2FA(self, client): # decrypt and parse reply to extract challenge response try: reply_encrypt = d['reply_encrypt'] - except Exception as e: + except Exception: self.give_error("No response received from 2FA!", True) reply_decrypt = client.cc.card_crypt_transaction_2FA( reply_encrypt, False) @@ -559,10 +558,10 @@ def card_verify_authenticity(self, client): try: cert_pem = client.cc.card_export_perso_certificate() _logger.info('Cert PEM: ' + str(cert_pem)) - except CardError as ex: + except CardError: txt_error = ''.join(["Unable to get device certificate: feature unsupported! \n", "Authenticity validation is only available starting with Satochip v0.12 and higher"]) - except CardNotPresentError as ex: + except CardNotPresentError: txt_error = "No card found! Please insert card." except UnexpectedSW12Error as ex: txt_error = "Exception during device certificate export: " + \ @@ -782,16 +781,12 @@ def __init__(self, device): self.pw = PasswordLineEdit() self.pw.setMinimumWidth(32) - # self.pw.setMaximumWidth(32) - # self.pw.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) vbox.addWidget(WWLabel("Enter new PIN:")) vbox.addWidget(self.pw) self.addLayout(vbox) self.pw2 = PasswordLineEdit() self.pw2.setMinimumWidth(32) - # self.pw2.setMaximumWidth(32) - # self.pw2.setValidator(QRegExpValidator(QRegExp('[1-9]{4,16}'))) vbox2 = QVBoxLayout() vbox2.addWidget(WWLabel("Confirm new PIN:")) vbox2.addWidget(self.pw2) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index d0992127242c..e8b72e02e31d 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -3,37 +3,29 @@ import time # electrum -from electrum import mnemonic from electrum import constants -from electrum import descriptor -from electrum.bitcoin import TYPE_ADDRESS, var_int +from electrum.bitcoin import var_int from electrum.i18n import _ from electrum.plugin import Device, DeviceInfo from electrum.keystore import Hardware_KeyStore, bip39_to_seed, bip39_is_checksum_valid, ScriptTypeNotSupported -from electrum.transaction import Transaction, Sighash +from electrum.transaction import Sighash from electrum.wallet import Standard_Wallet from electrum.wizard import NewWalletWizard -from electrum.util import bfh, versiontuple, UserFacingException +from electrum.util import UserFacingException from electrum.crypto import hash_160, sha256d -from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig, ECPubkey +from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath from electrum.logging import get_logger -from electrum.simple_config import SimpleConfig -from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog from ..hw_wallet import HW_PluginBase, HardwareClientBase # pysatochip from pysatochip.CardConnector import CardConnector -from pysatochip.CardConnector import UninitializedSeedError, CardNotPresentError, UnexpectedSW12Error, WrongPinError, PinBlockedError, PinRequiredError -from pysatochip.JCconstants import JCconstants -from pysatochip.TxParser import TxParser +from pysatochip.CardConnector import CardNotPresentError, UnexpectedSW12Error, WrongPinError, PinBlockedError, PinRequiredError from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST -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.Exceptions import CardRequestTimeoutException from smartcard.CardType import AnyCardType from smartcard.CardRequest import CardRequest @@ -138,7 +130,7 @@ def verify_PIN(self, pin=None): (is_PIN, pin) = self.PIN_dialog(msg) if is_PIN is False: return False - except PinRequiredError as ex: + except PinRequiredError: # no pin value cached in pysatochip msg = f'Enter the PIN for your card:' (is_PIN, pin) = self.PIN_dialog(msg) @@ -152,7 +144,7 @@ def verify_PIN(self, pin=None): return False # unrecoverable errors - except PinBlockedError as ex: + except PinBlockedError: raise UserFacingException( f"Too many failed attempts! Your device has been blocked! \n\nYou need to factory reset your card (error code 0x9C0C)") except UnexpectedSW12Error as ex: @@ -166,7 +158,7 @@ def get_xpub(self, bip32_path, xtype): assert xtype in SatochipPlugin.SUPPORTED_XTYPES # needs PIN - is_ok = self.verify_PIN() + self.verify_PIN() # bip32_path is of the form 44'/0'/1' _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}") @@ -190,15 +182,6 @@ def get_xpub(self, bip32_path, xtype): _logger.info(f"[SatochipClient] get_xpub(): xpub={str(xpub)}") return xpub - def ping_check(self): - # check connection is working - try: - _logger.info('[SatochipClient] ping_check()') # debug - # atr= self.cc.card_get_ATR() - except Exception as e: - _logger.exception(f"Exception: {str(e)}") - raise RuntimeError("Communication issue with Satochip") - def request(self, request_type, *args): _logger.info('[SatochipClient] client request: ' + str(request_type)) @@ -290,9 +273,6 @@ def dump(self): d = Hardware_KeyStore.dump(self) return d - def get_derivation(self): - return self.derivation - def give_error(self, message, clear_client=False): _logger.error(f"[Satochip_KeyStore] give_error() {message}") if not self.ux_busy: @@ -315,7 +295,6 @@ def sign_message(self, sequence, message, password, *, script_type=None): if not is_ok: return b'' - # self.get_derivation()[2:] + "/%d/%d"%sequence address_path = self.get_derivation_prefix() + "/%d/%d" % sequence _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}") self.handler.show_message( @@ -356,7 +335,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): def sign_transaction(self, tx, password): _logger.info(f"In sign_transaction(): tx: {str(tx)}") client = self.get_client() - is_ok = client.verify_PIN() + client.verify_PIN() segwitTransaction = False # outputs (bytes format) @@ -389,9 +368,6 @@ def sign_transaction(self, tx, password): # should never happen self.give_error("Coinbase not supported") - if script_type in ['p2sh']: - p2shTransaction = True - if script_type in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: segwitTransaction = True @@ -400,7 +376,6 @@ def sign_transaction(self, tx, password): # should never happen self.give_error("No matching pubkey for sign_transaction") inputPath = convert_bip32_intpath_to_strpath(inputPath) # [2:] - inputHash = sha256d(tx.serialize_preimage(i)) # get corresponing extended key (depth, bytepath) = bip32path2bytes(inputPath) @@ -489,7 +464,7 @@ def do_challenge_response(self, msg): Satochip2FA.do_challenge_response(d, server_name=server_2FA) # decrypt and parse reply to extract challenge response reply_encrypt = d['reply_encrypt'] - except Exception as e: + except Exception: status_msg += f"\nFailed to contact cosigner! \n=> Select another 2FA server in Satochip settings\n\n" self.handler.show_message(status_msg) if reply_encrypt is not None: @@ -537,7 +512,6 @@ def detect_smartcard_reader(self): except Exception as exc: _logger.info(f"Error during connection:{str(exc)}") return [] - return [] def create_client(self, device, handler): _logger.info(f"[SatochipPlugin] create_client()") @@ -563,7 +537,6 @@ def get_xpub(self, device_id, derivation, xtype, wizard): devmgr = self.device_manager() client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) - client.ping_check() xpub = client.get_xpub(derivation, xtype) return xpub @@ -576,8 +549,6 @@ def get_client(self, keystore, force_pair=True, *, devices=None, allow_user_inte devices=devices, allow_user_interaction=allow_user_interaction) # returns the client for a given keystore. can use xpub - if client is not None: - client.ping_check() return client def _setup_device(self, settings, device_id, handler): @@ -665,7 +636,7 @@ def _import_seed(self, settings, device_id, handler): raise Exception('Wrong BIP39 mnemonic format!') # verify pin: - is_ok = client.verify_PIN() + client.verify_PIN() # import seed try: From f075129d9f9384a40e0ddc02774b5689d884ead4 Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 21 Jun 2024 10:39:36 +0100 Subject: [PATCH 09/20] Satochip plugin: improve 2FA UX Close message dialog correctly when using 2FA --- electrum/plugins/satochip/satochip.py | 40 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index e8b72e02e31d..ec52a03c1d76 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -297,8 +297,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): address_path = self.get_derivation_prefix() + "/%d/%d" % sequence _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}") - self.handler.show_message( - "Signing message ...\r\nMessage hash: " + message_hash) + # check if 2FA is required hmac = b'' if (client.cc.needs_2FA is None): @@ -312,6 +311,10 @@ def sign_message(self, sequence, message, password, *, script_type=None): # do challenge-response with 2FA device... hmac = self.do_challenge_response(msg) hmac = bytes.fromhex(hmac) + else: + self.handler.show_message( + "Signing message ...\r\nMessage hash: " + message_hash) + try: keynbr = 0xFF # for extended key (depth, bytepath) = bip32path2bytes(address_path) @@ -324,9 +327,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): return compsig except Exception as e: - # self.give_error(e, True) _logger.info(f"[Satochip_KeyStore] sign_message: Exception {e}") - # self.handler.show_error(e) return b'' finally: _logger.info(f"[Satochip_KeyStore] sign_message: finally") @@ -459,20 +460,27 @@ def do_challenge_response(self, msg): server_2FA = self.plugin.config.get( "satochip_2FA_server", default=SERVER_LIST[0]) status_msg += f"2FA request sent to '{server_2FA}' \nApprove or reject request on your second device." - self.handler.show_message(status_msg) try: - Satochip2FA.do_challenge_response(d, server_name=server_2FA) - # decrypt and parse reply to extract challenge response - reply_encrypt = d['reply_encrypt'] - except Exception: - status_msg += f"\nFailed to contact cosigner! \n=> Select another 2FA server in Satochip settings\n\n" self.handler.show_message(status_msg) - if reply_encrypt is not None: - reply_decrypt = client.cc.card_crypt_transaction_2FA( - reply_encrypt, False) - _logger.info("challenge:response= " + reply_decrypt) - reply_decrypt = reply_decrypt.split(":") - hmac = reply_decrypt[1] + try: + Satochip2FA.do_challenge_response(d, server_name=server_2FA) + # decrypt and parse reply to extract challenge response + reply_encrypt = d['reply_encrypt'] + except Exception: + status_msg += f"\nFailed to contact cosigner! \n=> Select another 2FA server in Satochip settings\n\n" + self.handler.show_message(status_msg) + if reply_encrypt is not None: + reply_decrypt = client.cc.card_crypt_transaction_2FA( + reply_encrypt, False) + _logger.info("challenge:response= " + reply_decrypt) + reply_decrypt = reply_decrypt.split(":") + hmac = reply_decrypt[1] + except Exception as ex: + _logger.info(f"do_challenge_response: exception with handler: {ex}") + finally: + _logger.info(f"[Satochip_KeyStore] do_challenge_response: finally") + self.handler.finished() + return hmac # return a hexstring From 12ff4d15aa129bc9cee1780db1de19347a2b74bf Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 25 Jun 2024 13:27:26 +0100 Subject: [PATCH 10/20] Satochip plugin: using SeedLayout to import seed in setup Wizard --- electrum/plugins/satochip/qt.py | 151 ++++++++++++-------------- electrum/plugins/satochip/satochip.py | 33 ++---- 2 files changed, 79 insertions(+), 105 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 628a879ca3f1..ad20aa3454fd 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -2,11 +2,13 @@ from electrum.logging import get_logger from electrum.keystore import bip39_is_checksum_valid from electrum.simple_config import SimpleConfig -from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, +from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, icon_path, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) from electrum.gui.qt.qrcodewidget import QRDialog -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard +from electrum.gui.qt.wizard.wallet import (WCHaveSeed, WCEnterExt, WCScriptAndDerivation, + WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard) from electrum.plugin import hook +from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt, pyqtSignal, QRegExp from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) @@ -25,19 +27,27 @@ from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION -# Seed import wizard msg -PASSPHRASE_HELP_SHORT = _( - "A passphrase is an optional feature that allows you to extend your seed with additional entropy. " - "A passphrase is not a PIN.") -PASSPHRASE_NOT_PIN = _( - "If set, you will need your passphrase " - "along with your BIP39 seed to restore your wallet from a backup. " - "If you are not sure, leave this field empty!") - _logger = get_logger(__name__) -MSG_USE_2FA = _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") - +MSG_USE_2FA = _( + "Do you want to use 2-Factor-Authentication (2FA)?" + "\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. " + "First you have to install the Satochip-2FA android app on google play. " + "Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. " + "\n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!" +) + +MSG_SEED_IMPORT = [ + _("Your Satochip is currently unseeded. "), + _("To use it, you need to import a BIP39 Seed. "), + _("To do so, select BIP39 in the options in the next screen. "), + _("Note that Electrum seeds are not supported by hardware wallets. "), + " ", + _("Optionally, you can also enable a passphrase in the options. "), + _("A passphrase is an optional feature that allows you to extend your seed with additional entropy. "), + _("A passphrase is not a PIN. "), + _("If set, you will need your passphrase along with your BIP39 seed to restore your wallet from a backup. "), +] class Plugin(SatochipPlugin, QtPluginBase): icon_unpaired = "satochip_unpaired.png" @@ -88,8 +98,25 @@ def extend_wizard(self, wizard: 'QENewWalletWizard'): 'satochip_xpub': {'gui': WCHWXPub}, 'satochip_not_setup': {'gui': WCSatochipSetupParams}, 'satochip_do_setup': {'gui': WCSatochipSetup}, - 'satochip_not_seeded': {'gui': WCSatochipImportSeedParams}, - 'satochip_import_seed': {'gui': WCSatochipImportSeed}, + 'satochip_not_seeded': { + 'gui': WCSeedMessage, + 'next': 'satochip_have_seed' + }, + 'satochip_have_seed': { + 'gui': WCHaveSeed, + 'next': lambda d: 'satochip_have_ext' if wizard.wants_ext(d) else 'satochip_import_seed' + }, + 'satochip_have_ext': { + 'gui': WCEnterExt, + 'next': 'satochip_import_seed', + }, + 'satochip_import_seed': { + 'gui': WCSatochipImportSeed, + 'next': 'satochip_success_seed', + }, + 'satochip_success_seed': { + 'gui': WCSeedSuccess, + }, 'satochip_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -907,84 +934,39 @@ def apply(self): # Import seed wizard # ########################## +class WCSeedMessage(WalletWizardComponent): + def __init__(self, parent, wizard): + WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip needs a seed')) -class SatochipSeedLayout(QVBoxLayout): - validChanged = pyqtSignal([bool], arguments=['valid']) - - def __init__(self, device): - QVBoxLayout.__init__(self) - - label = QLabel(_("Enter a label to name your device:")) - self.label_e = QLineEdit() - hl = QHBoxLayout() - hl.addWidget(label) - hl.addWidget(self.label_e) - hl.addStretch(1) - self.addLayout(hl) - - self.text_e = QTextEdit() - self.text_e.setMaximumHeight(60) - msg = _("Enter your BIP39 mnemonic:") + self.layout().addWidget(WWLabel('\n'.join(MSG_SEED_IMPORT))) + self.layout().addStretch(1) - # TODO: validation? - def set_enabled(): - item = ' '.join(str(clean_text(self.text_e)).split()) - (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(item) - self.validChanged.emit(is_checksum_valid and is_wordlist_valid) - self.text_e.textChanged.connect(set_enabled) - - self.addWidget(QLabel(msg)) - self.addWidget(self.text_e) - - passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - self.addWidget(passphrase_msg) - self.addWidget(passphrase_warning) - # self.cb_phrase = QCheckBox(_('Enable passphrases')) - # self.cb_phrase.setChecked(False) - # self.addWidget(self.cb_phrase) - - self.passphrase_e = QLineEdit() - self.passphrase_e.setMinimumWidth(100) - passphrase_label = _("Enter your BIP39 passphrase (optional):") - # TODO: validation? - - self.addWidget(QLabel(passphrase_label)) - self.addWidget(self.passphrase_e) + self._valid = True - def get_settings(self): - item = ' '.join(str(clean_text(self.text_e)).split()) - return self.label_e.text(), item, self.passphrase_e.text() + def apply(self): + pass -class WCSatochipImportSeedParams(WalletWizardComponent): +class WCSeedSuccess(WalletWizardComponent): def __init__(self, parent, wizard): - WalletWizardComponent.__init__( - self, parent, wizard, title=_('Satochip Setup')) - self.plugins = wizard.plugins - self._busy = True + WalletWizardComponent.__init__(self, parent, wizard, title=_('Success!')) def on_ready(self): - current_cosigner = self.wizard.current_cosigner(self.wizard_data) - _name, _info = current_cosigner['hardware_device'] - self.settings_layout = SatochipSeedLayout(_info.device.id_) - self.settings_layout.validChanged.connect( - self.on_settings_valid_changed) - self.layout().addLayout(self.settings_layout) + w_icon = QLabel() + w_icon.setPixmap(QPixmap(icon_path('confirmed.png')).scaledToWidth(48, mode=Qt.SmoothTransformation)) + w_icon.setAlignment(Qt.AlignCenter) + label = WWLabel(_("Seed imported successfully!")) + label.setAlignment(Qt.AlignCenter) self.layout().addStretch(1) - - # TODO #current_cosigner['satochip_init'] != TIM_PRIVKEY # TODO: only privkey is validated - self.valid = True - self.busy = False - - def on_settings_valid_changed(self, is_valid: bool): - self.valid = is_valid + self.layout().addWidget(w_icon) + self.layout().addWidget(label) + self.layout().addStretch(1) + # self.layout().addWidget(WWLabel("Seed imported successfully!") + # self.layout().addStretch(1) + self._valid = True def apply(self): - current_cosigner = self.wizard.current_cosigner(self.wizard_data) - current_cosigner['satochip_seed_settings'] = self.settings_layout.get_settings( - ) + pass class WCSatochipImportSeed(WalletWizardComponent): @@ -1000,8 +982,9 @@ def __init__(self, parent, wizard): def on_ready(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) - settings = current_cosigner['satochip_seed_settings'] - # method = current_cosigner['satochip_init'] + + settings = current_cosigner['seed_type'], current_cosigner['seed'], current_cosigner['seed_extra_words'] + _name, _info = current_cosigner['hardware_device'] device_id = _info.device.id_ client = self.plugins.device_manager.client_by_id( diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index ec52a03c1d76..7ea1ba3a73c7 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -629,7 +629,13 @@ def _import_seed(self, settings, device_id, handler): if not client: raise Exception(_("The device was disconnected.")) - label, seed, passphrase = settings + seed_type, seed, passphrase = settings + + # check seed type: + if seed_type != 'bip39': + _logger.error( + f"[SatochipPlugin] _import_seed() wrong seed type!") + raise Exception(f'Wrong seed type {seed_type}: only BIP39 is supported!') # check seed validity (is_checksum_valid, is_wordlist_valid) = bip39_is_checksum_valid(seed) @@ -651,31 +657,13 @@ def _import_seed(self, settings, device_id, handler): authentikey = client.cc.card_bip32_import_seed(masterseed_list) _logger.info( f"[SatochipPlugin] _import_seed(): seed imported successfully!") - client.handler.show_message(f"seed imported successfully!") hex_authentikey = authentikey.get_public_key_hex(compressed=True) _logger.info( f"[SatochipPlugin] _import_seed(): authentikey={hex_authentikey}") except Exception as ex: _logger.error( f"[SatochipPlugin] _import_seed(): exception during seed import: {ex}") - client.handler.show_error(f"Exception during seed import: {ex}") - - # import label - (response, sw1, sw2) = client.cc.card_set_label(label) - if (sw1 == 0x90 and sw2 == 0x00): - _logger.info( - f"[SatochipPlugin] _import_seed(): card label changed successfully") - # client.handler.show_message(_("Card label changed successfully!")) - elif (sw1 == 0x6D and sw2 == 0x00): - _logger.info( - f"[SatochipPlugin] _import_seed(): failed to set label: card does not support label (code {hex(sw1*256+sw2)})") - # starts with satochip v0.12 - client.handler.show_error(_("Error: card does not support label!")) - else: - _logger.info( - f"[SatochipPlugin] _import_seed(): unknown error while setting label (code {hex(sw1*256+sw2)})") - client.handler.show_error( - f"Error while setting card label (code {hex(sw1*256+sw2)})") + raise ex def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str: _logger.info(f"[SatochipPlugin] wizard_entry_for_device()") @@ -722,9 +710,12 @@ def extend_wizard(self, wizard: 'NewWalletWizard'): 'next': 'satochip_not_seeded', }, 'satochip_not_seeded': { - 'next': 'satochip_import_seed', + 'next': 'satochip_have_seed', }, 'satochip_import_seed': { + 'next': 'satochip_success_seed', + }, + 'satochip_success_seed': { 'next': 'satochip_start', }, 'satochip_unlock': { From 37f719bf2c3293a2753cda9614258901971b5778 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 25 Sep 2024 20:36:26 +0100 Subject: [PATCH 11/20] Remove unnecessary parenthesis in 'if' statements --- electrum/plugins/satochip/qt.py | 35 ++++++++++++--------------- electrum/plugins/satochip/satochip.py | 28 ++++++++++----------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index ad20aa3454fd..6d103d957fd8 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -266,7 +266,7 @@ def show_values(self, client): self.sw_version.setText('%s' % sw_rel) (response, sw1, sw2, d) = client.cc.card_get_status() - if (sw1 == 0x90 and sw2 == 0x00): + if sw1 == 0x90 and sw2 == 0x00: # fw_rel= 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) fw_rel = 'v' + str(d["protocol_major_version"]) + '.' + str(d["protocol_minor_version"]) + \ '-' + str(d["applet_major_version"]) + '.' + \ @@ -298,7 +298,7 @@ def show_values(self, client): # card label (response, sw1, sw2, label) = client.cc.card_get_label() - if (label == ""): + if label == "": label = "(none)" self.card_label.setText('%s' % label) @@ -319,14 +319,14 @@ def change_pin(self, client): 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): + if not is_pin: return oldpin = list(oldpin) newpin = list(newpin) try: (response, sw1, sw2) = client.cc.card_change_PIN(0, oldpin, newpin) - if (sw1 == 0x90 and sw2 == 0x00): + if sw1 == 0x90 and sw2 == 0x00: msg = _("PIN changed successfully!") self.window.show_message(msg) else: @@ -341,11 +341,6 @@ def change_pin(self, client): def reset_seed(self, client): _logger.info("In reset_seed") - # is_ok= client.verify_PIN() - # if not is_ok: - # msg= f"action cancelled by user" - # self.window.show_error(msg) - # return # pin msg = ''.join([ @@ -355,14 +350,14 @@ def reset_seed(self, client): _("To proceed, enter the PIN for your Satochip:") ]) password = self.reset_seed_dialog(msg) - if (password is None): + if password is None: return pin = password.encode('utf8') pin = list(pin) # if 2FA is enabled, get challenge-response hmac = [] - if (client.cc.needs_2FA is None): + if client.cc.needs_2FA is None: (response, sw1, sw2, d) = client.cc.card_get_status() if client.cc.needs_2FA: # challenge based on authentikey @@ -397,12 +392,12 @@ def reset_seed(self, client): # send request (response, sw1, sw2) = client.cc.card_reset_seed(pin, hmac) - if (sw1 == 0x90 and sw2 == 0x00): + if sw1 == 0x90 and sw2 == 0x00: msg = _( "Seed reset successfully!\nYou should close this wallet and launch the wizard to generate a new wallet.") self.window.show_message(msg) # to do: close client? - elif (sw1 == 0x9c and sw2 == 0x0b): + elif sw1 == 0x9c and sw2 == 0x0b: msg = _( f"Failed to reset seed: request rejected by 2FA device (error code: {hex(256*sw1+sw2)})") self.window.show_message(msg) @@ -432,7 +427,7 @@ def reset_seed_dialog(self, msg): def set_2FA(self, client): if not client.cc.needs_2FA: use_2FA = client.handler.yes_no_question(MSG_USE_2FA) - if (use_2FA): + if use_2FA: # verify PIN is_ok = client.verify_PIN() if not is_ok: @@ -448,7 +443,7 @@ def set_2FA(self, client): d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, help_text=help_txt, show_copy_text_btn=True, show_cancel_btn=True, config=self.config) result = d.exec_() # result should be 0 or 1 - if (result == 1): + if result == 1: # further communications will require an id and an encryption key (for privacy). # Both are derived from the secret_2FA using a one-way function inside the Satochip amount_limit = 0 # i.e. always use @@ -510,11 +505,11 @@ def reset_2FA(self, client): # send request (response, sw1, sw2) = client.cc.card_reset_2FA_key(hmac) - if (sw1 == 0x90 and sw2 == 0x00): + if sw1 == 0x90 and sw2 == 0x00: msg = _("2FA reset successfully!") client.cc.needs_2FA = False self.window.show_message(msg) - elif (sw1 == 0x9c and sw2 == 0x17): + elif sw1 == 0x9c and sw2 == 0x17: msg = _( f"Failed to reset 2FA: \nyou must reset the seed first (error code {hex(256*sw1+sw2)})") self.window.show_error(msg) @@ -635,9 +630,9 @@ def change_card_label(self, client): # set new label (response, sw1, sw2) = client.cc.card_set_label(label) - if (sw1 == 0x90 and sw2 == 0x00): + if sw1 == 0x90 and sw2 == 0x00: self.window.show_message(_("Card label changed successfully!")) - elif (sw1 == 0x6D and sw2 == 0x00): + elif sw1 == 0x6D and sw2 == 0x00: # starts with satochip v0.12 self.window.show_error(_("Error: card does not support label!")) else: @@ -820,7 +815,7 @@ def __init__(self, device): self.addLayout(vbox2) # PIN validation - if (self.pw.text() == "" or self.pw.text() is None): + if self.pw.text() == "" or self.pw.text() is None: self.validChanged.emit(False) def set_enabled(): diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 7ea1ba3a73c7..38d4ef5ac578 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -81,16 +81,16 @@ def is_initialized(self): (response, sw1, sw2, d) = self.cc.card_get_status() # if setup is not done, we return None - if (not self.cc.setup_done): + if not self.cc.setup_done: _logger.info(f"SATOCHIP is_initialized() None (no setup)") return None # if not seeded, return False - if (self.cc.setup_done and not self.cc.is_seeded): + if self.cc.setup_done and not self.cc.is_seeded: _logger.info( f"SATOCHIP is_initialized() False (PIN set but card not seeded)") return False # initialized if pin is set and device is seeded - if (self.cc.setup_done and self.cc.is_seeded): + if self.cc.setup_done and self.cc.is_seeded: _logger.info( f"SATOCHIP is_initialized() True (PIN set and card seeded)") return True @@ -186,13 +186,13 @@ def request(self, request_type, *args): _logger.info('[SatochipClient] client request: ' + str(request_type)) if self.handler is not None: - if (request_type == 'update_status'): + if request_type == 'update_status': reply = self.handler.update_status(*args) return reply - elif (request_type == 'show_error'): + elif request_type == 'show_error': reply = self.handler.show_error(*args) return reply - elif (request_type == 'show_message'): + elif request_type == 'show_message': reply = self.handler.show_message(*args) return reply else: @@ -230,7 +230,7 @@ def PIN_setup_dialog(self, msg, msg_confirm, msg_error): # return (False, None) raise RuntimeError( ('A PIN confirmation is required to initialize the Satochip!')) - if (pin != pin_confirm): + if pin != pin_confirm: self.request('show_error', msg_error) else: return (is_PIN, pin) @@ -238,21 +238,21 @@ def PIN_setup_dialog(self, msg, msg_confirm, msg_error): 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): + 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): + 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): + if not is_PIN: self.request('show_message', msg_cancel) return (False, None, None) - if (newpin != pin_confirm): + if newpin != pin_confirm: self.request('show_error', msg_error) else: return (True, oldpin, newpin) @@ -300,7 +300,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): # check if 2FA is required hmac = b'' - if (client.cc.needs_2FA is None): + if client.cc.needs_2FA is None: (response, sw1, sw2, d) = client.cc.card_get_status() if client.cc.needs_2FA: # challenge based on sha256(btcheader+msg) @@ -321,7 +321,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): (pubkey, chaincode) = client.cc.card_bip32_get_extendedkey(bytepath) (response2, sw1, sw2, compsig) = client.cc.card_sign_message( keynbr, pubkey, message_byte, hmac) - if (compsig == b''): + if compsig == b'': self.handler.show_error( _("Wrong signature!\nThe 2FA device may have rejected the action.")) return compsig @@ -568,7 +568,7 @@ def _setup_device(self, settings, device_id, handler): raise Exception(_("The device was disconnected.")) # check that card is indeed a Satochip - if (client.cc.card_type != "Satochip"): + if client.cc.card_type != "Satochip": raise Exception(_('Failed to create a client for this device.') + '\n' + _('Inserted card is not a Satochip!')) From b06e332be74e748dea55e0491fc2fb1fe755ce8f Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 25 Sep 2024 22:29:26 +0100 Subject: [PATCH 12/20] qt desktop gui: upgrade qt5->qt6 --- electrum/plugins/satochip/qt.py | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 6d103d957fd8..70932b97eb76 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -8,9 +8,9 @@ from electrum.gui.qt.wizard.wallet import (WCHaveSeed, WCEnterExt, WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard) from electrum.plugin import hook -from PyQt5.QtGui import QPixmap -from PyQt5.QtCore import Qt, pyqtSignal, QRegExp -from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) from functools import partial @@ -83,7 +83,7 @@ def connect(): def show_dialog(device_id): if device_id: SatochipSettingsDialog( - window, self, keystore, device_id).exec_() + window, self, keystore, device_id).exec() keystore.thread.add(connect, on_success=show_dialog) @hook @@ -160,9 +160,9 @@ def connect_and_doit(): title = QLabel('''
Satochip Wallet
satochip.io''') - title.setTextInteractionFlags(Qt.LinksAccessibleByMouse) + title.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse) - grid.addWidget(title, 0, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(title, 0, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y = 3 rows = [ @@ -176,10 +176,10 @@ def connect_and_doit(): for row_num, (member_name, label) in enumerate(rows): widget = QLabel('') widget.setTextInteractionFlags( - Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) + Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard) - grid.addWidget(QLabel(label), y, 0, 1, 1, Qt.AlignRight) - grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft) + grid.addWidget(QLabel(label), y, 0, 1, 1, Qt.AlignmentFlag.AlignRight) + grid.addWidget(widget, y, 1, 1, 1, Qt.AlignmentFlag.AlignLeft) setattr(self, member_name, widget) y += 1 @@ -231,21 +231,21 @@ def _change_card_label(): change_card_label_btn.clicked.connect(_change_card_label) y += 3 - grid.addWidget(pin_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(pin_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(seed_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(seed_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(set_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(set_2FA_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(reset_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(reset_2FA_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(change_2FA_server_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(change_2FA_server_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(verify_card_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(verify_card_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(change_card_label_btn, y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(change_card_label_btn, y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) y += 2 - grid.addWidget(CloseButton(self), y, 0, 1, 2, Qt.AlignHCenter) + grid.addWidget(CloseButton(self), y, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) dialog_vbox = QVBoxLayout(self) dialog_vbox.addWidget(body) @@ -421,7 +421,7 @@ def reset_seed_dialog(self, msg): vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) d.setLayout(vbox) - passphrase = pw.text() if d.exec_() else None + passphrase = pw.text() if d.exec() else None return passphrase def set_2FA(self, client): @@ -442,7 +442,7 @@ def set_2FA(self, client): help_txt = "Scan the QR-code with your Satochip-2FA app and make a backup of the following secret: " + secret_2FA_hex d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, help_text=help_txt, show_copy_text_btn=True, show_cancel_btn=True, config=self.config) - result = d.exec_() # result should be 0 or 1 + result = d.exec() # result should be 0 or 1 if result == 1: # further communications will require an id and an encryption key (for privacy). # Both are derived from the secret_2FA using a one-way function inside the Satochip @@ -529,7 +529,7 @@ def change_2FA_server(self, client): title = "Select 2FA server" d = SelectOptionsDialog(option_name=option_name, options=options, parent=None, title=title, help_text=help_txt, config=self.config) - result = d.exec_() # result should be 0 or 1 + result = d.exec() # result should be 0 or 1 def verify_card(self, client): # verify pin @@ -571,7 +571,7 @@ def verify_card(self, client): txt_subca=txt_subca, txt_device=txt_device, ) - result = d.exec_() + result = d.exec() # todo: add this function in pysatochip def card_verify_authenticity(self, client): @@ -654,7 +654,7 @@ def change_card_label_dialog(self, client, msg): vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) d.setLayout(vbox) - label = pw.text() if d.exec_() else None + label = pw.text() if d.exec() else None if label is None or len(label.encode('utf-8')) <= 64: return label else: @@ -948,10 +948,10 @@ def __init__(self, parent, wizard): def on_ready(self): w_icon = QLabel() - w_icon.setPixmap(QPixmap(icon_path('confirmed.png')).scaledToWidth(48, mode=Qt.SmoothTransformation)) - w_icon.setAlignment(Qt.AlignCenter) + w_icon.setPixmap(QPixmap(icon_path('confirmed.png')).scaledToWidth(48, mode=Qt.TransformationMode.SmoothTransformation)) + w_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) label = WWLabel(_("Seed imported successfully!")) - label.setAlignment(Qt.AlignCenter) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.layout().addStretch(1) self.layout().addWidget(w_icon) self.layout().addWidget(label) From c4709011fa84b963aac12e8537d89e2a552b1b22 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 13 Nov 2024 15:04:30 +0100 Subject: [PATCH 13/20] satochip: fixes and error handling --- electrum/plugins/satochip/qt.py | 45 ++++++++++++++++----------- electrum/plugins/satochip/satochip.py | 7 +++-- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 70932b97eb76..5b16151f115f 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -1,26 +1,27 @@ +from functools import partial +from os import urandom +import textwrap +import threading + +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, + QWidget, QGridLayout, QComboBox, QLineEdit, QTabWidget) + from electrum.i18n import _ from electrum.logging import get_logger -from electrum.keystore import bip39_is_checksum_valid +from electrum.util import UserFacingException from electrum.simple_config import SimpleConfig from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, icon_path, OkButton, CancelButton, WindowModalDialog, WWLabel, PasswordLineEdit) from electrum.gui.qt.qrcodewidget import QRDialog from electrum.gui.qt.wizard.wallet import (WCHaveSeed, WCEnterExt, WCScriptAndDerivation, - WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard) + WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard) from electrum.plugin import hook -from PyQt6.QtGui import QPixmap -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, - QWidget, QGridLayout, QComboBox, QLineEdit, QTextEdit, QTabWidget) - -from functools import partial -from os import urandom -import textwrap -import threading +from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase # satochip from .satochip import SatochipPlugin -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase # pysatochip from pysatochip.CardConnector import UnexpectedSW12Error, CardError, CardNotPresentError, WrongPinError @@ -49,6 +50,7 @@ _("If set, you will need your passphrase along with your BIP39 seed to restore your wallet from a backup. "), ] + class Plugin(SatochipPlugin, QtPluginBase): icon_unpaired = "satochip_unpaired.png" icon_paired = "satochip.png" @@ -104,7 +106,8 @@ def extend_wizard(self, wizard: 'QENewWalletWizard'): }, 'satochip_have_seed': { 'gui': WCHaveSeed, - 'next': lambda d: 'satochip_have_ext' if wizard.wants_ext(d) else 'satochip_import_seed' + 'next': lambda d: 'satochip_have_ext' if wizard.wants_ext(d) else 'satochip_import_seed', + 'params': {'seed_options': ['ext', 'bip39']} }, 'satochip_have_ext': { 'gui': WCEnterExt, @@ -255,10 +258,14 @@ def _change_card_label(): def show_values(self, client): _logger.info("Show value!") - is_ok = client.verify_PIN() - if not is_ok: - msg = f"action cancelled by user" - self.window.show_error(msg) + try: + is_ok = client.verify_PIN() + if not is_ok: + msg = f"action cancelled by user" + self.window.show_error(msg) + return + except UserFacingException as e: + self.window.show_error(str(e)) return sw_rel = 'v' + str(SATOCHIP_PROTOCOL_MAJOR_VERSION) + \ @@ -412,7 +419,7 @@ def reset_seed_dialog(self, msg): parent = self.top_level_window() d = WindowModalDialog(parent, _("Enter PIN")) pw = QLineEdit() - pw.setEchoMode(2) + pw.setEchoMode(QLineEdit.EchoMode.Password) pw.setMinimumWidth(200) vbox = QVBoxLayout() @@ -645,7 +652,6 @@ def change_card_label_dialog(self, client, msg): parent = self.top_level_window() d = WindowModalDialog(parent, _("Enter Label")) pw = QLineEdit() - pw.setEchoMode(0) pw.setMinimumWidth(200) vbox = QVBoxLayout() @@ -929,6 +935,7 @@ def apply(self): # Import seed wizard # ########################## + class WCSeedMessage(WalletWizardComponent): def __init__(self, parent, wizard): WalletWizardComponent.__init__(self, parent, wizard, title=_('Satochip needs a seed')) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 38d4ef5ac578..29775b6200a1 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -20,7 +20,7 @@ from ..hw_wallet import HW_PluginBase, HardwareClientBase # pysatochip -from pysatochip.CardConnector import CardConnector +from pysatochip.CardConnector import CardConnector, UninitializedSeedError from pysatochip.CardConnector import CardNotPresentError, UnexpectedSW12Error, WrongPinError, PinBlockedError, PinRequiredError from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST @@ -163,7 +163,10 @@ def get_xpub(self, bip32_path, xtype): # bip32_path is of the form 44'/0'/1' _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}") (depth, bytepath) = bip32path2bytes(bip32_path) - (childkey, childchaincode) = self.cc.card_bip32_get_extendedkey(bytepath) + try: + (childkey, childchaincode) = self.cc.card_bip32_get_extendedkey(bytepath) + except UninitializedSeedError as e: + raise UserFacingException(str(e)) if depth == 0: # masterkey fingerprint = bytes([0, 0, 0, 0]) child_number = bytes([0, 0, 0, 0]) From 133ade3c99a2824b87a40b143c792adcf516bc59 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 21 Jan 2025 16:27:13 +0100 Subject: [PATCH 14/20] (minor) correct some PEP8 warnings --- electrum/plugins/satochip/satochip.py | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 29775b6200a1..fe7b8194ca14 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -38,7 +38,11 @@ SATOCHIP_VID = 0 # 0x096E SATOCHIP_PID = 0 # 0x0503 -MSG_USE_2FA = _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!") +MSG_USE_2FA = _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on " + "a second device such as your smartphone. First you have to install the Satochip-2FA android app on " + "google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the " + "next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have " + "to reinstall the app!") def bip32path2bytes(bip32path: str) -> (int, bytes): @@ -47,7 +51,7 @@ def bip32path2bytes(bip32path: str) -> (int, bytes): bytePath = b'' for index in intPath: bytePath += index.to_bytes(4, byteorder='big', signed=False) - return (depth, bytePath) + return depth, bytePath class SatochipClient(HardwareClientBase): @@ -118,7 +122,7 @@ def has_usable_connection_with_device(self): return True def verify_PIN(self, pin=None): - while (True): + while True: try: # when pin is None, pysatochip use a cached pin if available (response, sw1, sw2) = self.cc.card_verify_PIN_simple(pin) @@ -222,7 +226,7 @@ def PIN_dialog(self, msg): return True, password def PIN_setup_dialog(self, msg, msg_confirm, msg_error): - while (True): + while True: (is_PIN, pin) = self.PIN_dialog(msg) if not is_PIN: # return (False, None) @@ -231,34 +235,33 @@ def PIN_setup_dialog(self, msg, msg_confirm, msg_error): (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!')) + 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) + 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) + return False, None, None # new pin - while (True): + while True: (is_PIN, newpin) = self.PIN_dialog(msg_newpin) if not is_PIN: self.request('show_message', msg_cancel) - return (False, None, None) + 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) + return False, None, None if newpin != pin_confirm: self.request('show_error', msg_error) else: - return (True, oldpin, newpin) + return True, oldpin, newpin class Satochip_KeyStore(Hardware_KeyStore): @@ -635,7 +638,7 @@ def _import_seed(self, settings, device_id, handler): seed_type, seed, passphrase = settings # check seed type: - if seed_type != 'bip39': + if seed_type != 'bip39': _logger.error( f"[SatochipPlugin] _import_seed() wrong seed type!") raise Exception(f'Wrong seed type {seed_type}: only BIP39 is supported!') From 44cd93d72f82e7bd203be55cda04a319d0fd8888 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 21 Jan 2025 16:48:54 +0100 Subject: [PATCH 15/20] Patch: electrum.ecc is now a distinct package --- electrum/plugins/satochip/satochip.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index fe7b8194ca14..96742c607c0f 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -1,6 +1,7 @@ from os import urandom import hashlib import time +import electrum_ecc as ecc # electrum from electrum import constants @@ -13,7 +14,7 @@ from electrum.wizard import NewWalletWizard from electrum.util import UserFacingException from electrum.crypto import hash_160, sha256d -from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig +#from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath from electrum.logging import get_logger @@ -432,10 +433,10 @@ def sign_transaction(self, tx, password): # enforce low-S signature (BIP 62) tx_sig = bytes(tx_sig) # bytearray(tx_sig) - r, s = get_r_and_s_from_ecdsa_der_sig(tx_sig) - if s > CURVE_ORDER // 2: - s = CURVE_ORDER - s - tx_sig = ecdsa_der_sig_from_r_and_s(r, s) + r, s = ecc.get_r_and_s_from_ecdsa_der_sig(tx_sig) + if s > ecc.CURVE_ORDER // 2: + s = ecc.CURVE_ORDER - s + tx_sig = ecc.ecdsa_der_sig_from_r_and_s(r, s) # update tx with signature tx_sig = tx_sig + Sighash.to_sigbytes(Sighash.ALL) tx.add_signature_to_txin( From 5bc0b720bcb7b0f01a47d94d89c9f074371d6dae Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 21 Jan 2025 16:50:51 +0100 Subject: [PATCH 16/20] Moved Satochip icons from electrum/gui to the satochip plugin folder --- .../{gui/icons => plugins/satochip}/satochip.png | Bin .../satochip}/satochip_unpaired.png | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename electrum/{gui/icons => plugins/satochip}/satochip.png (100%) rename electrum/{gui/icons => plugins/satochip}/satochip_unpaired.png (100%) diff --git a/electrum/gui/icons/satochip.png b/electrum/plugins/satochip/satochip.png similarity index 100% rename from electrum/gui/icons/satochip.png rename to electrum/plugins/satochip/satochip.png diff --git a/electrum/gui/icons/satochip_unpaired.png b/electrum/plugins/satochip/satochip_unpaired.png similarity index 100% rename from electrum/gui/icons/satochip_unpaired.png rename to electrum/plugins/satochip/satochip_unpaired.png From aa9c241d3caf2137290a253e1766bc676efa9446 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 22 Jan 2025 08:49:06 +0100 Subject: [PATCH 17/20] Remove commented import --- electrum/plugins/satochip/satochip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 96742c607c0f..063c40fe4e01 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -14,7 +14,6 @@ from electrum.wizard import NewWalletWizard from electrum.util import UserFacingException from electrum.crypto import hash_160, sha256d -#from electrum.ecc import CURVE_ORDER, ecdsa_der_sig_from_r_and_s, get_r_and_s_from_ecdsa_der_sig from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath from electrum.logging import get_logger From 525715e17f03d5dd0082c25ea2880254029336b8 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 17 Jun 2025 22:49:25 +0200 Subject: [PATCH 18/20] Update Satochip plugin * add manifest.json for Satochip plugin * move hw_wallet.py from plugins to electrum library --- electrum/plugins/satochip/manifest.json | 9 +++++++++ electrum/plugins/satochip/qt.py | 4 ++-- electrum/plugins/satochip/satochip.py | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 electrum/plugins/satochip/manifest.json diff --git a/electrum/plugins/satochip/manifest.json b/electrum/plugins/satochip/manifest.json new file mode 100644 index 000000000000..bf2924397b5f --- /dev/null +++ b/electrum/plugins/satochip/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "satochip", + "fullname": "Satochip Wallet", + "description": "Provides support for Satochip hardware wallet", + "requires": [["pysatochip","pypi.org/project/pysatochip/"]], + "registers_keystore": ["hardware", "satochip", "Satochip wallet"], + "icon":"satochip.png", + "available_for": ["qt"] +} diff --git a/electrum/plugins/satochip/qt.py b/electrum/plugins/satochip/qt.py index 5b16151f115f..29af81c095ee 100644 --- a/electrum/plugins/satochip/qt.py +++ b/electrum/plugins/satochip/qt.py @@ -18,7 +18,7 @@ from electrum.gui.qt.wizard.wallet import (WCHaveSeed, WCEnterExt, WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent, QENewWalletWizard) from electrum.plugin import hook -from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase +from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase # satochip from .satochip import SatochipPlugin @@ -985,7 +985,7 @@ def __init__(self, parent, wizard): def on_ready(self): current_cosigner = self.wizard.current_cosigner(self.wizard_data) - settings = current_cosigner['seed_type'], current_cosigner['seed'], current_cosigner['seed_extra_words'] + settings = current_cosigner['seed_type'], current_cosigner['seed'], current_cosigner['seed_extra_words'] if current_cosigner['seed_extend'] else '' _name, _info = current_cosigner['hardware_device'] device_id = _info.device.id_ diff --git a/electrum/plugins/satochip/satochip.py b/electrum/plugins/satochip/satochip.py index 063c40fe4e01..c1a11219c6f7 100644 --- a/electrum/plugins/satochip/satochip.py +++ b/electrum/plugins/satochip/satochip.py @@ -17,7 +17,7 @@ from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath, convert_bip32_intpath_to_strpath from electrum.logging import get_logger -from ..hw_wallet import HW_PluginBase, HardwareClientBase +from electrum.hw_wallet import HW_PluginBase, HardwareClientBase # pysatochip from pysatochip.CardConnector import CardConnector, UninitializedSeedError From 861cf34b87303788ed374571e5dbf5b052f42686 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 17 Jun 2025 23:41:38 +0200 Subject: [PATCH 19/20] update pyscard from v2.0.7 to v2.2.2 in requirements --- .../deterministic-build/requirements-hw.txt | 32 +++++++++++-------- contrib/requirements/requirements-hw.txt | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index a83eda3352aa..0cc02d845c30 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -403,19 +403,25 @@ pycparser==2.22 \ pysatochip==0.12.6 \ --hash=sha256:f751ae93ea784dc3ef77508da56df6222a195d8f7ead53f87d29ea84c7bc8f90 \ --hash=sha256:d1255c5126c0c76b86f5eb1289b1cdc0b14a6f1265c82a63ce07074f6ccb2903 -pyscard==2.0.7 \ - --hash=sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf \ - --hash=sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed \ - --hash=sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646 \ - --hash=sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab \ - --hash=sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046 \ - --hash=sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9 \ - --hash=sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2 \ - --hash=sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc \ - --hash=sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43 \ - --hash=sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c \ - --hash=sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b \ - --hash=sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a +pyscard==2.2.2 \ + --hash=sha256:e658f240276c12f836c28159120da499b0593cf6178469095e09a4ada869b9c6 \ + --hash=sha256:81e3d3ae40cf47725b5835970433197c4a6a9030af9f08ca74b4ddaf08c339c4 \ + --hash=sha256:8f545e55918f6e44eb3dbc6abb6c290efa3f992872e94a4af1ee43f6ecbd160d \ + --hash=sha256:44eaafcec0b0bab0344be5209b1456bc2f34a15303e0ae584e2fe6abbe67f777 \ + --hash=sha256:77577e6c847c253c642b880087616b08223b74e3942f07d3a6019f7067fe2369 \ + --hash=sha256:4e3642fc5b800b4e7ff88742eef01cbb8bd3b3d0f951f56f3fa4fca51ab41bf2 \ + --hash=sha256:88e277d809fce5fc29e737ecd135f3871a0813f7d8b27fad5537ed9fa4442a20 \ + --hash=sha256:535d03d04477ef0cb9812ca0fad4b71fee6984b30ad72f05f644b6e4e743ccd5 \ + --hash=sha256:427164199171d26c565db0d2a577c491253dfdf160408dcd605bd0e8f4f01060 \ + --hash=sha256:9ca0f5f3e38b753539f3c65335536dea8a20ca4c660b320f87368070e7febdbc \ + --hash=sha256:1480fc9e760487e4fe18b668647cd88bb4d0fd94e075bba6a00a582a93b6def7 \ + --hash=sha256:ab1a875666330880ddecacbadc8193dc5c6eb799329e3d6e99281a1de113a4cd \ + --hash=sha256:48e8e2e004ef105b488422c9943eca5f6f38648a8e377a94017fa07203d05b4a \ + --hash=sha256:b634d762de8058a039cf013dff3946e5eed6ee5d06fd18fe100ecd6af3eb6c35 \ + --hash=sha256:ac9eaf1988c9c563a5bf5d54b8c6058ef267a1ad5755d353b9d5a68ec5b2210b \ + --hash=sha256:17b50d6aba530e9ef9a1a87d2761d52f378453bbb1735fc0bbdcaadc9597fef1 \ + --hash=sha256:3975dc4527996552d9317cc8b3e6a9e7e98c85616c9dc4a34152f622c3c0ffd3 \ + --hash=sha256:c77481fb86f4a17bc441d7b36551c1d36a9d3a48c4bb30ab8118886e6f275081 pyserial==3.5 \ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 1f58891bd23d..8a93fcea1a0c 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -24,7 +24,7 @@ ckcc-protocol>=0.7.7 bitbox02>=6.2.0 # device plugin: satochip -pyscard>=1.9.9 +pyscard>=2.0.7 pysatochip==0.12.6 # device plugin: jade From 424b6d9055a581d19bf3a5cb9ef4f680f0240d47 Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 20 Jun 2025 11:36:44 +0200 Subject: [PATCH 20/20] use manifest.json instead of loading init file for plugin registration --- electrum/plugins/satochip/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/electrum/plugins/satochip/__init__.py b/electrum/plugins/satochip/__init__.py index 75b91583a857..e69de29bb2d1 100644 --- a/electrum/plugins/satochip/__init__.py +++ b/electrum/plugins/satochip/__init__.py @@ -1,6 +0,0 @@ - -fullname = 'Satochip Wallet' -description = 'Provides support for Satochip hardware wallet' -requires = [('satochip', 'github.com/Toporin/pysatochip')] -registers_keystore = ('hardware', 'satochip', "Satochip wallet") -available_for = ['qt']