electrum

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

commit 1e4fa83098104b34eb104a695ccddb884e519cde
parent 94065414564c24dc2a1724f736b4a0651d1eec9e
Author: ThomasV <thomasv@electrum.org>
Date:   Wed,  2 Dec 2020 10:03:00 +0100

Kivy: use the same password for all wallets

When the app is started, the password is checked against all
wallets in the directory.

If the test passes:
 - subsequent wallet creations will use the same password
 - subsequent password updates will be performed on all wallets
 - wallets that are not storage encrypted will encrypted
   on the next password update (even if they are watching-only)

This behaviour is restricted on Android, with a 'single_password' config variable.
Wallet creation without password is disabled if single_password is set

Diffstat:
Melectrum/gui/kivy/main_window.py | 30+++++++++++++++++++++++++++---
Melectrum/gui/kivy/uix/dialogs/installwizard.py | 5++---
Melectrum/gui/kivy/uix/dialogs/password_dialog.py | 20+++++++++++++++-----
Melectrum/gui/kivy/uix/dialogs/settings.py | 2+-
Melectrum/gui/kivy/uix/dialogs/wallets.py | 8++++++--
Melectrum/wallet.py | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mrun_electrum | 1+
7 files changed, 105 insertions(+), 16 deletions(-)

diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence from electrum.storage import WalletStorage, StorageReadWriteError from electrum.wallet_db import WalletDB from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet +from electrum.wallet import check_password_for_directory, update_password_for_directory + from electrum.plugin import run_hook from electrum import util from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter, @@ -367,6 +369,7 @@ class ElectrumWindow(App, Logger): self.pause_time = 0 self.asyncio_loop = asyncio.get_event_loop() self.password = None + self._use_single_password = False App.__init__(self)#, **kwargs) Logger.__init__(self) @@ -634,6 +637,9 @@ class ElectrumWindow(App, Logger): def on_wizard_success(self, storage, db, password): self.password = password + if self.electrum_config.get('single_password'): + self._use_single_password = check_password_for_directory(self.electrum_config, password) + self.logger.info(f'use single password: {self._use_single_password}') wallet = Wallet(db, storage, config=self.electrum_config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) @@ -649,6 +655,12 @@ class ElectrumWindow(App, Logger): return if self.wallet and self.wallet.storage.path == path: return + if self.password and self._use_single_password: + storage = WalletStorage(path) + # call check_password to decrypt + storage.check_password(self.password) + self.on_open_wallet(self.password, storage) + return d = OpenWalletDialog(self, path, self.on_open_wallet) d.open() @@ -724,10 +736,13 @@ class ElectrumWindow(App, Logger): if self._channels_dialog: Clock.schedule_once(lambda dt: self._channels_dialog.update()) + def is_wallet_creation_disabled(self): + return bool(self.electrum_config.get('single_password')) and self.password is None + def wallets_dialog(self): from .uix.dialogs.wallets import WalletDialog dirname = os.path.dirname(self.electrum_config.get_wallet_path()) - d = WalletDialog(dirname, self.load_wallet_by_name) + d = WalletDialog(dirname, self.load_wallet_by_name, self.is_wallet_creation_disabled()) d.open() def popup_dialog(self, name): @@ -1219,9 +1234,18 @@ class ElectrumWindow(App, Logger): def change_password(self, cb): def on_success(old_password, new_password): - self.wallet.update_password(old_password, new_password) + # called if old_password works on self.wallet self.password = new_password - self.show_info(_("Your password was updated")) + if self._use_single_password: + path = self.wallet.storage.path + self.stop_wallet() + update_password_for_directory(self.electrum_config, old_password, new_password) + self.load_wallet_by_name(path) + msg = _("Password updated successfully") + else: + self.wallet.update_password(old_password, new_password) + msg = _("Password updated for {}").format(os.path.basename(self.wallet.storage.path)) + self.show_info(msg) on_failure = lambda: self.show_error(_("Password not updated")) d = ChangePasswordDialog(self, self.wallet, on_success, on_failure) d.open() diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -1149,9 +1149,8 @@ class InstallWizard(BaseWizard, Widget): Clock.schedule_once(lambda dt: self.app.show_error(msg)) def request_password(self, run_next, force_disable_encrypt_cb=False): - if force_disable_encrypt_cb: - # do not request PIN for watching-only wallets - run_next(None, False) + if self.app.password is not None: + run_next(self.app.password, True) return def on_success(old_pw, pw): assert old_pw is None diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -29,6 +29,7 @@ Builder.load_string(''' message: '' basename:'' is_change: False + hide_wallet_label: False require_password: True BoxLayout: size_hint: 1, 1 @@ -45,13 +46,15 @@ Builder.load_string(''' font_size: '20dp' text: _('Wallet') + ': ' + root.basename text_size: self.width, None + disabled: root.hide_wallet_label + opacity: 0 if root.hide_wallet_label else 1 IconButton: size_hint: 0.15, None height: '40dp' icon: f'atlas://{KIVY_GUI_PATH}/theming/light/btn_create_account' on_release: root.select_file() - disabled: root.is_change - opacity: 0 if root.is_change else 1 + disabled: root.hide_wallet_label or root.is_change + opacity: 0 if root.hide_wallet_label or root.is_change else 1 Widget: size_hint: 1, 0.05 Label: @@ -267,6 +270,7 @@ class PasswordDialog(AbstractPasswordDialog): def __init__(self, app, **kwargs): AbstractPasswordDialog.__init__(self, app, **kwargs) + self.hide_wallet_label = app._use_single_password def clear_password(self): self.ids.textinput_generic_password.text = '' @@ -320,6 +324,7 @@ class ChangePasswordDialog(PasswordDialog): class OpenWalletDialog(PasswordDialog): + """This dialog will let the user choose another wallet file if they don't remember their the password""" def __init__(self, app, path, callback): self.app = app @@ -331,7 +336,7 @@ class OpenWalletDialog(PasswordDialog): def select_file(self): dirname = os.path.dirname(self.app.electrum_config.get_wallet_path()) - d = WalletDialog(dirname, self.init_storage_from_path) + d = WalletDialog(dirname, self.init_storage_from_path, self.app.is_wallet_creation_disabled()) d.open() def init_storage_from_path(self, path): @@ -343,9 +348,14 @@ class OpenWalletDialog(PasswordDialog): elif self.storage.is_encrypted(): if not self.storage.is_encrypted_with_user_pw(): raise Exception("Kivy GUI does not support this type of encrypted wallet files.") - self.require_password = True self.pw_check = self.storage.check_password - self.message = self.enter_pw_message + if self.app.password and self.check_password(self.app.password): + self.pw = self.app.password # must be set so that it is returned in callback + self.require_password = False + self.message = _('Press Next to open') + else: + self.require_password = True + self.message = self.enter_pw_message else: # it is a bit wasteful load the wallet here and load it again in main_window, # but that is fine, because we are progressively enforcing storage encryption. diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py @@ -87,7 +87,7 @@ Builder.load_string(''' CardSeparator SettingsItem: title: _('Password') - description: _("Change wallet password.") + description: _('Change your password') if app._use_single_password else _("Change your password for this wallet.") action: root.change_password CardSeparator SettingsItem: diff --git a/electrum/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py @@ -16,6 +16,7 @@ Builder.load_string(''' title: _('Wallets') id: popup path: '' + disable_new: True BoxLayout: orientation: 'vertical' padding: '10dp' @@ -33,7 +34,8 @@ Builder.load_string(''' cols: 3 size_hint_y: 0.1 Button: - id: open_button + id: new_button + disabled: root.disable_new size_hint: 0.1, None height: '48dp' text: _('New') @@ -53,12 +55,14 @@ Builder.load_string(''' class WalletDialog(Factory.Popup): - def __init__(self, path, callback): + def __init__(self, path, callback, disable_new): Factory.Popup.__init__(self) self.path = path self.callback = callback + self.disable_new = disable_new def new_wallet(self, dirname): + assert self.disable_new is False def cb(filename): if not filename: return diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -2951,12 +2951,63 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig, if gap_limit is not None: db.put('gap_limit', gap_limit) wallet = Wallet(db, storage, config=config) - assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.synchronize() msg = ("This wallet was restored offline. It may contain more addresses than displayed. " "Start a daemon and use load_wallet to sync its history.") - wallet.save_db() return {'wallet': wallet, 'msg': msg} + + +def check_password_for_directory(config, old_password, new_password=None): + """Checks password against all wallets and returns True if they can all be updated. + If new_password is not None, update all wallet passwords to new_password. + """ + dirname = os.path.dirname(config.get_wallet_path()) + failed = [] + for filename in os.listdir(dirname): + path = os.path.join(dirname, filename) + basename = os.path.basename(path) + storage = WalletStorage(path) + if not storage.is_encrypted(): + # it is a bit wasteful load the wallet here, but that is fine + # because we are progressively enforcing storage encryption. + db = WalletDB(storage.read(), manual_upgrades=False) + wallet = Wallet(db, storage, config=config) + if wallet.has_keystore_encryption(): + try: + wallet.check_password(old_password) + except: + failed.append(basename) + continue + if new_password: + wallet.update_password(old_password, new_password) + else: + if new_password: + wallet.update_password(None, new_password) + continue + if not storage.is_encrypted_with_user_pw(): + failed.append(basename) + continue + try: + storage.check_password(old_password) + except: + failed.append(basename) + continue + db = WalletDB(storage.read(), manual_upgrades=False) + wallet = Wallet(db, storage, config=config) + try: + wallet.check_password(old_password) + except: + failed.append(basename) + continue + if new_password: + wallet.update_password(old_password, new_password) + return failed == [] + + +def update_password_for_directory(config, old_password, new_password) -> bool: + assert new_password is not None + assert check_password_for_directory(config, old_password, None) + return check_password_for_directory(config, old_password, new_password) diff --git a/run_electrum b/run_electrum @@ -317,6 +317,7 @@ def main(): 'verbosity': '*' if build_config.DEBUG else '', 'cmd': 'gui', 'gui': 'kivy', + 'single_password':True, } else: config_options = args.__dict__