commit 889976915afde6ce66bf2812d493331f2f750c79
parent eb865779eb8181df0bfab9545c2150dfa9c5d22a
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:
5 files changed, 123 insertions(+), 4 deletions(-)
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
@@ -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
diff --git a/plugins/keepkey/client.py b/plugins/keepkey/client.py
@@ -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)
diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py
@@ -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):
diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
@@ -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 "
diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
@@ -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()
@@ -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)
@@ -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()
@@ -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"),