URI: 
       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"),