URI: 
       tstorage: read/write sanity checks - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit a05dab2c4dcf0ce2f69e9a4258bd3c88c818a91d
   DIR parent 4dda162336679bae52bb9ee14c5e5d924f56b7ce
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Tue, 10 Sep 2019 21:17:15 +0200
       
       storage: read/write sanity checks
       
       related: #4110
       supersedes: #4528
       
       Diffstat:
         M electrum/gui/kivy/main_window.py    |      15 +++++++++------
         M electrum/gui/kivy/uix/dialogs/wall… |      17 +++++++++++++----
         M electrum/gui/qt/installwizard.py    |      25 +++++++++++++++----------
         M electrum/storage.py                 |      26 +++++++++++++++++++++++++-
       
       4 files changed, 62 insertions(+), 21 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -7,10 +7,10 @@ import traceback
        from decimal import Decimal
        import threading
        import asyncio
       -from typing import TYPE_CHECKING
       +from typing import TYPE_CHECKING, Optional
        
        from electrum.bitcoin import TYPE_ADDRESS
       -from electrum.storage import WalletStorage
       +from electrum.storage import WalletStorage, StorageReadWriteError
        from electrum.wallet import Wallet, InternalAddressCorruption
        from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
        from electrum.plugin import run_hook
       t@@ -79,7 +79,10 @@ from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog
        from .uix.dialogs.lightning_channels import LightningChannelsDialog
        
        if TYPE_CHECKING:
       +    from . import ElectrumGui
            from electrum.simple_config import SimpleConfig
       +    from electrum.wallet import Abstract_Wallet
       +    from electrum.plugin import Plugins
        
        
        class ElectrumWindow(App):
       t@@ -309,7 +312,7 @@ class ElectrumWindow(App):
                self.nfcscanner = None
                self.tabs = None
                self.is_exit = False
       -        self.wallet = None
       +        self.wallet = None  # type: Optional[Abstract_Wallet]
                self.pause_time = 0
                self.asyncio_loop = asyncio.get_event_loop()
        
       t@@ -330,8 +333,8 @@ class ElectrumWindow(App):
                    self.proxy_config = net_params.proxy if net_params.proxy else {}
                    self.update_proxy_str(self.proxy_config)
        
       -        self.plugins = kwargs.get('plugins', [])
       -        self.gui_object = kwargs.get('gui_object', None)
       +        self.plugins = kwargs.get('plugins', None)  # type: Plugins
       +        self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
                self.daemon = self.gui_object.daemon
                self.fx = self.daemon.fx
        
       t@@ -548,6 +551,7 @@ class ElectrumWindow(App):
                    self.network.register_callback(self.on_channel, ['channel'])
                    self.network.register_callback(self.on_payment_status, ['payment_status'])
                # load wallet
       +        # FIXME if this raises, the whole app quits, without any user feedback or exc reporting
                self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
                # URI passed in config
                uri = self.electrum_config.get('url')
       t@@ -1072,7 +1076,6 @@ class ElectrumWindow(App):
        
            def __delete_wallet(self, pw):
                wallet_path = self.get_wallet_path()
       -        dirname = os.path.dirname(wallet_path)
                basename = os.path.basename(wallet_path)
                if self.wallet.has_password():
                    try:
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py
       t@@ -6,6 +6,7 @@ from kivy.properties import ObjectProperty
        from kivy.lang import Builder
        
        from electrum.util import base_units
       +from electrum.storage import StorageReadWriteError
        
        from ...i18n import _
        from .label_dialog import LabelDialog
       t@@ -54,12 +55,20 @@ Builder.load_string('''
        class WalletDialog(Factory.Popup):
        
            def new_wallet(self, app, dirname):
       -        def cb(text):
       -            if text:
       -                app.load_wallet_by_name(os.path.join(dirname, text))
       +        def cb(filename):
       +            if not filename:
       +                return
       +            # FIXME? "filename" might contain ".." (etc) and hence sketchy path traversals are possible
       +            try:
       +                app.load_wallet_by_name(os.path.join(dirname, filename))
       +            except StorageReadWriteError:
       +                app.show_error(_("R/W error accessing path"))
                d = LabelDialog(_('Enter wallet name'), '', cb)
                d.open()
        
            def open_wallet(self, app):
       -        app.load_wallet_by_name(self.ids.wallet_selector.selection[0])
       +        try:
       +            app.load_wallet_by_name(self.ids.wallet_selector.selection[0])
       +        except StorageReadWriteError:
       +            app.show_error(_("R/W error accessing path"))
        
   DIR diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
       t@@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (QWidget, QDialog, QLabel, QHBoxLayout, QMessageBox,
                                     QGridLayout, QSlider, QScrollArea, QApplication)
        
        from electrum.wallet import Wallet, Abstract_Wallet
       -from electrum.storage import WalletStorage
       +from electrum.storage import WalletStorage, StorageReadWriteError
        from electrum.util import UserCancelled, InvalidPassword, WalletFileException
        from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET, GoBack
        from electrum.i18n import _
       t@@ -193,8 +193,11 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
                vbox.addLayout(hbox2)
                self.set_layout(vbox, title=_('Electrum wallet'))
        
       -        temp_storage = WalletStorage(path, manual_upgrades=True)
       -        wallet_folder = os.path.dirname(temp_storage.path)
       +        try:
       +            temp_storage = WalletStorage(path, manual_upgrades=True)
       +        except StorageReadWriteError:
       +            temp_storage = None  # type: Optional[WalletStorage]
       +        wallet_folder = os.path.dirname(path)
        
                def on_choose():
                    path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)
       t@@ -202,19 +205,21 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
                        self.name_e.setText(path)
        
                def on_filename(filename):
       +            # FIXME? "filename" might contain ".." (etc) and hence sketchy path traversals are possible
                    nonlocal temp_storage
       +            temp_storage = None
                    path = os.path.join(wallet_folder, filename)
                    wallet_from_memory = get_wallet_from_daemon(path)
                    try:
                        if wallet_from_memory:
       -                    temp_storage = wallet_from_memory.storage
       +                    temp_storage = wallet_from_memory.storage  # type: Optional[WalletStorage]
                        else:
                            temp_storage = WalletStorage(path, manual_upgrades=True)
       -                self.next_button.setEnabled(True)
       -            except BaseException:
       +            except StorageReadWriteError:
       +                pass
       +            except Exception:
                        self.logger.exception('')
       -                temp_storage = None
       -                self.next_button.setEnabled(False)
       +            self.next_button.setEnabled(temp_storage is not None)
                    user_needs_to_enter_password = False
                    if temp_storage:
                        if not temp_storage.file_exists():
       t@@ -246,12 +251,12 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
        
                button.clicked.connect(on_choose)
                self.name_e.textChanged.connect(on_filename)
       -        n = os.path.basename(temp_storage.path)
       -        self.name_e.setText(n)
       +        self.name_e.setText(os.path.basename(path))
        
                while True:
                    if self.loop.exec_() != 2:  # 2 = next
                        raise UserCancelled
       +            assert temp_storage
                    if temp_storage.file_exists() and not temp_storage.is_encrypted():
                        break
                    if not temp_storage.file_exists():
   DIR diff --git a/electrum/storage.py b/electrum/storage.py
       t@@ -50,6 +50,9 @@ class StorageEncryptionVersion(IntEnum):
            XPUB_PASSWORD = 2
        
        
       +class StorageReadWriteError(Exception): pass
       +
       +
        class WalletStorage(Logger):
        
            def __init__(self, path, *, manual_upgrades=False):
       t@@ -61,7 +64,7 @@ class WalletStorage(Logger):
                DB_Class = JsonDB
                self.logger.info(f"wallet path {self.path}")
                self.pubkey = None
       -        # TODO we should test r/w permissions here (whether file exists or not)
       +        self._test_read_write_permissions(self.path)
                if self.file_exists():
                    with open(self.path, "r", encoding='utf-8') as f:
                        self.raw = f.read()
       t@@ -74,6 +77,27 @@ class WalletStorage(Logger):
                    # avoid new wallets getting 'upgraded'
                    self.db = DB_Class('', manual_upgrades=False)
        
       +    @classmethod
       +    def _test_read_write_permissions(cls, path):
       +        # note: There might already be a file at 'path'.
       +        #       Make sure we do NOT overwrite/corrupt that!
       +        temp_path = "%s.tmptest.%s" % (path, os.getpid())
       +        echo = "fs r/w test"
       +        try:
       +            # test READ permissions for actual path
       +            if os.path.exists(path):
       +                with open(path, "r", encoding='utf-8') as f:
       +                    f.read(1)  # read 1 byte
       +            # test R/W sanity for "similar" path
       +            with open(temp_path, "w", encoding='utf-8') as f:
       +                f.write(echo)
       +            with open(temp_path, "r", encoding='utf-8') as f:
       +                echo2 = f.read()
       +            os.remove(temp_path)
       +        except Exception as e:
       +            raise StorageReadWriteError(e) from e
       +        if echo != echo2:
       +            raise StorageReadWriteError('echo sanity-check failed')
        
            def load_plugins(self):
                wallet_type = self.db.get('wallet_type')