electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 5259fcb6fd0cbb2ae77c55dc4efa1ca37350baab
parent c798e5d9a19f42c86aa09bb48fda465ddc6e774d
Author: SomberNight <somber.night@protonmail.com>
Date:   Tue,  7 Apr 2020 18:04:04 +0200

qt PasswordLineEdit: try to clear password from memory

If an attacker has access to the process' memory, it's probably already game over,
still we can make their life a bit harder.

I really tried but failed to encapsulate this logic inside PasswordLineEdit.
The destroyed signal arrives too late.
deleteLater is not called.
__del__ gets called too late.

Diffstat:
Melectrum/gui/qt/installwizard.py | 101+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Melectrum/gui/qt/password_dialog.py | 23+++++++++++++++++------
Melectrum/gui/qt/util.py | 6++++++
3 files changed, 78 insertions(+), 52 deletions(-)

diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py @@ -281,51 +281,57 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): name_e.textChanged.connect(on_filename) name_e.setText(os.path.basename(path)) - while True: - if self.loop.exec_() != 2: # 2 = next - raise UserCancelled - assert temp_storage - if temp_storage.file_exists() and not temp_storage.is_encrypted(): - break - if not temp_storage.file_exists(): - break - wallet_from_memory = get_wallet_from_daemon(temp_storage.path) - if wallet_from_memory: - raise WalletAlreadyOpenInMemory(wallet_from_memory) - if temp_storage.file_exists() and temp_storage.is_encrypted(): - if temp_storage.is_encrypted_with_user_pw(): - password = pw_e.text() - try: - temp_storage.decrypt(password) - break - except InvalidPassword as e: - self.show_message(title=_('Error'), msg=str(e)) - continue - except BaseException as e: - self.logger.exception('') - self.show_message(title=_('Error'), msg=repr(e)) - raise UserCancelled() - elif temp_storage.is_encrypted_with_hw_device(): - try: - self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET, storage=temp_storage) - except InvalidPassword as e: - self.show_message(title=_('Error'), - msg=_('Failed to decrypt using this hardware device.') + '\n' + - _('If you use a passphrase, make sure it is correct.')) - self.reset_stack() - return self.select_storage(path, get_wallet_from_daemon) - except (UserCancelled, GoBack): - raise - except BaseException as e: - self.logger.exception('') - self.show_message(title=_('Error'), msg=repr(e)) - raise UserCancelled() - if temp_storage.is_past_initial_decryption(): - break + def run_user_interaction_loop(): + while True: + if self.loop.exec_() != 2: # 2 = next + raise UserCancelled + assert temp_storage + if temp_storage.file_exists() and not temp_storage.is_encrypted(): + break + if not temp_storage.file_exists(): + break + wallet_from_memory = get_wallet_from_daemon(temp_storage.path) + if wallet_from_memory: + raise WalletAlreadyOpenInMemory(wallet_from_memory) + if temp_storage.file_exists() and temp_storage.is_encrypted(): + if temp_storage.is_encrypted_with_user_pw(): + password = pw_e.text() + try: + temp_storage.decrypt(password) + break + except InvalidPassword as e: + self.show_message(title=_('Error'), msg=str(e)) + continue + except BaseException as e: + self.logger.exception('') + self.show_message(title=_('Error'), msg=repr(e)) + raise UserCancelled() + elif temp_storage.is_encrypted_with_hw_device(): + try: + self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET, storage=temp_storage) + except InvalidPassword as e: + self.show_message(title=_('Error'), + msg=_('Failed to decrypt using this hardware device.') + '\n' + + _('If you use a passphrase, make sure it is correct.')) + self.reset_stack() + return self.select_storage(path, get_wallet_from_daemon) + except (UserCancelled, GoBack): + raise + except BaseException as e: + self.logger.exception('') + self.show_message(title=_('Error'), msg=repr(e)) + raise UserCancelled() + if temp_storage.is_past_initial_decryption(): + break + else: + raise UserCancelled() else: - raise UserCancelled() - else: - raise Exception('Unexpected encryption version') + raise Exception('Unexpected encryption version') + + try: + run_user_interaction_loop() + finally: + pw_e.clear() return temp_storage.path, (temp_storage if temp_storage.file_exists() else None) @@ -482,8 +488,11 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): playout = PasswordLayout(msg=msg, kind=kind, OK_button=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() + try: + self.exec_layout(playout.layout()) + return playout.new_password(), playout.encrypt_cb.isChecked() + finally: + playout.clear_password_fields() @wizard_dialog def request_password(self, run_next, force_disable_encrypt_cb=False): diff --git a/electrum/gui/qt/password_dialog.py b/electrum/gui/qt/password_dialog.py @@ -25,6 +25,7 @@ import re import math +from functools import partial from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap @@ -165,6 +166,10 @@ class PasswordLayout(object): pw = None return pw + def clear_password_fields(self): + for field in [self.pw, self.new_pw, self.conf_pw]: + field.clear() + class PasswordLayoutForHW(object): @@ -258,9 +263,12 @@ class ChangePasswordDialogForSW(ChangePasswordDialogBase): force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) def run(self): - if not self.exec_(): - return False, None, None, None - return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() + try: + if not self.exec_(): + return False, None, None, None + return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() + finally: + self.playout.clear_password_fields() class ChangePasswordDialogForHW(ChangePasswordDialogBase): @@ -301,6 +309,9 @@ class PasswordDialog(WindowModalDialog): run_hook('password_dialog', pw, grid, 1) def run(self): - if not self.exec_(): - return - return self.pw.text() + try: + if not self.exec_(): + return + return self.pw.text() + finally: + self.pw.clear() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py @@ -753,6 +753,12 @@ class PasswordLineEdit(QLineEdit): QLineEdit.__init__(self, *args, **kwargs) self.setEchoMode(QLineEdit.Password) + def clear(self): + # Try to actually overwrite the memory. + # This is really just a best-effort thing... + self.setText(len(self.text()) * " ") + super().clear() + class TaskThread(QThread): '''Thread that runs background tasks. Callbacks are guaranteed