tKeepKey: Implement secure recovery from seed - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 889976915afde6ce66bf2812d493331f2f750c79 DIR parent eb865779eb8181df0bfab9545c2150dfa9c5d22a HTML Author: Neil Booth <kyuupichan@gmail.com> Date: Sat, 23 Jan 2016 12:09:52 +0900 KeepKey: Implement secure recovery from seed This method relies on having a large screen so only works with KeepKey firmware. Diffstat: M RELEASE-NOTES | 1 + M plugins/keepkey/client.py | 2 +- M plugins/trezor/clientbase.py | 5 +++++ M plugins/trezor/plugin.py | 2 +- M plugins/trezor/qt_generic.py | 117 ++++++++++++++++++++++++++++++- 5 files changed, 123 insertions(+), 4 deletions(-) --- DIR diff --git a/RELEASE-NOTES b/RELEASE-NOTES t@@ -15,6 +15,7 @@ 2) you enter a seed 3) you enter a BIP39 mnemonic to generate the seed 4) you enter a master private key + - KeepKey secure seed recovery (KeepKey only) - change / set / disable PIN - set homescreen (Trezor only) - set a session timeout. Once a session has timed out, further use DIR diff --git a/plugins/keepkey/client.py b/plugins/keepkey/client.py t@@ -8,7 +8,7 @@ class KeepKeyClient(TrezorClientBase, ProtocolMixin, BaseClient): TrezorClientBase.__init__(self, handler, plugin, proto) def recovery_device(self, *args): - ProtocolMixin.recovery_device(self, True, *args) + ProtocolMixin.recovery_device(self, False, *args) TrezorClientBase.wrap_methods(KeepKeyClient) DIR diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py t@@ -66,6 +66,11 @@ class GuiMixin(object): # Unfortunately the device can't handle self.proto.Cancel() return self.proto.WordAck(word=word) + def callback_CharacterRequest(self, msg): + char_info = self.handler.get_char(msg) + if not char_info: + return self.proto.Cancel() + return self.proto.CharacterAck(**char_info) class TrezorClientBase(GuiMixin, PrintError): DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py t@@ -285,7 +285,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob): (item, label, pin_protection, passphrase_protection) \ = wallet.handler.request_trezor_init_settings(method, self.device) - if method == TIM_RECOVER: + if method == TIM_RECOVER and self.device == 'Trezor': # Warn user about firmware lameness wallet.handler.show_error(_( "You will be asked to enter 24 words regardless of your " DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py t@@ -27,26 +27,122 @@ PASSPHRASE_NOT_PIN = _( "If you forget a passphrase you will be unable to access any " "bitcoins in the wallet behind it. A passphrase is not a PIN. " "Only change this if you are sure you understand it.") +CHARACTER_RECOVERY = ( + "Use the recovery cipher shown on your device to input your seed words. " + "The cipher updates with every letter. After at most 4 letters the " + "device will auto-complete each word.\n" + "Press SPACE or the Accept Word button to accept the device's auto-" + "completed word and advance to the next one.\n" + "Press BACKSPACE to go back a character or word.\n" + "Press ENTER or the Seed Entered button once the last word in your " + "seed is auto-completed.") + +class CharacterButton(QPushButton): + def __init__(self, text=None): + QPushButton.__init__(self, text) + + def keyPressEvent(self, event): + event.setAccepted(False) # Pass through Enter and Space keys + + +class CharacterDialog(WindowModalDialog): + + def __init__(self, parent): + super(CharacterDialog, self).__init__(parent) + self.setWindowTitle(_("KeepKey Seed Recovery")) + self.character_pos = 0 + self.word_pos = 0 + self.loop = QEventLoop() + self.word_help = QLabel() + self.char_buttons = [] + + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(CHARACTER_RECOVERY)) + hbox = QHBoxLayout() + hbox.addWidget(self.word_help) + for i in range(4): + char_button = CharacterButton('*') + char_button.setMaximumWidth(36) + self.char_buttons.append(char_button) + hbox.addWidget(char_button) + self.accept_button = CharacterButton(_("Accept Word")) + self.accept_button.clicked.connect(partial(self.process_key, 32)) + self.rejected.connect(partial(self.loop.exit, 1)) + hbox.addWidget(self.accept_button) + hbox.addStretch(1) + vbox.addLayout(hbox) + + self.finished_button = QPushButton(_("Seed Entered")) + self.cancel_button = QPushButton(_("Cancel")) + self.finished_button.clicked.connect(partial(self.process_key, + Qt.Key_Return)) + self.cancel_button.clicked.connect(self.rejected) + buttons = Buttons(self.finished_button, self.cancel_button) + vbox.addSpacing(40) + vbox.addLayout(buttons) + self.refresh() + self.show() + + def refresh(self): + self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1)) + self.accept_button.setEnabled(self.character_pos >= 3) + self.finished_button.setEnabled((self.word_pos in (11, 17, 23) + and self.character_pos >= 3)) + for n, button in enumerate(self.char_buttons): + button.setEnabled(n == self.character_pos) + if n == self.character_pos: + button.setFocus() + + def process_key(self, key): + self.data = None + if key == Qt.Key_Return and self.finished_button.isEnabled(): + self.data = {'done': True} + elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos): + self.data = {'delete': True} + elif ((key >= ord('a') and key <= ord('z')) + or (key >= ord('A') and key <= ord('Z')) + or (key == ord(' ') and self.character_pos >= 3)): + char = chr(key).lower() + self.data = {'character': char} + if self.data: + self.loop.exit(0) + + def keyPressEvent(self, event): + self.process_key(event.key()) + if not self.data: + QDialog.keyPressEvent(self, event) + + def get_char(self, word_pos, character_pos): + self.word_pos = word_pos + self.character_pos = character_pos + self.refresh() + if self.loop.exec_(): + self.data = None # User cancelled # By far the trickiest thing about this handler is the window stack; # MacOSX is very fussy the modal dialogs are perfectly parented -class QtHandler(PrintError): +class QtHandler(QObject, PrintError): '''An interface between the GUI (here, QT) and the device handling logic for handling I/O. This is a generic implementation of the Trezor protocol; derived classes can customize it.''' + charSig = pyqtSignal(object) + def __init__(self, win, pin_matrix_widget_class, device): + super(QtHandler, self).__init__() win.connect(win, SIGNAL('clear_dialog'), self.clear_dialog) win.connect(win, SIGNAL('error_dialog'), self.error_dialog) 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.charSig.connect(self.update_character_dialog) self.win = win self.pin_matrix_widget_class = pin_matrix_widget_class self.device = device self.dialog = None self.done = threading.Event() + self.character_dialog = None def top_level_window(self): return self.win.top_level_window() t@@ -63,6 +159,15 @@ class QtHandler(PrintError): def finished(self): self.win.emit(SIGNAL('clear_dialog')) + def get_char(self, msg): + self.done.clear() + self.charSig.emit(msg) + self.done.wait() + data = self.character_dialog.data + if not data or 'done' in data: + self.character_dialog.accept() + return data + def get_pin(self, msg): self.done.clear() self.win.emit(SIGNAL('pin_dialog'), msg) t@@ -116,6 +221,12 @@ class QtHandler(PrintError): self.word = unicode(text.text()) self.done.set() + def update_character_dialog(self, msg): + if not self.character_dialog: + self.character_dialog = CharacterDialog(self.top_level_window()) + self.character_dialog.get_char(msg.word_pos, msg.character_pos) + self.done.set() + def message_dialog(self, msg, on_cancel): # Called more than once during signing, to confirm output and fee self.clear_dialog() t@@ -154,7 +265,9 @@ class QtHandler(PrintError): gb = QGroupBox() vbox1 = QVBoxLayout() gb.setLayout(vbox1) - vbox.addWidget(gb) + # KeepKey recovery doesn't need a word count + if method == TIM_NEW or self.device == 'Trezor': + vbox.addWidget(gb) gb.setTitle(_("Select your seed length:")) choices = [ _("12 words"),