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