1
0

allow encrypting watch-only wallets. initial support for hw wallet storage encryption.

This commit is contained in:
SomberNight
2017-12-07 11:35:10 +01:00
committed by SomberNight
parent 743ef9ec8f
commit c811c5c9d9
20 changed files with 507 additions and 146 deletions

View File

@@ -807,7 +807,7 @@ class InstallWizard(BaseWizard, Widget):
popup.init(message, callback)
popup.open()
def request_password(self, run_next):
def request_password(self, run_next, force_disable_encrypt_cb=False):
def callback(pin):
if pin:
self.run('confirm_password', pin, run_next)

View File

@@ -10,13 +10,13 @@ from PyQt5.QtWidgets import *
from electrum import Wallet, WalletStorage
from electrum.util import UserCancelled, InvalidPassword
from electrum.base_wizard import BaseWizard
from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET
from electrum.i18n import _
from .seed_dialog import SeedLayout, KeysLayout
from .network_dialog import NetworkChoiceLayout
from .util import *
from .password_dialog import PasswordLayout, PW_NEW
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
class GoBack(Exception):
@@ -29,6 +29,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_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
MSG_RESTORE_PASSPHRASE = \
_("Please enter your seed derivation passphrase. "
"Note: this is NOT your encryption password. "
@@ -196,12 +200,18 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
msg =_("This file does not exist.") + '\n' \
+ _("Press 'Next' to create this wallet, or choose another file.")
pw = False
elif self.storage.file_exists() and self.storage.is_encrypted():
msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.')
pw = True
else:
msg = _("Press 'Next' to open this wallet.")
pw = False
if self.storage.is_encrypted_with_user_pw():
msg = _("This file is encrypted with a password.") + '\n' \
+ _('Enter your password or choose another file.')
pw = True
elif self.storage.is_encrypted_with_hw_device():
msg = _("This file is encrypted using a hardware device.") + '\n' \
+ _("Press 'Next' to choose device to decrypt.")
pw = False
else:
msg = _("Press 'Next' to open this wallet.")
pw = False
else:
msg = _('Cannot read file')
pw = False
@@ -227,17 +237,40 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if not self.storage.file_exists():
break
if self.storage.file_exists() and self.storage.is_encrypted():
password = self.pw_e.text()
try:
self.storage.decrypt(password)
break
except InvalidPassword as e:
QMessageBox.information(None, _('Error'), str(e))
continue
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
if self.storage.is_encrypted_with_user_pw():
password = self.pw_e.text()
try:
self.storage.decrypt(password)
break
except InvalidPassword as e:
QMessageBox.information(None, _('Error'), str(e))
continue
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
elif self.storage.is_encrypted_with_hw_device():
try:
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
except InvalidPassword as e:
# FIXME if we get here because of mistyped passphrase
# then that passphrase gets "cached"
QMessageBox.information(
None, _('Error'),
_('Failed to decrypt using this hardware device.') + '\n' +
_('If you use a passphrase, make sure it is correct.'))
self.stack = []
return self.run_and_get_wallet()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
if self.storage.is_past_initial_decryption():
break
else:
return
else:
raise Exception('Unexpected encryption version')
path = self.storage.path
if self.storage.requires_split():
@@ -386,17 +419,25 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.exec_layout(slayout)
return slayout.is_ext
def pw_layout(self, msg, kind):
playout = PasswordLayout(None, msg, kind, self.next_button)
def pw_layout(self, msg, kind, force_disable_encrypt_cb):
playout = PasswordLayout(None, msg, kind, self.next_button,
force_disable_encrypt_cb=force_disable_encrypt_cb)
playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout())
return playout.new_password(), playout.encrypt_cb.isChecked()
@wizard_dialog
def request_password(self, run_next):
def request_password(self, run_next, force_disable_encrypt_cb=False):
"""Request the user enter a new password and confirm it. Return
the password or None for no password."""
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW)
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
@wizard_dialog
def request_storage_encryption(self, run_next):
playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout())
return playout.encrypt_cb.isChecked()
def show_restore(self, wallet, network):
# FIXME: these messages are shown after the install wizard is

View File

