Skip to content

Commit

Permalink
Kivy: use the same password for all wallets
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ecdsa committed Jan 13, 2021
1 parent 9406541 commit 1e4fa83
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 16 deletions.
30 changes: 27 additions & 3 deletions electrum/gui/kivy/main_window.py
Expand Up @@ -12,6 +12,8 @@
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,
Expand Down Expand Up @@ -367,6 +369,7 @@ def __init__(self, **kwargs):
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)
Expand Down Expand Up @@ -634,6 +637,9 @@ def get_wallet_path(self):

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)
Expand All @@ -649,6 +655,12 @@ def load_wallet_by_name(self, path):
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()

Expand Down Expand Up @@ -724,10 +736,13 @@ def on_channels(self, evt, wallet):
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):
Expand Down Expand Up @@ -1219,9 +1234,18 @@ def check_pin_code(self, pin):

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()
Expand Down
5 changes: 2 additions & 3 deletions electrum/gui/kivy/uix/dialogs/installwizard.py
Expand Up @@ -1149,9 +1149,8 @@ def show_error(self, msg):
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
Expand Down
20 changes: 15 additions & 5 deletions electrum/gui/kivy/uix/dialogs/password_dialog.py
Expand Up @@ -29,6 +29,7 @@
message: ''
basename:''
is_change: False
hide_wallet_label: False
require_password: True
BoxLayout:
size_hint: 1, 1
Expand All @@ -45,13 +46,15 @@
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:
Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -320,6 +324,7 @@ def __init__(self, app, wallet, on_success, on_failure):


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
Expand All @@ -331,7 +336,7 @@ def __init__(self, app, path, callback):

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):
Expand All @@ -343,9 +348,14 @@ def init_storage_from_path(self, path):
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.
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/kivy/uix/dialogs/settings.py
Expand Up @@ -87,7 +87,7 @@
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:
Expand Down
8 changes: 6 additions & 2 deletions electrum/gui/kivy/uix/dialogs/wallets.py
Expand Up @@ -16,6 +16,7 @@
title: _('Wallets')
id: popup
path: ''
disable_new: True
BoxLayout:
orientation: 'vertical'
padding: '10dp'
Expand All @@ -33,7 +34,8 @@
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')
Expand All @@ -53,12 +55,14 @@

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
Expand Down
55 changes: 53 additions & 2 deletions electrum/wallet.py
Expand Up @@ -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)
1 change: 1 addition & 0 deletions run_electrum
Expand Up @@ -317,6 +317,7 @@ def main():
'verbosity': '*' if build_config.DEBUG else '',
'cmd': 'gui',
'gui': 'kivy',
'single_password':True,
}
else:
config_options = args.__dict__
Expand Down

0 comments on commit 1e4fa83

Please sign in to comment.