URI: 
       twizard: add derivation passphrase and bip39 support - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit b907a668ec59ac5500df5137f58201fbd1a8af4e
   DIR parent 808703bacbad9486e06ef9ab1b82f71d17eec20f
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu, 25 Aug 2016 09:48:11 +0200
       
       wizard: add derivation passphrase and bip39 support
       
       Diffstat:
         M electrum                            |      24 ++++++++++++++++--------
         M gui/qt/installwizard.py             |      54 +++++++++++++++++++++----------
         M lib/base_wizard.py                  |      38 +++++++++++++------------------
         M lib/keystore.py                     |      12 +++++-------
       
       4 files changed, 74 insertions(+), 54 deletions(-)
       ---
   DIR diff --git a/electrum b/electrum
       t@@ -88,7 +88,7 @@ from electrum.util import print_msg, print_stderr, json_encode, json_decode
        from electrum.util import set_verbosity, InvalidPassword, check_www_dir
        from electrum.commands import get_parser, known_commands, Commands, config_variables
        from electrum import daemon
       -from electrum.keystore import from_text, is_private
       +from electrum import keystore
        from electrum.mnemonic import Mnemonic
        
        # get password routine
       t@@ -117,13 +117,18 @@ def run_non_RPC(config):
        
            if cmdname == 'restore':
                text = config.get('text')
       -        password = password_dialog() if is_private(text) else None
       -        try:
       -            k = from_text(text, password)
       -        except BaseException as e:
       +        passphrase = config.get('passphrase', '')
       +        password = password_dialog() if keystore.is_private(text) else None
       +        if keystore.is_seed(text):
       +            k = keystore.from_seed(text, passphrase, password)
       +        elif keystore.is_any_key(text):
       +            k = keystore.from_keys(text, password)
       +        else:
                    sys.exit(str(e))
       -        k.save(storage, 'x/')
       +        storage.put('keystore', k.dump())
                storage.put('wallet_type', 'standard')
       +        storage.put('use_encryption', bool(password))
       +        storage.write()
                wallet = Wallet(storage)
                if not config.get('offline'):
                    network = Network(config)
       t@@ -139,10 +144,13 @@ def run_non_RPC(config):
        
            elif cmdname == 'create':
                password = password_dialog()
       +        passphrase = config.get('passphrase', '')
                seed = Mnemonic('en').make_seed()
       -        k = from_text(seed, password)
       -        k.save(storage, 'x/')
       +        k = keystore.from_seed(seed, passphrase, password)
       +        storage.put('keystore', k.dump())
                storage.put('wallet_type', 'standard')
       +        storage.put('use_encryption', bool(password))
       +        storage.write()
                wallet = Wallet(storage)
                wallet.synchronize()
                print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
   DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
       t@@ -28,10 +28,10 @@ MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or x
        MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
        MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
                             + _("Leave this field empty if you want to disable encryption.")
       -MSG_RESTORE_PASSPHRASE = \
       -    _("Please enter the passphrase you used when creating your %s wallet.  "
       -      "Note this is NOT a password.  Enter nothing if you did not use "
       -      "one or are unsure.")
       +MSG_PASSPHRASE = \
       +    _("Please enter your seed derivation passphrase. "
       +      "Note: this is NOT your encryption password. "
       +      "Leave this field empty if you did not use one or are unsure.")
        
        def clean_text(seed_e):
            text = unicode(seed_e.toPlainText()).strip()
       t@@ -247,17 +247,39 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
            def remove_from_recently_open(self, filename):
                self.config.remove_from_recently_open(filename)
        
       -    def text_input(self, title, message, is_valid):
       +    def text_input_layout(self, title, message, is_valid):
                slayout = SeedInputLayout(title=message)
       -        def sanitized_seed():
       -            return clean_text(slayout.seed_edit())
       -        def set_enabled():
       -            self.next_button.setEnabled(is_valid(sanitized_seed()))
       -        slayout.seed_edit().textChanged.connect(set_enabled)
       +        slayout.is_valid = is_valid
       +        slayout.sanitized_text = lambda: clean_text(slayout.seed_edit())
       +        slayout.set_enabled = lambda: self.next_button.setEnabled(slayout.is_valid(slayout.sanitized_text()))
       +        slayout.seed_edit().textChanged.connect(slayout.set_enabled)
       +        return slayout
       +
       +    def text_input(self, title, message, is_valid):
       +        slayout = self.text_input_layout(title, message, is_valid)
                self.set_main_layout(slayout.layout(), title, next_enabled=False)
       -        seed = sanitized_seed()
       +        seed = slayout.sanitized_text()
                return seed
        
       +    def seed_input(self, title, message, is_valid, bip39=False):
       +        slayout = self.text_input_layout(title, message, is_valid)
       +        vbox = QVBoxLayout()
       +        vbox.addLayout(slayout.layout())
       +        cb_passphrase = QCheckBox(_('Add a passphrase to this seed'))
       +        vbox.addWidget(cb_passphrase)
       +        cb_bip39 = QCheckBox(_('BIP39/BIP44 seed'))
       +        cb_bip39.setVisible(bip39)
       +        vbox.addWidget(cb_bip39)
       +        def f(b):
       +            slayout.is_valid = (lambda x: bool(x)) if b else is_valid
       +            slayout.set_enabled()
       +        cb_bip39.toggled.connect(f)
       +        self.set_main_layout(vbox, title, next_enabled=False)
       +        seed = slayout.sanitized_text()
       +        add_passphrase = cb_passphrase.isChecked()
       +        is_bip39 = cb_bip39.isChecked()
       +        return seed, add_passphrase, is_bip39
       +
            @wizard_dialog
            def restore_keys_dialog(self, title, message, is_valid, run_next):
                return self.text_input(title, message, is_valid)
       t@@ -275,8 +297,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
            def restore_seed_dialog(self, run_next, is_valid):
                title = _('Enter Seed')
                message = _('Please enter your seed phrase in order to restore your wallet.')
       -        text = self.text_input(title, message, is_valid)
       -        return text, False, True
       +        return self.seed_input(title, message, is_valid, bip39=True)
        
            @wizard_dialog
            def confirm_seed_dialog(self, run_next, is_valid):
       t@@ -286,7 +307,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
                    _('Your seed is important!'),
                    _('To make sure that you have properly saved your seed, please retype it here.')
                ])
       -        return self.text_input(title, message, is_valid)
       +        return self.seed_input(title, message, is_valid)
        
            @wizard_dialog
            def show_seed_dialog(self, run_next, seed_text):
       t@@ -300,12 +321,11 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
                return playout.new_password()
        
            @wizard_dialog
       -    def request_passphrase(self, device_text, run_next):
       +    def request_passphrase(self, run_next):
                """When restoring a wallet, request the passphrase that was used for
                the wallet on the given device and confirm it.  Should return
                a unicode string."""
       -        phrase = self.pw_layout(MSG_RESTORE_PASSPHRASE % device_text,
       -                                PW_PASSPHRASE)
       +        phrase = self.pw_layout(MSG_PASSPHRASE, PW_PASSPHRASE)
                if phrase is None:
                    raise UserCancelled
                return phrase
   DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py
       t@@ -159,10 +159,13 @@ class BaseWizard(object):
                self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v)
        
            def on_restore_from_key(self, text):
       +        def f(password):
       +            k = keystore.from_keys(text, password)
       +            self.on_keystore(k, password)
                if keystore.is_private(text):
       -            self.add_password(text)
       +            self.run('request_password', run_next=f)
                else:
       -            self.create_keystore(text, None)
       +            f(None)
        
            def choose_hw_device(self):
                title = _('Hardware Keystore')
       t@@ -221,18 +224,18 @@ class BaseWizard(object):
                self.on_keystore(k, None)
        
            def restore_from_seed(self):
       -        self.restore_seed_dialog(run_next=self.on_restore_from_seed, is_valid=keystore.is_seed)
       +        self.restore_seed_dialog(run_next=self.on_seed, is_valid=keystore.is_seed)
        
       -    def on_restore_from_seed(self, seed, is_bip39, is_passphrase):
       +    def on_seed(self, seed, add_passphrase, is_bip39):
                self.is_bip39 = is_bip39
                f = lambda x: self.run('on_passphrase', seed, x)
       -        if is_passphrase:
       -            self.request_passphrase(self.storage.get('hw_type'), run_next=f)
       +        if add_passphrase:
       +            self.request_passphrase(run_next=f)
                else:
       -            self.run('on_passphrase', seed, '')
       +            f('')
        
            def on_passphrase(self, seed, passphrase):
       -        f = lambda x: self.run('on_password', seed, passphrase, password)
       +        f = lambda x: self.run('on_password', seed, passphrase, x)
                self.request_password(run_next=f)
        
            def on_password(self, seed, passphrase, password):
       t@@ -240,15 +243,14 @@ class BaseWizard(object):
                    f = lambda account_id: self.run('on_bip44', seed, passphrase, password, account_id)
                    self.account_id_dialog(run_next=f)
                else:
       -            self.create_keystore(seed, passphrase, password)
       +            k = keystore.from_seed(seed, passphrase, password)
       +            self.on_keystore(k, password)
        
            def on_bip44(self, seed, passphrase, password, account_id):
                import keystore
       -        k = keystore.BIP32_KeyStore()
       -        k.add_seed(seed, password)
       +        k = keystore.BIP32_KeyStore({})
                bip32_seed = keystore.bip39_to_seed(seed, passphrase)
                derivation = "m/44'/0'/%d'"%account_id
       -        self.storage.put('account_id', account_id)
                k.add_xprv_from_seed(bip32_seed, derivation, password)
                self.on_keystore(k, password)
        
       t@@ -283,7 +285,7 @@ class BaseWizard(object):
                self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub)
        
            def on_cosigner(self, text, password, i):
       -        k = keystore.from_text(text, password)
       +        k = keystore.from_keys(text, password)
                self.on_keystore(k)
        
            def create_seed(self):
       t@@ -292,15 +294,7 @@ class BaseWizard(object):
                self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
        
            def confirm_seed(self, seed):
       -        self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
       -
       -    def add_password(self, text):
       -        f = lambda pw: self.run('create_keystore', text, pw)
       -        self.request_password(run_next=f)
       -
       -    def create_keystore(self, text, password):
       -        k = keystore.from_text(text, password)
       -        self.on_keystore(k, password)
       +        self.confirm_seed_dialog(run_next=self.on_seed, is_valid=lambda x: x==seed)
        
            def create_addresses(self):
                def task():
   DIR diff --git a/lib/keystore.py b/lib/keystore.py
       t@@ -623,14 +623,14 @@ is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
        def bip44_derivation(account_id):
            return "m/44'/0'/%d'"% int(account_id)
        
       -def from_seed(seed, password):
       +def from_seed(seed, passphrase, password):
            if is_old_seed(seed):
                keystore = Old_KeyStore({})
                keystore.add_seed(seed, password)
            elif is_new_seed(seed):
                keystore = BIP32_KeyStore({})
                keystore.add_seed(seed, password)
       -        bip32_seed = Mnemonic.mnemonic_to_seed(seed, '')
       +        bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase)
                keystore.add_xprv_from_seed(bip32_seed, "m/", password)
            return keystore
        
       t@@ -663,12 +663,12 @@ def xprv_from_seed(seed, password):
            xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, ''))
            return from_xprv(xprv, password)
        
       -def xpub_from_seed(seed):
       +def xpub_from_seed(seed, passphrase):
            # store only master xpub
            xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed,''))
            return from_xpub(xpub)
        
       -def from_text(text, password):
       +def from_keys(text, password):
            if is_xprv(text):
                k = from_xprv(text, password)
            elif is_old_mpk(text):
       t@@ -677,8 +677,6 @@ def from_text(text, password):
                k = from_xpub(text)
            elif is_private_key_list(text):
                k = from_private_key_list(text, password)
       -    elif is_seed(text):
       -        k = from_seed(text, password)
            else:
       -        raise BaseException('Invalid seedphrase or key')
       +        raise BaseException('Invalid key')
            return k