tTrezor: all four available device initializations - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 9b29c6c2e61fc304baac257e2c2b3c2024fb5a21 DIR parent bdb4782b36e339c2e5c8889074892b24d402aba3 HTML 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: M gui/qt/installwizard.py | 94 ++++++++++++++++++++++--------- M lib/wizard.py | 25 ++++++++++++++++--------- M plugins/trezor/client.py | 13 +++++++------ M plugins/trezor/plugin.py | 57 +++++++++++++++++++++++++------ M plugins/trezor/qt_generic.py | 23 ++++++++++++++++++++++- 5 files changed, 157 insertions(+), 55 deletions(-) --- DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py t@@ -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) t@@ -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()) DIR diff --git a/lib/wizard.py b/lib/wizard.py t@@ -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 t@@ -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): DIR diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py t@@ -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) t@@ -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 DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py t@@ -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 t@@ -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 t@@ -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) DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py t@@ -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 t@@ -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) t@@ -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() t@@ -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()