twizard: show passphrase in the same window as the seed - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 99a3250b3ff94075f32c8fa9c57ca321b4ba48c3 DIR parent 5e90b3a42d53c55515b7e62bf8568aeb35582e90 HTML Author: ThomasV <thomasv@electrum.org> Date: Mon, 29 Aug 2016 15:33:16 +0200 wizard: show passphrase in the same window as the seed Diffstat: M gui/kivy/uix/dialogs/installwizard… | 54 +++++++++++++++++++++++++------ M gui/qt/installwizard.py | 71 +++++++++---------------------- M gui/qt/main_window.py | 6 ++++-- M gui/qt/password_dialog.py | 57 +++++++++++++------------------ M gui/qt/seed_dialog.py | 139 ++++++++++++++++++++++++------- M gui/qt/util.py | 6 ++++++ M lib/base_wizard.py | 18 +++++++++--------- M lib/keystore.py | 2 +- 8 files changed, 218 insertions(+), 135 deletions(-) --- DIR diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py t@@ -385,7 +385,6 @@ Builder.load_string(''' <ShowSeedDialog> - message: '' spacing: '12dp' value: 'next' BigLabel: t@@ -398,10 +397,29 @@ Builder.load_string(''' height: self.minimum_height orientation: 'vertical' spacing: '12dp' - SeedButton: - text: root.seed_text SeedLabel: text: root.message + SeedButton: + text: root.seed_text + +<PassphraseDialog> + + BigLabel: + text: "SEED PASSPHRASE" + SeedLabel: + text: root.passphrase_message + GridLayout: + cols: 2 + size_hint: 1, None + height: '27dp' + BigLabel: + text: _('Passphrase') + TextInput: + id: passphrase_input + multiline: False + size_hint: 1, None + height: '27dp' + ''') t@@ -492,11 +510,27 @@ class WizardChoiceDialog(WizardDialog): def get_params(self, button): return (button.action,) -class ShowSeedDialog(WizardDialog): + +class PassphraseDialog(WizardDialog): + passphrase = StringProperty('') + passphrase_message = ' '.join([ + _("You may extend your seed with a derivation passphrase."), + '\n\n', + _("Note: This is NOT your encryption password."), + _("Leave this field empty if you are not sure about what it is."), + ]) + + def __init__(self, wizard, **kwargs): + WizardDialog.__init__(self, wizard, **kwargs) + self.ids.next.disabled = False + + def get_params(self, b): + return (self.ids.passphrase_input.text,) + +class ShowSeedDialog(WizardDialog): seed_text = StringProperty('') - message = _("If you forget your PIN or lose your device, your seed phrase will be the " - "only way to recover your funds.") + message = _("If you forget your PIN or lose your device, your seed phrase will be the only way to recover your funds.") def on_parent(self, instance, value): if value: t@@ -504,7 +538,7 @@ class ShowSeedDialog(WizardDialog): self._back = _back = partial(self.ids.back.dispatch, 'on_release') def get_params(self, b): - return(self.seed_text,) + return(self.seed_text, '') class WordButton(Button): t@@ -518,7 +552,7 @@ class RestoreSeedDialog(WizardDialog): def __init__(self, wizard, **kwargs): super(RestoreSeedDialog, self).__init__(wizard, **kwargs) - self._test = kwargs['is_valid'] + self._test = kwargs['is_seed'] from electrum.mnemonic import Mnemonic from electrum.old_mnemonic import words as old_wordlist self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) t@@ -581,7 +615,7 @@ class RestoreSeedDialog(WizardDialog): return text def get_params(self, b): - return (self.get_text(), False, False) + return (self.get_text(), '', False) def update_text(self, c): c = c.lower() t@@ -671,7 +705,6 @@ class AddXpubDialog(WizardDialog): - class InstallWizard(BaseWizard, Widget): ''' events:: t@@ -711,6 +744,7 @@ class InstallWizard(BaseWizard, Widget): def choice_dialog(self, **kwargs): WizardChoiceDialog(self, **kwargs).open() def multisig_dialog(self, **kwargs): WizardMultisigDialog(self, **kwargs).open() def show_seed_dialog(self, **kwargs): ShowSeedDialog(self, **kwargs).open() + def passphrase_dialog(self, **kwargs): PassphraseDialog(self, **kwargs).open() def confirm_seed_dialog(self, **kwargs): kwargs['title'] = _('Confirm Seed') DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py t@@ -12,10 +12,10 @@ from electrum.util import UserCancelled from electrum.base_wizard import BaseWizard from electrum.i18n import _ -from seed_dialog import SeedDisplayLayout, SeedWarningLayout, SeedInputLayout +from seed_dialog import SeedDisplayLayout, CreateSeedLayout, SeedInputLayout, TextInputLayout from network_dialog import NetworkChoiceLayout from util import * -from password_dialog import PasswordLayout, PW_NEW, PW_PASSPHRASE +from password_dialog import PasswordLayout, PW_NEW class GoBack(Exception): t@@ -28,15 +28,11 @@ 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_PASSPHRASE = \ +MSG_RESTORE_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() - text = ' '.join(text.split()) - return text class CosignWidget(QWidget): size = 120 t@@ -248,31 +244,18 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): def remove_from_recently_open(self, filename): self.config.remove_from_recently_open(filename) - def text_input_layout(self, title, message, is_valid): - slayout = SeedInputLayout(title=message) - 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) + slayout = TextInputLayout(self, message, is_valid) self.set_main_layout(slayout.layout(), title, next_enabled=False) - seed = slayout.sanitized_text() - return seed + return slayout.get_text() - def seed_input(self, title, message, is_valid): - slayout = self.text_input_layout(title, message, is_valid) + def seed_input(self, title, message, is_seed, is_passphrase): + slayout = SeedInputLayout(self, message, is_seed, is_passphrase) vbox = QVBoxLayout() vbox.addLayout(slayout.layout()) - if self.opt_ext or self.opt_bip39: - vbox.addStretch(1) - vbox.addWidget(QLabel(_('Options')+ ':')) - if self.opt_ext: - cb_ext = QCheckBox(_('Extend this seed with a passphrase')) - vbox.addWidget(cb_ext) if self.opt_bip39: + vbox.addStretch(1) + vbox.addWidget(QLabel(_('Options') + ':')) def f(b): slayout.is_valid = (lambda x: bool(x)) if b else is_valid slayout.set_enabled() t@@ -280,10 +263,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): cb_bip39.toggled.connect(f) vbox.addWidget(cb_bip39) self.set_main_layout(vbox, title, next_enabled=False) - seed = slayout.sanitized_text() - is_ext = cb_ext.isChecked() if self.opt_ext else False + seed = slayout.get_seed() + passphrase = slayout.get_passphrase() is_bip39 = cb_bip39.isChecked() if self.opt_bip39 else False - return seed, is_ext, is_bip39 + return seed, passphrase, is_bip39 @wizard_dialog def restore_keys_dialog(self, title, message, is_valid, run_next): t@@ -299,13 +282,14 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return self.text_input(title, message, is_valid) @wizard_dialog - def restore_seed_dialog(self, run_next, is_valid): + def restore_seed_dialog(self, run_next, is_seed): title = _('Enter Seed') message = _('Please enter your seed phrase in order to restore your wallet.') - return self.seed_input(title, message, is_valid) + is_passphrase = lambda x: True + return self.seed_input(title, message, is_seed, is_passphrase) @wizard_dialog - def confirm_seed_dialog(self, run_next, is_valid): + def confirm_seed_dialog(self, run_next, is_seed, is_passphrase): self.app.clipboard().clear() title = _('Confirm Seed') message = ' '.join([ t@@ -313,13 +297,13 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): _('If you lose your seed, your money will be permanently lost.'), _('To make sure that you have properly saved your seed, please retype it here.') ]) - return self.seed_input(title, message, is_valid) + return self.seed_input(title, message, is_seed, is_passphrase) @wizard_dialog def show_seed_dialog(self, run_next, seed_text): - slayout = SeedWarningLayout(seed_text) + slayout = CreateSeedLayout(seed_text) self.set_main_layout(slayout.layout()) - return seed_text + return seed_text, slayout.passphrase() def pw_layout(self, msg, kind): playout = PasswordLayout(None, msg, kind, self.next_button) t@@ -327,16 +311,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return playout.new_password() @wizard_dialog - 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_PASSPHRASE, PW_PASSPHRASE) - if phrase is None: - raise UserCancelled - return phrase - - @wizard_dialog def request_password(self, run_next): """Request the user enter a new password and confirm it. Return the password or None for no password.""" t@@ -405,13 +379,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.set_main_layout(vbox, '') return clayout.selected_index() - def get_passphrase(self, msg, confirm): - phrase = self.pw_layout(msg, PW_PASSPHRASE) - if phrase is None: - raise UserCancelled - return phrase - - @wizard_dialog def account_id_dialog(self, run_next): message = '\n'.join([ DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -1696,13 +1696,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not self.wallet.has_seed(): self.show_message(_('This wallet has no seed')) return + keystore = self.wallet.get_keystore() try: - mnemonic = self.wallet.get_mnemonic(password) + mnemonic = keystore.get_mnemonic(password) + passphrase = keystore.get_passphrase(password) except BaseException as e: self.show_error(str(e)) return from seed_dialog import SeedDialog - d = SeedDialog(self, mnemonic) + d = SeedDialog(self, mnemonic, passphrase) d.exec_() DIR diff --git a/gui/qt/password_dialog.py b/gui/qt/password_dialog.py t@@ -47,12 +47,12 @@ def check_password_strength(password): return password_strength[min(3, int(score))] -PW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3) +PW_NEW, PW_CHANGE = range(0, 2) class PasswordLayout(object): - titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] + titles = [_("Enter Password"), _("Change Password")] def __init__(self, wallet, msg, kind, OK_button): self.wallet = wallet t@@ -60,8 +60,8 @@ class PasswordLayout(object): self.pw = QLineEdit() self.pw.setEchoMode(2) self.new_pw = QLineEdit() - self.new_pw.setEchoMode(2) self.conf_pw = QLineEdit() + self.new_pw.setEchoMode(2) self.conf_pw.setEchoMode(2) self.kind = kind self.OK_button = OK_button t@@ -76,31 +76,24 @@ class PasswordLayout(object): grid.setColumnMinimumWidth(1, 100) grid.setColumnStretch(1,1) - if kind == PW_PASSPHRASE: - vbox.addWidget(label) - msgs = [_('Passphrase:'), _('Confirm Passphrase:')] + logo_grid = QGridLayout() + logo_grid.setSpacing(8) + logo_grid.setColumnMinimumWidth(0, 70) + logo_grid.setColumnStretch(1,1) + logo = QLabel() + logo.setAlignment(Qt.AlignCenter) + logo_grid.addWidget(logo, 0, 0) + logo_grid.addWidget(label, 0, 1, 1, 2) + vbox.addLayout(logo_grid) + m1 = _('New Password:') if kind == PW_NEW else _('Password:') + msgs = [m1, _('Confirm Password:')] + if wallet and wallet.has_password(): + grid.addWidget(QLabel(_('Current Password:')), 0, 0) + grid.addWidget(self.pw, 0, 1) + lockfile = ":icons/lock.png" else: - logo_grid = QGridLayout() - logo_grid.setSpacing(8) - logo_grid.setColumnMinimumWidth(0, 70) - logo_grid.setColumnStretch(1,1) - - logo = QLabel() - logo.setAlignment(Qt.AlignCenter) - - logo_grid.addWidget(logo, 0, 0) - logo_grid.addWidget(label, 0, 1, 1, 2) - vbox.addLayout(logo_grid) - - m1 = _('New Password:') if kind == PW_NEW else _('Password:') - msgs = [m1, _('Confirm Password:')] - if wallet and wallet.has_password(): - grid.addWidget(QLabel(_('Current Password:')), 0, 0) - grid.addWidget(self.pw, 0, 1) - lockfile = ":icons/lock.png" - else: - lockfile = ":icons/unlock.png" - logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) + lockfile = ":icons/unlock.png" + logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) grid.addWidget(QLabel(msgs[0]), 1, 0) grid.addWidget(self.new_pw, 1, 1) t@@ -110,10 +103,9 @@ class PasswordLayout(object): vbox.addLayout(grid) # Password Strength Label - if kind != PW_PASSPHRASE: - self.pw_strength = QLabel() - grid.addWidget(self.pw_strength, 3, 0, 1, 2) - self.new_pw.textChanged.connect(self.pw_changed) + self.pw_strength = QLabel() + grid.addWidget(self.pw_strength, 3, 0, 1, 2) + self.new_pw.textChanged.connect(self.pw_changed) def enable_OK(): OK_button.setEnabled(self.new_pw.text() == self.conf_pw.text()) t@@ -147,8 +139,7 @@ class PasswordLayout(object): def new_password(self): pw = unicode(self.new_pw.text()) - # Empty passphrases are fine and returned empty. - if pw == "" and self.kind != PW_PASSPHRASE: + if pw == "": pw = None return pw DIR diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py t@@ -38,13 +38,6 @@ def icon_filename(sid): else: return ":icons/seed.png" -class SeedDialog(WindowModalDialog): - def __init__(self, parent, seed): - WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed'))) - self.setMinimumWidth(400) - vbox = QVBoxLayout(self) - vbox.addLayout(SeedWarningLayout(seed).layout()) - vbox.addLayout(Buttons(CloseButton(self))) class SeedLayoutBase(object): def _seed_layout(self, seed=None, title=None, sid=None): t@@ -75,34 +68,124 @@ class SeedLayoutBase(object): return self.seed_e -class SeedInputLayout(SeedLayoutBase): - def __init__(self, title=None, sid=None): - self.layout_ = self._seed_layout(title=title, sid=sid) - class SeedDisplayLayout(SeedLayoutBase): def __init__(self, seed, title=None, sid=None): self.layout_ = self._seed_layout(seed=seed, title=title, sid=sid) -class SeedWarningLayout(SeedLayoutBase): - def __init__(self, seed, title=None): - if title is None: - title = _("Your wallet generation seed is:") - msg = ''.join([ - "<p>", - _("Please save these %d words on paper (order is important). "), - _("This seed will allow you to recover your wallet in case " - "of computer failure."), - "</p>", - "<b>" + _("WARNING") + ":</b> ", - "<ul>", - "<li>" + _("Never disclose your seed.") + "</li>", - "<li>" + _("Never type it on a website.") + "</li>", - "<li>" + _("Do not send your seed to a printer.") + "</li>", - "</ul>" - ]) % len(seed.split()) + +def seed_warning_msg(seed): + return ''.join([ + "<p>", + _("Please save these %d words on paper (order is important). "), + _("This seed will allow you to recover your wallet in case " + "of computer failure."), + "</p>", + "<b>" + _("WARNING") + ":</b> ", + "<ul>", + "<li>" + _("Never disclose your seed.") + "</li>", + "<li>" + _("Never type it on a website.") + "</li>", + "<li>" + _("Do not send your seed to a printer.") + "</li>", + "</ul>" + ]) % len(seed.split()) + + +class CreateSeedLayout(SeedLayoutBase): + + def __init__(self, seed): + title = _("Your wallet generation seed is:") + tooltip = '\n'.join([ + _('You may extend your seed with a passphrase.'), + _('Note tha this is NOT your encryption password.'), + _('If you do not know what it is, leave it empty.'), + ]) vbox = QVBoxLayout() vbox.addLayout(self._seed_layout(seed=seed, title=title)) + self.passphrase_e = QLineEdit() + self.passphrase_e.setToolTip(tooltip) + hbox = QHBoxLayout() + hbox.addStretch() + label = QLabel(_('Passphrase') + ':') + label.setToolTip(tooltip) + hbox.addWidget(label) + hbox.addWidget(self.passphrase_e) + vbox.addLayout(hbox) + msg = seed_warning_msg(seed) vbox.addWidget(WWLabel(msg)) self.layout_ = vbox + + def passphrase(self): + return unicode(self.passphrase_e.text()).strip() + + +class TextInputLayout(SeedLayoutBase): + + def __init__(self, parent, title, is_valid): + self.is_valid = is_valid + self.parent = parent + self.layout_ = self._seed_layout(title=title) + self.seed_e.textChanged.connect(self.on_edit) + + def get_text(self): + return clean_text(self.seed_edit()) + + def on_edit(self): + self.parent.next_button.setEnabled(self.is_valid(self.get_text())) + + +class SeedInputLayout(SeedLayoutBase): + + def __init__(self, parent, title, is_seed, is_passphrase): + vbox = QVBoxLayout() + vbox.addLayout(self._seed_layout(title=title)) + self.passphrase_e = QLineEdit() + hbox = QHBoxLayout() + hbox.addStretch() + hbox.addWidget(QLabel(_('Passphrase') + ':')) + hbox.addWidget(self.passphrase_e) + vbox.addLayout(hbox) + self.layout_ = vbox + self.parent = parent + self.is_seed = is_seed + self.is_passphrase = is_passphrase + self.seed_e.textChanged.connect(self.on_edit) + self.passphrase_e.textChanged.connect(self.on_edit) + + def get_passphrase(self): + return unicode(self.passphrase_e.text()).strip() + + def get_seed(self): + return clean_text(self.seed_edit()) + + def on_edit(self): + self.parent.next_button.setEnabled(self.is_seed(self.get_seed()) and self.is_passphrase(self.get_passphrase())) + + + +class ShowSeedLayout(SeedLayoutBase): + + def __init__(self, seed, passphrase): + title = _("Your wallet generation seed is:") + vbox = QVBoxLayout() + vbox.addLayout(self._seed_layout(seed=seed, title=title)) + if passphrase: + hbox = QHBoxLayout() + passphrase_e = QLineEdit() + passphrase_e.setText(passphrase) + passphrase_e.setReadOnly(True) + hbox.addWidget(QLabel('Your seed passphrase is')) + hbox.addWidget(passphrase_e) + vbox.addLayout(hbox) + msg = seed_warning_msg(seed) + vbox.addWidget(WWLabel(msg)) + self.layout_ = vbox + + +class SeedDialog(WindowModalDialog): + def __init__(self, parent, seed, passphrase): + WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed'))) + self.setMinimumWidth(400) + vbox = QVBoxLayout(self) + vbox.addLayout(ShowSeedLayout(seed, passphrase).layout()) + vbox.addLayout(Buttons(CloseButton(self))) DIR diff --git a/gui/qt/util.py b/gui/qt/util.py t@@ -49,6 +49,12 @@ expiration_values = [ ] +def clean_text(seed_e): + text = unicode(seed_e.toPlainText()).strip() + text = ' '.join(text.split()) + return text + + class Timer(QThread): stopped = False DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py t@@ -40,6 +40,7 @@ class BaseWizard(object): self.stack = [] self.plugin = None self.keystores = [] + self.is_kivy = config.get('gui') == 'kivy' def run(self, *args): action = args[0] t@@ -229,15 +230,15 @@ class BaseWizard(object): def restore_from_seed(self): self.opt_bip39 = True self.opt_ext = True - self.restore_seed_dialog(run_next=self.on_seed, is_valid=keystore.is_seed) + self.restore_seed_dialog(run_next=self.on_seed, is_seed=keystore.is_seed) - def on_seed(self, seed, add_passphrase, is_bip39): + def on_seed(self, seed, passphrase, is_bip39): self.is_bip39 = is_bip39 - f = lambda x: self.run('create_keystore', seed, x) - if add_passphrase: - self.request_passphrase(run_next=f) + if self.is_kivy: + f = lambda x: self.run('create_keystore', seed, x) + self.passphrase_dialog(run_next=f) else: - f('') + self.run('create_keystore', seed, passphrase) def create_keystore(self, seed, passphrase): if self.is_bip39: t@@ -248,7 +249,6 @@ class BaseWizard(object): self.on_keystore(k) def on_bip44(self, seed, passphrase, account_id): - import keystore k = keystore.BIP32_KeyStore({}) bip32_seed = keystore.bip39_to_seed(seed, passphrase) derivation = "m/44'/0'/%d'"%account_id t@@ -311,8 +311,8 @@ class BaseWizard(object): self.opt_ext = True self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed) - def confirm_seed(self, seed): - self.confirm_seed_dialog(run_next=self.on_seed, is_valid=lambda x: x==seed) + def confirm_seed(self, seed, passphrase): + self.confirm_seed_dialog(run_next=self.on_seed, is_seed = lambda x: x==seed, is_passphrase=lambda x: x==passphrase) def create_addresses(self): def task(): DIR diff --git a/lib/keystore.py b/lib/keystore.py t@@ -209,7 +209,7 @@ class Deterministic_KeyStore(Software_KeyStore): return pw_decode(self.seed, password).encode('utf8') def get_passphrase(self, password): - return pw_decode(self.passphrase, password).encode('utf8') + return pw_decode(self.passphrase, password).encode('utf8') if self.passphrase else ''