twizard: it is better to use a separate screen for passphrase - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit ebab390b1a08d7c5c04288662989a2492b865441 DIR parent 19e62ba64334edcf4d7415189df30595afff9980 HTML Author: ThomasV <thomasv@electrum.org> Date: Tue, 30 Aug 2016 09:51:53 +0200 wizard: it is better to use a separate screen for passphrase Diffstat: M gui/kivy/uix/dialogs/installwizard… | 38 ++++++++++++++++---------------- M gui/qt/installwizard.py | 42 ++++++++++++++----------------- M gui/qt/password_dialog.py | 57 ++++++++++++++++++------------- M gui/qt/seed_dialog.py | 32 ++----------------------------- M lib/base_wizard.py | 76 ++++++++++++++++++++++++------- 5 files changed, 133 insertions(+), 112 deletions(-) --- DIR diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py t@@ -402,18 +402,18 @@ Builder.load_string(''' SeedButton: text: root.seed_text -<PassphraseDialog> +<LineDialog> BigLabel: - text: "SEED PASSPHRASE" + text: root.title SeedLabel: - text: root.passphrase_message + text: root.message GridLayout: cols: 2 size_hint: 1, None height: '27dp' BigLabel: - text: _('Passphrase') + text: '' TextInput: id: passphrase_input multiline: False t@@ -512,14 +512,9 @@ class WizardChoiceDialog(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."), - ]) +class LineDialog(WizardDialog): + title = StringProperty('') + message = StringProperty('') def __init__(self, wizard, **kwargs): WizardDialog.__init__(self, wizard, **kwargs) t@@ -538,7 +533,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@@ -552,7 +547,7 @@ class RestoreSeedDialog(WizardDialog): def __init__(self, wizard, **kwargs): super(RestoreSeedDialog, self).__init__(wizard, **kwargs) - self._test = kwargs['is_seed'] + self._test = kwargs['test'] 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@@ -614,9 +609,6 @@ class RestoreSeedDialog(WizardDialog): text = ' '.join(text.split()) return text - def get_params(self, b): - return (self.get_text(), '', False) - def update_text(self, c): c = c.lower() text = self.ids.text_input_seed.text t@@ -653,6 +645,14 @@ class RestoreSeedDialog(WizardDialog): tis._keyboard.unbind(on_key_down=self.on_key_down) tis.focus = False + def get_params(self, b): + return (self.get_text(), False) + + +class ConfirmSeedDialog(RestoreSeedDialog): + def get_params(self, b): + return (self.get_text(),) + class ShowXpubDialog(WizardDialog): t@@ -744,12 +744,12 @@ 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 line_dialog(self, **kwargs): LineDialog(self, **kwargs).open() def confirm_seed_dialog(self, **kwargs): kwargs['title'] = _('Confirm Seed') kwargs['message'] = _('Please retype your seed phrase, to confirm that you properly saved it') - RestoreSeedDialog(self, **kwargs).open() + ConfirmSeedDialog(self, **kwargs).open() def restore_seed_dialog(self, **kwargs): RestoreSeedDialog(self, **kwargs).open() DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py t@@ -249,24 +249,23 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.set_main_layout(slayout.layout(), title, next_enabled=False) return slayout.get_text() - def seed_input(self, title, message, is_seed, is_passphrase): - slayout = SeedInputLayout(self, message, is_seed, is_passphrase) + def seed_input(self, title, message, is_seed): + slayout = SeedInputLayout(self, message, is_seed) vbox = QVBoxLayout() vbox.addLayout(slayout.layout()) 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() + slayout.is_seed = (lambda x: bool(x)) if b else is_valid + slayout.on_edit() cb_bip39 = QCheckBox(_('BIP39/BIP44 seed')) cb_bip39.toggled.connect(f) vbox.addWidget(cb_bip39) self.set_main_layout(vbox, title, next_enabled=False) seed = slayout.get_seed() - passphrase = slayout.get_passphrase() is_bip39 = cb_bip39.isChecked() if self.opt_bip39 else False - return seed, passphrase, is_bip39 + return seed, is_bip39 @wizard_dialog def restore_keys_dialog(self, title, message, is_valid, run_next): t@@ -282,14 +281,13 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return self.text_input(title, message, is_valid) @wizard_dialog - def restore_seed_dialog(self, run_next, is_seed): + def restore_seed_dialog(self, run_next, test): title = _('Enter Seed') message = _('Please enter your seed phrase in order to restore your wallet.') - is_passphrase = lambda x: True - return self.seed_input(title, message, is_seed, is_passphrase) + return self.seed_input(title, message, test) @wizard_dialog - def confirm_seed_dialog(self, run_next, is_seed, is_passphrase): + def confirm_seed_dialog(self, run_next, test): self.app.clipboard().clear() title = _('Confirm Seed') message = ' '.join([ t@@ -297,13 +295,14 @@ 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_seed, is_passphrase) + seed, is_bip39 = self.seed_input(title, message, test) + return seed @wizard_dialog def show_seed_dialog(self, run_next, seed_text): slayout = CreateSeedLayout(seed_text) self.set_main_layout(slayout.layout()) - return seed_text, slayout.passphrase() + return seed_text def pw_layout(self, msg, kind): playout = PasswordLayout(None, msg, kind, self.next_button) t@@ -380,20 +379,17 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return clayout.selected_index() @wizard_dialog - def account_id_dialog(self, run_next): - message = '\n'.join([ - _('Enter your account number here.'), - _('If you are not sure what this is, leave this field to zero.') - ]) - default = '0' - title = _('Account Number') + def line_dialog(self, run_next, title, message, default, test): + vbox = QVBoxLayout() + vbox.addWidget(WWLabel(message)) line = QLineEdit() line.setText(default) - vbox = QVBoxLayout() - vbox.addWidget(QLabel(message)) + def f(text): + self.next_button.setEnabled(test(text)) + line.textEdited.connect(f) vbox.addWidget(line) - self.set_main_layout(vbox, title) - return int(line.text()) + self.set_main_layout(vbox, title, next_enabled=test(default)) + return ' '.join(unicode(line.text()).split()) @wizard_dialog def show_xpub_dialog(self, xpub, run_next): 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 = range(0, 2) +PW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3) class PasswordLayout(object): - titles = [_("Enter Password"), _("Change Password")] + titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] 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.conf_pw = QLineEdit() self.new_pw.setEchoMode(2) + self.conf_pw = QLineEdit() self.conf_pw.setEchoMode(2) self.kind = kind self.OK_button = OK_button t@@ -76,24 +76,31 @@ class PasswordLayout(object): grid.setColumnMinimumWidth(1, 100) grid.setColumnStretch(1,1) - 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" + if kind == PW_PASSPHRASE: + vbox.addWidget(label) + msgs = [_('Passphrase:'), _('Confirm Passphrase:')] else: - lockfile = ":icons/unlock.png" - logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) + 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)) grid.addWidget(QLabel(msgs[0]), 1, 0) grid.addWidget(self.new_pw, 1, 1) t@@ -103,9 +110,10 @@ class PasswordLayout(object): vbox.addLayout(grid) # Password Strength Label - self.pw_strength = QLabel() - grid.addWidget(self.pw_strength, 3, 0, 1, 2) - self.new_pw.textChanged.connect(self.pw_changed) + 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) def enable_OK(): OK_button.setEnabled(self.new_pw.text() == self.conf_pw.text()) t@@ -139,7 +147,8 @@ class PasswordLayout(object): def new_password(self): pw = unicode(self.new_pw.text()) - if pw == "": + # Empty passphrases are fine and returned empty. + if pw == "" and self.kind != PW_PASSPHRASE: pw = None return pw DIR diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py t@@ -95,29 +95,12 @@ 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): t@@ -136,30 +119,19 @@ class TextInputLayout(SeedLayoutBase): class SeedInputLayout(SeedLayoutBase): - def __init__(self, parent, title, is_seed, is_passphrase): + def __init__(self, parent, title, is_seed): 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())) + self.parent.next_button.setEnabled(self.is_seed(self.get_seed())) DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py t@@ -207,8 +207,21 @@ class BaseWizard(object): self.plugin = self.plugins.get_plugin(name) self.plugin.setup_device(device_info, self) print device_info - f = lambda x: self.run('on_hardware_account_id', name, device_info, x) - self.account_id_dialog(run_next=f) + f = lambda x: self.run('on_hardware_account_id', name, device_info, int(x)) + self.account_id_dialog(f) + + def account_id_dialog(self, f): + message = '\n'.join([ + _('Enter your BIP44 account number here.'), + _('If you are not sure what this is, leave this field to zero.') + ]) + def is_int(x): + try: + int(x) + return True + except: + return False + self.line_dialog(run_next=f, title=_('Account Number'), message=message, default='0', test=is_int) def on_hardware_account_id(self, name, device_info, account_id): from keystore import hardware_keystore, bip44_derivation t@@ -230,23 +243,31 @@ 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_seed=keystore.is_seed) + self.restore_seed_dialog(run_next=self.on_restore_seed, test=keystore.is_seed) + + def on_restore_seed(self, seed, is_bip39): + if keystore.is_new_seed(seed) or is_bip39: + message = '\n'.join([ + _('You may have extended your seed with a passphrase.'), + _('If that is the case, enter it here.'), + _('Note that this is NOT your encryption password.'), + _('If you do not know what this is, leave this field empty.'), + ]) + f = lambda x: self.on_restore_passphrase(seed, x, is_bip39) + self.line_dialog(title=_('Passphrase'), message=message, default='', test=lambda x:True, run_next=f) + else: + self.on_restore_passphrase(seed, '', False) - def on_seed(self, seed, passphrase, is_bip39): - self.is_bip39 = is_bip39 - if self.is_kivy: - f = lambda x: self.run('create_keystore', seed, x) - self.passphrase_dialog(run_next=f) + def on_restore_passphrase(self, seed, passphrase, is_bip39): + if is_bip39: + f = lambda x: self.run('on_bip44', seed, passphrase, int(x)) + self.account_id_dialog(f) else: self.run('create_keystore', seed, passphrase) def create_keystore(self, seed, passphrase): - if self.is_bip39: - f = lambda account_id: self.run('on_bip44', seed, passphrase, account_id) - self.account_id_dialog(run_next=f) - else: - k = keystore.from_seed(seed, passphrase) - self.on_keystore(k) + k = keystore.from_seed(seed, passphrase) + self.on_keystore(k) def on_bip44(self, seed, passphrase, account_id): k = keystore.BIP32_KeyStore({}) t@@ -309,10 +330,33 @@ class BaseWizard(object): seed = Mnemonic('en').make_seed() self.opt_bip39 = False self.opt_ext = True - self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed) + self.show_seed_dialog(run_next=self.request_passphrase, seed_text=seed) + + def request_passphrase(self, seed): + title = _('Passphrase') + message = '\n'.join([ + _('You may extend your seed with a passphrase.'), + _('Note that this is NOT your encryption password.'), + _('If you do not know what this is, leave this field empty.'), + ]) + f = lambda x: self.confirm_seed(seed, x) + self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x:True) 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) + f = lambda x: self.confirm_passphrase(seed, passphrase) + self.confirm_seed_dialog(run_next=f, test=lambda x: x==seed) + + def confirm_passphrase(self, seed, passphrase): + if passphrase: + title = _('Confirm Passphrase') + message = '\n'.join([ + _('Your passphrase must be saved with your seed.'), + _('Please type it here.'), + ]) + f = lambda x: self.create_keystore(seed, x) + self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase) + else: + self.create_keystore(seed, '') def create_addresses(self): def task():