Browse Source

qt: multisig checks with hardware cosigners

master
Sander van Grieken 2 years ago
parent
commit
902290ee8c
  1. 28
      electrum/gui/qt/wizard/server_connect.py
  2. 35
      electrum/gui/qt/wizard/wallet.py
  3. 12
      electrum/gui/qt/wizard/wizard.py
  4. 90
      electrum/plugins/trezor/qt.py
  5. 1
      electrum/plugins/trezor/trezor.py
  6. 54
      electrum/wizard.py

28
electrum/gui/qt/wizard/server_connect.py

@ -1,12 +1,10 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtWidgets import QApplication
from electrum.i18n import _ from electrum.i18n import _
from .wizard import QEAbstractWizard, WizardComponent from .wizard import QEAbstractWizard, WizardComponent
from electrum.wizard import ServerConnectWizard from electrum.wizard import ServerConnectWizard
from ..network_dialog import ProxyWidget, ServerWidget from electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget
from ..util import ChoicesLayout from electrum.gui.qt.util import ChoiceWidget
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
@ -19,8 +17,7 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: 'Daemon', parent=None): def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: 'Daemon', parent=None):
ServerConnectWizard.__init__(self, daemon) ServerConnectWizard.__init__(self, daemon)
QEAbstractWizard.__init__(self, config, app, plugins, daemon) QEAbstractWizard.__init__(self, config, app)
self._daemon = daemon
# attach view names # attach view names
self.navmap_merge({ self.navmap_merge({
@ -40,14 +37,16 @@ class WCAutoConnect(WizardComponent):
"hardware. In most cases you simply want to let Electrum " "hardware. In most cases you simply want to let Electrum "
"pick one at random. However if you prefer feel free to " "pick one at random. However if you prefer feel free to "
"select a server manually.") "select a server manually.")
choices = [_("Auto connect"), _("Select server manually")] choices = [('autoconnect', _("Auto connect")),
self.clayout = ChoicesLayout(message, choices, on_clicked=self.on_updated) ('select', _("Select server manually"))]
self.layout().addLayout(self.clayout.layout()) self.choice_w = ChoiceWidget(message=message, choices=choices)
self.choice_w.itemSelected.connect(self.on_updated)
self.layout().addWidget(self.choice_w)
self.layout().addStretch(1) self.layout().addStretch(1)
self._valid = True self._valid = True
def apply(self): def apply(self):
r = self.clayout.selected_index() r = self.choice_w.selected_index
self.wizard_data['autoconnect'] = (r == 0) self.wizard_data['autoconnect'] = (r == 0)
# if r == 1: # if r == 1:
# nlayout = NetworkChoiceLayout(network, self.config, wizard=True) # nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
@ -63,14 +62,15 @@ class WCProxyAsk(WizardComponent):
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_("Proxy")) WizardComponent.__init__(self, parent, wizard, title=_("Proxy"))
message = _("Do you use a local proxy service such as TOR to reach the internet?") message = _("Do you use a local proxy service such as TOR to reach the internet?")
choices = [_("Yes"), _("No")] choices = [('yes', _("Yes")),
self.clayout = ChoicesLayout(message, choices) ('no', _("No"))]
self.layout().addLayout(self.clayout.layout()) self.choice_w = ChoiceWidget(message=message, choices=choices)
self.layout().addWidget(self.choice_w)
self.layout().addStretch(1) self.layout().addStretch(1)
self._valid = True self._valid = True
def apply(self): def apply(self):
r = self.clayout.selected_index() r = self.choice_w.selected_index
self.wizard_data['want_proxy'] = (r == 0) self.wizard_data['want_proxy'] = (r == 0)

35
electrum/gui/qt/wizard/wallet.py

@ -51,8 +51,8 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: Daemon, path, parent=None): def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: Daemon, path, parent=None):
NewWalletWizard.__init__(self, daemon, plugins) NewWalletWizard.__init__(self, daemon, plugins)
QEAbstractWizard.__init__(self, config, app, plugins, daemon) QEAbstractWizard.__init__(self, config, app)
self._daemon = daemon # TODO: dedupe
self._path = path self._path = path
# attach gui classes to views # attach gui classes to views
@ -408,7 +408,7 @@ class WCEnterExt(WizardComponent):
self.valid = False self.valid = False
return return
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
if self.wizard_data['wallet_type'] == 'multisig': if self.wizard_data['wallet_type'] == 'multisig':
if 'seed_variant' in cosigner_data and cosigner_data['seed_variant'] in ['bip39', 'slip39']: if 'seed_variant' in cosigner_data and cosigner_data['seed_variant'] in ['bip39', 'slip39']:
@ -429,7 +429,7 @@ class WCEnterExt(WizardComponent):
self.valid = True self.valid = True
def apply(self): def apply(self):
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['seed_extra_words'] = self.ext_edit.text() cosigner_data['seed_extra_words'] = self.ext_edit.text()
@ -521,7 +521,7 @@ class WCHaveSeed(WizardComponent):
self.valid = seed_valid self.valid = seed_valid
def apply(self): def apply(self):
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['seed'] = self.slayout.get_seed() cosigner_data['seed'] = self.slayout.get_seed()
cosigner_data['seed_variant'] = self.slayout.seed_type cosigner_data['seed_variant'] = self.slayout.seed_type
@ -619,28 +619,19 @@ class WCScriptAndDerivation(WizardComponent):
def validate(self): def validate(self):
self.apply() self.apply()
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
derivation_valid = is_bip32_derivation(cosigner_data['derivation_path']) derivation_valid = is_bip32_derivation(cosigner_data['derivation_path'])
# TODO: refactor to wizard.current_cosigner_is_hardware if derivation_valid:
cosigner_is_hardware = cosigner_data == self.wizard_data and self.wizard_data['keystore_type'] == 'hardware' valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
if 'cosigner_keystore_type' in self.wizard_data and self.wizard_data['cosigner_keystore_type'] == 'hardware': if not valid:
cosigner_is_hardware = True
if self.wizard.is_multisig(self.wizard_data) and derivation_valid and not cosigner_is_hardware:
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!')
# TODO: user feedback
derivation_valid = False
elif self.wizard.has_heterogeneous_masterkeys(self.wizard_data):
self._logger.debug('Heterogenous master keys!')
# TODO: user feedback # TODO: user feedback
derivation_valid = False self.logger.error(error)
self.valid = derivation_valid self.valid = valid
def apply(self): def apply(self):
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['script_type'] = self.choice_w.selected_item[0] cosigner_data['script_type'] = self.choice_w.selected_item[0]
cosigner_data['derivation_path'] = str(self.derivation_path_edit.text()) cosigner_data['derivation_path'] = str(self.derivation_path_edit.text())
@ -745,7 +736,7 @@ class WCHaveMasterKey(WizardComponent):
def apply(self): def apply(self):
text = self.slayout.get_text() text = self.slayout.get_text()
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['master_key'] = text cosigner_data['master_key'] = text

