From f4e278178693e66c5569f2d4963edd3841e3e8f6 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 10 Oct 2019 17:31:04 +0200 Subject: [PATCH 1/5] trezor: link button messages to enum names --- electrum/plugins/trezor/clientbase.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index a5b5a6c0c..99059f442 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -11,19 +11,27 @@ from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, Hardw from trezorlib.client import TrezorClient from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError -from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType +from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType, ButtonRequestType import trezorlib.btc import trezorlib.device MESSAGES = { - 3: _("Confirm the transaction output on your {} device"), - 4: _("Confirm internal entropy on your {} device to begin"), - 5: _("Write down the seed word shown on your {}"), - 6: _("Confirm on your {} that you want to wipe it clean"), - 7: _("Confirm on your {} device the message to sign"), - 8: _("Confirm the total amount spent and the transaction fee on your {} device"), - 10: _("Confirm wallet address on your {} device"), - 14: _("Choose on your {} device where to enter your passphrase"), + ButtonRequestType.ConfirmOutput: + _("Confirm the transaction output on your {} device"), + ButtonRequestType.ResetDevice: + _("Complete the initialization process on your {} device"), + ButtonRequestType.ConfirmWord: + _("Write down the seed word shown on your {}"), + ButtonRequestType.WipeDevice: + _("Confirm on your {} that you want to wipe it clean"), + ButtonRequestType.ProtectCall: + _("Confirm on your {} device the message to sign"), + ButtonRequestType.SignTx: + _("Confirm the total amount spent and the transaction fee on your {} device"), + ButtonRequestType.Address: + _("Confirm wallet address on your {} device"), + ButtonRequestType.PassphraseType: + _("Choose on your {} device where to enter your passphrase"), 'default': _("Check your {} device to continue"), } From 3fc70bd97ab6355f07118db5a5fe1ad89682bb13 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 10 Oct 2019 17:31:12 +0200 Subject: [PATCH 2/5] trezor: implement support for Shamir recovery --- electrum/plugins/trezor/qt.py | 94 ++++++++++++++++++++++++++----- electrum/plugins/trezor/trezor.py | 28 ++++++--- 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 281e3f56d..bba618fad 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -16,7 +16,7 @@ from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings, - RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX) + Capability, BackupType, RecoveryDeviceType) PASSPHRASE_HELP_SHORT =_( @@ -199,6 +199,8 @@ class QtPlugin(QtPluginBase): raise Exception(_("The device was disconnected.")) model = client.get_trezor_model() fw_version = client.client.version + capabilities = client.client.features.capabilities + have_shamir = Capability.Shamir in capabilities # label label = QLabel(_("Enter a label to name your device:")) @@ -209,22 +211,87 @@ class QtPlugin(QtPluginBase): hl.addStretch(1) vbox.addLayout(hl) + # Backup type + gb_backuptype = QGroupBox() + hbox_backuptype = QHBoxLayout() + gb_backuptype.setLayout(hbox_backuptype) + vbox.addWidget(gb_backuptype) + gb_backuptype.setTitle(_('Select backup type:')) + bg_backuptype = QButtonGroup() + + rb_single = QRadioButton(gb_backuptype) + rb_single.setText(_('Single seed (BIP39)')) + bg_backuptype.addButton(rb_single) + bg_backuptype.setId(rb_single, BackupType.Bip39) + hbox_backuptype.addWidget(rb_single) + rb_single.setChecked(not have_shamir) + + rb_shamir = QRadioButton(gb_backuptype) + rb_shamir.setText(_('Shamir')) + bg_backuptype.addButton(rb_shamir) + bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic) + hbox_backuptype.addWidget(rb_shamir) + rb_shamir.setChecked(have_shamir) + rb_shamir.setEnabled(Capability.Shamir in capabilities) + + rb_shamir_groups = QRadioButton(gb_backuptype) + rb_shamir_groups.setText(_('Super Shamir')) + bg_backuptype.addButton(rb_shamir_groups) + bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced) + hbox_backuptype.addWidget(rb_shamir_groups) + rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities) + # word count - gb = QGroupBox() + word_count_buttons = {} + + gb_numwords = QGroupBox() hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) + gb_numwords.setLayout(hbox1) + vbox.addWidget(gb_numwords) + gb_numwords.setTitle(_("Select seed/share length:")) bg_numwords = QButtonGroup() - word_counts = (12, 18, 24) - for i, count in enumerate(word_counts): - rb = QRadioButton(gb) + for count in (12, 18, 20, 24, 33): + rb = QRadioButton(gb_numwords) + word_count_buttons[count] = rb rb.setText(_("{:d} words").format(count)) bg_numwords.addButton(rb) - bg_numwords.setId(rb, i) + bg_numwords.setId(rb, count) hbox1.addWidget(rb) rb.setChecked(True) + def configure_word_counts(): + if model == "1": + checked_wordcount = 24 + else: + checked_wordcount = 12 + + if method == TIM_RECOVER: + if have_shamir: + valid_word_counts = (12, 18, 20, 24, 33) + else: + valid_word_counts = (12, 18, 24) + elif rb_single.isChecked(): + valid_word_counts = (12, 18, 24) + gb_numwords.setTitle(_('Select seed length:')) + else: + valid_word_counts = (20, 33) + checked_wordcount = 20 + gb_numwords.setTitle(_('Select share length:')) + + word_count_buttons[checked_wordcount].setChecked(True) + for c, btn in word_count_buttons.items(): + btn.setVisible(c in valid_word_counts) + + bg_backuptype.buttonClicked.connect(configure_word_counts) + configure_word_counts() + + # set up conditional visibility: + # 1. backup_type is only visible when creating new seed + gb_backuptype.setVisible(method == TIM_NEW) + # 2. word_count is not visible when recovering on TT + if method == TIM_RECOVER and model != "1": + gb_numwords.setVisible(False) + # PIN cb_pin = QCheckBox(_('Enable PIN protection')) cb_pin.setChecked(True) @@ -255,7 +322,7 @@ class QtPlugin(QtPluginBase): # ask for recovery type (random word order OR matrix) bg_rectype = None - if method == TIM_RECOVER and not model == 'T': + if method == TIM_RECOVER and model == '1': gb_rectype = QGroupBox() hbox_rectype = QHBoxLayout() gb_rectype.setLayout(hbox_rectype) @@ -266,14 +333,14 @@ class QtPlugin(QtPluginBase): rb1 = QRadioButton(gb_rectype) rb1.setText(_('Scrambled words')) bg_rectype.addButton(rb1) - bg_rectype.setId(rb1, RECOVERY_TYPE_SCRAMBLED_WORDS) + bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords) hbox_rectype.addWidget(rb1) rb1.setChecked(True) rb2 = QRadioButton(gb_rectype) rb2.setText(_('Matrix')) bg_rectype.addButton(rb2) - bg_rectype.setId(rb2, RECOVERY_TYPE_MATRIX) + bg_rectype.setId(rb2, RecoveryDeviceType.Matrix) hbox_rectype.addWidget(rb2) # no backup @@ -293,11 +360,12 @@ class QtPlugin(QtPluginBase): wizard.exec_layout(vbox, next_enabled=next_enabled) return TrezorInitSettings( - word_count=word_counts[bg_numwords.checkedId()], + word_count=bg_numwords.checkedId(), label=name.text(), pin_enabled=cb_pin.isChecked(), passphrase_enabled=cb_phrase.isChecked(), recovery_type=bg_rectype.checkedId() if bg_rectype else None, + backup_type=bg_backuptype.checkedId(), no_backup=cb_no_backup.isChecked() if cb_no_backup else False, ) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 9af4a1b89..4c3de3ec5 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -28,19 +28,29 @@ try: from .clientbase import TrezorClientBase from trezorlib.messages import ( - RecoveryDeviceType, HDNodeType, HDNodePathType, + Capability, BackupType, RecoveryDeviceType, HDNodeType, HDNodePathType, InputScriptType, OutputScriptType, MultisigRedeemScriptType, TxInputType, TxOutputType, TxOutputBinType, TransactionType, SignTx) - RECOVERY_TYPE_SCRAMBLED_WORDS = RecoveryDeviceType.ScrambledWords - RECOVERY_TYPE_MATRIX = RecoveryDeviceType.Matrix - TREZORLIB = True except Exception as e: _logger.exception('error importing trezorlib') TREZORLIB = False - RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(2) + class _EnumMissing: + def __init__(self): + self.counter = 0 + self.values = {} + + def __getattr__(self, key): + if key not in self.values: + self.values[key] = self.counter + self.counter += 1 + return self.values[key] + + Capability = _EnumMissing() + BackupType = _EnumMissing() + RecoveryDeviceType = _EnumMissing() # Trezor initialization methods @@ -87,6 +97,7 @@ class TrezorInitSettings(NamedTuple): pin_enabled: bool passphrase_enabled: bool recovery_type: Any = None + backup_type: int = BackupType.Bip39 no_backup: bool = False @@ -211,7 +222,7 @@ class TrezorPlugin(HW_PluginBase): wizard.loop.exit(exit_code) def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler): - if method == TIM_RECOVER and settings.recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: + if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceType.ScrambledWords: handler.show_error(_( "You will be asked to enter 24 words regardless of your " "seed's actual length. If you enter a word incorrectly or " @@ -226,12 +237,13 @@ class TrezorPlugin(HW_PluginBase): raise Exception(_("The device was disconnected.")) if method == TIM_NEW: - strength_from_word_count = {12: 128, 18: 192, 24: 256} + strength_from_word_count = {12: 128, 18: 192, 20: 128, 24: 256, 33: 256} client.reset_device( strength=strength_from_word_count[settings.word_count], passphrase_protection=settings.passphrase_enabled, pin_protection=settings.pin_enabled, label=settings.label, + backup_type=settings.backup_type, no_backup=settings.no_backup) elif method == TIM_RECOVER: client.recover_device( @@ -240,7 +252,7 @@ class TrezorPlugin(HW_PluginBase): passphrase_protection=settings.passphrase_enabled, pin_protection=settings.pin_enabled, label=settings.label) - if settings.recovery_type == RECOVERY_TYPE_MATRIX: + if settings.recovery_type == RecoveryDeviceType.Matrix: handler.close_matrix_dialog() else: raise RuntimeError("Unsupported recovery method") From da41e4c289cc96349fbf772b9138f12f6efea44e Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 10 Oct 2019 17:36:36 +0200 Subject: [PATCH 3/5] trezor: bump library requirement --- contrib/requirements/requirements-hw.txt | 2 +- electrum/plugins/trezor/trezor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 9f10a1f3a..52d05e9db 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,4 +1,4 @@ -trezor[hidapi]>=0.11.0 +trezor[hidapi]>=0.11.5 safet[hidapi]>=0.1.0 keepkey>=6.0.3 btchip-python>=0.1.26 diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 4c3de3ec5..15f322d45 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -112,7 +112,7 @@ class TrezorPlugin(HW_PluginBase): libraries_URL = 'https://github.com/trezor/python-trezor' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore - minimum_library = (0, 11, 0) + minimum_library = (0, 11, 5) maximum_library = (0, 12) SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') DEVICE_IDS = (TREZOR_PRODUCT_KEY,) From 006c6c1a58557f0aa217b9afd68f22dac917e3ca Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 12:35:35 +0100 Subject: [PATCH 4/5] trezor: use BIP39 backup by default even if Shamir is available --- electrum/plugins/trezor/qt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index bba618fad..87472784f 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -224,14 +224,13 @@ class QtPlugin(QtPluginBase): bg_backuptype.addButton(rb_single) bg_backuptype.setId(rb_single, BackupType.Bip39) hbox_backuptype.addWidget(rb_single) - rb_single.setChecked(not have_shamir) + rb_single.setChecked(True) rb_shamir = QRadioButton(gb_backuptype) rb_shamir.setText(_('Shamir')) bg_backuptype.addButton(rb_shamir) bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic) hbox_backuptype.addWidget(rb_shamir) - rb_shamir.setChecked(have_shamir) rb_shamir.setEnabled(Capability.Shamir in capabilities) rb_shamir_groups = QRadioButton(gb_backuptype) From 18209fc782c08bb4b41d878d8fdedce86d9fa309 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 19 Dec 2019 16:50:35 +0100 Subject: [PATCH 5/5] trezor: when restoring, hide Shamir options by default They become visible once user clicks "Show expert settings" --- electrum/plugins/trezor/qt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 87472784f..3a8b2cba3 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -232,6 +232,7 @@ class QtPlugin(QtPluginBase): bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic) hbox_backuptype.addWidget(rb_shamir) rb_shamir.setEnabled(Capability.Shamir in capabilities) + rb_shamir.setVisible(False) # visible with "expert settings" rb_shamir_groups = QRadioButton(gb_backuptype) rb_shamir_groups.setText(_('Super Shamir')) @@ -239,6 +240,7 @@ class QtPlugin(QtPluginBase): bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced) hbox_backuptype.addWidget(rb_shamir_groups) rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities) + rb_shamir_groups.setVisible(False) # visible with "expert settings" # word count word_count_buttons = {} @@ -306,6 +308,8 @@ class QtPlugin(QtPluginBase): def show_expert_settings(): expert_button.setVisible(False) expert_widget.setVisible(True) + rb_shamir.setVisible(True) + rb_shamir_groups.setVisible(True) expert_button.clicked.connect(show_expert_settings) vbox.addWidget(expert_button)