Merge pull request #5692 from matejcik/trezor-shamir

Trezor: support for Shamir backup and recovery
This commit is contained in:
ghost43 2019-12-19 15:54:41 +00:00 committed by GitHub
commit ace61d2d20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 123 additions and 32 deletions

View file

@ -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

View file

@ -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"),
}

View file

@ -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,
)

View file

@ -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")