12
electrum/gui/qt/wizard/wizard.py

@ -22,11 +22,10 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
_logger = get_logger(__name__) _logger = get_logger(__name__)
# def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'): # def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'):
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: 'Daemon'): def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'):
QDialog.__init__(self, None) QDialog.__init__(self, None)
self.app = app self.app = app
self.config = config self.config = config
# self.plugins = plugins
# compat # compat
self.gui_thread = threading.current_thread() self.gui_thread = threading.current_thread()
@ -266,12 +265,3 @@ class WizardComponent(QWidget):
@pyqtSlot() @pyqtSlot()
def on_updated(self, *args): def on_updated(self, *args):
self.updated.emit(self) self.updated.emit(self)
# returns (sub)dict of current cosigner (or root if first)
# TODO: maybe just always expose self.cosigner_data in wizardcomponent so we can avoid this call
def _current_cosigner(self, wizard_data):
wdata = wizard_data
if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data:
cosigner = wizard_data['multisig_current_cosigner']
wdata = wizard_data['multisig_cosigner_data'][str(cosigner)]
return wdata

90
electrum/plugins/trezor/qt.py

@ -5,20 +5,23 @@ from PyQt5.QtCore import Qt, QEventLoop, pyqtSignal
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
QHBoxLayout, QButtonGroup, QGroupBox, QDialog, QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
QLineEdit, QRadioButton, QCheckBox, QWidget, QLineEdit, QRadioButton, QCheckBox, QWidget,
QMessageBox, QFileDialog, QSlider, QTabWidget) QMessageBox, QSlider, QTabWidget)
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoicesLayout)
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import Logger
from electrum.plugin import hook from electrum.plugin import hook
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget)
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation
from electrum.gui.qt.wizard.wizard import WizardComponent
from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings, from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType) PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType)
from ...gui.qt.wizard.wallet import WCScriptAndDerivation
from ...gui.qt.wizard.wizard import WizardComponent
from ...logging import Logger
PASSPHRASE_HELP_SHORT =_( PASSPHRASE_HELP_SHORT =_(
"Passphrases allow you to access new wallets, each " "Passphrases allow you to access new wallets, each "
@ -261,15 +264,7 @@ class QtPlugin(QtPluginBase):
wizard.exec_layout(vbox) wizard.exec_layout(vbox)
return TrezorInitSettings( return vbox.get_settings()
word_count=vbox.bg_numwords.checkedId(),
label=vbox.name.text(),
pin_enabled=vbox.cb_pin.isChecked(),
passphrase_enabled=vbox.cb_phrase.isChecked(),
recovery_type=vbox.bg_rectype.checkedId() if vbox.bg_rectype else None,
backup_type=vbox.bg_backuptype.checkedId(),
no_backup=vbox.cb_no_backup.isChecked() if vbox.cb_no_backup else False,
)
class InitSettingsLayout(QVBoxLayout): class InitSettingsLayout(QVBoxLayout):
@ -443,6 +438,17 @@ class InitSettingsLayout(QVBoxLayout):
self.addWidget(expert_widget) self.addWidget(expert_widget)
def get_settings(self):
return TrezorInitSettings(
word_count=self.bg_numwords.checkedId(),
label=self.name.text(),
pin_enabled=self.cb_pin.isChecked(),
passphrase_enabled=self.cb_phrase.isChecked(),
recovery_type=self.bg_rectype.checkedId() if self.bg_rectype else None,
backup_type=self.bg_backuptype.checkedId(),
no_backup=self.cb_no_backup.isChecked() if self.cb_no_backup else False,
)
class Plugin(TrezorPlugin, QtPlugin): class Plugin(TrezorPlugin, QtPlugin):
icon_unpaired = "trezor_unpaired.png" icon_unpaired = "trezor_unpaired.png"
@ -798,9 +804,13 @@ class WCTrezorXPub(WizardComponent, Logger):
self.plugins = wizard.plugins self.plugins = wizard.plugins
self.plugin = self.plugins.get_plugin('trezor') self.plugin = self.plugins.get_plugin('trezor')
self._busy = True self._busy = True
self.xpub = None self.xpub = None
self.root_fingerprint = None
self.label = None
self.soft_device_id = None
self.ok_l = WWLabel('Retrieved Hardware Information') self.ok_l = WWLabel(_('Hardware keystore added to wallet'))
self.ok_l.setAlignment(Qt.AlignCenter) self.ok_l.setAlignment(Qt.AlignCenter)
self.layout().addWidget(self.ok_l) self.layout().addWidget(self.ok_l)
@ -810,7 +820,7 @@ class WCTrezorXPub(WizardComponent, Logger):
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
client.handler = self.plugin.create_handler(self.wizard) client.handler = self.plugin.create_handler(self.wizard)
cosigner = self._current_cosigner(self.wizard_data) cosigner = self.wizard.current_cosigner(self.wizard_data)
xtype = cosigner['script_type'] xtype = cosigner['script_type']
derivation = cosigner['derivation_path'] derivation = cosigner['derivation_path']
@ -836,18 +846,24 @@ class WCTrezorXPub(WizardComponent, Logger):
def validate(self): def validate(self):
if self.xpub and not self.error: if self.xpub and not self.error:
self.valid = True self.apply()
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
if not valid:
self.error = '\n'.join([
_('Could not add hardware keystore to wallet'),
error
])
self.valid = valid
else: else:
self.valid = False self.valid = False
def apply(self): def apply(self):
if self.valid: cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data = self._current_cosigner(self.wizard_data) cosigner_data['hw_type'] = 'trezor'
cosigner_data['hw_type'] = 'trezor' cosigner_data['master_key'] = self.xpub
cosigner_data['master_key'] = self.xpub cosigner_data['root_fingerprint'] = self.root_fingerprint
cosigner_data['root_fingerprint'] = self.root_fingerprint cosigner_data['label'] = self.label
cosigner_data['label'] = self.label cosigner_data['soft_device_id'] = self.soft_device_id
cosigner_data['soft_device_id'] = self.soft_device_id
class WCTrezorInitMethod(WizardComponent, Logger): class WCTrezorInitMethod(WizardComponent, Logger):
@ -863,16 +879,14 @@ class WCTrezorInitMethod(WizardComponent, Logger):
(TIM_NEW, _("Let the device generate a completely new seed randomly")), (TIM_NEW, _("Let the device generate a completely new seed randomly")),
(TIM_RECOVER, _("Recover from a seed you have previously written down")), (TIM_RECOVER, _("Recover from a seed you have previously written down")),
] ]
self.c_values = [x[0] for x in choices] self.choice_w = ChoiceWidget(message=message, choices=choices)
c_titles = [x[1] for x in choices] self.layout().addWidget(self.choice_w)
self.clayout = ChoicesLayout(message, c_titles)
self.layout().addLayout(self.clayout.layout())
self.layout().addStretch(1) self.layout().addStretch(1)
self._valid = True self._valid = True
def apply(self): def apply(self):
self.wizard_data['trezor_init'] = self.c_values[self.clayout.selected_index()] self.wizard_data['trezor_init'] = self.choice_w.selected_item[0]
class WCTrezorInitParams(WizardComponent): class WCTrezorInitParams(WizardComponent):
@ -891,17 +905,7 @@ class WCTrezorInitParams(WizardComponent):
self.busy = False self.busy = False
def apply(self): def apply(self):
vbox = self.settings_layout self.wizard_data['trezor_settings'] = self.settings_layout.get_settings()
trezor_settings = TrezorInitSettings(
word_count=vbox.bg_numwords.checkedId(),
label=vbox.name.text(),
pin_enabled=vbox.cb_pin.isChecked(),
passphrase_enabled=vbox.cb_phrase.isChecked(),
recovery_type=vbox.bg_rectype.checkedId() if vbox.bg_rectype else None,
backup_type=vbox.bg_backuptype.checkedId(),
no_backup=vbox.cb_no_backup.isChecked() if vbox.cb_no_backup else False,
)
self.wizard_data['trezor_settings'] = trezor_settings
class WCTrezorInit(WizardComponent, Logger): class WCTrezorInit(WizardComponent, Logger):

