Trezor: all four available device initializations
Trezor and KeepKey devices can now be initialized by: - device-generated seed - existing seed - BIP39 mnemonic - master private key
This commit is contained in:
parent
bdb4782b36
commit
9b29c6c2e6
|
@ -332,6 +332,11 @@ class InstallWizard(WindowModalDialog, WizardBase):
|
||||||
def query_choice(self, msg, choices):
|
def query_choice(self, msg, choices):
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
self.set_layout(vbox)
|
self.set_layout(vbox)
|
||||||
|
if len(msg) > 50:
|
||||||
|
label = QLabel(msg)
|
||||||
|
label.setWordWrap(True)
|
||||||
|
vbox.addWidget(label)
|
||||||
|
msg = ""
|
||||||
gb2 = QGroupBox(msg)
|
gb2 = QGroupBox(msg)
|
||||||
vbox.addWidget(gb2)
|
vbox.addWidget(gb2)
|
||||||
|
|
||||||
|
@ -402,22 +407,25 @@ class InstallWizard(WindowModalDialog, WizardBase):
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
raise UserCancelled
|
raise UserCancelled
|
||||||
|
|
||||||
def request_trezor_reset_settings(self, device):
|
def request_trezor_init_settings(self, method, device):
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
|
|
||||||
main_label = QLabel(_("Choose how to initialize your %s device:")
|
main_label = QLabel(_("Initialization settings for your %s:") % device)
|
||||||
% device)
|
|
||||||
vbox.addWidget(main_label)
|
vbox.addWidget(main_label)
|
||||||
|
|
||||||
msg = _("Select your seed length and strength:")
|
OK_button = OkButton(self, _('Next'))
|
||||||
choices = [
|
|
||||||
_("12 words (low)"),
|
if method in [self.TIM_NEW, self.TIM_RECOVER]:
|
||||||
_("18 words (medium)"),
|
gb = QGroupBox()
|
||||||
_("24 words (high)"),
|
|
||||||
]
|
|
||||||
gb = QGroupBox(msg)
|
|
||||||
vbox1 = QVBoxLayout()
|
vbox1 = QVBoxLayout()
|
||||||
gb.setLayout(vbox1)
|
gb.setLayout(vbox1)
|
||||||
|
vbox.addWidget(gb)
|
||||||
|
gb.setTitle(_("Select your seed length:"))
|
||||||
|
choices = [
|
||||||
|
_("12 words"),
|
||||||
|
_("18 words"),
|
||||||
|
_("24 words"),
|
||||||
|
]
|
||||||
bg = QButtonGroup()
|
bg = QButtonGroup()
|
||||||
for i, choice in enumerate(choices):
|
for i, choice in enumerate(choices):
|
||||||
rb = QRadioButton(gb)
|
rb = QRadioButton(gb)
|
||||||
|
@ -426,30 +434,60 @@ class InstallWizard(WindowModalDialog, WizardBase):
|
||||||
bg.setId(rb, i)
|
bg.setId(rb, i)
|
||||||
vbox1.addWidget(rb)
|
vbox1.addWidget(rb)
|
||||||
rb.setChecked(True)
|
rb.setChecked(True)
|
||||||
vbox.addWidget(gb)
|
cb_pin = QCheckBox(_('Enable PIN protection'))
|
||||||
|
cb_pin.setChecked(True)
|
||||||
|
else:
|
||||||
|
text = QTextEdit()
|
||||||
|
text.setMaximumHeight(60)
|
||||||
|
vbox.addWidget(text)
|
||||||
|
if method == self.TIM_MNEMONIC:
|
||||||
|
msg = _("Enter your BIP39 mnemonic:")
|
||||||
|
else:
|
||||||
|
msg = _("Enter the master private key beginning with xprv:")
|
||||||
|
def set_enabled():
|
||||||
|
OK_button.setEnabled(Wallet.is_xprv(
|
||||||
|
self.get_seed_text(text)))
|
||||||
|
text.textChanged.connect(set_enabled)
|
||||||
|
OK_button.setEnabled(False)
|
||||||
|
|
||||||
|
vbox.addWidget(QLabel(msg))
|
||||||
|
pin = QLineEdit()
|
||||||
|
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,10}')))
|
||||||
|
pin.setMaximumWidth(100)
|
||||||
|
hbox_pin = QHBoxLayout()
|
||||||
|
hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
|
||||||
|
hbox_pin.addWidget(pin)
|
||||||
|
hbox_pin.addStretch(1)
|
||||||
|
|
||||||
label = QLabel(_("Enter a label to name your device:"))
|
label = QLabel(_("Enter a label to name your device:"))
|
||||||
name = QLineEdit()
|
name = QLineEdit()
|
||||||
hl = QHBoxLayout()
|
hl = QHBoxLayout()
|
||||||
hl.addWidget(label)
|
hl.addWidget(label)
|
||||||
hl.addWidget(name)
|
hl.addWidget(name)
|
||||||
hl.addStretch(2)
|
hl.addStretch(1)
|
||||||
vbox.addLayout(hl)
|
vbox.addLayout(hl)
|
||||||
|
|
||||||
cb_pin = QCheckBox(_('Enable PIN protection'))
|
if method in [self.TIM_NEW, self.TIM_RECOVER]:
|
||||||
cb_pin.setChecked(True)
|
|
||||||
vbox.addWidget(cb_pin)
|
vbox.addWidget(cb_pin)
|
||||||
|
else:
|
||||||
|
vbox.addLayout(hbox_pin)
|
||||||
|
|
||||||
cb_phrase = QCheckBox(_('Enable Passphrase protection'))
|
cb_phrase = QCheckBox(_('Enable Passphrase protection'))
|
||||||
cb_phrase.setChecked(False)
|
cb_phrase.setChecked(False)
|
||||||
vbox.addWidget(cb_phrase)
|
vbox.addWidget(cb_phrase)
|
||||||
|
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
|
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
||||||
self.set_layout(vbox)
|
self.set_layout(vbox)
|
||||||
|
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
raise UserCancelled
|
raise UserCancelled
|
||||||
|
|
||||||
return (bg.checkedId(), unicode(name.text()),
|
if method in [self.TIM_NEW, self.TIM_RECOVER]:
|
||||||
cb_pin.isChecked(), cb_phrase.isChecked())
|
item = bg.checkedId()
|
||||||
|
pin = cb_pin.isChecked()
|
||||||
|
else:
|
||||||
|
item = ' '.join(str(self.get_seed_text(text)).split())
|
||||||
|
pin = str(pin.text())
|
||||||
|
|
||||||
|
return (item, unicode(name.text()), pin, cb_phrase.isChecked())
|
||||||
|
|
|
@ -48,7 +48,7 @@ class WizardBase(PrintError):
|
||||||
('multisig', _("Multi-signature wallet")),
|
('multisig', _("Multi-signature wallet")),
|
||||||
('hardware', _("Hardware wallet")),
|
('hardware', _("Hardware wallet")),
|
||||||
]
|
]
|
||||||
|
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
|
||||||
|
|
||||||
# Derived classes must set:
|
# Derived classes must set:
|
||||||
# self.language_for_seed
|
# self.language_for_seed
|
||||||
|
@ -103,14 +103,21 @@ class WizardBase(PrintError):
|
||||||
dynamic feedback. If not provided, Wallet.is_any is used."""
|
dynamic feedback. If not provided, Wallet.is_any is used."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def request_trezor_reset_settings(self, device):
|
def request_trezor_init_settings(self, method, device):
|
||||||
"""Ask the user how they want to initialize a trezor compatible
|
"""Ask the user for the information needed to initialize a trezor-
|
||||||
device. device is the device kind, e.g. "Keepkey", to be used
|
compatible device. Method is one of the TIM_ trezor init
|
||||||
in dialog messages. Returns a 4-tuple: (strength, label,
|
method constants. TIM_NEW and TIM_RECOVER should ask how many
|
||||||
pinprotection, passphraseprotection). Strength is 0, 1 or 2
|
seed words to use, and return 0, 1 or 2 for a 12, 18 or 24
|
||||||
for a 12, 18 or 24 word seed, respectively. Label is a name
|
word seed respectively. TIM_MNEMONIC should ask for a
|
||||||
to give the device. PIN protection and passphrase protection
|
mnemonic. TIM_PRIVKEY should ask for a master private key.
|
||||||
are booleans and should default to True and False respectively."""
|
All four methods should additionally ask for a name to label
|
||||||
|
the device, PIN information and whether passphrase protection is
|
||||||
|
to be enabled (True/False, default to False). For TIM_NEW and
|
||||||
|
TIM_RECOVER, the pin information is whether pin protection
|
||||||
|
is required (True/False, default to True); for TIM_MNEMONIC and
|
||||||
|
TIM_PRIVKEY is is the pin as a string of digits 1-9.
|
||||||
|
The result is a 4-tuple: (TIM specific data, label, pininfo,
|
||||||
|
passphraseprotection)."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def request_many(self, n, xpub_hot=None):
|
def request_many(self, n, xpub_hot=None):
|
||||||
|
|
|
@ -53,10 +53,10 @@ class GuiMixin(object):
|
||||||
return self.proto.PassphraseAck(passphrase=passphrase)
|
return self.proto.PassphraseAck(passphrase=passphrase)
|
||||||
|
|
||||||
def callback_WordRequest(self, msg):
|
def callback_WordRequest(self, msg):
|
||||||
# TODO
|
msg = _("Enter seed word as explained on your %s") % self.device
|
||||||
stderr.write("Enter one word of mnemonic:\n")
|
word = self.handler().get_word(msg)
|
||||||
stderr.flush()
|
if word is None:
|
||||||
word = raw_input()
|
return self.proto.Cancel()
|
||||||
return self.proto.WordAck(word=word)
|
return self.proto.WordAck(word=word)
|
||||||
|
|
||||||
|
|
||||||
|
@ -184,8 +184,9 @@ def trezor_client_class(protocol_mixin, base_client, proto):
|
||||||
|
|
||||||
cls = TrezorClient
|
cls = TrezorClient
|
||||||
for method in ['apply_settings', 'change_pin', 'get_address',
|
for method in ['apply_settings', 'change_pin', 'get_address',
|
||||||
'get_public_node', 'reset_device', 'sign_message',
|
'get_public_node', 'load_device_by_mnemonic',
|
||||||
'sign_tx', 'wipe_device']:
|
'load_device_by_xprv', 'recovery_device',
|
||||||
|
'reset_device', 'sign_message', 'sign_tx', 'wipe_device']:
|
||||||
setattr(cls, method, wrapper(getattr(cls, method)))
|
setattr(cls, method, wrapper(getattr(cls, method)))
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
|
@ -14,6 +14,7 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
|
||||||
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
||||||
from electrum.util import ThreadJob
|
from electrum.util import ThreadJob
|
||||||
from electrum.plugins import DeviceMgr
|
from electrum.plugins import DeviceMgr
|
||||||
|
from electrum.wizard import WizardBase
|
||||||
|
|
||||||
class DeviceDisconnectedError(Exception):
|
class DeviceDisconnectedError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -251,16 +252,47 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
# Prevent timeouts during initialization
|
# Prevent timeouts during initialization
|
||||||
wallet.last_operation = self.prevent_timeout
|
wallet.last_operation = self.prevent_timeout
|
||||||
|
|
||||||
(strength, label, pin_protection, passphrase_protection) \
|
# Initialization method
|
||||||
= wizard.request_trezor_reset_settings(self.device)
|
msg = _("Please select how you want to initialize your %s.\n"
|
||||||
|
"The first two are secure as no secret information is entered "
|
||||||
|
"onto your computer.\nFor the last two methods you enter "
|
||||||
|
"secrets into your computer and upload them to the device, "
|
||||||
|
"and so should only be done on a computer you know to be "
|
||||||
|
"trustworthy and free of malware."
|
||||||
|
) % self.device
|
||||||
|
|
||||||
assert strength in range(0, 3)
|
methods = [
|
||||||
strength = 64 * (strength + 2) # 128, 192 or 256
|
_("Let the device generate a completely new seed randomly"),
|
||||||
language = ''
|
_("Recover from an existing %s seed you have previously written "
|
||||||
|
"down" % self.device),
|
||||||
|
_("Upload a BIP39 mnemonic to generate the seed"),
|
||||||
|
_("Upload a master private key")
|
||||||
|
]
|
||||||
|
|
||||||
|
method = wizard.query_choice(msg, methods)
|
||||||
|
(item, label, pin_protection, passphrase_protection) \
|
||||||
|
= wizard.request_trezor_init_settings(method, self.device)
|
||||||
|
|
||||||
client = self.get_client(wallet)
|
client = self.get_client(wallet)
|
||||||
|
language = 'english'
|
||||||
|
|
||||||
|
if method == WizardBase.TIM_NEW:
|
||||||
|
strength = 64 * (item + 2) # 128, 192 or 256
|
||||||
client.reset_device(True, strength, passphrase_protection,
|
client.reset_device(True, strength, passphrase_protection,
|
||||||
pin_protection, label, language)
|
pin_protection, label, language)
|
||||||
|
elif method == WizardBase.TIM_RECOVER:
|
||||||
|
word_count = 6 * (item + 2) # 12, 18 or 24
|
||||||
|
client.recovery_device(word_count, passphrase_protection,
|
||||||
|
pin_protection, label, language)
|
||||||
|
elif method == WizardBase.TIM_MNEMONIC:
|
||||||
|
pin = pin_protection # It's the pin, not a boolean
|
||||||
|
client.load_device_by_mnemonic(str(item), pin,
|
||||||
|
passphrase_protection,
|
||||||
|
label, language)
|
||||||
|
else:
|
||||||
|
pin = pin_protection # It's the pin, not a boolean
|
||||||
|
client.load_device_by_xprv(item, pin, passphrase_protection,
|
||||||
|
label, language)
|
||||||
|
|
||||||
def select_device(self, wallet, wizard):
|
def select_device(self, wallet, wizard):
|
||||||
'''Called when creating a new wallet. Select the device to use. If
|
'''Called when creating a new wallet. Select the device to use. If
|
||||||
|
@ -268,9 +300,12 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
process.'''
|
process.'''
|
||||||
self.device_manager().scan_devices()
|
self.device_manager().scan_devices()
|
||||||
clients = self.device_manager().clients_of_type(self.client_class)
|
clients = self.device_manager().clients_of_type(self.client_class)
|
||||||
suffixes = [_("An unnamed device (wiped)"), _(" (initialized)")]
|
suffixes = [_(" (wiped)"), _(" (initialized)")]
|
||||||
labels = [client.label() + suffixes[client.is_initialized()]
|
def client_desc(client):
|
||||||
for client in clients]
|
label = client.label() or _("An unnamed device")
|
||||||
|
return label + suffixes[client.is_initialized()]
|
||||||
|
labels = list(map(client_desc, clients))
|
||||||
|
|
||||||
msg = _("Please select which %s device to use:") % self.device
|
msg = _("Please select which %s device to use:") % self.device
|
||||||
client = clients[wizard.query_choice(msg, labels)]
|
client = clients[wizard.query_choice(msg, labels)]
|
||||||
self.device_manager().pair_wallet(wallet, client)
|
self.device_manager().pair_wallet(wallet, client)
|
||||||
|
|
|
@ -28,6 +28,7 @@ class QtHandler(PrintError):
|
||||||
win.connect(win, SIGNAL('message_dialog'), self.message_dialog)
|
win.connect(win, SIGNAL('message_dialog'), self.message_dialog)
|
||||||
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
|
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
|
||||||
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
|
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
|
||||||
|
win.connect(win, SIGNAL('word_dialog'), self.word_dialog)
|
||||||
self.window_stack = [win]
|
self.window_stack = [win]
|
||||||
self.win = win
|
self.win = win
|
||||||
self.pin_matrix_widget_class = pin_matrix_widget_class
|
self.pin_matrix_widget_class = pin_matrix_widget_class
|
||||||
|
@ -53,6 +54,12 @@ class QtHandler(PrintError):
|
||||||
self.done.wait()
|
self.done.wait()
|
||||||
return self.response
|
return self.response
|
||||||
|
|
||||||
|
def get_word(self, msg):
|
||||||
|
self.done.clear()
|
||||||
|
self.win.emit(SIGNAL('word_dialog'), msg)
|
||||||
|
self.done.wait()
|
||||||
|
return self.word
|
||||||
|
|
||||||
def get_passphrase(self, msg):
|
def get_passphrase(self, msg):
|
||||||
self.done.clear()
|
self.done.clear()
|
||||||
self.win.emit(SIGNAL('passphrase_dialog'), msg)
|
self.win.emit(SIGNAL('passphrase_dialog'), msg)
|
||||||
|
@ -82,6 +89,20 @@ class QtHandler(PrintError):
|
||||||
self.passphrase = passphrase
|
self.passphrase = passphrase
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
|
def word_dialog(self, msg):
|
||||||
|
dialog = WindowModalDialog(self.window_stack[-1], "")
|
||||||
|
hbox = QHBoxLayout(dialog)
|
||||||
|
hbox.addWidget(QLabel(msg))
|
||||||
|
text = QLineEdit()
|
||||||
|
text.setMaximumWidth(100)
|
||||||
|
text.returnPressed.connect(dialog.accept)
|
||||||
|
hbox.addWidget(text)
|
||||||
|
hbox.addStretch(1)
|
||||||
|
if not self.exec_dialog(dialog):
|
||||||
|
return None
|
||||||
|
self.word = unicode(text.text())
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
def message_dialog(self, msg, cancel_callback):
|
def message_dialog(self, msg, cancel_callback):
|
||||||
# Called more than once during signing, to confirm output and fee
|
# Called more than once during signing, to confirm output and fee
|
||||||
self.clear_dialog()
|
self.clear_dialog()
|
||||||
|
@ -108,7 +129,7 @@ class QtHandler(PrintError):
|
||||||
def exec_dialog(self, dialog):
|
def exec_dialog(self, dialog):
|
||||||
self.window_stack.append(dialog)
|
self.window_stack.append(dialog)
|
||||||
try:
|
try:
|
||||||
dialog.exec_()
|
return dialog.exec_()
|
||||||
finally:
|
finally:
|
||||||
assert dialog == self.window_stack.pop()
|
assert dialog == self.window_stack.pop()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue