diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index fe5a01531..286dc6ac4 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1921,19 +1921,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def change_password_dialog(self): from electrum.storage import StorageEncryptionVersion - if self.wallet.get_available_storage_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD: + if StorageEncryptionVersion.XPUB_PASSWORD in self.wallet.get_available_storage_encryption_versions(): from .password_dialog import ChangePasswordDialogForHW d = ChangePasswordDialogForHW(self, self.wallet) - ok, encrypt_file = d.run() + ok, old_password, new_password, encrypt_with_xpub = d.run() if not ok: return - + has_xpub_encryption = self.wallet.storage.get_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD def on_password(hw_dev_pw): - old_password = hw_dev_pw if self.wallet.has_password() else None - new_password = hw_dev_pw if encrypt_file else None self._update_wallet_password( - old_password=old_password, new_password=new_password, encrypt_storage=encrypt_file) - + old_password = hw_dev_pw if has_xpub_encryption else old_password, + new_password = hw_dev_pw if encrypt_with_xpub else new_password, + xpub_encrypt=encrypt_with_xpub, + ) self.thread.add( self.wallet.keystore.get_password_for_storage_encryption, on_success=on_password) @@ -1944,12 +1944,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if not ok: return self._update_wallet_password( - old_password=old_password, new_password=new_password, encrypt_storage=encrypt_file) + old_password=old_password, new_password=new_password) self.update_lock_menu() - def _update_wallet_password(self, *, old_password, new_password, encrypt_storage: bool): + def _update_wallet_password(self, *, old_password, new_password, xpub_encrypt=False): try: - self.wallet.update_password(old_password, new_password, encrypt_storage=encrypt_storage) + self.wallet.update_password(old_password, new_password, encrypt_storage=True, xpub_encrypt=xpub_encrypt) except InvalidPassword as e: self.show_error(str(e)) return diff --git a/electrum/gui/qt/password_dialog.py b/electrum/gui/qt/password_dialog.py index 48d59be3f..44e11434f 100644 --- a/electrum/gui/qt/password_dialog.py +++ b/electrum/gui/qt/password_dialog.py @@ -76,7 +76,7 @@ class PasswordLayout(object): label = QLabel(msg + "\n") label.setWordWrap(True) - grid = QGridLayout() + self.grid = grid = QGridLayout() grid.setSpacing(8) grid.setColumnMinimumWidth(0, 150) grid.setColumnMinimumWidth(1, 100) @@ -100,7 +100,7 @@ class PasswordLayout(object): m1 = _('New Password:') if kind == PW_CHANGE else _('Password:') msgs = [m1, _('Confirm Password:')] - if wallet and wallet.has_password(): + if wallet and wallet.has_password() and not wallet.storage.is_encrypted_with_hw_device(): grid.addWidget(QLabel(_('Current Password:')), 0, 0) grid.addWidget(self.pw, 0, 1) lockfile = "lock.png" @@ -109,10 +109,12 @@ class PasswordLayout(object): logo.setPixmap(QPixmap(icon_path(lockfile)) .scaledToWidth(36, mode=Qt.TransformationMode.SmoothTransformation)) - grid.addWidget(QLabel(msgs[0]), 1, 0) + self.new_password_label = QLabel(msgs[0]) + grid.addWidget(self.new_password_label, 1, 0) grid.addWidget(self.new_pw, 1, 1) - grid.addWidget(QLabel(msgs[1]), 2, 0) + self.confirm_password_label = QLabel(msgs[1]) + grid.addWidget(self.confirm_password_label, 2, 0) grid.addWidget(self.conf_pw, 2, 1) vbox.addLayout(grid) @@ -166,55 +168,26 @@ class PasswordLayout(object): field.clear() -class PasswordLayoutForHW(object): +class PasswordLayoutForHW(PasswordLayout): - def __init__(self, msg, wallet=None): - self.wallet = wallet + def __init__(self, msg, kind, OK_button, wallet=None): + PasswordLayout.__init__(self, msg, kind, OK_button, wallet=wallet) + self.encrypt_cb = QCheckBox(_('Encrypt wallet file using hardware wallet device')) + self.encrypt_cb.setToolTip(_('If you enable this setting, you will need your hardware device to open your wallet.')) + self.encrypt_cb.stateChanged.connect(self.on_encrypt_cb) + self.grid.addWidget(self.encrypt_cb, 4, 0, 1, 2) + self.encrypt_cb.setChecked(wallet.storage.is_encrypted_with_hw_device() if wallet else True) - vbox = QVBoxLayout() - label = QLabel(msg + "\n") - label.setWordWrap(True) + def on_encrypt_cb(self, checked): + checked = bool(checked) + self.new_pw.setVisible(not checked) + self.conf_pw.setVisible(not checked) + self.new_password_label.setVisible(not checked) + self.confirm_password_label.setVisible(not checked) - 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.AlignmentFlag.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 = "lock.png" - else: - lockfile = "unlock.png" - logo.setPixmap(QPixmap(icon_path(lockfile)) - .scaledToWidth(36, mode=Qt.TransformationMode.SmoothTransformation)) - - vbox.addLayout(grid) - - self.encrypt_cb = QCheckBox(_('Encrypt wallet file')) - grid.addWidget(self.encrypt_cb, 1, 0, 1, 2) - - self.vbox = vbox - - def should_encrypt_storage(self): + def should_encrypt_storage_with_xpub(self): return self.encrypt_cb.isChecked() - def title(self): - return _("Toggle Encryption") - - def layout(self): - return self.vbox class ChangePasswordDialogBase(WindowModalDialog): @@ -298,16 +271,20 @@ class ChangePasswordDialogForHW(ChangePasswordDialogBase): 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(msg) + if wallet.storage.is_encrypted_with_hw_device(): + msg = _('Your wallet file is encrypted with your hardware device.') + else: + msg = _('Your wallet file is password-encrypted.') + self.playout = PasswordLayoutForHW( + msg=msg, + kind=PW_CHANGE, + OK_button=OK_button, + wallet=wallet) def run(self): if not self.exec(): - return False, None - return True, self.playout.should_encrypt_storage() - + return False, None, None, None + return True, self.playout.old_password(), self.playout.new_password(), self.playout.should_encrypt_storage_with_xpub() class PasswordDialog(WindowModalDialog): diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index dc6b3092e..cb8ff4204 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -45,8 +45,8 @@ WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\ 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.") + + _("It also contains your master public key that allows watching your addresses.") + class QENewWalletWizard(NewWalletWizard, QEAbstractWizard, MessageBoxMixin): @@ -984,16 +984,15 @@ class WCImport(WalletWizardComponent): class WCWalletPassword(WalletWizardComponent): + def __init__(self, parent, wizard): WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Password')) - # TODO: PasswordLayout assumes a button, refactor PasswordLayout # for now, fake next_button.setEnabled class Hack: def setEnabled(self2, b): self.valid = b self.next_button = Hack() - self.pw_layout = PasswordLayout( msg=MSG_ENTER_PASSWORD, kind=PW_NEW, @@ -1250,25 +1249,40 @@ class WCChooseHWDevice(WalletWizardComponent, Logger): class WCWalletPasswordHardware(WalletWizardComponent): + def __init__(self, parent, wizard): WalletWizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware')) self.plugins = wizard.plugins - - self.playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION) + # TODO: PasswordLayout assumes a button, refactor PasswordLayout + # for now, fake next_button.setEnabled + class Hack: + def setEnabled(self2, b): + self.valid = b + self.next_button = Hack() + self.playout = PasswordLayoutForHW( + MSG_HW_STORAGE_ENCRYPTION, + kind=PW_NEW, + OK_button=self.next_button, + ) self.layout().addLayout(self.playout.layout()) self.layout().addStretch(1) self._valid = True def apply(self): - self.wizard_data['encrypt'] = self.playout.should_encrypt_storage() - _name, _info = self.wizard_data['hardware_device'] - device_id = _info.device.id_ - client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) - # client.handler = self.plugin.create_handler(self.wizard) - # FIXME client can be None if it was recently disconnected. - # also, even if not None, this might raise (e.g. if it disconnected *just now*): - self.wizard_data['password'] = client.get_password_for_storage_encryption() + self.wizard_data['encrypt'] = True + if self.playout.should_encrypt_storage_with_xpub(): + self.wizard_data['xpub_encrypt'] = True + _name, _info = self.wizard_data['hardware_device'] + device_id = _info.device.id_ + client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + # client.handler = self.plugin.create_handler(self.wizard) + # FIXME client can be None if it was recently disconnected. + # also, even if not None, this might raise (e.g. if it disconnected *just now*): + self.wizard_data['password'] = client.get_password_for_storage_encryption() + else: + self.wizard_data['xpub_encrypt'] = False + self.wizard_data['password'] = self.playout.new_password() class WCHWUnlock(WalletWizardComponent, Logger): diff --git a/electrum/wallet.py b/electrum/wallet.py index f01ffa2fc..f76bcb85b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -441,7 +441,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): self.test_addresses_sanity() if self.storage and self.has_storage_encryption(): - if (se := self.storage.get_encryption_version()) != (ae := self.get_available_storage_encryption_version()): + if (se := self.storage.get_encryption_version()) not in (ae := self.get_available_storage_encryption_versions()): raise WalletFileException(f"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}") self.register_callbacks() @@ -3040,16 +3040,16 @@ class Abstract_Wallet(ABC, Logger, EventListener): def can_have_keystore_encryption(self): return self.keystore and self.keystore.may_have_password() - def get_available_storage_encryption_version(self) -> StorageEncryptionVersion: + def get_available_storage_encryption_versions(self) -> Sequence[StorageEncryptionVersion]: """Returns the type of storage encryption offered to the user. A wallet file (storage) is either encrypted with this version or is stored in plaintext. """ + out = [StorageEncryptionVersion.USER_PASSWORD] if isinstance(self.keystore, Hardware_KeyStore): - return StorageEncryptionVersion.XPUB_PASSWORD - else: - return StorageEncryptionVersion.USER_PASSWORD + out.append(StorageEncryptionVersion.XPUB_PASSWORD) + return out def has_keystore_encryption(self) -> bool: """Returns whether encryption is enabled for the keystore. @@ -3079,13 +3079,14 @@ class Abstract_Wallet(ABC, Logger, EventListener): if self.has_storage_encryption(): self.storage.check_password(password) - def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True): + def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True, xpub_encrypt: bool = False): if old_pw is None and self.has_password(): raise InvalidPassword() self.check_password(old_pw) if self.storage: if encrypt_storage: - enc_version = self.get_available_storage_encryption_version() + enc_version = StorageEncryptionVersion.XPUB_PASSWORD if xpub_encrypt else StorageEncryptionVersion.USER_PASSWORD + assert enc_version in self.get_available_storage_encryption_versions() else: enc_version = StorageEncryptionVersion.PLAINTEXT self.storage.set_password(new_pw, enc_version) @@ -4054,9 +4055,9 @@ class Multisig_Wallet(Deterministic_Wallet): if self.has_storage_encryption(): self.storage.check_password(password) - def get_available_storage_encryption_version(self): + def get_available_storage_encryption_versions(self) -> Sequence[StorageEncryptionVersion]: # multisig wallets are not offered hw device encryption - return StorageEncryptionVersion.USER_PASSWORD + return [StorageEncryptionVersion.USER_PASSWORD] def has_seed(self): return self.keystore.has_seed() diff --git a/electrum/wizard.py b/electrum/wizard.py index 9f9dc7676..8d38f06ae 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -644,9 +644,11 @@ class NewWalletWizard(AbstractWizard): k.update_password(None, data['password']) if data['encrypt']: - enc_version = StorageEncryptionVersion.USER_PASSWORD - if data.get('keystore_type') == 'hardware' and data['wallet_type'] == 'standard': + if data.get('xpub_encrypt'): + assert data.get('keystore_type') == 'hardware' and data['wallet_type'] == 'standard' enc_version = StorageEncryptionVersion.XPUB_PASSWORD + else: + enc_version = StorageEncryptionVersion.USER_PASSWORD storage.set_password(data['password'], enc_version=enc_version) db = WalletDB('', storage=storage, upgrade=True)