From ecc3004aee09321b878a624f595cb41dbaf09411 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 29 Aug 2023 12:49:09 +0200 Subject: [PATCH] qt: safe_t device init Note: untested, don't have device --- electrum/plugins/safe_t/qt.py | 203 +++++++++++++++++++++++------- electrum/plugins/safe_t/safe_t.py | 10 +- 2 files changed, 164 insertions(+), 49 deletions(-) diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 73e21a595..be4b38ffc 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -1,3 +1,4 @@ +import threading from functools import partial from typing import TYPE_CHECKING @@ -9,14 +10,17 @@ from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, QMessageBox, QFileDialog, QSlider, QTabWidget) from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, - OkButton, CloseButton, getOpenFileName) + OkButton, CloseButton, getOpenFileName, ChoiceWidget) from electrum.i18n import _ from electrum.plugin import hook from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available -from .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC +from .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY + from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUninitialized, WCHWUnlock, WCHWXPub +from electrum.gui.qt.wizard.wizard import WizardComponent +from ...logging import Logger if TYPE_CHECKING: from electrum.gui.qt.wizard.wallet import QENewWalletWizard @@ -95,84 +99,99 @@ class QtPlugin(QtPluginBase): keystore.thread.add(connect, on_success=show_dialog) def request_safe_t_init_settings(self, wizard, method, device): - vbox = QVBoxLayout() - next_enabled = True + safe_t_init_layout = SafeTInitLayout(method, device) + safe_t_init_layout.validChanged.connect(wizard.next_button.setEnabled) + next_enabled = method != TIM_PRIVKEY + wizard.exec_layout(safe_t_init_layout, next_enabled=next_enabled) + + return safe_t_init_layout.get_settings() + + +def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + +class SafeTInitLayout(QVBoxLayout): + validChanged = pyqtSignal([bool], arguments=['valid']) + + def __init__(self, method, device): + super().__init__() + + self.method = method + label = QLabel(_("Enter a label to name your device:")) - name = QLineEdit() + self.label_e = QLineEdit() hl = QHBoxLayout() hl.addWidget(label) - hl.addWidget(name) + hl.addWidget(self.label_e) hl.addStretch(1) - vbox.addLayout(hl) - - def clean_text(widget): - text = widget.toPlainText().strip() - return ' '.join(text.split()) + self.addLayout(hl) if method in [TIM_NEW, TIM_RECOVER]: gb = QGroupBox() hbox1 = QHBoxLayout() gb.setLayout(hbox1) - vbox.addWidget(gb) + self.addWidget(gb) gb.setTitle(_("Select your seed length:")) - bg = QButtonGroup() + self.bg = QButtonGroup() for i, count in enumerate([12, 18, 24]): rb = QRadioButton(gb) rb.setText(_("{:d} words").format(count)) - bg.addButton(rb) - bg.setId(rb, i) + self.bg.addButton(rb) + self.bg.setId(rb, i) hbox1.addWidget(rb) rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) + self.cb_pin = QCheckBox(_('Enable PIN protection')) + self.cb_pin.setChecked(True) else: - text = QTextEdit() - text.setMaximumHeight(60) + self.text_e = QTextEdit() + self.text_e.setMaximumHeight(60) if method == TIM_MNEMONIC: msg = _("Enter your BIP39 mnemonic:") + # TODO: no validation? else: msg = _("Enter the master private key beginning with xprv:") + def set_enabled(): from electrum.bip32 import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False - - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) + self.validChanged.emit(is_xprv(clean_text(self.text_e))) + self.text_e.textChanged.connect(set_enabled) + + self.addWidget(QLabel(msg)) + self.addWidget(self.text_e) + self.pin = QLineEdit() + self.pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) + self.pin.setMaximumWidth(100) hbox_pin = QHBoxLayout() hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) + hbox_pin.addWidget(self.pin) hbox_pin.addStretch(1) if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) + self.addWidget(WWLabel(RECOMMEND_PIN)) + self.addWidget(self.cb_pin) else: - vbox.addLayout(hbox_pin) + self.addLayout(hbox_pin) passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) passphrase_warning.setStyleSheet("color: red") - cb_phrase = QCheckBox(_('Enable passphrases')) - cb_phrase.setChecked(False) - vbox.addWidget(passphrase_msg) - vbox.addWidget(passphrase_warning) - vbox.addWidget(cb_phrase) - - wizard.exec_layout(vbox, next_enabled=next_enabled) - - if method in [TIM_NEW, TIM_RECOVER]: - item = bg.checkedId() - pin = cb_pin.isChecked() + self.cb_phrase = QCheckBox(_('Enable passphrases')) + self.cb_phrase.setChecked(False) + self.addWidget(passphrase_msg) + self.addWidget(passphrase_warning) + self.addWidget(self.cb_phrase) + + def get_settings(self): + if self.method in [TIM_NEW, TIM_RECOVER]: + item = self.bg.checkedId() + pin = self.cb_pin.isChecked() else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) + item = ' '.join(str(clean_text(self.text_e)).split()) + pin = str(self.pin.text()) - return (item, name.text(), pin, cb_phrase.isChecked()) + return item, self.label_e.text(), pin, self.cb_phrase.isChecked() class Plugin(SafeTPlugin, QtPlugin): @@ -193,12 +212,13 @@ class Plugin(SafeTPlugin, QtPlugin): # insert safe_t pages in new wallet wizard def extend_wizard(self, wizard: 'QENewWalletWizard'): - # TODO: device initialization not ported yet to new wizard super().extend_wizard(wizard) views = { 'safet_start': {'gui': WCScriptAndDerivation}, 'safet_xpub': {'gui': WCHWXPub}, - 'safet_not_initialized': {'gui': WCHWUninitialized}, + 'safet_not_initialized': {'gui': WCSafeTInitMethod}, + 'safet_choose_new_recover': {'gui': WCSafeTInitParams}, + 'safet_do_init': {'gui': WCSafeTInit}, 'safet_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -520,3 +540,90 @@ class SettingsDialog(WindowModalDialog): # Update information invoke_client(None) + + +class WCSafeTInitMethod(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('HW Setup')) + + def on_ready(self): + _name, _info = self.wizard_data['hardware_device'] + msg = _("Choose how you want to initialize your {}.\n\n" + "The first two methods are secure as no secret information " + "is entered into your computer.\n\n" + "For the last two methods you input secrets on your keyboard " + "and upload them to your {}, and so you should " + "only do those on a computer you know to be trustworthy " + "and free of malware." + ).format(_info.model_name, _info.model_name) + choices = [ + # Must be short as QT doesn't word-wrap radio button text + (TIM_NEW, _("Let the device generate a completely new seed randomly")), + (TIM_RECOVER, _("Recover from a seed you have previously written down")), + (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), + (TIM_PRIVKEY, _("Upload a master private key")) + ] + self.choice_w = ChoiceWidget(message=msg, choices=choices) + self.layout().addWidget(self.choice_w) + self.layout().addStretch(1) + + self._valid = True + + def apply(self): + self.wizard_data['safe_t_init'] = self.choice_w.selected_item[0] + + +class WCSafeTInitParams(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Set-up safe-t')) + self.plugins = wizard.plugins + self._busy = True + + def on_ready(self): + _name, _info = self.wizard_data['hardware_device'] + self.settings_layout = SafeTInitLayout(self.plugins.device_manager, self.wizard_data['safe_t_init'], _info.device.id_) + self.layout().addLayout(self.settings_layout) + self.layout().addStretch(1) + + self.valid = self.wizard_data['safe_t_init'] != TIM_PRIVKEY + self.busy = False + + def apply(self): + self.wizard_data['safe_t_settings'] = self.settings_layout.get_settings() + + +class WCSafeTInit(WizardComponent, Logger): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Set-up safe-t')) + Logger.__init__(self) + self.plugins = wizard.plugins + self.plugin = self.plugins.get_plugin('safe_t') + + self.layout().addWidget(WWLabel('Done')) + + self._busy = True + + def on_ready(self): + settings = self.wizard_data['safe_t_settings'] + method = self.wizard_data['safe_t_init'] + _name, _info = self.wizard_data['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, method, device_id, wizard, handler): + self.plugin._initialize_device(settings, method, device_id, wizard, handler) + self.init_done() + + t = threading.Thread( + target=initialize_device_task, + args=(settings, method, device_id, None, client.handler), + daemon=True) + t.start() + + def init_done(self): + self.logger.info('Done initialize device') + self.busy = False + + def apply(self): + pass diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index dc26f9b6f..64f4eb4e0 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -482,7 +482,15 @@ class SafeTPlugin(HW_PluginBase): 'accept': wizard.maybe_master_pubkey, 'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) }, - 'safet_not_initialized': {}, + 'safet_not_initialized': { + 'next': 'safet_choose_new_recover', + }, + 'safet_choose_new_recover': { + 'next': 'safet_do_init', + }, + 'safet_do_init': { + 'next': 'safet_start', + }, 'safet_unlock': { 'last': True },