From 5ab083b87e31b4f0161aef3c233f97ef19911d02 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 29 Aug 2023 13:14:21 +0200 Subject: [PATCH] qt: keepkey device init Note: untested, don't have device --- electrum/plugins/keepkey/keepkey.py | 12 +- electrum/plugins/keepkey/qt.py | 195 +++++++++++++++++++++------- 2 files changed, 159 insertions(+), 48 deletions(-) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index ce7f1a519..ea9e7a488 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -219,7 +219,7 @@ class KeepKeyPlugin(HW_PluginBase): ] def f(method): import threading - settings = self.request_trezor_init_settings(wizard, method, self.device) + settings = self.request_keepkey_init_settings(wizard, method, self.device) t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler)) t.daemon = True t.start() @@ -510,7 +510,15 @@ class KeepKeyPlugin(HW_PluginBase): 'accept': wizard.maybe_master_pubkey, 'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) }, - 'keepkey_not_initialized': {}, + 'keepkey_not_initialized': { + 'next': 'keepkey_choose_new_recover', + }, + 'keepkey_choose_new_recover': { + 'next': 'keepkey_do_init', + }, + 'keepkey_do_init': { + 'next': 'keepkey_start', + }, 'keepkey_unlock': { 'last': True }, diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index 2ee5c2b69..6e0fba8ce 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -1,3 +1,4 @@ +import threading from functools import partial from typing import TYPE_CHECKING @@ -6,18 +7,19 @@ from PyQt5.QtGui import QRegExpValidator from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, QHBoxLayout, QButtonGroup, QGroupBox, QDialog, QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget, - QMessageBox, QFileDialog, QSlider, QTabWidget) + QMessageBox, QSlider, QTabWidget) from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, - OkButton, CloseButton) + OkButton, CloseButton, 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 .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC +from .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUninitialized, WCHWUnlock, WCHWXPub +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub +from electrum.gui.qt.wizard.wizard import WizardComponent if TYPE_CHECKING: from electrum.gui.qt.wizard.wallet import QENewWalletWizard @@ -47,6 +49,7 @@ CHARACTER_RECOVERY = ( "Press ENTER or the Seed Entered button once the last word in your " "seed is auto-completed.") + class CharacterButton(QPushButton): def __init__(self, text=None): QPushButton.__init__(self, text) @@ -138,7 +141,6 @@ class CharacterDialog(WindowModalDialog): class QtHandler(QtHandlerBase): - char_signal = pyqtSignal(object) pin_signal = pyqtSignal(object, object) close_char_dialog_signal = pyqtSignal() @@ -192,7 +194,6 @@ class QtHandler(QtHandlerBase): self.done.set() - class QtPlugin(QtPluginBase): # Derived classes must provide the following class-static variables: # icon_file @@ -219,54 +220,68 @@ class QtPlugin(QtPluginBase): SettingsDialog(window, self, keystore, device_id).exec_() keystore.thread.add(connect, on_success=show_dialog) - def request_trezor_init_settings(self, wizard, method, device): - vbox = QVBoxLayout() - next_enabled = True + def request_keepkey_init_settings(self, wizard, method, device): + keepkey_init_layout = KeepkeyInitLayout(method, device) + keepkey_init_layout.validChanged.connect(wizard.next_button.setEnabled) + next_enabled = method != TIM_PRIVKEY + wizard.exec_layout(keepkey_init_layout, next_enabled=next_enabled) + + return keepkey_init_layout.get_settings() + + +def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + +class KeepkeyInitLayout(QVBoxLayout): + validChanged = pyqtSignal([bool], arguments=['valid']) + + def __init__(self, method, device): + 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]: + if self.method in [TIM_NEW, TIM_RECOVER]: gb = QGroupBox() hbox1 = QHBoxLayout() gb.setLayout(hbox1) # KeepKey recovery doesn't need a word count - if method == TIM_NEW: - vbox.addWidget(gb) + if self.method == TIM_NEW: + 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(_("{} 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) 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: 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 + self.validChanged.emit(is_xprv(clean_text(self.text_e))) + self.text_e.textChanged.connect(set_enabled) - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) + self.addWidget(QLabel(msg)) + self.addWidget(self.text_e) pin = QLineEdit() pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) pin.setMaximumWidth(100) @@ -276,30 +291,29 @@ class QtPlugin(QtPluginBase): 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(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()) + 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(KeepKeyPlugin, QtPlugin): @@ -324,7 +338,9 @@ class Plugin(KeepKeyPlugin, QtPlugin): views = { 'keepkey_start': {'gui': WCScriptAndDerivation}, 'keepkey_xpub': {'gui': WCHWXPub}, - 'keepkey_not_initialized': {'gui': WCHWUninitialized}, + 'safet_not_initialized': {'gui': WCKeepkeyInitMethod}, + 'safet_choose_new_recover': {'gui': WCKeepkeyInitParams}, + 'safet_do_init': {'gui': WCKeepkeyInit}, 'keepkey_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -590,3 +606,90 @@ class SettingsDialog(WindowModalDialog): # Update information invoke_client(None) + + +class WCKeepkeyInitMethod(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['keepkey_init'] = self.choice_w.selected_item[0] + + +class WCKeepkeyInitParams(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Set-up keepkey')) + self.plugins = wizard.plugins + self._busy = True + + def on_ready(self): + _name, _info = self.wizard_data['hardware_device'] + self.settings_layout = KeepkeyInitLayout(self.plugins.device_manager, self.wizard_data['keepkey_init'], _info.device.id_) + self.layout().addLayout(self.settings_layout) + self.layout().addStretch(1) + + self.valid = self.wizard_data['keepkey_init'] != TIM_PRIVKEY + self.busy = False + + def apply(self): + self.wizard_data['keepkey_settings'] = self.settings_layout.get_settings() + + +class WCKeepkeyInit(WizardComponent, Logger): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Set-up Keepkey')) + Logger.__init__(self) + self.plugins = wizard.plugins + self.plugin = self.plugins.get_plugin('keepkey') + + self.layout().addWidget(WWLabel('Done')) + + self._busy = True + + def on_ready(self): + settings = self.wizard_data['keepkey_settings'] + method = self.wizard_data['keepkey_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