commit 9b29c6c2e61fc304baac257e2c2b3c2024fb5a21
parent bdb4782b36e339c2e5c8889074892b24d402aba3
Author: Neil Booth <kyuupichan@gmail.com>
Date: Sun, 3 Jan 2016 23:44:33 +0900
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
Diffstat:
5 files changed, 157 insertions(+), 55 deletions(-)
diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
@@ -332,6 +332,11 @@ class InstallWizard(WindowModalDialog, WizardBase):
def query_choice(self, msg, choices):
vbox = QVBoxLayout()
self.set_layout(vbox)
+ if len(msg) > 50:
+ label = QLabel(msg)
+ label.setWordWrap(True)
+ vbox.addWidget(label)
+ msg = ""
gb2 = QGroupBox(msg)
vbox.addWidget(gb2)
@@ -402,54 +407,87 @@ class InstallWizard(WindowModalDialog, WizardBase):
if not self.exec_():
raise UserCancelled
- def request_trezor_reset_settings(self, device):
+ def request_trezor_init_settings(self, method, device):
vbox = QVBoxLayout()
- main_label = QLabel(_("Choose how to initialize your %s device:")
- % device)
+ main_label = QLabel(_("Initialization settings for your %s:") % device)
vbox.addWidget(main_label)
- msg = _("Select your seed length and strength:")
- choices = [
- _("12 words (low)"),
- _("18 words (medium)"),
- _("24 words (high)"),
- ]
- gb = QGroupBox(msg)
- vbox1 = QVBoxLayout()
- gb.setLayout(vbox1)
- bg = QButtonGroup()
- for i, choice in enumerate(choices):
- rb = QRadioButton(gb)
- rb.setText(choice)
- bg.addButton(rb)
- bg.setId(rb, i)
- vbox1.addWidget(rb)
- rb.setChecked(True)
- vbox.addWidget(gb)
+ OK_button = OkButton(self, _('Next'))
+
+ if method in [self.TIM_NEW, self.TIM_RECOVER]:
+ gb = QGroupBox()
+ vbox1 = QVBoxLayout()
+ gb.setLayout(vbox1)
+ vbox.addWidget(gb)
+ gb.setTitle(_("Select your seed length:"))
+ choices = [
+ _("12 words"),
+ _("18 words"),
+ _("24 words"),
+ ]
+ bg = QButtonGroup()
+ for i, choice in enumerate(choices):
+ rb = QRadioButton(gb)
+ rb.setText(choice)
+ bg.addButton(rb)
+ bg.setId(rb, i)
+ vbox1.addWidget(rb)
+ rb.setChecked(True)
+ 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:"))
name = QLineEdit()
hl = QHBoxLayout()
hl.addWidget(label)
hl.addWidget(name)
- hl.addStretch(2)
+ hl.addStretch(1)
vbox.addLayout(hl)
- cb_pin = QCheckBox(_('Enable PIN protection'))
- cb_pin.setChecked(True)
- vbox.addWidget(cb_pin)
+ if method in [self.TIM_NEW, self.TIM_RECOVER]:
+ vbox.addWidget(cb_pin)
+ else:
+ vbox.addLayout(hbox_pin)
cb_phrase = QCheckBox(_('Enable Passphrase protection'))
cb_phrase.setChecked(False)
vbox.addWidget(cb_phrase)
vbox.addStretch(1)
- vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
+ vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.set_layout(vbox)
if not self.exec_():
raise UserCancelled
- return (bg.checkedId(), unicode(name.text()),
- cb_pin.isChecked(), cb_phrase.isChecked())
+ if method in [self.TIM_NEW, self.TIM_RECOVER]:
+ 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())
diff --git a/lib/wizard.py b/lib/wizard.py
@@ -48,7 +48,7 @@ class WizardBase(PrintError):
('multisig', _("Multi-signature wallet")),
('hardware', _("Hardware wallet")),
]
-
+ TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
# Derived classes must set:
# self.language_for_seed
@@ -103,14 +103,21 @@ class WizardBase(PrintError):
dynamic feedback. If not provided, Wallet.is_any is used."""
raise NotImplementedError
- def request_trezor_reset_settings(self, device):
- """Ask the user how they want to initialize a trezor compatible
- device. device is the device kind, e.g. "Keepkey", to be used
- in dialog messages. Returns a 4-tuple: (strength, label,
- pinprotection, passphraseprotection). Strength is 0, 1 or 2
- for a 12, 18 or 24 word seed, respectively. Label is a name
- to give the device. PIN protection and passphrase protection
- are booleans and should default to True and False respectively."""
+ def request_trezor_init_settings(self, method, device):
+ """Ask the user for the information needed to initialize a trezor-
+ compatible device. Method is one of the TIM_ trezor init
+ method constants. TIM_NEW and TIM_RECOVER should ask how many
+ seed words to use, and return 0, 1 or 2 for a 12, 18 or 24
+ word seed respectively. TIM_MNEMONIC should ask for a
+ mnemonic. TIM_PRIVKEY should ask for a master private key.
+ 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
def request_many(self, n, xpub_hot=None):
diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
@@ -53,10 +53,10 @@ class GuiMixin(object):
return self.proto.PassphraseAck(passphrase=passphrase)
def callback_WordRequest(self, msg):
- # TODO
- stderr.write("Enter one word of mnemonic:\n")
- stderr.flush()
- word = raw_input()
+ msg = _("Enter seed word as explained on your %s") % self.device
+ word = self.handler().get_word(msg)
+ if word is None:
+ return self.proto.Cancel()
return self.proto.WordAck(word=word)
@@ -184,8 +184,9 @@ def trezor_client_class(protocol_mixin, base_client, proto):
cls = TrezorClient
for method in ['apply_settings', 'change_pin', 'get_address',
- 'get_public_node', 'reset_device', 'sign_message',
- 'sign_tx', 'wipe_device']:
+ 'get_public_node', 'load_device_by_mnemonic',
+ 'load_device_by_xprv', 'recovery_device',
+ 'reset_device', 'sign_message', 'sign_tx', 'wipe_device']:
setattr(cls, method, wrapper(getattr(cls, method)))
return cls
diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
@@ -14,6 +14,7 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
from electrum.util import ThreadJob
from electrum.plugins import DeviceMgr
+from electrum.wizard import WizardBase
class DeviceDisconnectedError(Exception):
pass
@@ -251,16 +252,47 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
# Prevent timeouts during initialization
wallet.last_operation = self.prevent_timeout
- (strength, label, pin_protection, passphrase_protection) \
- = wizard.request_trezor_reset_settings(self.device)
-
- assert strength in range(0, 3)
- strength = 64 * (strength + 2) # 128, 192 or 256
- language = ''
+ # Initialization method
+ 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
+
+ methods = [
+ _("Let the device generate a completely new seed randomly"),
+ _("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.reset_device(True, strength, passphrase_protection,
- pin_protection, label, language)
+ language = 'english'
+
+ if method == WizardBase.TIM_NEW:
+ strength = 64 * (item + 2) # 128, 192 or 256
+ client.reset_device(True, strength, passphrase_protection,
+ 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):
'''Called when creating a new wallet. Select the device to use. If
@@ -268,9 +300,12 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
process.'''
self.device_manager().scan_devices()
clients = self.device_manager().clients_of_type(self.client_class)
- suffixes = [_("An unnamed device (wiped)"), _(" (initialized)")]
- labels = [client.label() + suffixes[client.is_initialized()]
- for client in clients]
+ suffixes = [_(" (wiped)"), _(" (initialized)")]
+ def client_desc(client):
+ 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
client = clients[wizard.query_choice(msg, labels)]
self.device_manager().pair_wallet(wallet, client)
diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
@@ -28,6 +28,7 @@ class QtHandler(PrintError):
win.connect(win, SIGNAL('message_dialog'), self.message_dialog)
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
+ win.connect(win, SIGNAL('word_dialog'), self.word_dialog)
self.window_stack = [win]
self.win = win
self.pin_matrix_widget_class = pin_matrix_widget_class
@@ -53,6 +54,12 @@ class QtHandler(PrintError):
self.done.wait()
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):
self.done.clear()
self.win.emit(SIGNAL('passphrase_dialog'), msg)
@@ -82,6 +89,20 @@ class QtHandler(PrintError):
self.passphrase = passphrase
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):
# Called more than once during signing, to confirm output and fee
self.clear_dialog()
@@ -108,7 +129,7 @@ class QtHandler(PrintError):
def exec_dialog(self, dialog):
self.window_stack.append(dialog)
try:
- dialog.exec_()
+ return dialog.exec_()
finally:
assert dialog == self.window_stack.pop()