Skip to content

Commit

Permalink
Refactor 2FA challenge-response
Browse files Browse the repository at this point in the history
* 2FA server in a user settings saved in user config
* 2FA server url can be selected via satochip settings
  • Loading branch information
Toporin committed May 24, 2023
1 parent 63d6ed7 commit ce80c48
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 16 deletions.
73 changes: 69 additions & 4 deletions electrum/plugins/satochip/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel)
from electrum.gui.qt.qrcodewidget import QRCodeWidget, QRDialog, QRDialogCancellable
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout, QLineEdit, QCheckBox)
from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QGridLayout, QComboBox, QLineEdit, QCheckBox)
from functools import partial
from os import urandom

Expand All @@ -14,7 +14,7 @@

#pysatochip
from pysatochip.CardConnector import CardConnector, UnexpectedSW12Error, CardError, CardNotPresentError
from pysatochip.Satochip2FA import Satochip2FA
from pysatochip.Satochip2FA import Satochip2FA, SERVER_LIST
from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION

_logger = get_logger(__name__)
Expand Down Expand Up @@ -150,6 +150,11 @@ def _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)
Expand All @@ -170,6 +175,8 @@ def _change_card_label():
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)
Expand Down Expand Up @@ -404,7 +411,17 @@ def reset_2FA(self, client):
else:
msg= _(f"2FA is already disabled!")
self.window.show_error(msg)


def change_2FA_server(self, client):
_logger.info("in change_2FA_server")
config = SimpleConfig()
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=config)
result=d.exec_() # result should be 0 or 1

def verify_card(self, client):
is_authentic, txt_ca, txt_subca, txt_device, txt_error = self.card_verify_authenticity(client)

Expand Down Expand Up @@ -499,5 +516,53 @@ def change_card_label_dialog(self, client, msg):
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())

25 changes: 13 additions & 12 deletions electrum/plugins/satochip/satochip.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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
Expand Down Expand Up @@ -421,20 +422,20 @@ def do_challenge_response(self, msg):
_logger.info("id_2FA: "+id_2FA)

reply_encrypt= None
hmac= 20*"00" #bytes.fromhex(20*"00") # default response (reject)
hmac= 20*"00" # default response (reject)
status_msg=""
for server in SERVER_LIST:
status_msg += f"2FA request sent to '{server}' \nApprove or reject request on your second device."

config = SimpleConfig()
server_2FA = 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)
try:
Satochip2FA.do_challenge_response(d, server_name= server)
# decrypt and parse reply to extract challenge response
reply_encrypt= d['reply_encrypt']
break
except Exception as e:
status_msg += f"\nFailed to contact cosigner! \n=>trying another server\n\n"
self.handler.show_message(status_msg)
#self.handler.show_error(f"No response received from '{server}', trying another server")
if reply_encrypt is not None:
reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
_logger.info("challenge:response= "+ reply_decrypt)
Expand Down

0 comments on commit ce80c48

Please sign in to comment.