Browse Source

qt: keepkey device init

Note: untested, don't have device
master
Sander van Grieken 2 years ago
parent
commit
5ab083b87e
  1. 12
      electrum/plugins/keepkey/keepkey.py
  2. 195
      electrum/plugins/keepkey/qt.py

12
electrum/plugins/keepkey/keepkey.py

@ -219,7 +219,7 @@ class KeepKeyPlugin(HW_PluginBase):
] ]
def f(method): def f(method):
import threading 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 = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler))
t.daemon = True t.daemon = True
t.start() t.start()
@ -510,7 +510,15 @@ class KeepKeyPlugin(HW_PluginBase):
'accept': wizard.maybe_master_pubkey, 'accept': wizard.maybe_master_pubkey,
'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) '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': { 'keepkey_unlock': {
'last': True 'last': True
}, },

195
electrum/plugins/keepkey/qt.py

@ -1,3 +1,4 @@
import threading
from functools import partial from functools import partial
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -6,18 +7,19 @@ from PyQt5.QtGui import QRegExpValidator
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
QHBoxLayout, QButtonGroup, QGroupBox, QDialog, QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget, QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,
QMessageBox, QFileDialog, QSlider, QTabWidget) QMessageBox, QSlider, QTabWidget)
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
OkButton, CloseButton) OkButton, CloseButton, ChoiceWidget)
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import hook from electrum.plugin import hook
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available 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: if TYPE_CHECKING:
from electrum.gui.qt.wizard.wallet import QENewWalletWizard 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 " "Press ENTER or the Seed Entered button once the last word in your "
"seed is auto-completed.") "seed is auto-completed.")
class CharacterButton(QPushButton): class CharacterButton(QPushButton):
def __init__(self, text=None): def __init__(self, text=None):
QPushButton.__init__(self, text) QPushButton.__init__(self, text)
@ -138,7 +141,6 @@ class CharacterDialog(WindowModalDialog):
class QtHandler(QtHandlerBase): class QtHandler(QtHandlerBase):
char_signal = pyqtSignal(object) char_signal = pyqtSignal(object)
pin_signal = pyqtSignal(object, object) pin_signal = pyqtSignal(object, object)
close_char_dialog_signal = pyqtSignal() close_char_dialog_signal = pyqtSignal()
@ -192,7 +194,6 @@ class QtHandler(QtHandlerBase):
self.done.set() self.done.set()
class QtPlugin(QtPluginBase): class QtPlugin(QtPluginBase):
# Derived classes must provide the following class-static variables: # Derived classes must provide the following class-static variables:
# icon_file # icon_file
@ -219,54 +220,68 @@ class QtPlugin(QtPluginBase):
SettingsDialog(window, self, keystore, device_id).exec_() SettingsDialog(window, self, keystore, device_id).exec_()
keystore.thread.add(connect, on_success=show_dialog) keystore.thread.add(connect, on_success=show_dialog)
def request_trezor_init_settings(self, wizard, method, device): def request_keepkey_init_settings(self, wizard, method, device):
vbox = QVBoxLayout() keepkey_init_layout = KeepkeyInitLayout(method, device)
next_enabled = True 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:")) label = QLabel(_("Enter a label to name your device:"))
name = QLineEdit() self.label_e = QLineEdit()
hl = QHBoxLayout() hl = QHBoxLayout()
hl.addWidget(label) hl.addWidget(label)
hl.addWidget(name) hl.addWidget(self.label_e)
hl.addStretch(1) hl.addStretch(1)
vbox.addLayout(hl) self.addLayout(hl)
def clean_text(widget):
text = widget.toPlainText().strip()
return ' '.join(text.split())
if method in [TIM_NEW, TIM_RECOVER]: if self.method in [TIM_NEW, TIM_RECOVER]:
gb = QGroupBox() gb = QGroupBox()
hbox1 = QHBoxLayout() hbox1 = QHBoxLayout()
gb.setLayout(hbox1) gb.setLayout(hbox1)
# KeepKey recovery doesn't need a word count # KeepKey recovery doesn't need a word count
if method == TIM_NEW: if self.method == TIM_NEW:
vbox.addWidget(gb) self.addWidget(gb)
gb.setTitle(_("Select your seed length:")) gb.setTitle(_("Select your seed length:"))
bg = QButtonGroup() self.bg = QButtonGroup()
for i, count in enumerate([12, 18, 24]): for i, count in enumerate([12, 18, 24]):
rb = QRadioButton(gb) rb = QRadioButton(gb)
rb.setText(_("{} words").format(count)) rb.setText(_("{} words").format(count))
bg.addButton(rb) self.bg.addButton(rb)
bg.setId(rb, i) self.bg.setId(rb, i)
hbox1.addWidget(rb) hbox1.addWidget(rb)
rb.setChecked(True) rb.setChecked(True)
cb_pin = QCheckBox(_('Enable PIN protection')) cb_pin = QCheckBox(_('Enable PIN protection'))
cb_pin.setChecked(True) cb_pin.setChecked(True)
else: else:
text = QTextEdit() self.text_e = QTextEdit()
text.setMaximumHeight(60) self.text_e.setMaximumHeight(60)
if method == TIM_MNEMONIC: if method == TIM_MNEMONIC:
msg = _("Enter your BIP39 mnemonic:") msg = _("Enter your BIP39 mnemonic:")
# TODO: validation?
else: else:
msg = _("Enter the master private key beginning with xprv:") msg = _("Enter the master private key beginning with xprv:")
def set_enabled(): def set_enabled():
from electrum.bip32 import is_xprv from electrum.bip32 import is_xprv
wizard.next_button.setEnabled(is_xprv(clean_text(text))) self.validChanged.emit(is_xprv(clean_text(self.text_e)))
text.textChanged.connect(set_enabled) self.text_e.textChanged.connect(set_enabled)
next_enabled = False
vbox.addWidget(QLabel(msg)) self.addWidget(QLabel(msg))
vbox.addWidget(text) self.addWidget(self.text_e)
pin = QLineEdit() pin = QLineEdit()
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
pin.setMaximumWidth(100) pin.setMaximumWidth(100)
@ -276,30 +291,29 @@ class QtPlugin(QtPluginBase):
hbox_pin.addStretch(1) hbox_pin.addStretch(1)
if method in [TIM_NEW, TIM_RECOVER]: if method in [TIM_NEW, TIM_RECOVER]:
vbox.addWidget(WWLabel(RECOMMEND_PIN)) self.addWidget(WWLabel(RECOMMEND_PIN))
vbox.addWidget(cb_pin) self.addWidget(cb_pin)
else: else:
vbox.addLayout(hbox_pin) self.addLayout(hbox_pin)
passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
passphrase_warning.setStyleSheet("color: red") passphrase_warning.setStyleSheet("color: red")
cb_phrase = QCheckBox(_('Enable passphrases')) self.cb_phrase = QCheckBox(_('Enable passphrases'))
cb_phrase.setChecked(False) self.cb_phrase.setChecked(False)
vbox.addWidget(passphrase_msg) self.addWidget(passphrase_msg)
vbox.addWidget(passphrase_warning) self.addWidget(passphrase_warning)
vbox.addWidget(cb_phrase) self.addWidget(self.cb_phrase)
wizard.exec_layout(vbox, next_enabled=next_enabled) def get_settings(self):
if self.method in [TIM_NEW, TIM_RECOVER]:
if method in [TIM_NEW, TIM_RECOVER]: item = self.bg.checkedId()
item = bg.checkedId() pin = self.cb_pin.isChecked()
pin = cb_pin.isChecked()
else: else:
item = ' '.join(str(clean_text(text)).split()) 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): class Plugin(KeepKeyPlugin, QtPlugin):
@ -324,7 +338,9 @@ class Plugin(KeepKeyPlugin, QtPlugin):
views = { views = {
'keepkey_start': {'gui': WCScriptAndDerivation}, 'keepkey_start': {'gui': WCScriptAndDerivation},
'keepkey_xpub': {'gui': WCHWXPub}, '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} 'keepkey_unlock': {'gui': WCHWUnlock}
} }
wizard.navmap_merge(views) wizard.navmap_merge(views)
@ -590,3 +606,90 @@ class SettingsDialog(WindowModalDialog):
# Update information # Update information
invoke_client(None) 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

Loading…
Cancel
Save