mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
Merge pull request #5692 from matejcik/trezor-shamir
Trezor: support for Shamir backup and recovery
This commit is contained in:
commit
ace61d2d20
4 changed files with 123 additions and 32 deletions
|
@ -1,4 +1,4 @@
|
||||||
trezor[hidapi]>=0.11.0
|
trezor[hidapi]>=0.11.5
|
||||||
safet[hidapi]>=0.1.0
|
safet[hidapi]>=0.1.0
|
||||||
keepkey>=6.0.3
|
keepkey>=6.0.3
|
||||||
btchip-python>=0.1.26
|
btchip-python>=0.1.26
|
||||||
|
|
|
@ -11,19 +11,27 @@ from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, Hardw
|
||||||
|
|
||||||
from trezorlib.client import TrezorClient
|
from trezorlib.client import TrezorClient
|
||||||
from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
|
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.btc
|
||||||
import trezorlib.device
|
import trezorlib.device
|
||||||
|
|
||||||
MESSAGES = {
|
MESSAGES = {
|
||||||
3: _("Confirm the transaction output on your {} device"),
|
ButtonRequestType.ConfirmOutput:
|
||||||
4: _("Confirm internal entropy on your {} device to begin"),
|
_("Confirm the transaction output on your {} device"),
|
||||||
5: _("Write down the seed word shown on your {}"),
|
ButtonRequestType.ResetDevice:
|
||||||
6: _("Confirm on your {} that you want to wipe it clean"),
|
_("Complete the initialization process on your {} device"),
|
||||||
7: _("Confirm on your {} device the message to sign"),
|
ButtonRequestType.ConfirmWord:
|
||||||
8: _("Confirm the total amount spent and the transaction fee on your {} device"),
|
_("Write down the seed word shown on your {}"),
|
||||||
10: _("Confirm wallet address on your {} device"),
|
ButtonRequestType.WipeDevice:
|
||||||
14: _("Choose on your {} device where to enter your passphrase"),
|
_("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"),
|
'default': _("Check your {} device to continue"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from electrum.util import bh2u
|
||||||
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 .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
|
from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
|
||||||
RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX)
|
Capability, BackupType, RecoveryDeviceType)
|
||||||
|
|
||||||
|
|
||||||
PASSPHRASE_HELP_SHORT =_(
|
PASSPHRASE_HELP_SHORT =_(
|
||||||
|
@ -199,6 +199,8 @@ class QtPlugin(QtPluginBase):
|
||||||
raise Exception(_("The device was disconnected."))
|
raise Exception(_("The device was disconnected."))
|
||||||
model = client.get_trezor_model()
|
model = client.get_trezor_model()
|
||||||
fw_version = client.client.version
|
fw_version = client.client.version
|
||||||
|
capabilities = client.client.features.capabilities
|
||||||
|
have_shamir = Capability.Shamir in capabilities
|
||||||
|
|
||||||
# label
|
# label
|
||||||
label = QLabel(_("Enter a label to name your device:"))
|
label = QLabel(_("Enter a label to name your device:"))
|
||||||
|
@ -209,22 +211,88 @@ class QtPlugin(QtPluginBase):
|
||||||
hl.addStretch(1)
|
hl.addStretch(1)
|
||||||
vbox.addLayout(hl)
|
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
|
# word count
|
||||||
gb = QGroupBox()
|
word_count_buttons = {}
|
||||||
|
|
||||||
|
gb_numwords = QGroupBox()
|
||||||
hbox1 = QHBoxLayout()
|
hbox1 = QHBoxLayout()
|
||||||
gb.setLayout(hbox1)
|
gb_numwords.setLayout(hbox1)
|
||||||
vbox.addWidget(gb)
|
vbox.addWidget(gb_numwords)
|
||||||
gb.setTitle(_("Select your seed length:"))
|
gb_numwords.setTitle(_("Select seed/share length:"))
|
||||||
bg_numwords = QButtonGroup()
|
bg_numwords = QButtonGroup()
|
||||||
word_counts = (12, 18, 24)
|
for count in (12, 18, 20, 24, 33):
|
||||||
for i, count in enumerate(word_counts):
|
rb = QRadioButton(gb_numwords)
|
||||||
rb = QRadioButton(gb)
|
word_count_buttons[count] = rb
|
||||||
rb.setText(_("{:d} words").format(count))
|
rb.setText(_("{:d} words").format(count))
|
||||||
bg_numwords.addButton(rb)
|
bg_numwords.addButton(rb)
|
||||||
bg_numwords.setId(rb, i)
|
bg_numwords.setId(rb, count)
|
||||||
hbox1.addWidget(rb)
|
hbox1.addWidget(rb)
|
||||||
rb.setChecked(True)
|
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
|
# PIN
|
||||||
cb_pin = QCheckBox(_('Enable PIN protection'))
|
cb_pin = QCheckBox(_('Enable PIN protection'))
|
||||||
cb_pin.setChecked(True)
|
cb_pin.setChecked(True)
|
||||||
|
@ -240,6 +308,8 @@ class QtPlugin(QtPluginBase):
|
||||||
def show_expert_settings():
|
def show_expert_settings():
|
||||||
expert_button.setVisible(False)
|
expert_button.setVisible(False)
|
||||||
expert_widget.setVisible(True)
|
expert_widget.setVisible(True)
|
||||||
|
rb_shamir.setVisible(True)
|
||||||
|
rb_shamir_groups.setVisible(True)
|
||||||
expert_button.clicked.connect(show_expert_settings)
|
expert_button.clicked.connect(show_expert_settings)
|
||||||
vbox.addWidget(expert_button)
|
vbox.addWidget(expert_button)
|
||||||
|
|
||||||
|
@ -255,7 +325,7 @@ class QtPlugin(QtPluginBase):
|
||||||
|
|
||||||
# ask for recovery type (random word order OR matrix)
|
# ask for recovery type (random word order OR matrix)
|
||||||
bg_rectype = None
|
bg_rectype = None
|
||||||
if method == TIM_RECOVER and not model == 'T':
|
if method == TIM_RECOVER and model == '1':
|
||||||
gb_rectype = QGroupBox()
|
gb_rectype = QGroupBox()
|
||||||
hbox_rectype = QHBoxLayout()
|
hbox_rectype = QHBoxLayout()
|
||||||
gb_rectype.setLayout(hbox_rectype)
|
gb_rectype.setLayout(hbox_rectype)
|
||||||
|
@ -266,14 +336,14 @@ class QtPlugin(QtPluginBase):
|
||||||
rb1 = QRadioButton(gb_rectype)
|
rb1 = QRadioButton(gb_rectype)
|
||||||
rb1.setText(_('Scrambled words'))
|
rb1.setText(_('Scrambled words'))
|
||||||
bg_rectype.addButton(rb1)
|
bg_rectype.addButton(rb1)
|
||||||
bg_rectype.setId(rb1, RECOVERY_TYPE_SCRAMBLED_WORDS)
|
bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
|
||||||
hbox_rectype.addWidget(rb1)
|
hbox_rectype.addWidget(rb1)
|
||||||
rb1.setChecked(True)
|
rb1.setChecked(True)
|
||||||
|
|
||||||
rb2 = QRadioButton(gb_rectype)
|
rb2 = QRadioButton(gb_rectype)
|
||||||
rb2.setText(_('Matrix'))
|
rb2.setText(_('Matrix'))
|
||||||
bg_rectype.addButton(rb2)
|
bg_rectype.addButton(rb2)
|
||||||
bg_rectype.setId(rb2, RECOVERY_TYPE_MATRIX)
|
bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
|
||||||
hbox_rectype.addWidget(rb2)
|
hbox_rectype.addWidget(rb2)
|
||||||
|
|
||||||
# no backup
|
# no backup
|
||||||
|
@ -293,11 +363,12 @@ class QtPlugin(QtPluginBase):
|
||||||
wizard.exec_layout(vbox, next_enabled=next_enabled)
|
wizard.exec_layout(vbox, next_enabled=next_enabled)
|
||||||
|
|
||||||
return TrezorInitSettings(
|
return TrezorInitSettings(
|
||||||
word_count=word_counts[bg_numwords.checkedId()],
|
word_count=bg_numwords.checkedId(),
|
||||||
label=name.text(),
|
label=name.text(),
|
||||||
pin_enabled=cb_pin.isChecked(),
|
pin_enabled=cb_pin.isChecked(),
|
||||||
passphrase_enabled=cb_phrase.isChecked(),
|
passphrase_enabled=cb_phrase.isChecked(),
|
||||||
recovery_type=bg_rectype.checkedId() if bg_rectype else None,
|
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,
|
no_backup=cb_no_backup.isChecked() if cb_no_backup else False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -28,19 +28,29 @@ try:
|
||||||
from .clientbase import TrezorClientBase
|
from .clientbase import TrezorClientBase
|
||||||
|
|
||||||
from trezorlib.messages import (
|
from trezorlib.messages import (
|
||||||
RecoveryDeviceType, HDNodeType, HDNodePathType,
|
Capability, BackupType, RecoveryDeviceType, HDNodeType, HDNodePathType,
|
||||||
InputScriptType, OutputScriptType, MultisigRedeemScriptType,
|
InputScriptType, OutputScriptType, MultisigRedeemScriptType,
|
||||||
TxInputType, TxOutputType, TxOutputBinType, TransactionType, SignTx)
|
TxInputType, TxOutputType, TxOutputBinType, TransactionType, SignTx)
|
||||||
|
|
||||||
RECOVERY_TYPE_SCRAMBLED_WORDS = RecoveryDeviceType.ScrambledWords
|
|
||||||
RECOVERY_TYPE_MATRIX = RecoveryDeviceType.Matrix
|
|
||||||
|
|
||||||
TREZORLIB = True
|
TREZORLIB = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.exception('error importing trezorlib')
|
_logger.exception('error importing trezorlib')
|
||||||
TREZORLIB = False
|
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
|
# Trezor initialization methods
|
||||||
|
@ -87,6 +97,7 @@ class TrezorInitSettings(NamedTuple):
|
||||||
pin_enabled: bool
|
pin_enabled: bool
|
||||||
passphrase_enabled: bool
|
passphrase_enabled: bool
|
||||||
recovery_type: Any = None
|
recovery_type: Any = None
|
||||||
|
backup_type: int = BackupType.Bip39
|
||||||
no_backup: bool = False
|
no_backup: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +112,7 @@ class TrezorPlugin(HW_PluginBase):
|
||||||
libraries_URL = 'https://github.com/trezor/python-trezor'
|
libraries_URL = 'https://github.com/trezor/python-trezor'
|
||||||
minimum_firmware = (1, 5, 2)
|
minimum_firmware = (1, 5, 2)
|
||||||
keystore_class = TrezorKeyStore
|
keystore_class = TrezorKeyStore
|
||||||
minimum_library = (0, 11, 0)
|
minimum_library = (0, 11, 5)
|
||||||
maximum_library = (0, 12)
|
maximum_library = (0, 12)
|
||||||
SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
|
SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
|
||||||
DEVICE_IDS = (TREZOR_PRODUCT_KEY,)
|
DEVICE_IDS = (TREZOR_PRODUCT_KEY,)
|
||||||
|
@ -211,7 +222,7 @@ class TrezorPlugin(HW_PluginBase):
|
||||||
wizard.loop.exit(exit_code)
|
wizard.loop.exit(exit_code)
|
||||||
|
|
||||||
def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler):
|
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(_(
|
handler.show_error(_(
|
||||||
"You will be asked to enter 24 words regardless of your "
|
"You will be asked to enter 24 words regardless of your "
|
||||||
"seed's actual length. If you enter a word incorrectly or "
|
"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."))
|
raise Exception(_("The device was disconnected."))
|
||||||
|
|
||||||
if method == TIM_NEW:
|
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(
|
client.reset_device(
|
||||||
strength=strength_from_word_count[settings.word_count],
|
strength=strength_from_word_count[settings.word_count],
|
||||||
passphrase_protection=settings.passphrase_enabled,
|
passphrase_protection=settings.passphrase_enabled,
|
||||||
pin_protection=settings.pin_enabled,
|
pin_protection=settings.pin_enabled,
|
||||||
label=settings.label,
|
label=settings.label,
|
||||||
|
backup_type=settings.backup_type,
|
||||||
no_backup=settings.no_backup)
|
no_backup=settings.no_backup)
|
||||||
elif method == TIM_RECOVER:
|
elif method == TIM_RECOVER:
|
||||||
client.recover_device(
|
client.recover_device(
|
||||||
|
@ -240,7 +252,7 @@ class TrezorPlugin(HW_PluginBase):
|
||||||
passphrase_protection=settings.passphrase_enabled,
|
passphrase_protection=settings.passphrase_enabled,
|
||||||
pin_protection=settings.pin_enabled,
|
pin_protection=settings.pin_enabled,
|
||||||
label=settings.label)
|
label=settings.label)
|
||||||
if settings.recovery_type == RECOVERY_TYPE_MATRIX:
|
if settings.recovery_type == RecoveryDeviceType.Matrix:
|
||||||
handler.close_matrix_dialog()
|
handler.close_matrix_dialog()
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Unsupported recovery method")
|
raise RuntimeError("Unsupported recovery method")
|
||||||
|
|
Loading…
Add table
Reference in a new issue