diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index d01ac5512..1228e24ad 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -1199,21 +1199,31 @@ class ElectrumWindow(App): on_success=on_success, on_failure=on_failure, is_change=1) self._password_dialog.open() - def save_backup(self): + def change_backup_password(self): from .uix.dialogs.password_dialog import PasswordDialog from electrum.util import get_backup_dir + from electrum.storage import WalletStorage if self._password_dialog is None: self._password_dialog = PasswordDialog() message = _("Create backup.") + '\n' + _("Enter your current PIN:") def on_success(old_password, new_password): - new_path = os.path.join(get_backup_dir(self.electrum_config), self.wallet.basename() + '.backup') - self.wallet.save_backup(new_path, old_password=old_password, new_password=new_password) - self.show_info(_("Backup saved:") + f"\n{new_path}") - on_failure = lambda: self.show_error(_("PIN codes do not match")) + backup_pubkey = WalletStorage.get_eckey_from_password(new_password).get_public_key_hex() + # TODO: use a unique PIN for all wallets + self.electrum_config.set_key('pin_code', old_password) + self.electrum_config.set_key('backup_pubkey', backup_pubkey) + self.show_info(_("Backup password set")) + on_failure = lambda: self.show_error(_("Passwords do not match")) self._password_dialog.init(self, wallet=self.wallet, msg=message, on_success=on_success, on_failure=on_failure, is_change=1, is_backup=True) self._password_dialog.open() + def save_backup(self): + new_path = self.wallet.save_backup() + if new_path: + self.show_info(_("Backup saved:") + f"\n{new_path}") + else: + self.show_error(_("Backup directory not configured")) + def export_private_keys(self, pk_label, addr): if self.wallet.is_watching_only(): self.show_info(_('This is a watching-only wallet. It does not contain private keys.')) diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py index dddd501be..4f9fe3c9d 100644 --- a/electrum/gui/kivy/uix/dialogs/settings.py +++ b/electrum/gui/kivy/uix/dialogs/settings.py @@ -82,6 +82,11 @@ Builder.load_string(''' description: _("Send your change to separate addresses.") message: _('Send excess coins to change addresses') action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message) + CardSeparator + SettingsItem: + title: _('Backups') + description: _("Set password for encrypted backups.") + action: root.change_backup_password # disabled: there is currently only one coin selection policy #CardSeparator @@ -121,6 +126,9 @@ class SettingsDialog(Factory.Popup): def change_password(self, item, dt): self.app.change_password(self.update) + def change_backup_password(self, dt): + self.app.change_backup_password() + def language_dialog(self, item, dt): if self._language_dialog is None: l = self.config.get('language', 'en_UK') diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index bbfacbee1..d07456854 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -562,20 +562,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.gui_object.new_window(filename) def backup_wallet(self): - path = self.wallet.storage.path - wallet_folder = os.path.dirname(path) - filename, __ = QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) - if not filename: - return - new_path = os.path.join(wallet_folder, filename) - if new_path == path: - return try: - self.wallet.save_backup(new_path) + new_path = self.wallet.save_backup() except BaseException as reason: self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup")) return - self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created")) + if new_path: + self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created")) + else: + self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not created")) def update_recently_visited(self, filename): recent = self.config.get('recently_open', []) @@ -617,7 +612,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.recently_visited_menu = file_menu.addMenu(_("&Recently open")) file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open) file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New) - file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) + file_menu.addAction(_("&Save backup"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) file_menu.addAction(_("Delete"), self.remove_wallet) file_menu.addSeparator() file_menu.addAction(_("&Quit"), self.close) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 246bf9537..b9de66d92 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -145,6 +145,14 @@ class SettingsDialog(WindowModalDialog): # lightning lightning_widgets = [] + + backup_help = _("""A backup of your wallet file will be saved to that directory everytime you create a new channel. The backup cannot be used to perform lightning transactions; it may only be used to retrieve the funds in your open channels, using data loss protect (channels will be force closed).""") + backup_dir = self.config.get('backup_dir') + backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help) + self.backup_dir_e = QPushButton(backup_dir) + self.backup_dir_e.clicked.connect(self.select_backup_dir) + lightning_widgets.append((backup_dir_label, self.backup_dir_e)) + help_persist = _("""If this option is checked, Electrum will persist as a daemon after you close all your wallet windows. Your local watchtower will keep running, and it will protect your channels even if your wallet is not @@ -546,6 +554,13 @@ that is always connected to the internet. Configure a port if you want it to be if alias: self.window.fetch_alias() + def select_backup_dir(self, b): + name = self.config.get('backup_dir', '') + dirname = QFileDialog.getExistingDirectory(self, "Select your SSL certificate file", name) + if dirname: + self.config.set_key('backup_dir', dirname) + self.backup_dir_e.setText(dirname) + def select_ssl_certfile(self, b): name = self.config.get('ssl_certfile', '') filename, __ = QFileDialog.getOpenFileName(self, "Select your SSL certificate file", name) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 49cba4f93..aa3495886 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -842,6 +842,7 @@ class LNWallet(LNWorker): with self.lock: self.channels[chan.channel_id] = chan self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) + self.wallet.save_backup() @log_exceptions async def add_peer(self, connect_str: str) -> Peer: diff --git a/electrum/util.py b/electrum/util.py index cba0cbbd4..9e6d613dd 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -442,7 +442,7 @@ def android_data_dir(): return PythonActivity.mActivity.getFilesDir().getPath() + '/data' def get_backup_dir(config): - return android_backup_dir() if 'ANDROID_DATA' in os.environ else config.path + return android_backup_dir() if 'ANDROID_DATA' in os.environ else config.get('backup_dir') def ensure_sparse_file(filename): diff --git a/electrum/wallet.py b/electrum/wallet.py index 9e22e6ab0..cc8d33d88 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -51,7 +51,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) -from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN +from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN, get_backup_dir from .simple_config import SimpleConfig from .bitcoin import (COIN, is_address, address_to_script, is_minikey, relayfee, dust_threshold) @@ -263,15 +263,25 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.storage: self.db.write(self.storage) - def save_backup(self, path, *, old_password=None, new_password=None): + def save_backup(self): new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db.put('is_backup', True) - new_storage = WalletStorage(path) - new_storage._encryption_version = StorageEncryptionVersion.PLAINTEXT - w2 = Wallet(new_db, new_storage, config=self.config) - if new_password: - w2.update_password(old_password, new_password, encrypt_storage=True) - w2.save_db() + new_path = os.path.join(get_backup_dir(self.config), self.basename() + '.backup') + if new_path is None: + return + new_storage = WalletStorage(new_path) + if 'ANDROID_DATA' in os.environ: + pin_code = self.config.get('pin_code') + w2 = Wallet(new_db, None, config=self.config) + w2.update_password(pin_code, None) + new_storage._encryption_version = StorageEncryptionVersion.USER_PASSWORD + new_storage.pubkey = self.config.get('backup_pubkey') + else: + new_storage._encryption_version = self.storage._encryption_version + new_storage.pubkey = self.storage.pubkey + new_db.set_modified(True) + new_db.write(new_storage) + return new_path def has_lightning(self): return bool(self.lnworker)