From 62eceeb573b0fcd6874f41e22ce142d2d99ea3d0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 30 Jan 2020 11:43:59 +0100 Subject: [PATCH 01/12] Save and read lighting backups (Qt) --- electrum/gui/qt/__init__.py | 1 + electrum/gui/qt/channels_list.py | 17 +++++++++++------ electrum/gui/qt/main_window.py | 27 ++++++++++++++++++++------- electrum/wallet.py | 12 +++++++++++- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index a07469a92..43bc5585e 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -233,6 +233,7 @@ class ElectrumGui(Logger): run_hook('on_new_window', w) w.warn_if_testnet() w.warn_if_watching_only() + w.warn_if_lightning_backup() return w def count_wizards_in_progress(func): diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index be34aa9d7..255ff251d 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates from electrum.i18n import _ -from electrum.lnchannel import Channel +from electrum.lnchannel import Channel, peer_states from electrum.wallet import Abstract_Wallet from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT @@ -84,10 +84,14 @@ class ChannelsList(MyTreeView): WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure) def force_close(self, channel_id): - def task(): - coro = self.lnworker.force_close_channel(channel_id) - return self.network.run_from_another_thread(coro) - if self.parent.question('Force-close channel?\nReclaimed funds will not be immediately available.'): + if self.lnworker.wallet.is_lightning_backup(): + msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?') + else: + msg = _('Force-close channel?\nReclaimed funds will not be immediately available.') + if self.parent.question(msg): + def task(): + coro = self.lnworker.force_close_channel(channel_id) + return self.network.run_from_another_thread(coro) WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure) def remove_channel(self, channel_id): @@ -105,7 +109,8 @@ class ChannelsList(MyTreeView): menu.addAction(_("Details..."), lambda: self.details(channel_id)) self.add_copy_menu(menu, idx) if not chan.is_closed(): - menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) + if chan.peer_state == peer_states.GOOD: + menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id)) else: menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 65d8644f9..bbfacbee1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -513,6 +513,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): ]) self.show_warning(msg, title=_('Watch-only wallet')) + def warn_if_lightning_backup(self): + if self.wallet.is_lightning_backup(): + msg = '\n\n'.join([ + _("This file is a backup of a lightning wallet."), + _("You will not be able to perform lightning payments using this file, and the lightning balance displayed in this wallet might be outdated.") + ' ' + \ + _("If you have lost the original wallet file, you can use this file to trigger a forced closure of your channels."), + _("Do you want to have your channels force-closed?") + ]) + if self.question(msg, title=_('Lightning Backup')): + self.network.maybe_init_lightning() + self.wallet.lnworker.start_network(self.network) + def warn_if_testnet(self): if not constants.net.TESTNET: return @@ -549,7 +561,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return self.gui_object.new_window(filename) - def backup_wallet(self): path = self.wallet.storage.path wallet_folder = os.path.dirname(path) @@ -557,12 +568,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if not filename: return new_path = os.path.join(wallet_folder, filename) - if new_path != path: - try: - shutil.copy2(path, new_path) - self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created")) - 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")) + if new_path == path: + return + try: + self.wallet.save_backup(new_path) + 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")) def update_recently_visited(self, filename): recent = self.config.get('recently_open', []) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6543e5498..542cd1178 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -263,6 +263,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.storage: self.db.write(self.storage) + def save_backup(self, path): + # fixme: we need to change password... + new_storage = WalletStorage(path) + self.db.put('is_backup', True) + self.db.write(new_storage) + self.db.put('is_backup', None) + def has_lightning(self): return bool(self.lnworker) @@ -285,6 +292,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.db.put('lightning_privkey2', None) self.save_db() + def is_lightning_backup(self): + return self.has_lightning() and self.db.get('is_backup') + def stop_threads(self): super().stop_threads() if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): @@ -301,7 +311,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): def start_network(self, network): AddressSynchronizer.start_network(self, network) - if self.lnworker and network: + if self.lnworker and network and not self.is_lightning_backup(): network.maybe_init_lightning() self.lnworker.start_network(network) From 2f10bc2f4d79dcfde39989772e23b4e17e02bea9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Feb 2020 15:57:12 +0100 Subject: [PATCH 02/12] reestablish: force_close if we are a recent backup --- electrum/lnpeer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index cc9d6e36e..63e8486b6 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -848,6 +848,10 @@ class Peer(Logger): self.logger.warning(f"channel_reestablish: we are ahead of remote! trying to force-close.") await self.lnworker.force_close_channel(chan_id) return + elif self.lnworker.wallet.is_lightning_backup(): + self.logger.warning(f"channel_reestablish: force-closing because we are a recent backup") + await self.lnworker.force_close_channel(chan_id) + return chan.peer_state = peer_states.GOOD # note: chan.short_channel_id being set implies the funding txn is already at sufficient depth From d6b4b19824b6cb4cdd86ad5ec28b65fc8bbeab86 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 19 Dec 2019 20:39:12 +0100 Subject: [PATCH 03/12] kivy: allow generic passwords for wallets --- electrum/gui/kivy/theming/light/eye1.png | Bin 0 -> 2910 bytes .../gui/kivy/uix/dialogs/password_dialog.py | 68 +++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 electrum/gui/kivy/theming/light/eye1.png diff --git a/electrum/gui/kivy/theming/light/eye1.png b/electrum/gui/kivy/theming/light/eye1.png new file mode 100644 index 0000000000000000000000000000000000000000..0bded339f0f49f4ebb530dde10d07c60302e4e8f GIT binary patch literal 2910 zcmV-k3!(IhP)u5FtW@2oWMgh!7z{ga{ELMEHM0{Lt-{ zmM%_kY1J51mkCud0idW>6rEM)R=PKDezyaVKUjypPvbLZ-ZD~Zl^Y~X1||U$fw&>6 zbxOnm1-T#CFWlma$BAvJPQzFK2lBvluIYg?7R4eKy$GbZoKg>LtcLFGg64x1*H4rQF=WdkH@ci)yes|B zI*me6)@5W_;&He<*c|TgjR=88qs8rXVu(w`?X+X>Z1WcnXe(91T{(HOuE;<@En8m)7u6fE%7)$4UEzHK2_Po2yc`wszNh>2&+b#oEIyZ-658tyF~ z&y+DKj7d#kQjUd&b}Qd>^@MFpC<;gw)#JgGm_mE~NmP$FXt$NmXX@mM96wQw+wJ!G zajh!!bMkTuw^mml=@|9^6qhcVCM8=D_{VfnK|W7D_7K-!dkt^Cy^5BW^8jQ_xSp61 zDgL_CE>EQ>E6M+?D1y`kBkR^}q^qSKiHad6j)5~$C<<<;6QO93s>IsX06=?tC(9pL z%1t+1M^keP_2(LVRZ^s)-kLk+ian=J9y|AK{Q6c~)Qmah3nc6UMtU7kShBdBl`9@2 zGb0VF)kf8^6TO|*Fd{QApuRaKD#)fe#z@1_&!|7Vm*YFvaAfPdIBi{lozrN&BLRp{ z%L3sIQ2n_E8XB8POibkA2Y<~k?zs!CR_m*lDuviRqqMwiSOPG;WYIjN{3j3vU`~kV z*{2?5$>Lj4dOKHLbJ{;A<1FcD^nnhaZte`Sd*tXb)PAeGwXK=sAFW5Kfm7J$nBEwJ zA=d1dpRBI#{U<1$HIwI7Jb}gHA7xP>D~d}N%^AwSK?6`cXW?8SSPkd^k}XNR^xRWS zC>kH2S6}ZRf*9jR1lc)qrj5PF8 z7kD_#&B^Au6;F_v;a`{aQn03Y_M+K?`4~0;#iiwwq!gyPE3)o`vWOfX-m_|F>GZ8Azhp%+pUk zMoxB?PZT8;)=i(i@X8?NUP1<-v~;mqA}as`KuWTO7gs)oCF!ytU5`fw9NzlBv4PlZ zcG|va4_ofFH^?p~`ns!zJRCt>Y%G8L!y{yk%Jhj0ibBPW^XDgo(hn5?;llYgkOg2g z8hPT;-(pUP4^niyRa`C)iRM1rE_?8s@e!Sn6t>KVDKSt-%D#ZO9QHn2(b3^;n4kTrUge@EzSJ{z=>`_LOre35cp<2 zZkM-BM-R4+^M3gid4XGPC^9qBF(>qmP9fyzpmqleTvB-rXaMNy>gI{3U%=^f`O617 z58inJ6CFaFIG$ke?s6!OVhNM=<4cX;*}E+ zA>OP`r$evTv;Uy?Le$xKhJ=x$(MJz{{||?=$A;?h(A84Mxx;(?^732eF=bMi8#wUJ zs`psEcD?^u!asIwUb8Hm{-7tpX;a4E2dwfvZ~AZ>ue|z~fHk2%vu51P85Buoaq z4OUiG%1IOQH}<%=0>}qAU3-Qv4;*4bQ2{YArT~3KQJ6A$B6~jl9IMTS>UPmqdkme? zgvo3PyCwuU;&kG$b<ioYd08TY{>#K381~BgI$%MkZie(QJEP5`UZo6qQdd)`(i(K zyB#Sd9p`H3Y&wG>)=ZQsmT-wEXOES(GbgD#@DZI2r~UG{*jOHW_(3uUv<2{=y`S>b zvoFxp)a;iDr&&){Yk%yL(l57c(*VQ-h z=39T~(AS6ii=9%)`*&7sLjGfNj;r^0(cnop+HYM43WX39!S7@3mn*L66Z zY}mMob^qFg-EI$ME-X<|QOqrwMfolBFdF+VL5)q#e6V>7|K7e6x4YlymaC&iLX z{ 1: @@ -126,8 +172,12 @@ class PasswordDialog(Factory.Popup): text += c kb.password = text - def on_password(self, pw): - if len(pw) == 6: + def on_password(self, pw: str, *, is_generic=False): + if is_generic: + if len(pw) < 6: + self.app.show_error(_('Password is too short (min {} characters)').format(6)) + return + if len(pw) >= 6: if self.check_password(pw): if self.is_change == 0: self.success = True @@ -138,11 +188,13 @@ class PasswordDialog(Factory.Popup): self.pw = pw self.message = _('Enter new PIN') self.ids.kb.password = '' + self.ids.textinput_generic_password.text = '' self.is_change = 2 elif self.is_change == 2: self.new_password = pw self.message = _('Confirm new PIN') self.ids.kb.password = '' + self.ids.textinput_generic_password.text = '' self.is_change = 3 elif self.is_change == 3: self.success = pw == self.new_password From 87b7d2c0c0cd0b210f2dc04572408e41b8833faf Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Feb 2020 19:13:18 +0100 Subject: [PATCH 04/12] wallet backup function for kivy/android --- electrum/gui/kivy/main_window.py | 15 ++++++ electrum/gui/kivy/tools/buildozer.spec | 2 +- .../gui/kivy/uix/dialogs/password_dialog.py | 48 ++++++++----------- electrum/gui/kivy/uix/ui_screens/status.kv | 13 ++--- electrum/util.py | 14 ++++++ electrum/wallet.py | 14 ++++-- 6 files changed, 66 insertions(+), 40 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index b7b3b57af..d01ac5512 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -1199,6 +1199,21 @@ class ElectrumWindow(App): on_success=on_success, on_failure=on_failure, is_change=1) self._password_dialog.open() + def save_backup(self): + from .uix.dialogs.password_dialog import PasswordDialog + from electrum.util import get_backup_dir + 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")) + 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 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/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index f4e4ca78e..df44fbec1 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -67,7 +67,7 @@ fullscreen = False # # (list) Permissions -android.permissions = INTERNET, CAMERA +android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE # (int) Android API to use android.api = 28 diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py index 9f73267e5..1642c3cbe 100644 --- a/electrum/gui/kivy/uix/dialogs/password_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -19,6 +19,7 @@ Builder.load_string(''' id: popup + is_generic: False title: 'Electrum' message: '' BoxLayout: @@ -26,31 +27,17 @@ Builder.load_string(''' orientation: 'vertical' Widget: size_hint: 1, 0.05 - BoxLayout: - size_hint: 1, None - orientation: 'horizontal' - Label: - size_hint: 0.70, None - font_size: '20dp' - text: root.message - text_size: self.width, None - Label: - size_hint: 0.23, None - font_size: '9dp' - text: _('Generic password') - CheckBox: - size_hint: 0.07, None - id: cb_generic_password - on_active: - box_generic_password.visible = self.active - kb.disabled = box_generic_password.visible - textinput_generic_password.focus = box_generic_password.visible + Label: + size_hint: 0.70, None + font_size: '20dp' + text: root.message + text_size: self.width, None Widget: size_hint: 1, 0.05 BoxLayout: orientation: 'horizontal' id: box_generic_password - visible: False + visible: root.is_generic size_hint_y: 0.05 opacity: 1 if self.visible else 0 disabled: not self.visible @@ -59,10 +46,11 @@ Builder.load_string(''' valign: 'center' multiline: False on_text_validate: - popup.on_password(self.text, is_generic=True) + popup.on_password(self.text) password: True size_hint: 0.9, None unfocus_on_touch: False + focus: root.is_generic Button: size_hint: 0.1, None valign: 'center' @@ -75,7 +63,7 @@ Builder.load_string(''' textinput_generic_password.password = False if textinput_generic_password.password else True Label: id: label_pin - visible: not box_generic_password.visible + visible: not root.is_generic size_hint_y: 0.05 opacity: 1 if self.visible else 0 disabled: not self.visible @@ -86,6 +74,7 @@ Builder.load_string(''' size_hint: 1, 0.05 GridLayout: id: kb + disabled: root.is_generic size_hint: 1, None height: self.minimum_height update_amount: popup.update_password @@ -125,8 +114,9 @@ class PasswordDialog(Factory.Popup): def init(self, app: 'ElectrumWindow', *, wallet: Union['Abstract_Wallet', 'WalletStorage'] = None, msg: str, on_success: Callable = None, on_failure: Callable = None, - is_change: int = 0): + is_change: int = 0, is_backup: bool = False): self.app = app + self.is_backup = is_backup self.wallet = wallet self.message = msg self.on_success = on_success @@ -138,7 +128,7 @@ class PasswordDialog(Factory.Popup): self.pw = None self.new_password = None self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '') - self.ids.cb_generic_password.active = False + #self.ids.cb_generic_password.active = False def check_password(self, password): if self.is_change > 1: @@ -172,8 +162,8 @@ class PasswordDialog(Factory.Popup): text += c kb.password = text - def on_password(self, pw: str, *, is_generic=False): - if is_generic: + def on_password(self, pw: str): + if self.is_generic: if len(pw) < 6: self.app.show_error(_('Password is too short (min {} characters)').format(6)) return @@ -186,18 +176,20 @@ class PasswordDialog(Factory.Popup): self.dismiss() elif self.is_change == 1: self.pw = pw - self.message = _('Enter new PIN') + self.message = _('Enter a strong password for your backup') if self.is_backup else _('Enter new PIN') self.ids.kb.password = '' self.ids.textinput_generic_password.text = '' self.is_change = 2 + self.is_generic = self.is_backup elif self.is_change == 2: self.new_password = pw - self.message = _('Confirm new PIN') + self.message = _('Confirm backup password') if self.is_backup else _('Confirm new PIN') self.ids.kb.password = '' self.ids.textinput_generic_password.text = '' self.is_change = 3 elif self.is_change == 3: self.success = pw == self.new_password + self.is_generic = False self.dismiss() else: self.app.show_error(_('Wrong PIN')) diff --git a/electrum/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv index f23188bac..e774ac81e 100644 --- a/electrum/gui/kivy/uix/ui_screens/status.kv +++ b/electrum/gui/kivy/uix/ui_screens/status.kv @@ -80,6 +80,13 @@ Popup: on_release: root.dismiss() app.delete_wallet() + Button: + size_hint: 0.5, None + height: '48dp' + text: _('Save Backup') + on_release: + root.dismiss() + app.save_backup() Button: size_hint: 0.5, None height: '48dp' @@ -87,9 +94,3 @@ Popup: on_release: root.dismiss() app.toggle_lightning() - Button: - size_hint: 0.5, None - height: '48dp' - text: _('Close') - on_release: - root.dismiss() diff --git a/electrum/util.py b/electrum/util.py index 9555be489..cba0cbbd4 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -425,11 +425,25 @@ def profiler(func): return lambda *args, **kw_args: do_profile(args, kw_args) +def android_ext_dir(): + import jnius + env = jnius.autoclass('android.os.Environment') + return env.getExternalStorageDirectory().getPath() + +def android_backup_dir(): + d = android_ext_dir() + '/org.electrum.electrum' + if not os.path.exists(d): + os.mkdir(d) + return d + def android_data_dir(): import jnius PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') return PythonActivity.mActivity.getFilesDir().getPath() + '/data' +def get_backup_dir(config): + return android_backup_dir() if 'ANDROID_DATA' in os.environ else config.path + def ensure_sparse_file(filename): # On modern Linux, no need to do anything. diff --git a/electrum/wallet.py b/electrum/wallet.py index 542cd1178..c6ccaae55 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -263,12 +263,16 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.storage: self.db.write(self.storage) - def save_backup(self, path): - # fixme: we need to change password... + def save_backup(self, path, *, old_password=None, new_password=None): + new_db = WalletDB(self.storage.read(), manual_upgrades=False) + new_db.put('is_backup', True) new_storage = WalletStorage(path) - self.db.put('is_backup', True) - self.db.write(new_storage) - self.db.put('is_backup', None) + #new_storage._encryption_version = self.storage.get_encryption_version() + 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() def has_lightning(self): return bool(self.lnworker) From 88d5cdf87cca23c84ff942605b9858b433161af4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Feb 2020 19:23:09 +0100 Subject: [PATCH 05/12] fix test_lnpeer --- electrum/tests/test_lnpeer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 692fab1f5..103a9b069 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -73,6 +73,8 @@ class MockWallet: pass def save_db(self): pass + def is_lightning_backup(self): + return False class MockLNWallet: def __init__(self, remote_keypair, local_keypair, chan, tx_queue): From cee86072181aed942e025703fc7e98157b67602d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Feb 2020 11:49:05 +0100 Subject: [PATCH 06/12] save_backup: use db.dump() to clone the db --- electrum/wallet.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index c6ccaae55..9e22e6ab0 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -264,10 +264,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.db.write(self.storage) def save_backup(self, path, *, old_password=None, new_password=None): - new_db = WalletDB(self.storage.read(), manual_upgrades=False) + new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db.put('is_backup', True) new_storage = WalletStorage(path) - #new_storage._encryption_version = self.storage.get_encryption_version() new_storage._encryption_version = StorageEncryptionVersion.PLAINTEXT w2 = Wallet(new_db, new_storage, config=self.config) if new_password: From 2dad87cbb44a48300fbe81192cc7cee5443ede3a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Feb 2020 13:43:10 +0100 Subject: [PATCH 07/12] Automate backups: - backup wallet file on each channel creation - on android, a backup password is entered in settings - on desktop, the backup path is in settings --- electrum/gui/kivy/main_window.py | 20 ++++++++++++----- electrum/gui/kivy/uix/dialogs/settings.py | 8 +++++++ electrum/gui/qt/main_window.py | 17 ++++++--------- electrum/gui/qt/settings_dialog.py | 15 +++++++++++++ electrum/lnworker.py | 1 + electrum/util.py | 2 +- electrum/wallet.py | 26 ++++++++++++++++------- 7 files changed, 64 insertions(+), 25 deletions(-) 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) From d5dc8d1ab260bd69b02dda50ac7d5997b48e0ed4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Feb 2020 16:08:54 +0100 Subject: [PATCH 08/12] kivy: save pin code in memory --- electrum/gui/kivy/main_window.py | 17 +++++--- .../gui/kivy/uix/dialogs/password_dialog.py | 42 +++++++++++-------- electrum/wallet.py | 10 +++-- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 1228e24ad..e19b45ce2 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -326,6 +326,7 @@ class ElectrumWindow(App): self.wallet = None # type: Optional[Abstract_Wallet] self.pause_time = 0 self.asyncio_loop = asyncio.get_event_loop() + self.pin_code = None App.__init__(self)#, **kwargs) @@ -619,9 +620,13 @@ class ElectrumWindow(App): return wallet = self.daemon.load_wallet(path, None) if wallet: - if platform == 'android' and wallet.has_password(): + if wallet.has_password(): + def on_success(x): + # save pin_code so that we can create backups + self.pin_code = x + self.load_wallet(wallet) self.password_dialog(wallet=wallet, msg=_('Enter PIN code'), - on_success=lambda x: self.load_wallet(wallet), on_failure=self.stop) + on_success=on_success, on_failure=self.stop) else: self.load_wallet(wallet) else: @@ -637,6 +642,7 @@ class ElectrumWindow(App): if not storage.is_encrypted_with_user_pw(): raise Exception("Kivy GUI does not support this type of encrypted wallet files.") def on_password(pw): + self.pin_code = pw storage.decrypt(pw) self._on_decrypted_storage(storage) self.password_dialog(wallet=storage, msg=_('Enter PIN code'), @@ -1196,7 +1202,7 @@ class ElectrumWindow(App): self.show_info(_("Your PIN code was updated")) on_failure = lambda: self.show_error(_("PIN codes do not match")) self._password_dialog.init(self, wallet=self.wallet, msg=message, - on_success=on_success, on_failure=on_failure, is_change=1) + on_success=on_success, on_failure=on_failure, is_change=True) self._password_dialog.open() def change_backup_password(self): @@ -1209,16 +1215,15 @@ class ElectrumWindow(App): def on_success(old_password, new_password): 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) + on_success=on_success, on_failure=on_failure, is_change=True, is_backup=True) self._password_dialog.open() def save_backup(self): - new_path = self.wallet.save_backup() + new_path = self.wallet.save_backup(pin_code=self.pin_code) if new_path: self.show_info(_("Backup saved:") + f"\n{new_path}") else: diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py index 1642c3cbe..86b3567aa 100644 --- a/electrum/gui/kivy/uix/dialogs/password_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -114,24 +114,34 @@ class PasswordDialog(Factory.Popup): def init(self, app: 'ElectrumWindow', *, wallet: Union['Abstract_Wallet', 'WalletStorage'] = None, msg: str, on_success: Callable = None, on_failure: Callable = None, - is_change: int = 0, is_backup: bool = False): + is_change: bool = False, is_backup: bool = False): self.app = app self.is_backup = is_backup self.wallet = wallet self.message = msg self.on_success = on_success self.on_failure = on_failure - self.ids.kb.password = '' - self.ids.textinput_generic_password.text = '' self.success = False self.is_change = is_change self.pw = None self.new_password = None self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '') - #self.ids.cb_generic_password.active = False + self.level = 1 if is_backup else 0 + self.is_generic = self.is_backup + self.update_screen() + + def update_screen(self): + self.ids.kb.password = '' + self.ids.textinput_generic_password.text = '' + if self.level == 0: + self.message = _('Enter your PIN') + elif self.level == 1: + self.message = _('Enter a strong password for your backup') if self.is_backup else _('Enter new PIN') + elif self.level == 2: + self.message = _('Confirm backup password') if self.is_backup else _('Confirm new PIN') def check_password(self, password): - if self.is_change > 1: + if self.level > 0: return True try: self.wallet.check_password(password) @@ -162,6 +172,7 @@ class PasswordDialog(Factory.Popup): text += c kb.password = text + def on_password(self, pw: str): if self.is_generic: if len(pw) < 6: @@ -169,25 +180,20 @@ class PasswordDialog(Factory.Popup): return if len(pw) >= 6: if self.check_password(pw): - if self.is_change == 0: + if self.is_change is False: self.success = True self.pw = pw self.message = _('Please wait...') self.dismiss() - elif self.is_change == 1: + elif self.level == 0: + self.level = 1 self.pw = pw - self.message = _('Enter a strong password for your backup') if self.is_backup else _('Enter new PIN') - self.ids.kb.password = '' - self.ids.textinput_generic_password.text = '' - self.is_change = 2 - self.is_generic = self.is_backup - elif self.is_change == 2: + self.update_screen() + elif self.level == 1: + self.level = 2 self.new_password = pw - self.message = _('Confirm backup password') if self.is_backup else _('Confirm new PIN') - self.ids.kb.password = '' - self.ids.textinput_generic_password.text = '' - self.is_change = 3 - elif self.is_change == 3: + self.update_screen() + elif self.level == 2: self.success = pw == self.new_password self.is_generic = False self.dismiss() diff --git a/electrum/wallet.py b/electrum/wallet.py index cc8d33d88..811739495 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -263,19 +263,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.storage: self.db.write(self.storage) - def save_backup(self): + def save_backup(self, pin_code=None): new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db.put('is_backup', True) 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') + if pin_code: + backup_pubkey = self.config.get('backup_pubkey') + if backup_pubkey is None: + return 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') + new_storage.pubkey = backup_pubkey else: new_storage._encryption_version = self.storage._encryption_version new_storage.pubkey = self.storage.pubkey From 497d6019e14dfc0ccb992b8cbab26e3734f39c90 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Feb 2020 19:02:43 +0100 Subject: [PATCH 09/12] kivy/android: ask for STORAGE permission at runtime --- electrum/gui/kivy/main_window.py | 19 +++++++++++++++++-- electrum/util.py | 7 +++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index e19b45ce2..fbd23e236 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -7,7 +7,7 @@ import traceback from decimal import Decimal import threading import asyncio -from typing import TYPE_CHECKING, Optional, Union, Callable +from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence from electrum.storage import WalletStorage, StorageReadWriteError from electrum.wallet_db import WalletDB @@ -1223,11 +1223,26 @@ class ElectrumWindow(App): self._password_dialog.open() def save_backup(self): + if platform != 'android': + self._save_backup() + return + + from android.permissions import request_permissions, Permission + def cb(permissions, grant_results: Sequence[bool]): + if not grant_results or not grant_results[0]: + self.show_error(_("Cannot save backup without STORAGE permission")) + return + # note: Clock.schedule_once is a hack so that we get called on a non-daemon thread + # (needed for WalletDB.write) + Clock.schedule_once(lambda dt: self._save_backup()) + request_permissions([Permission.WRITE_EXTERNAL_STORAGE], cb) + + def _save_backup(self): new_path = self.wallet.save_backup(pin_code=self.pin_code) if new_path: self.show_info(_("Backup saved:") + f"\n{new_path}") else: - self.show_error(_("Backup directory not configured")) + self.show_error(_("Backup NOT saved. Backup directory or password not configured.")) def export_private_keys(self, pk_label, addr): if self.wallet.is_watching_only(): diff --git a/electrum/util.py b/electrum/util.py index 9e6d613dd..932602ac8 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -426,12 +426,11 @@ def profiler(func): def android_ext_dir(): - import jnius - env = jnius.autoclass('android.os.Environment') - return env.getExternalStorageDirectory().getPath() + from android.storage import primary_external_storage_path + return primary_external_storage_path() def android_backup_dir(): - d = android_ext_dir() + '/org.electrum.electrum' + d = os.path.join(android_ext_dir(), 'org.electrum.electrum') if not os.path.exists(d): os.mkdir(d) return d From d9172ade714e659d87614e0376e53d8bad155159 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Feb 2020 13:01:19 +0100 Subject: [PATCH 10/12] kivy: use password + pin_code - password is per wallet, is retained in memory - pin code is saved in config --- electrum/gui/kivy/main_window.py | 98 ++++++++++--------- .../gui/kivy/uix/dialogs/password_dialog.py | 31 +++--- electrum/gui/kivy/uix/dialogs/settings.py | 27 ++--- electrum/wallet.py | 15 +-- 4 files changed, 89 insertions(+), 82 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index fbd23e236..e4611a32b 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -31,6 +31,7 @@ from kivy.clock import Clock from kivy.factory import Factory from kivy.metrics import inch from kivy.lang import Builder +from .uix.dialogs.password_dialog import PasswordDialog ## lazy imports for factory so that widgets can be used in kv #Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard') @@ -326,7 +327,7 @@ class ElectrumWindow(App): self.wallet = None # type: Optional[Abstract_Wallet] self.pause_time = 0 self.asyncio_loop = asyncio.get_event_loop() - self.pin_code = None + self.password = None App.__init__(self)#, **kwargs) @@ -623,10 +624,12 @@ class ElectrumWindow(App): if wallet.has_password(): def on_success(x): # save pin_code so that we can create backups - self.pin_code = x + self.password = x self.load_wallet(wallet) - self.password_dialog(wallet=wallet, msg=_('Enter PIN code'), - on_success=on_success, on_failure=self.stop) + self.password_dialog( + check_password=wallet.check_password, + on_success=on_success, + on_failure=self.stop) else: self.load_wallet(wallet) else: @@ -642,11 +645,13 @@ class ElectrumWindow(App): if not storage.is_encrypted_with_user_pw(): raise Exception("Kivy GUI does not support this type of encrypted wallet files.") def on_password(pw): - self.pin_code = pw + self.password = pw storage.decrypt(pw) self._on_decrypted_storage(storage) - self.password_dialog(wallet=storage, msg=_('Enter PIN code'), - on_success=on_password, on_failure=self.stop) + self.password_dialog( + check_password=storage.check_password, + on_success=on_password, + on_failure=self.stop) return self._on_decrypted_storage(storage) if not ask_if_wizard: @@ -940,7 +945,7 @@ class ElectrumWindow(App): def on_resume(self): now = time.time() if self.wallet and self.wallet.has_password() and now - self.pause_time > 60: - self.password_dialog(wallet=self.wallet, msg=_('Enter PIN'), on_success=None, on_failure=self.stop) + self.password_dialog(check_password=self.check_pin_code, on_success=None, on_failure=self.stop, is_password=False) if self.nfcscanner: self.nfcscanner.nfc_enable() @@ -1102,12 +1107,12 @@ class ElectrumWindow(App): def on_fee(self, event, *arg): self.fee_status = self.electrum_config.get_fee_status() - def protected(self, msg, f, args): - if self.wallet.has_password(): - on_success = lambda pw: f(*(args + (pw,))) - self.password_dialog(wallet=self.wallet, msg=msg, on_success=on_success, on_failure=lambda: None) + def protected(self, f, args): + if self.electrum_config.get('pin_code'): + on_success = lambda pw: f(*(args + (self.password,))) + self.password_dialog(check_password=self.check_pin_code, on_success=on_success, on_failure=lambda: None, is_password=False) else: - f(*(args + (None,))) + f(*(args + (self.password,))) def toggle_lightning(self): if self.wallet.has_lightning(): @@ -1167,59 +1172,64 @@ class ElectrumWindow(App): self.load_wallet_by_name(new_path) def show_seed(self, label): - self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,)) + self.protected(self._show_seed, (label,)) def _show_seed(self, label, password): if self.wallet.has_password() and password is None: return keystore = self.wallet.keystore - try: - seed = keystore.get_seed(password) - passphrase = keystore.get_passphrase(password) - except: - self.show_error("Invalid PIN") - return + seed = keystore.get_seed(password) + passphrase = keystore.get_passphrase(password) label.data = seed if passphrase: label.data += '\n\n' + _('Passphrase') + ': ' + passphrase - def password_dialog(self, *, wallet: Union[Abstract_Wallet, WalletStorage], - msg: str, on_success: Callable = None, on_failure: Callable = None): - from .uix.dialogs.password_dialog import PasswordDialog + def has_pin_code(self): + return bool(self.electrum_config.get('pin_code')) + + def check_pin_code(self, pin): + if pin != self.electrum_config.get('pin_code'): + raise InvalidPassword + + def password_dialog(self, *, check_password: Callable = None, + on_success: Callable = None, on_failure: Callable = None, + is_password=True): if self._password_dialog is None: self._password_dialog = PasswordDialog() - self._password_dialog.init(self, wallet=wallet, msg=msg, - on_success=on_success, on_failure=on_failure) + self._password_dialog.init( + self, check_password = check_password, + on_success=on_success, on_failure=on_failure, + is_password=is_password) self._password_dialog.open() def change_password(self, cb): - from .uix.dialogs.password_dialog import PasswordDialog if self._password_dialog is None: self._password_dialog = PasswordDialog() - message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:") def on_success(old_password, new_password): self.wallet.update_password(old_password, new_password) - self.show_info(_("Your PIN code was updated")) - on_failure = lambda: self.show_error(_("PIN codes do not match")) - self._password_dialog.init(self, wallet=self.wallet, msg=message, - on_success=on_success, on_failure=on_failure, is_change=True) + self.password = new_password + self.show_info(_("Your password was updated")) + on_failure = lambda: self.show_error(_("Password not updated")) + self._password_dialog.init( + self, check_password = self.wallet.check_password, + on_success=on_success, on_failure=on_failure, + is_change=True, is_password=True, + has_password=self.wallet.has_password()) self._password_dialog.open() - def change_backup_password(self): - from .uix.dialogs.password_dialog import PasswordDialog - from electrum.util import get_backup_dir - from electrum.storage import WalletStorage + def change_pin_code(self, cb): 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): - 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('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=True, is_backup=True) + self.electrum_config.set_key('pin_code', new_password) + cb() + self.show_info(_("PIN updated") if new_password else _('PIN disabled')) + on_failure = lambda: self.show_error(_("PIN not updated")) + self._password_dialog.init( + self, check_password=self.check_pin_code, + on_success=on_success, on_failure=on_failure, + is_change=True, is_password=False, + has_password = self.has_pin_code()) self._password_dialog.open() def save_backup(self): @@ -1238,7 +1248,7 @@ class ElectrumWindow(App): request_permissions([Permission.WRITE_EXTERNAL_STORAGE], cb) def _save_backup(self): - new_path = self.wallet.save_backup(pin_code=self.pin_code) + new_path = self.wallet.save_backup() if new_path: self.show_info(_("Backup saved:") + f"\n{new_path}") else: diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py index 86b3567aa..6f49a096e 100644 --- a/electrum/gui/kivy/uix/dialogs/password_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -112,44 +112,48 @@ Builder.load_string(''' class PasswordDialog(Factory.Popup): def init(self, app: 'ElectrumWindow', *, - wallet: Union['Abstract_Wallet', 'WalletStorage'] = None, - msg: str, on_success: Callable = None, on_failure: Callable = None, - is_change: bool = False, is_backup: bool = False): + check_password = None, + on_success: Callable = None, on_failure: Callable = None, + is_change: bool = False, + is_password: bool = False, + has_password: bool = False): self.app = app - self.is_backup = is_backup - self.wallet = wallet - self.message = msg + self.pw_check = check_password + self.message = '' self.on_success = on_success self.on_failure = on_failure self.success = False self.is_change = is_change self.pw = None self.new_password = None - self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '') - self.level = 1 if is_backup else 0 - self.is_generic = self.is_backup + self.title = 'Electrum' + self.level = 1 if is_change and not has_password else 0 + self.is_generic = is_password self.update_screen() def update_screen(self): self.ids.kb.password = '' self.ids.textinput_generic_password.text = '' if self.level == 0: - self.message = _('Enter your PIN') + self.message = _('Enter your password') if self.is_generic else _('Enter your PIN') elif self.level == 1: - self.message = _('Enter a strong password for your backup') if self.is_backup else _('Enter new PIN') + self.message = _('Enter new password') if self.is_generic else _('Enter new PIN') elif self.level == 2: - self.message = _('Confirm backup password') if self.is_backup else _('Confirm new PIN') + self.message = _('Confirm new password') if self.is_generic else _('Confirm new PIN') def check_password(self, password): if self.level > 0: return True try: - self.wallet.check_password(password) + self.pw_check(password) return True except InvalidPassword as e: return False def on_dismiss(self): + if self.level == 1 and not self.is_generic and self.on_success: + self.on_success(self.pw, None) + return False if not self.success: if self.on_failure: self.on_failure() @@ -195,7 +199,6 @@ class PasswordDialog(Factory.Popup): self.update_screen() elif self.level == 2: self.success = pw == self.new_password - self.is_generic = False self.dismiss() else: self.app.show_error(_('Wrong PIN')) diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py index 4f9fe3c9d..2f58e7e64 100644 --- a/electrum/gui/kivy/uix/dialogs/settings.py +++ b/electrum/gui/kivy/uix/dialogs/settings.py @@ -18,7 +18,8 @@ Builder.load_string(''' id: settings title: _('Electrum Settings') - disable_pin: False + disable_password: False + has_pin_code: False use_encryption: False BoxLayout: orientation: 'vertical' @@ -36,10 +37,10 @@ Builder.load_string(''' action: partial(root.language_dialog, self) CardSeparator SettingsItem: - disabled: root.disable_pin - title: _('PIN code') - description: _("Change your PIN code.") - action: partial(root.change_password, self) + status: 'ON' if root.has_pin_code else 'OFF' + title: _('PIN code') + ': ' + self.status + description: _("Change your PIN code.") if root.has_pin_code else _("Add PIN code") + action: partial(root.change_pin_code, self) CardSeparator SettingsItem: bu: app.base_unit @@ -84,9 +85,10 @@ Builder.load_string(''' 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: root.disable_password + title: _('Password') + description: _("Change wallet password.") + action: root.change_password # disabled: there is currently only one coin selection policy #CardSeparator @@ -117,17 +119,18 @@ class SettingsDialog(Factory.Popup): def update(self): self.wallet = self.app.wallet - self.disable_pin = self.wallet.is_watching_only() if self.wallet else True + self.disable_password = self.wallet.is_watching_only() if self.wallet else True self.use_encryption = self.wallet.has_password() if self.wallet else False + self.has_pin_code = self.app.has_pin_code() def get_language_name(self): return languages.get(self.config.get('language', 'en_UK'), '') - def change_password(self, item, dt): + def change_password(self, dt): self.app.change_password(self.update) - def change_backup_password(self, dt): - self.app.change_backup_password() + def change_pin_code(self, label, dt): + self.app.change_pin_code(self.update) def language_dialog(self, item, dt): if self._language_dialog is None: diff --git a/electrum/wallet.py b/electrum/wallet.py index 811739495..c6d42958e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -263,24 +263,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.storage: self.db.write(self.storage) - def save_backup(self, pin_code=None): + def save_backup(self): new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db.put('is_backup', True) 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 pin_code: - backup_pubkey = self.config.get('backup_pubkey') - if backup_pubkey is None: - return - w2 = Wallet(new_db, None, config=self.config) - w2.update_password(pin_code, None) - new_storage._encryption_version = StorageEncryptionVersion.USER_PASSWORD - new_storage.pubkey = backup_pubkey - else: - new_storage._encryption_version = self.storage._encryption_version - new_storage.pubkey = self.storage.pubkey + 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 From e3ccfe64492cbbb5abf488797248bd54984d687d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Feb 2020 17:26:03 +0100 Subject: [PATCH 11/12] kivy: make backups optional --- electrum/gui/kivy/main_window.py | 6 +++++- electrum/gui/kivy/uix/dialogs/settings.py | 7 +++++++ electrum/util.py | 6 ++++-- electrum/wallet.py | 7 ++++--- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index e4611a32b..109d01089 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -164,6 +164,10 @@ class ElectrumWindow(App): def on_use_rbf(self, instance, x): self.electrum_config.set_key('use_rbf', self.use_rbf, True) + android_backups = BooleanProperty(False) + def on_android_backups(self, instance, x): + self.electrum_config.set_key('android_backups', self.android_backups, True) + use_change = BooleanProperty(False) def on_use_change(self, instance, x): if self.wallet: @@ -1252,7 +1256,7 @@ class ElectrumWindow(App): if new_path: self.show_info(_("Backup saved:") + f"\n{new_path}") else: - self.show_error(_("Backup NOT saved. Backup directory or password not configured.")) + self.show_error(_("Backup NOT saved. Backup directory not configured.")) def export_private_keys(self, pk_label, addr): if self.wallet.is_watching_only(): diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py index 2f58e7e64..b7fc3aa58 100644 --- a/electrum/gui/kivy/uix/dialogs/settings.py +++ b/electrum/gui/kivy/uix/dialogs/settings.py @@ -89,6 +89,13 @@ Builder.load_string(''' title: _('Password') description: _("Change wallet password.") action: root.change_password + CardSeparator + SettingsItem: + status: _('Yes') if app.android_backups else _('No') + title: _('Backups') + ': ' + self.status + description: _("Backup wallet to external storage.") + message: _("If this option is checked, a backup of your wallet will be written to external storage everytime you create a new channel. Make sure your wallet is protected with a strong password before you enable this option.") + action: partial(root.boolean_dialog, 'android_backups', _('Backups'), self.message) # disabled: there is currently only one coin selection policy #CardSeparator diff --git a/electrum/util.py b/electrum/util.py index 932602ac8..8051909fc 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -441,8 +441,10 @@ 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.get('backup_dir') - + if 'ANDROID_DATA' in os.environ: + return android_backup_dir() if config.get('android_backups') else None + else: + return config.get('backup_dir') def ensure_sparse_file(filename): # On modern Linux, no need to do anything. diff --git a/electrum/wallet.py b/electrum/wallet.py index c6d42958e..199b3e4d8 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -264,11 +264,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.db.write(self.storage) def save_backup(self): + backup_dir = get_backup_dir(self.config) + if backup_dir is None: + return new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db.put('is_backup', True) - new_path = os.path.join(get_backup_dir(self.config), self.basename() + '.backup') - if new_path is None: - return + new_path = os.path.join(backup_dir, self.basename() + '.backup') new_storage = WalletStorage(new_path) new_storage._encryption_version = self.storage._encryption_version new_storage.pubkey = self.storage.pubkey From 5ae6e433239818d282db8cfd3e3aad51ae9cb428 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Feb 2020 17:27:33 +0100 Subject: [PATCH 12/12] message formatting --- electrum/gui/qt/channels_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 255ff251d..08f6b82c3 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -85,7 +85,7 @@ class ChannelsList(MyTreeView): def force_close(self, channel_id): if self.lnworker.wallet.is_lightning_backup(): - msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?') + msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?') else: msg = _('Force-close channel?\nReclaimed funds will not be immediately available.') if self.parent.question(msg):