1
electrum/plugins/trezor/trezor.py

@ -538,6 +538,7 @@ class TrezorPlugin(HW_PluginBase):
}, },
'trezor_xpub': { 'trezor_xpub': {
'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore', 'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',
'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)
}, },
'trezor_not_initialized': { 'trezor_not_initialized': {

54
electrum/wizard.py

@ -1,8 +1,9 @@
import copy import copy
import os import os
from typing import List, NamedTuple, Any, Dict, Optional from typing import List, NamedTuple, Any, Dict, Optional, Tuple
from electrum.i18n import _
from electrum.keystore import hardware_keystore from electrum.keystore import hardware_keystore
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.plugin import run_hook from electrum.plugin import run_hook
@ -270,7 +271,7 @@ class NewWalletWizard(AbstractWizard):
raise NotImplementedError() raise NotImplementedError()
# returns (sub)dict of current cosigner (or root if first) # returns (sub)dict of current cosigner (or root if first)
def _current_cosigner(self, wizard_data): def current_cosigner(self, wizard_data):
wdata = wizard_data wdata = wizard_data
if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data: if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data:
cosigner = wizard_data['multisig_current_cosigner'] cosigner = wizard_data['multisig_current_cosigner']
@ -278,11 +279,11 @@ class NewWalletWizard(AbstractWizard):
return wdata return wdata
def needs_derivation_path(self, wizard_data): def needs_derivation_path(self, wizard_data):
wdata = self._current_cosigner(wizard_data) wdata = self.current_cosigner(wizard_data)
return 'seed_variant' in wdata and wdata['seed_variant'] in ['bip39', 'slip39'] return 'seed_variant' in wdata and wdata['seed_variant'] in ['bip39', 'slip39']
def wants_ext(self, wizard_data): def wants_ext(self, wizard_data):
wdata = self._current_cosigner(wizard_data) wdata = self.current_cosigner(wizard_data)
return 'seed_variant' in wdata and wdata['seed_extend'] return 'seed_variant' in wdata and wdata['seed_extend']
def is_multisig(self, wizard_data): def is_multisig(self, wizard_data):
@ -343,8 +344,8 @@ class NewWalletWizard(AbstractWizard):
}.get(t) }.get(t)
def on_have_cosigner_seed(self, wizard_data): def on_have_cosigner_seed(self, wizard_data):
current_cosigner_data = wizard_data['multisig_cosigner_data'][str(wizard_data['multisig_current_cosigner'])] current_cosigner = self.current_cosigner(wizard_data)
if self.needs_derivation_path(wizard_data) and 'derivation_path' not in current_cosigner_data: if self.needs_derivation_path(wizard_data) and 'derivation_path' not in current_cosigner:
return 'multisig_cosigner_script_and_derivation' return 'multisig_cosigner_script_and_derivation'
elif self.last_cosigner(wizard_data): elif self.last_cosigner(wizard_data):
return 'wallet_password' return 'wallet_password'
@ -421,6 +422,47 @@ class NewWalletWizard(AbstractWizard):
else: else:
raise Exception('no seed or master_key in data') raise Exception('no seed or master_key in data')
def is_current_cosigner_hardware(self, wizard_data):
cosigner_data = self.current_cosigner(wizard_data)
cosigner_is_hardware = cosigner_data == wizard_data and wizard_data['keystore_type'] == 'hardware'
if 'cosigner_keystore_type' in wizard_data and wizard_data['cosigner_keystore_type'] == 'hardware':
cosigner_is_hardware = True
return cosigner_is_hardware
def check_multisig_constraints(self, wizard_data: dict) -> Tuple[bool, str]:
if not self.is_multisig(wizard_data):
return True, ''
# current cosigner might be incomplete. In that case, return valid
cosigner_data = self.current_cosigner(wizard_data)
if self.needs_derivation_path(wizard_data):
if 'derivation_path' not in cosigner_data:
self.logger.debug('defer multisig check: missing derivation_path')
return True, ''
if self.wants_ext(wizard_data):
if 'seed_extra_words' not in cosigner_data:
self.logger.debug('defer multisig check: missing extra words')
return True, ''
if self.is_current_cosigner_hardware(wizard_data):
if 'master_key' not in cosigner_data:
self._logger.debug('defer multisig check: missing master_key')
return True, ''
user_info = ''
if self.has_duplicate_masterkeys(wizard_data):
self._logger.debug('Duplicate master keys!')
user_info = _('Duplicate master keys')
multisig_keys_valid = False
elif self.has_heterogeneous_masterkeys(wizard_data):
self._logger.debug('Heterogenous master keys!')
user_info = _('Heterogenous master keys')
multisig_keys_valid = False
else:
multisig_keys_valid = True
return multisig_keys_valid, user_info
def validate_seed(self, seed, seed_variant, wallet_type): def validate_seed(self, seed, seed_variant, wallet_type):
seed_type = '' seed_type = ''
seed_valid = False seed_valid = False

Loading…
Cancel
Save