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
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -317,6 +317,7 @@ def main():
|
||||
'verbosity': '*' if build_config.DEBUG else '',
|
||||
'cmd': 'gui',
|
||||
'gui': 'kivy',
|
||||
'single_password':True,
|
||||
}
|
||||
else:
|
||||
config_options = args.__dict__
|
||||
|
||||
Reference in New Issue
Block a user