Skip to content

Commit

Permalink
Satochip plugin: use BIP39 by default & improve user interface
Browse files Browse the repository at this point in the history
Seed generation: use BIP39 by default for better compatibility:
* Most (all?) hardware wallets use BIP39 seeds in combination with BIP43-style derivation paths (like m / purpose' / coin_type' / account' / change / address_index)
* Electrum uses BIP43 derivation paths for all hardware wallets, AND for BIP39 seeds imported in software wallets
* Electrum seeds (not BIP39) use seed versioning instead of purpose derivation path.
* Software wallets with Electrum seeds use "m / change / address_index" derivation paths

User interface improvements:
* 2FA config is removed from initial setup as it confuses some users
* Instead, 2FA can be activated from the option menu (clicking on the Satochip logo on the lower right corner

Other minor corrections:
* PIN can have a maximum length of 16, not 64
  • Loading branch information
Toporin committed Jan 11, 2021
1 parent 654bcdf commit cd562de
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 96 deletions.
143 changes: 102 additions & 41 deletions electroncash_plugins/satochip/qt.py
@@ -1,21 +1,25 @@
from electroncash.i18n import _
from electroncash.util import print_error
from electroncash.plugins import run_hook
from electroncash_gui.qt.util import EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel
from electroncash_gui.qt.qrcodewidget import QRCodeWidget, QRDialog

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout, QLineEdit, QCheckBox
from functools import partial
from os import urandom


from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
#satochip
from .satochip import SatochipPlugin
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase

#pysatochip
from pysatochip.CardConnector import CardConnector
from pysatochip.Satochip2FA import Satochip2FA
from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION

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"
Expand Down Expand Up @@ -48,9 +52,16 @@ def settings_dialog(self, window):

def show_settings_dialog(self, window, keystore):
# When they click on the icon for Satochip we come here.
device_id = self.choose_device(window, keystore)
if device_id:
SatochipSettingsDialog(window, self, keystore, device_id).exec_()
# device_id = self.choose_device(window, keystore)
# if device_id:
# SatochipSettingsDialog(window, self, keystore, device_id).exec_()
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)

class Satochip_Handler(QtHandlerBase):

Expand Down Expand Up @@ -92,7 +103,7 @@ def connect_and_doit():
<br><a href="https://satochip.io">satochip.io</a>''')
title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)

grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
grid.addWidget(title, 0, 0, 1, 2, Qt.AlignHCenter)
y = 3

rows = [
Expand All @@ -118,17 +129,34 @@ def _change_pin():
thread.add(connect_and_doit, on_success=self.change_pin)
pin_btn.clicked.connect(_change_pin)

seed_btn = QPushButton('reset seed')
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)

y += 3
grid.addWidget(pin_btn, y, 0)
grid.addWidget(seed_btn, y, 1)
y += 5
grid.addWidget(CloseButton(self), y, 1)
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(CloseButton(self), y, 0, 1, 2, Qt.AlignHCenter)

dialog_vbox = QVBoxLayout(self)
dialog_vbox.addWidget(body)
Expand Down Expand Up @@ -209,7 +237,7 @@ def reset_seed(self, client):
_("Please be sure that your wallet is empty and that you have a backup of the seed as a precaution.\n\n"),
_("To proceed, enter the PIN for your Satochip:")
])
(password, reset_2FA)= self.reset_seed_dialog(msg)
password = self.reset_seed_dialog(msg)
if (password is None):
return
pin = password.encode('utf8')
Expand Down Expand Up @@ -254,11 +282,59 @@ def reset_seed(self, client):
msg= _("Seed reset successfully!\nYou should close this wallet and launch the wizard to generate a new wallet.")
client.handler.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)})")
client.handler.show_message(msg)
#to do: close client?
else:
msg= _(f"Failed to reset seed with error code: {hex(sw1)}{hex(sw2)}")
msg= _(f"Failed to reset seed with error code: {hex(256*sw1+sw2)}")
client.handler.show_error(msg)

if reset_2FA and client.cc.needs_2FA:
def reset_seed_dialog(self, msg):
print_error("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):
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:
d = QRDialog(secret_2FA_hex, None, "Scan secret 2FA and save a copy", True)
d.exec_()
except Exception as e:
print_error("SatochipPlugin: setup 2FA error: "+str(e))
return
# 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:
print_error(f"Unable to set 2FA with error code:= {hex(256*sw1+sw2)}")#debugSatochip
raise RuntimeError(f'Unable to setup 2FA with error code: {hex(256*sw1+sw2)}')
else:
client.handler.show_message("2FA enabled successfully!")
else:
msg= _(f"2FA is already enabled!")
client.handler.show_error(msg)

def reset_2FA(self, client):
if client.cc.needs_2FA:
# challenge based on ID_2FA
# format & encrypt msg
import json
Expand All @@ -278,7 +354,7 @@ def reset_seed(self, client):
try:
reply_encrypt= d['reply_encrypt']
except Exception as e:
self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
self.give_error("No response received from 2FA!", True)
reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
print_error("challenge:response= "+ reply_decrypt)
reply_decrypt= reply_decrypt.split(":")
Expand All @@ -291,30 +367,15 @@ def reset_seed(self, client):
msg= _("2FA reset successfully!")
client.cc.needs_2FA= False
client.handler.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)})")
client.handler.show_error(msg)
elif (sw1==0x9c and sw2==0x0b):
msg= _(f"Failed to reset 2FA: \nrequest rejected by 2FA device (error code: {hex(256*sw1+sw2)})")
client.handler.show_message(msg)
else:
msg= _(f"Failed to reset 2FA with error code: {hex(sw1)}{hex(sw2)}")
client.handler.show_error(msg)

def reset_seed_dialog(self, msg):
print_error("In reset_seed_dialog")
parent = self.top_level_window()
d = WindowModalDialog(parent, _("Enter PIN"))
pw = QLineEdit()
pw.setEchoMode(2)
pw.setMinimumWidth(200)

cb_reset_2FA = QCheckBox(_('Also reset 2FA'))

vbox = QVBoxLayout()
vbox.addWidget(WWLabel(msg))
vbox.addWidget(pw)
vbox.addWidget(cb_reset_2FA)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
d.setLayout(vbox)

passphrase = pw.text() if d.exec_() else None

reset_2FA= cb_reset_2FA.isChecked()
return (passphrase, reset_2FA)


msg= _(f"Failed to reset 2FA with error code: {hex(256*sw1+sw2)}")
client.handler.show_error(msg)
else:
msg= _(f"2FA is already disabled!")
client.handler.show_error(msg)

0 comments on commit cd562de

Please sign in to comment.