URI: 
       tKivy: use the same password for all wallets - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 1e4fa83098104b34eb104a695ccddb884e519cde
   DIR parent 94065414564c24dc2a1724f736b4a0651d1eec9e
  HTML 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:
         M electrum/gui/kivy/main_window.py    |      30 +++++++++++++++++++++++++++---
         M electrum/gui/kivy/uix/dialogs/inst… |       5 ++---
         M electrum/gui/kivy/uix/dialogs/pass… |      20 +++++++++++++++-----
         M electrum/gui/kivy/uix/dialogs/sett… |       2 +-
         M electrum/gui/kivy/uix/dialogs/wall… |       8 ++++++--
         M electrum/wallet.py                  |      55 +++++++++++++++++++++++++++++--
         M run_electrum                        |       1 +
       
       7 files changed, 105 insertions(+), 16 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -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,
       t@@ -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)
       t@@ -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)
       t@@ -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()
        
       t@@ -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):
       t@@ -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()
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py
       t@@ -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
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py
       t@@ -29,6 +29,7 @@ Builder.load_string('''
            message: ''
            basename:''
            is_change: False
       +    hide_wallet_label: False
            require_password: True
            BoxLayout:
                size_hint: 1, 1
       t@@ -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:
       t@@ -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 = ''
       t@@ -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
       t@@ -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):
       t@@ -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.
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
       t@@ -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:
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py
       t@@ -16,6 +16,7 @@ Builder.load_string('''
            title: _('Wallets')
            id: popup
            path: ''
       +    disable_new: True
            BoxLayout:
                orientation: 'vertical'
                padding: '10dp'
       t@@ -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')
       t@@ -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
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -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)
   DIR diff --git a/run_electrum b/run_electrum
       t@@ -317,6 +317,7 @@ def main():
                    'verbosity': '*' if build_config.DEBUG else '',
                    'cmd': 'gui',
                    'gui': 'kivy',
       +            'single_password':True,
                }
            else:
                config_options = args.__dict__