1
0

allow password encryption in hardware wallets

This commit is contained in:
ThomasV
2025-05-23 12:19:14 +02:00
parent f95d4ce287
commit 45c35c0b00
5 changed files with 84 additions and 90 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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()

View File

@@ -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)