commit ace61d2d20f07db743eb716e71021cb8aacf70e0
parent 9b28f6df7beab821422537f816cbacbe6035f706
Author: ghost43 <somber.night@protonmail.com>
Date: Thu, 19 Dec 2019 15:54:41 +0000
Merge pull request #5692 from matejcik/trezor-shamir
Trezor: support for Shamir backup and recovery
Diffstat:
4 files changed, 123 insertions(+), 32 deletions(-)
diff --git 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/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"),
}
diff --git 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,88 @@ 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(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.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'))
+ 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)
+ rb_shamir_groups.setVisible(False) # visible with "expert settings"
+
# 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)
@@ -240,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)
@@ -255,7 +325,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 +336,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 +363,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
@@ -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
@@ -101,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,)
@@ -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")