@@ -372,7 +372,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
extra.append(_('watching only'))
title += ' [%s]'% ', '.join(extra)
self.setWindowTitle(title)
self.password_menu.setEnabled(self.wallet.can_change_password())
self.password_menu.setEnabled(self.wallet.may_have_password())
self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
self.import_address_menu.setVisible(self.wallet.can_import_address())
self.export_menu.setEnabled(self.wallet.can_export())
@@ -877,14 +877,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if alias_addr:
if self.wallet.is_mine(alias_addr):
msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
password = self.password_dialog(msg)
if password:
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
password = None
if self.wallet.has_keystore_encryption():
password = self.password_dialog(msg)
if not password:
return
else:
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
return
else:
return
@@ -1372,7 +1373,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def request_password(self, *args, **kwargs):
parent = self.top_level_window()
password = None
while self.wallet.has_password():
while self.wallet.has_keystore_encryption():
password = self.password_dialog(parent=parent)
if password is None:
# User cancelled password input
@@ -1506,7 +1507,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if fee > confirm_rate * tx.estimated_size() / 1000:
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
if self.wallet.has_password():
if self.wallet.has_keystore_encryption():
msg.append("")
msg.append(_("Enter your password to proceed"))
password = self.password_dialog('\n'.join(msg))
@@ -1909,17 +1910,37 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def update_buttons_on_seed(self):
self.seed_button.setVisible(self.wallet.has_seed())
self.password_button.setVisible(self.wallet.can_change_password())
self.password_button.setVisible(self.wallet.may_have_password())
self.send_button.setVisible(not self.wallet.is_watching_only())
def change_password_dialog(self):
from .password_dialog import ChangePasswordDialog
d = ChangePasswordDialog(self, self.wallet)
ok, password, new_password, encrypt_file = d.run()
from electrum.storage import STO_EV_XPUB_PW
if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW:
from .password_dialog import ChangePasswordDialogForHW
d = ChangePasswordDialogForHW(self, self.wallet)
ok, encrypt_file = d.run()
if not ok:
return
try:
hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
except UserCancelled:
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.show_error(str(e))
return
old_password = hw_dev_pw if self.wallet.has_password() else None
new_password = hw_dev_pw if encrypt_file else None
else:
from .password_dialog import ChangePasswordDialogForSW
d = ChangePasswordDialogForSW(self, self.wallet)
ok, old_password, new_password, encrypt_file = d.run()
if not ok:
return
try:
self.wallet.update_password(password, new_password, encrypt_file)
self.wallet.update_password(old_password, new_password, encrypt_file)
except BaseException as e:
self.show_error(str(e))
return
@@ -1927,7 +1948,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
traceback.print_exc(file=sys.stdout)
self.show_error(_('Failed to update password'))
return
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected')
msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
self.show_message(msg, title=_("Success"))
self.update_lock_icon()

View File

@@ -57,7 +57,7 @@ class PasswordLayout(object):
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
def __init__(self, wallet, msg, kind, OK_button):
def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
self.wallet = wallet
self.pw = QLineEdit()
@@ -126,7 +126,8 @@ class PasswordLayout(object):
def enable_OK():
ok = self.new_pw.text() == self.conf_pw.text()
OK_button.setEnabled(ok)
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()))
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
and not force_disable_encrypt_cb)
self.new_pw.textChanged.connect(enable_OK)
self.conf_pw.textChanged.connect(enable_OK)
@@ -163,11 +164,84 @@ class PasswordLayout(object):
return pw
class ChangePasswordDialog(WindowModalDialog):
class PasswordLayoutForHW(object):
def __init__(self, wallet, msg, kind, OK_button):
self.wallet = wallet
self.kind = kind
self.OK_button = OK_button
vbox = QVBoxLayout()
label = QLabel(msg + "\n")
label.setWordWrap(True)
grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnMinimumWidth(0, 150)
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)
if wallet and wallet.has_storage_encryption():
lockfile = ":icons/lock.png"
else:
lockfile = ":icons/unlock.png"
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
vbox.addLayout(grid)
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
self.vbox = vbox
def title(self):
return _("Toggle Encryption")
def layout(self):
return self.vbox
class ChangePasswordDialogBase(WindowModalDialog):
def __init__(self, parent, wallet):
WindowModalDialog.__init__(self, parent)
is_encrypted = wallet.storage.is_encrypted()
is_encrypted = wallet.has_storage_encryption()
OK_button = OkButton(self)
self.create_password_layout(wallet, is_encrypted, OK_button)
self.setWindowTitle(self.playout.title())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted)
def create_password_layout(self, wallet, is_encrypted, OK_button):
raise NotImplementedError()
class ChangePasswordDialogForSW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
if not wallet.has_password():
self.playout.encrypt_cb.setChecked(True)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not wallet.has_password():
msg = _('Your wallet is not protected.')
msg += ' ' + _('Use this dialog to add a password to your wallet.')
@@ -177,14 +251,9 @@ class ChangePasswordDialog(WindowModalDialog):
else:
msg = _('Your wallet is password protected and encrypted.')
msg += ' ' + _('Use this dialog to change your password.')
OK_button = OkButton(self)
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button)
self.setWindowTitle(self.playout.title())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
self.playout = PasswordLayout(
wallet, msg, PW_CHANGE, OK_button,
force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
def run(self):
if not self.exec_():
@@ -192,6 +261,26 @@ class ChangePasswordDialog(WindowModalDialog):
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
class ChangePasswordDialogForHW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not is_encrypted:
msg = _('Your wallet file is NOT encrypted.')
else:
msg = _('Your wallet file is encrypted.')
msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
msg += '\n' + _('Use this dialog to toggle encryption.')
self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
def run(self):
if not self.exec_():
return False, None
return True, self.playout.encrypt_cb.isChecked()
class PasswordDialog(WindowModalDialog):
def __init__(self, parent=None, msg=None):