From 8437df718ddc72bab6394e0392ffbeaa5986846f Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 20:36:46 +0200 Subject: [PATCH 01/36] test gocryptfs on travis --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5883e01a2..35d7b66b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,8 +58,19 @@ install: - cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys # start ssh-agent so that we can add private keys - eval `ssh-agent -s` + # install latest gocryptfs + - export GOCRYPTFS_TARBALL=$(curl -s https://api.github.com/repos/rfjakob/gocryptfs/releases/latest | grep browser_download_url | cut -d '"' -f 4 | grep -v ".asc$") + - wget $GOCRYPTFS_TARBALL -O gocryptfs.tar.gz + - mkdir -p ~/bin + - tar xzf gocryptfs.tar.gz -C ~/bin + - export PATH=~/bin:$PATH + - alias backintime-askpass=$(pwd)/common/backintime-askpass script: + # test gocryptfs + - mkdir cipher plain + - gocryptfs -init -extpass "echo foo" cipher + - gocryptfs -extpass "echo foo" cipher plain # compile all files - ensure that syntax is correct - python -m compileall common common/test common/plugins qt qt/test qt/plugins # run unit tests - ensure that functionality is correct From 48267cd3b7fc8ab6be69e2fa973790bf12fdbb18 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 20:42:01 +0200 Subject: [PATCH 02/36] fix backintime-askpass not found --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35d7b66b7..374d59897 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,8 +63,7 @@ install: - wget $GOCRYPTFS_TARBALL -O gocryptfs.tar.gz - mkdir -p ~/bin - tar xzf gocryptfs.tar.gz -C ~/bin - - export PATH=~/bin:$PATH - - alias backintime-askpass=$(pwd)/common/backintime-askpass + - export PATH=~/bin:$PATH:$(pwd)/common script: # test gocryptfs From 85b8dde5056c9590616b7d4a67df606e03d7164d Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:04:03 +0200 Subject: [PATCH 03/36] fix gocryptfs stay in foreground --- common/gocryptfstools.py | 136 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 common/gocryptfstools.py diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py new file mode 100644 index 000000000..90c45597b --- /dev/null +++ b/common/gocryptfstools.py @@ -0,0 +1,136 @@ +# Copyright (C) 2017 Germar Reitze +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import subprocess +from gettext import gettext + +import logger +import tools +from password_ipc import TempPasswordThread +from mount import MountControl +from exceptions import MountException + +_ = gettext + +class GoCryptFS_mount(MountControl): + """ + """ + def __init__(self, *args, **kwargs): + super(GoCryptFS_mount, self).__init__(*args, **kwargs) + + self.setattrKwargs('path', self.config.localGocryptfsPath(self.profile_id), **kwargs) + self.setattrKwargs('reverse', False, **kwargs) + self.setattrKwargs('password', None, store = False, **kwargs) + self.setattrKwargs('config_path', None, **kwargs) + + self.setDefaultArgs() + + self.mountproc = 'gocryptfs' + self.log_command = '%s: %s' % (self.mode, self.path) + self.symlink_subfolder = None + + def _mount(self): + """ + mount the service + """ + if self.password is None: + self.password = self.config.password(self.parent, self.profile_id, self.mode) + logger.debug('Provide password through temp FIFO', self) + thread = TempPasswordThread(self.password) + env = os.environ.copy() + env['ASKPASS_TEMP'] = thread.temp_file + + with thread.starter(): + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass'] + if self.reverse: + gocryptfs += ['-reverse'] + gocryptfs += [self.path, self.currentMountpoint] + logger.debug('Call mount command: %s' + %' '.join(gocryptfs), + self) + + proc = subprocess.Popen(gocryptfs, env = env) + # if stdout/err are piped into python gocryptfs v1.4 stays in + # foreground instead of forking away. So we can't redirect output + # for error messages. + proc.communicate() + #### self.backupConfig() + if proc.returncode: + raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ + % {'command': ' '.join(gocryptfs), + 'error': 'Take a look into syslog for error messages.'}) + + def init(self): + """ + init the cipher path + """ + if self.password is None: + self.password = self.config.password(self.parent, self.profile_id, self.mode) + logger.debug('Provide password through temp FIFO', self) + thread = TempPasswordThread(self.password) + env = os.environ.copy() + env['ASKPASS_TEMP'] = thread.temp_file + + with thread.starter(): + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass'] + gocryptfs.append('-init') + gocryptfs.append(self.path) + logger.debug('Call command to create gocryptfs config file: %s' + %' '.join(gocryptfs), + self) + + proc = subprocess.Popen(gocryptfs, env = env, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + universal_newlines = True) + output = proc.communicate()[0] + #### self.backupConfig() + if proc.returncode: + raise MountException(_('Can\'t init encrypted path \'%(command)s\':\n\n%(error)s') \ + % {'command': ' '.join(gocryptfs), 'error': output}) + + def preMountCheck(self, first_run = False): + """ + check what ever conditions must be given for the mount + """ + self.checkFuse() + if first_run: + pass + return True + + def configFile(self): + """ + return gocryptfs config file + """ + f = 'gocryptfs.conf' + if self.config_path is None: + cfg = os.path.join(self.path, f) + else: + cfg = os.path.join(self.config_path, f) + return cfg + + def isConfigured(self): + """ + Check if `gocryptfs.conf` exists. + """ + conf = self.configFile() + ret = os.path.exists(conf) + if ret: + logger.debug('Found gocryptfs config file in {}'.format(conf), self) + else: + logger.debug('No config in {}'.format(conf), self) + return ret From c481e114adc8f94b975eeb9a74534364b50b8dcb Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:07:26 +0200 Subject: [PATCH 04/36] reenable all tests --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 374d59897..1bd8a3079 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,10 +66,6 @@ install: - export PATH=~/bin:$PATH:$(pwd)/common script: - # test gocryptfs - - mkdir cipher plain - - gocryptfs -init -extpass "echo foo" cipher - - gocryptfs -extpass "echo foo" cipher plain # compile all files - ensure that syntax is correct - python -m compileall common common/test common/plugins qt qt/test qt/plugins # run unit tests - ensure that functionality is correct From 91975a444d5b064c58307ad57efe0ff353966295 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:11:36 +0200 Subject: [PATCH 05/36] set gocryptfs to be quiet --- common/gocryptfstools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 90c45597b..620183b62 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -55,7 +55,7 @@ def _mount(self): env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass'] + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass' '-quiet'] if self.reverse: gocryptfs += ['-reverse'] gocryptfs += [self.path, self.currentMountpoint] From ca7d7c665a61306d9779ad187dc129ed39cff6c1 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:14:32 +0200 Subject: [PATCH 06/36] typo --- common/gocryptfstools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 620183b62..ee10a3279 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -55,7 +55,7 @@ def _mount(self): env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass' '-quiet'] + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass', '-quiet'] if self.reverse: gocryptfs += ['-reverse'] gocryptfs += [self.path, self.currentMountpoint] From c5711fc31962b1b78ffd37cb1b654803c6012c82 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:46:02 +0200 Subject: [PATCH 07/36] fix settings --- common/mount.py | 71 ++++++++++++++++++++++++++++++-- qt/manageprofiles/tab_general.py | 16 +++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/common/mount.py b/common/mount.py index b9455029f..4df04bb96 100644 --- a/common/mount.py +++ b/common/mount.py @@ -377,6 +377,66 @@ def remount(self, new_profile_id, mode = None, hash_id = None, **kwargs): self.profile_id = new_profile_id return self.mount(mode = mode, **kwargs) + def isConfigured(self, mode = None, **kwargs): + """ + High-level check. Run :py:func:`MountControl.isConfigured` to check + if the backend is configured. + + Args: + mode (str): mode to use. One of 'local', 'ssh', + 'local_encfs' or 'ssh_encfs' + **kwargs: keyword arguments paste to low-level + :py:class:`MountControl` subclass backend + + Returns: + bool: ``True`` if backend is configured + """ + if mode is None: + mode = self.config.snapshotsMode(self.profile_id) + + if self.config.SNAPSHOT_MODES[mode][0] is None: + #mode doesn't need to mount + return True + else: + mounttools = self.config.SNAPSHOT_MODES[mode][0] + backend = mounttools(cfg = self.config, + profile_id = self.profile_id, + tmp_mount = self.tmp_mount, + mode = mode, + parent = self.parent, + **kwargs) + return backend.isConfigured() + + def init(self, mode = None, **kwargs): + """ + High-level init. Run :py:func:`MountControl.init` to initiade + the backend if not configured yet. + + Args: + mode (str): mode to use. One of 'local', 'ssh', + 'local_encfs' or 'ssh_encfs' + **kwargs: keyword arguments paste to low-level + :py:class:`MountControl` subclass backend + + Raises: + exceptions.MountException: if init failed + """ + if mode is None: + mode = self.config.snapshotsMode(self.profile_id) + + if self.config.SNAPSHOT_MODES[mode][0] is None: + #mode doesn't need to mount + return True + else: + mounttools = self.config.SNAPSHOT_MODES[mode][0] + backend = mounttools(cfg = self.config, + profile_id = self.profile_id, + tmp_mount = self.tmp_mount, + mode = mode, + parent = self.parent, + **kwargs) + return backend.init() + class MountControl: """This is the low-level mount API. This should be subclassed by backends. @@ -1067,10 +1127,13 @@ def removeSymlink(self, profile_id=None, tmp_mount=None): if tmp_mount is None: tmp_mount = self.tmp_mount - os.remove(self.config.snapshotsPath( - profile_id=profile_id, - mode=self.mode, - tmp_mount=tmp_mount)) + symlink = self.config.snapshotsPath( + profile_id = profile_id, + mode = self.mode, + tmp_mount = tmp_mount) + + if os.path.exists(symlink): + os.remove(symlink) def hash(self, s): """ diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index c38df6e96..241140d04 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -191,6 +191,9 @@ def __init__(self, parent): self.modeLocalEncfs = self.modeLocal self.modeSshEncfs = self.modeSsh + # gocryptfs + self.modeLocalGocryptfs = self.modeLocal + # password groupBox = QGroupBox(self) self.groupPassword1 = groupBox @@ -313,6 +316,10 @@ def load_values(self) -> Any: if self.mode == 'local_encfs': self.editSnapshotsPath.setText(self.config.localEncfsPath()) + # local_gocryptfs + if self.mode == 'local_gocryptfs': + self.editSnapshotsPath.setText(self.config.localGocryptfsPath()) + # password password_1 = self.config.password( mode=self.mode, pw_id=1, only_from_keyring=True) @@ -412,6 +419,9 @@ def store_values(self) -> bool: # save local_encfs self.config.setLocalEncfsPath(self.editSnapshotsPath.text()) + # save local_gocryptfs + self.config.setLocalGocryptfsPath(self.editSnapshotsPath.text()) + # schedule success = self._wdg_schedule.store_values(self.config) @@ -473,6 +483,12 @@ def _do_alot_pre_mount_checking(self, mnt, mount_kwargs): """ # preMountCheck + if not mnt.isConfigured(mode=self.config.snapshotsMode(), **mount_kwargs): + try: + mnt.init(mode = mode, **mount_kwargs) + except MountException as ex: + self.errorHandler(str(ex)) + return False try: # This will run several checks depending on the snapshots mode # used. Exceptions are raised if something goes wrong. On mode From 3af5aeb7eb8c0912408517dc2d19ad426481462f Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Mon, 24 Jul 2017 21:40:53 +0200 Subject: [PATCH 08/36] reenable piping gocryptfs output as the bug is fixed in https://github.com/rfjakob/gocryptfs/issues/130 --- common/gocryptfstools.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index ee10a3279..04015aa92 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -63,16 +63,15 @@ def _mount(self): %' '.join(gocryptfs), self) - proc = subprocess.Popen(gocryptfs, env = env) - # if stdout/err are piped into python gocryptfs v1.4 stays in - # foreground instead of forking away. So we can't redirect output - # for error messages. - proc.communicate() + proc = subprocess.Popen(gocryptfs, env = env, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + universal_newlines = True) + output = proc.communicate()[0] #### self.backupConfig() if proc.returncode: raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ - % {'command': ' '.join(gocryptfs), - 'error': 'Take a look into syslog for error messages.'}) + % {'command': ' '.join(gocryptfs), 'error': output}) def init(self): """ From e73cd7680ebcc59ea1a9972faf1932482dbc0dcb Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 20:36:46 +0200 Subject: [PATCH 09/36] test gocryptfs on travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1bd8a3079..374d59897 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,10 @@ install: - export PATH=~/bin:$PATH:$(pwd)/common script: + # test gocryptfs + - mkdir cipher plain + - gocryptfs -init -extpass "echo foo" cipher + - gocryptfs -extpass "echo foo" cipher plain # compile all files - ensure that syntax is correct - python -m compileall common common/test common/plugins qt qt/test qt/plugins # run unit tests - ensure that functionality is correct From 01138d110439d94f5bd60a88a5eb932da62aae38 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:04:03 +0200 Subject: [PATCH 10/36] fix gocryptfs stay in foreground --- common/gocryptfstools.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 04015aa92..ee10a3279 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -63,15 +63,16 @@ def _mount(self): %' '.join(gocryptfs), self) - proc = subprocess.Popen(gocryptfs, env = env, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) - output = proc.communicate()[0] + proc = subprocess.Popen(gocryptfs, env = env) + # if stdout/err are piped into python gocryptfs v1.4 stays in + # foreground instead of forking away. So we can't redirect output + # for error messages. + proc.communicate() #### self.backupConfig() if proc.returncode: raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ - % {'command': ' '.join(gocryptfs), 'error': output}) + % {'command': ' '.join(gocryptfs), + 'error': 'Take a look into syslog for error messages.'}) def init(self): """ From 171a7d9dc1d37ed3f35e47b24d6ab067b63df0de Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:07:26 +0200 Subject: [PATCH 11/36] reenable all tests --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 374d59897..1bd8a3079 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,10 +66,6 @@ install: - export PATH=~/bin:$PATH:$(pwd)/common script: - # test gocryptfs - - mkdir cipher plain - - gocryptfs -init -extpass "echo foo" cipher - - gocryptfs -extpass "echo foo" cipher plain # compile all files - ensure that syntax is correct - python -m compileall common common/test common/plugins qt qt/test qt/plugins # run unit tests - ensure that functionality is correct From ee4df40a51e903703af88f9949f68d8e6478c847 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:11:36 +0200 Subject: [PATCH 12/36] set gocryptfs to be quiet --- common/gocryptfstools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index ee10a3279..620183b62 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -55,7 +55,7 @@ def _mount(self): env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass', '-quiet'] + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass' '-quiet'] if self.reverse: gocryptfs += ['-reverse'] gocryptfs += [self.path, self.currentMountpoint] From 7ce058e8e2788134dc4d22858cb29c8acecc6c33 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Sat, 22 Jul 2017 22:14:32 +0200 Subject: [PATCH 13/36] typo --- common/gocryptfstools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 620183b62..ee10a3279 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -55,7 +55,7 @@ def _mount(self): env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass' '-quiet'] + gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass', '-quiet'] if self.reverse: gocryptfs += ['-reverse'] gocryptfs += [self.path, self.currentMountpoint] From 765045e24273a87efb31806630b189e55ac9f02d Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Mon, 24 Jul 2017 21:40:53 +0200 Subject: [PATCH 14/36] reenable piping gocryptfs output as the bug is fixed in https://github.com/rfjakob/gocryptfs/issues/130 --- common/gocryptfstools.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index ee10a3279..04015aa92 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -63,16 +63,15 @@ def _mount(self): %' '.join(gocryptfs), self) - proc = subprocess.Popen(gocryptfs, env = env) - # if stdout/err are piped into python gocryptfs v1.4 stays in - # foreground instead of forking away. So we can't redirect output - # for error messages. - proc.communicate() + proc = subprocess.Popen(gocryptfs, env = env, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + universal_newlines = True) + output = proc.communicate()[0] #### self.backupConfig() if proc.returncode: raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ - % {'command': ' '.join(gocryptfs), - 'error': 'Take a look into syslog for error messages.'}) + % {'command': ' '.join(gocryptfs), 'error': output}) def init(self): """ From e490e504e05a88df130a12a5cea3730071756bb8 Mon Sep 17 00:00:00 2001 From: Germar Reitze Date: Fri, 14 Apr 2017 17:51:29 +0200 Subject: [PATCH 15/36] integrate gocrypt into Settings --- common/config.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/common/config.py b/common/config.py index bc36e89da..aba07783f 100644 --- a/common/config.py +++ b/common/config.py @@ -43,6 +43,7 @@ import logger import sshtools import encfstools +import gocryptfstools import password import pluginmanager import schedule @@ -272,7 +273,7 @@ def __init__(self, config_path=None, data_path=None): sshtools.SSH, _('SSH'), _('SSH private key'), False), 'local_encfs': ( encfstools.EncFS_mount, - _('Local encrypted'), + _('Local encrypted') + ' (EncFS)', _('Encryption'), False ), @@ -281,7 +282,13 @@ def __init__(self, config_path=None, data_path=None): _('SSH encrypted'), _('SSH private key'), _('Encryption') - ) + ), + 'local_gocryptfs':( + gocryptfstools.GoCryptFS_mount, + _('Local encrypted') + ' (gocryptfs)', + _('Encryption'), + False + ), } self.SSH_CIPHERS = { @@ -694,6 +701,14 @@ def localEncfsPath(self, profile_id = None): def setLocalEncfsPath(self, value, profile_id = None): self.setProfileStrValue('snapshots.local_encfs.path', value, profile_id) + # gocryptfs + def localGocryptfsPath(self, profile_id = None): + #?Where to save snapshots in mode 'local_gocryptfs'.;absolute path + return self.profileStrValue('snapshots.local_gocryptfs.path', '', profile_id) + + def setLocalGocryptfsPath(self, value, profile_id = None): + self.setProfileStrValue('snapshots.local_gocryptfs.path', value, profile_id) + def passwordSave(self, profile_id = None, mode = None): if mode is None: mode = self.snapshotsMode(profile_id) From 3177815952e44fe8cb6db19c03040b4359350e06 Mon Sep 17 00:00:00 2001 From: David Wales Date: Tue, 15 Oct 2024 16:29:47 +1100 Subject: [PATCH 16/36] Use gocryptfs from packagemanager in Travis --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1bd8a3079..d438e0176 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ before_install: - sudo apt-key del 90CFB1F5 - sudo apt-get -qq update # install screen, and util-linux (provides flock) for test_sshtools - - sudo apt-get install -y sshfs screen util-linux libdbus-1-dev + - sudo apt-get install -y sshfs screen util-linux libdbus-1-dev gocryptfs jobs: exclude: @@ -58,12 +58,6 @@ install: - cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys # start ssh-agent so that we can add private keys - eval `ssh-agent -s` - # install latest gocryptfs - - export GOCRYPTFS_TARBALL=$(curl -s https://api.github.com/repos/rfjakob/gocryptfs/releases/latest | grep browser_download_url | cut -d '"' -f 4 | grep -v ".asc$") - - wget $GOCRYPTFS_TARBALL -O gocryptfs.tar.gz - - mkdir -p ~/bin - - tar xzf gocryptfs.tar.gz -C ~/bin - - export PATH=~/bin:$PATH:$(pwd)/common script: # compile all files - ensure that syntax is correct From bb09aea159f2cf274c4872ae993324de63628c30 Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 20:51:24 +1100 Subject: [PATCH 17/36] Update common/gocryptfstools.py Co-authored-by: buhtz --- common/gocryptfstools.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 04015aa92..4ce4218cb 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -1,18 +1,10 @@ -# Copyright (C) 2017 Germar Reitze +# SPDX-FileCopyrightText: © 2017 Germar Reitze # -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# SPDX-License-Identifier: GPL-2.0-or-later # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# This file is part of the program "Back In time" which is released under GNU +# General Public License v2 (GPLv2). See LICENSES directory or go to +# . import os import subprocess From 1b7820051dbf611c2aa8a00b23442994ec6982a5 Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 20:52:07 +1100 Subject: [PATCH 18/36] Remove explicit gettext definition Co-authored-by: buhtz --- common/gocryptfstools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 4ce4218cb..6a1b910fd 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -16,7 +16,6 @@ from mount import MountControl from exceptions import MountException -_ = gettext class GoCryptFS_mount(MountControl): """ From 9bf5c04b1d870ac02a8f51c38c6a2f6039ad12b6 Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 20:52:28 +1100 Subject: [PATCH 19/36] Remove explicit gettext definition Co-authored-by: buhtz --- common/gocryptfstools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 6a1b910fd..31e39fd63 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -8,7 +8,6 @@ import os import subprocess -from gettext import gettext import logger import tools From ec83c468c8b2e0a131090d4cd1cd2db5f2ed6a45 Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 21:05:15 +1100 Subject: [PATCH 20/36] Fix some pylint warnings - Remove unused import - Explicitly declare class members --- common/gocryptfstools.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 31e39fd63..cb5c272c5 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -10,7 +10,6 @@ import subprocess import logger -import tools from password_ipc import TempPasswordThread from mount import MountControl from exceptions import MountException @@ -22,6 +21,11 @@ class GoCryptFS_mount(MountControl): def __init__(self, *args, **kwargs): super(GoCryptFS_mount, self).__init__(*args, **kwargs) + # Workaround for some linters. + self.path = None + self.reverse = None + self.config_path = None + self.setattrKwargs('path', self.config.localGocryptfsPath(self.profile_id), **kwargs) self.setattrKwargs('reverse', False, **kwargs) self.setattrKwargs('password', None, store = False, **kwargs) From 2ee552938904aa2f6fc667f7ac10223e7266177f Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 21:51:26 +1100 Subject: [PATCH 21/36] Disable pylint duplicate check for encfs module - It's OK to remove these checks for encfs, as we plan to deprecate and remove the encfs code. --- common/encfstools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/encfstools.py b/common/encfstools.py index 9e1a2cc09..6980f6be2 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -44,6 +44,7 @@ def __init__(self, *args, **kwargs): self.setDefaultArgs() + # pylint: disable=duplicate-code self.mountproc = 'encfs' self.log_command = '%s: %s' % (self.mode, self.path) self.symlink_subfolder = None @@ -109,6 +110,7 @@ def configFile(self): return encfs config file """ f = '.encfs6.xml' + # pylint: disable=duplicate-code if self.config_path is None: cfg = os.path.join(self.path, f) else: From 0d8c5f3d8cdaac72c971412ccd20849b7f93c3d7 Mon Sep 17 00:00:00 2001 From: David Wales Date: Fri, 18 Oct 2024 22:08:31 +1100 Subject: [PATCH 22/36] Fix undefined variable --- qt/manageprofiles/tab_general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index 241140d04..fa6c2a8bc 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -485,7 +485,7 @@ def _do_alot_pre_mount_checking(self, mnt, mount_kwargs): if not mnt.isConfigured(mode=self.config.snapshotsMode(), **mount_kwargs): try: - mnt.init(mode = mode, **mount_kwargs) + mnt.init(mode=self.config.snapshotsMode(), **mount_kwargs) except MountException as ex: self.errorHandler(str(ex)) return False From 82cafeb505ffc7acdbe1aa417cb3caca56204426 Mon Sep 17 00:00:00 2001 From: David Wales Date: Sat, 30 Nov 2024 10:27:02 +1100 Subject: [PATCH 23/36] Add more gocryptfs details --- common/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/config.py b/common/config.py index aba07783f..2b097fd52 100644 --- a/common/config.py +++ b/common/config.py @@ -389,7 +389,7 @@ def get_snapshots_mountpoint(self, profile_id=None, mode=None, tmp_mount=False): if mode == 'local': return self.get_snapshots_path(profile_id) - # else: ssh/local_encfs/ssh_encfs + # else: ssh/local_encfs/ssh_encfs/local_gocryptfs symlink = f'{profile_id}_{os.getpid()}' if tmp_mount: @@ -427,7 +427,7 @@ def set_snapshots_path(self, value, profile_id=None): def snapshotsMode(self, profile_id=None): #? Use mode (or backend) for this snapshot. Look at 'man backintime' - #? section 'Modes'.;local|local_encfs|ssh|ssh_encfs + #? section 'Modes'.;local|local_encfs|ssh|ssh_encfs|local_gocryptfs return self.profileStrValue('snapshots.mode', 'local', profile_id) def setSnapshotsMode(self, value, profile_id = None): @@ -1573,6 +1573,8 @@ def _cron_line(self, profile_id): dest_path = self.snapshotsFullPath(profile_id) elif mode == 'local_encfs': dest_path = self.localEncfsPath(profile_id) + elif mode == 'local_gocryptfs': + dest_path = self.localGocryptfsPath(profile_id) else: logger.error( f"Udev scheduling doesn't work with mode {mode}", self) From d9a92b6d52d886eece7016711b9d82720bdfeed5 Mon Sep 17 00:00:00 2001 From: David Wales Date: Sat, 30 Nov 2024 10:27:57 +1100 Subject: [PATCH 24/36] Standardise GocryptfsMount class name --- common/config.py | 2 +- common/gocryptfstools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/config.py b/common/config.py index 2b097fd52..b7d8eb9cd 100644 --- a/common/config.py +++ b/common/config.py @@ -284,7 +284,7 @@ def __init__(self, config_path=None, data_path=None): _('Encryption') ), 'local_gocryptfs':( - gocryptfstools.GoCryptFS_mount, + gocryptfstools.GocryptfsMount, _('Local encrypted') + ' (gocryptfs)', _('Encryption'), False diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index cb5c272c5..214518ba8 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -15,11 +15,11 @@ from exceptions import MountException -class GoCryptFS_mount(MountControl): +class GocryptfsMount(MountControl): """ """ def __init__(self, *args, **kwargs): - super(GoCryptFS_mount, self).__init__(*args, **kwargs) + super(GocryptfsMount, self).__init__(*args, **kwargs) # Workaround for some linters. self.path = None From a6f55f68c58dca1420e2dc972676606b9314dbcf Mon Sep 17 00:00:00 2001 From: David Wales Date: Sat, 30 Nov 2024 10:49:56 +1100 Subject: [PATCH 25/36] Ensure GUI elements appear for local_gocryptfs mode --- qt/manageprofiles/tab_general.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index fa6c2a8bc..469daa24d 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -715,7 +715,9 @@ def handle_combo_modes_changed(self): self.mode = active_mode - self.modeLocal.setVisible(active_mode in ('local', 'local_encfs')) + self.modeLocal.setVisible( + active_mode in ('local', 'local_encfs', 'local_gocryptfs') + ) self.modeSsh.setVisible(active_mode in ('ssh', 'ssh_encfs')) # self.modeLocalEncfs = self.modeLocal # self.modeSshEncfs = self.modeSsh From a740e0ece3a2696700b4339e99b6cea20efad852 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Sat, 30 Nov 2024 10:24:04 +0100 Subject: [PATCH 26/36] fix errorHandler --- qt/manageprofiles/tab_general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index 469daa24d..fff9ab837 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -487,7 +487,7 @@ def _do_alot_pre_mount_checking(self, mnt, mount_kwargs): try: mnt.init(mode=self.config.snapshotsMode(), **mount_kwargs) except MountException as ex: - self.errorHandler(str(ex)) + messagebox.critical(self, str(ex)) return False try: # This will run several checks depending on the snapshots mode From 1ca3175b625923bcead5d53323565c8087a8c29e Mon Sep 17 00:00:00 2001 From: David Wales Date: Thu, 5 Dec 2024 08:00:48 +1100 Subject: [PATCH 27/36] Fix typo in docstring --- common/mount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/mount.py b/common/mount.py index 4df04bb96..0f7da2a29 100644 --- a/common/mount.py +++ b/common/mount.py @@ -409,7 +409,7 @@ def isConfigured(self, mode = None, **kwargs): def init(self, mode = None, **kwargs): """ - High-level init. Run :py:func:`MountControl.init` to initiade + High-level init. Run :py:func:`MountControl.init` to initiate the backend if not configured yet. Args: From 8dab4d9965fe08545dd39604c8cda11628eae1df Mon Sep 17 00:00:00 2001 From: David Wales Date: Thu, 9 Jan 2025 08:18:18 +1100 Subject: [PATCH 28/36] Rename init to init_backend To avoid confusion with __init__ --- common/gocryptfstools.py | 2 +- common/mount.py | 8 ++++---- qt/manageprofiles/tab_general.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 214518ba8..9a04d3791 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -67,7 +67,7 @@ def _mount(self): raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ % {'command': ' '.join(gocryptfs), 'error': output}) - def init(self): + def init_backend(self): """ init the cipher path """ diff --git a/common/mount.py b/common/mount.py index 0f7da2a29..25331c0d0 100644 --- a/common/mount.py +++ b/common/mount.py @@ -407,9 +407,9 @@ def isConfigured(self, mode = None, **kwargs): **kwargs) return backend.isConfigured() - def init(self, mode = None, **kwargs): + def init_backend(self, mode = None, **kwargs): """ - High-level init. Run :py:func:`MountControl.init` to initiate + High-level init. Run :py:func:`MountControl.init_backend` to initiate the backend if not configured yet. Args: @@ -419,7 +419,7 @@ def init(self, mode = None, **kwargs): :py:class:`MountControl` subclass backend Raises: - exceptions.MountException: if init failed + exceptions.MountException: if init_backend failed """ if mode is None: mode = self.config.snapshotsMode(self.profile_id) @@ -435,7 +435,7 @@ def init(self, mode = None, **kwargs): mode = mode, parent = self.parent, **kwargs) - return backend.init() + return backend.init_backend() class MountControl: """This is the low-level mount API. This should be subclassed by backends. diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index fff9ab837..2417eb576 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -485,7 +485,7 @@ def _do_alot_pre_mount_checking(self, mnt, mount_kwargs): if not mnt.isConfigured(mode=self.config.snapshotsMode(), **mount_kwargs): try: - mnt.init(mode=self.config.snapshotsMode(), **mount_kwargs) + mnt.init_backend(mode=self.config.snapshotsMode(), **mount_kwargs) except MountException as ex: messagebox.critical(self, str(ex)) return False From cf8a23a803be0ec6077b80ac442bfc3fee2de3d3 Mon Sep 17 00:00:00 2001 From: David Wales Date: Wed, 15 Jan 2025 23:10:29 +1100 Subject: [PATCH 29/36] Slightly improve code style --- common/encfstools.py | 8 ++++---- common/gocryptfstools.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/common/encfstools.py b/common/encfstools.py index 6980f6be2..2a1206cca 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -15,7 +15,7 @@ from packaging.version import Version import config import password -import password_ipc +from password_ipc import TempPasswordThread import tools import sshtools import logger @@ -56,7 +56,7 @@ def _mount(self): if self.password is None: self.password = self.config.password(self.parent, self.profile_id, self.mode) logger.debug('Provide password through temp FIFO', self) - thread = password_ipc.TempPasswordThread(self.password) + thread = TempPasswordThread(self.password) env = self.env() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): @@ -363,7 +363,7 @@ def startProcess(self): """ start 'encfsctl encode' process in pipe mode. """ - thread = password_ipc.TempPasswordThread(self.password) + thread = TempPasswordThread(self.password) env = self.encfs.env() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): @@ -595,7 +595,7 @@ def startProcess(self): """ start 'encfsctl decode' process in pipe mode. """ - thread = password_ipc.TempPasswordThread(self.password) + thread = TempPasswordThread(self.password) env = os.environ.copy() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 9a04d3791..dfdfbaf8c 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -82,9 +82,11 @@ def init_backend(self): gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass'] gocryptfs.append('-init') gocryptfs.append(self.path) - logger.debug('Call command to create gocryptfs config file: %s' - %' '.join(gocryptfs), - self) + logger.debug( + 'Call command to create gocryptfs config file: %s' + %' '.join(gocryptfs), + self + ) proc = subprocess.Popen(gocryptfs, env = env, stdout = subprocess.PIPE, @@ -93,8 +95,10 @@ def init_backend(self): output = proc.communicate()[0] #### self.backupConfig() if proc.returncode: - raise MountException(_('Can\'t init encrypted path \'%(command)s\':\n\n%(error)s') \ - % {'command': ' '.join(gocryptfs), 'error': output}) + raise MountException( + _("Can't init encrypted path '{command}':\n\n{error}") + .format(command=' '.join(gocryptfs), error=output) + ) def preMountCheck(self, first_run = False): """ From 0ef47409aeb47f37e786932ea9255965d3cb65c5 Mon Sep 17 00:00:00 2001 From: David Wales Date: Wed, 15 Jan 2025 23:13:28 +1100 Subject: [PATCH 30/36] Add init_backend method to match gocryptfs updates We are rebasing previous gocryptfs work from 2017. This work added an `init_backend` method which separates the concerns of first-time initialisation vs subsequent use. --- common/encfstools.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/common/encfstools.py b/common/encfstools.py index 2a1206cca..c112004a5 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -83,6 +83,41 @@ def _mount(self): .format(command=' '.join(encfs)), output)) + def init_backend(self): + """ + init the cipher path + """ + if self.password is None: + self.password = self.config.password(self.parent, self.profile_id, self.mode) + logger.debug('Provide password through temp FIFO', self) + thread = TempPasswordThread(self.password) + env = os.environ.copy() + env['ASKPASS_TEMP'] = thread.temp_file + + with thread.starter(): + encfs = [self.mountproc, '--extpass=backintime-askpass'] + if self.reverse: + encfs += ['--reverse'] + encfs += ['--standard'] + encfs += [self.path, self.currentMountpoint] + logger.debug( + 'Call command to create EncFS config file: %s' + %' '.join(encfs), + self + ) + + proc = subprocess.Popen(encfs, env = env, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + universal_newlines = True) + output = proc.communicate()[0] + self.backupConfig() + if proc.returncode: + raise MountException( + _("Can't init encrypted path '{command}':\n\n{error}") + .format(command=' '.join(encfs), error=output) + ) + def preMountCheck(self, first_run=False): """Check what ever conditions must be given for the mount. From 353b77c3ab0476d0dbe47bb698814dae603143f1 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Tue, 30 Dec 2025 16:29:50 +0100 Subject: [PATCH 31/36] [skip ci] --- qt/manageprofiles/tab_general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index 89ca15981..26ab8864f 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -171,7 +171,7 @@ def __init__(self, parent): # noqa: PLR0915 self._group_mode_ssh_encfs = self._group_mode_ssh # gocryptfs - self.modeLocalGocryptfs = self.modeLocal + self._group_mode_local_gocrypt = self._group_mode_local # password group_box = QGroupBox(self) From 457cefd4c7f4eecfbe531b54fd645787d032dfe5 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Tue, 30 Dec 2025 16:56:17 +0100 Subject: [PATCH 32/36] [skip ci] --- common/encfstools.py | 27 ++++++++++++++++++--------- common/gocryptfstools.py | 6 ++++-- qt/manageprofiles/tab_general.py | 13 +++++++++---- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/common/encfstools.py b/common/encfstools.py index a8d7d619b..c1a942dc0 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -67,27 +67,36 @@ def _mount(self): """ if self.password is None: self.password = self.config.password(self.parent, self.profile_id, self.mode) - logger.debug('Provide password through temp FIFO', self) + + logger.debug(f'Provide password through temp FIFO {self.password=}', self) + thread = TempPasswordThread(self.password) env = self.env() env['ASKPASS_TEMP'] = thread.temp_file + with thread.starter(): encfs = [self.mountproc, '--extpass=backintime-askpass'] + if self.reverse: encfs += ['--reverse'] + if not self.isConfigured(): encfs += ['--standard'] - encfs += [self.path, self.currentMountpoint] - logger.debug('Call mount command: %s' - %' '.join(encfs), - self) - proc = subprocess.Popen(encfs, env = env, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) + encfs += [self.path, self.currentMountpoint] + logger.debug('Call mount command: ' + ' '.join(encfs), self) + + proc = subprocess.Popen( + encfs, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) output = proc.communicate()[0] + self.backupConfig() + if proc.returncode: raise MountException( '{}:\n\n{}\n\n{}'.format( diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index dfdfbaf8c..73629197c 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -72,8 +72,10 @@ def init_backend(self): init the cipher path """ if self.password is None: - self.password = self.config.password(self.parent, self.profile_id, self.mode) - logger.debug('Provide password through temp FIFO', self) + self.password = self.config.password( + self.parent, self.profile_id, self.mode) + logger.debug(f'Provide password through temp FIFO {self.password=}', self) + thread = TempPasswordThread(self.password) env = os.environ.copy() env['ASKPASS_TEMP'] = thread.temp_file diff --git a/qt/manageprofiles/tab_general.py b/qt/manageprofiles/tab_general.py index 26ab8864f..dfe9e38d9 100644 --- a/qt/manageprofiles/tab_general.py +++ b/qt/manageprofiles/tab_general.py @@ -360,7 +360,7 @@ def load_values(self) -> Any: # local_gocryptfs if self.mode == 'local_gocryptfs': - self.editSnapshotsPath.setText(self.config.localGocryptfsPath()) + self._edit_backup_path.setText(self.config.localGocryptfsPath()) self._load_passwords() @@ -420,7 +420,7 @@ def store_values(self) -> bool: self.config.setLocalEncfsPath(self._edit_backup_path.text()) # save local_gocryptfs - self.config.setLocalGocryptfsPath(self.editSnapshotsPath.text()) + self.config.setLocalGocryptfsPath(self._edit_backup_path.text()) # schedule success = self._wdg_schedule.store_values(self.config) @@ -485,12 +485,17 @@ def _do_alot_pre_mount_checking(self, mnt, mount_kwargs): # noqa: PLR0911 # pylint: disable=too-many-return-statements # preMountCheck - if not mnt.isConfigured(mode=self.config.snapshotsMode(), **mount_kwargs): + if not mnt.isConfigured( + mode=self.config.snapshotsMode(), **mount_kwargs): + try: - mnt.init_backend(mode=self.config.snapshotsMode(), **mount_kwargs) + mnt.init_backend( + mode=self.config.snapshotsMode(), **mount_kwargs) + except MountException as ex: messagebox.critical(self, str(ex)) return False + try: # This will run several checks depending on the snapshots mode # used. Exceptions are raised if something goes wrong. On mode From 11efc8669b5ddfd15837e9aeacefe5119762c762 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Thu, 1 Jan 2026 18:14:10 +0100 Subject: [PATCH 33/36] Encfs_mount.init_backend() just empty --- common/encfstools.py | 2 ++ common/test/test_lint.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/encfstools.py b/common/encfstools.py index c1a942dc0..1d1488330 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -110,6 +110,8 @@ def init_backend(self): """ init the cipher path """ + return # DEBUG + if self.password is None: self.password = self.config.password(self.parent, self.profile_id, self.mode) logger.debug('Provide password through temp FIFO', self) diff --git a/common/test/test_lint.py b/common/test/test_lint.py index 17097a7b3..6ef60db62 100644 --- a/common/test/test_lint.py +++ b/common/test/test_lint.py @@ -51,8 +51,10 @@ 'daemon.py', 'event.py', 'encode.py', - 'languages.py', + 'gocryptfstools.py', 'inhibitsuspend.py', + 'languages.py', + # 'mount.py', 'schedule.py', 'shutdownagent.py', 'singleton.py', From 73cc8fb12faef0aabb2209f78b7978ae4fc671b4 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Mon, 5 Jan 2026 11:16:27 +0100 Subject: [PATCH 34/36] encfs fifo docu --- common/encfstools.py | 68 +++++++++++++++++++++++++++++----------- common/gocryptfstools.py | 2 ++ common/password_ipc.py | 9 ++++-- common/test/test_lint.py | 1 + 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/common/encfstools.py b/common/encfstools.py index 1d1488330..1691ba02a 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -1,5 +1,7 @@ # SPDX-FileCopyrightText: © 2012-2022 Germar Reitze # SPDX-FileCopyrightText: © 2012-2022 Taylor Raack +# SPDX-FileCopyrightText: © 2025 David Wales (@daviewales) +# SPDX-FileCopyrightText: © 2025 Christian Buhtz # # SPDX-License-Identifier: GPL-2.0-or-later # @@ -28,15 +30,7 @@ class EncFS_mount(MountControl): """Mount encrypted paths with encfs.""" def __init__(self, *args, **kwargs): - # # TODO: Remove these debug calls as they are just to help me - # # setup testing! - # logger.debug("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - # logger.debug("REMOVE THESE TEMPORARY DEBUG LINES!") - # logger.debug("EncFS_mount args:") - # logger.debug(str(args)) - # logger.debug("EncFS_mount kwargs:") - # logger.debug(str(kwargs)) - # logger.debug("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + # logger.debug("EncFS_mount.init() :: {args=} {kwargs=}") # DEBUG # init MountControl super(EncFS_mount, self).__init__(*args, **kwargs) @@ -46,11 +40,12 @@ def __init__(self, *args, **kwargs): self.reverse = None self.config_path = None - self.setattrKwargs('path', self.config.localEncfsPath(self.profile_id), **kwargs) - # print(f"{self.path=}") # DEBUG + self.setattrKwargs( + 'path', self.config.localEncfsPath(self.profile_id), **kwargs) + # logger.debug("EncFS_mount.init() :: {self.path=}") # DEBUG self.setattrKwargs('reverse', False, **kwargs) self.setattrKwargs('config_path', None, **kwargs) - self.setattrKwargs('password', None, store = False, **kwargs) + self.setattrKwargs('password', None, store=False, **kwargs) self.setattrKwargs('hash_id_1', None, **kwargs) self.setattrKwargs('hash_id_2', None, **kwargs) @@ -65,16 +60,50 @@ def _mount(self): """ mount the service """ - if self.password is None: - self.password = self.config.password(self.parent, self.profile_id, self.mode) - - logger.debug(f'Provide password through temp FIFO {self.password=}', self) + if self.password is None: + self.password = self.config.password( + self.parent, self.profile_id, self.mode) + + # DEBUG + logger.debug( + f'Provide password through temp FIFO {self.password=}', self) + + # Dev note (2026-01, buhtz): + # Password flow overview: + # + # 1. Back In Time creates a TempPasswordThread and passes the password + # to it. + # 2. The thread creates a temporary FIFO and blocks while writing the + # password to it, waiting for a reader. + # 3. Back In Time starts encfs with "--extpass=backintime-askpass". + # 4. The FIFO path is passed via the environment variable ASKPASS_TEMP. + # 5. encfs invokes backintime-askpass as an external password helper. + # 6. backintime-askpass reads the FIFO path from ASKPASS_TEMP, opens + # the FIFO, reads the password, and writes it to stdout. + # 7. encfs reads the password from backintime-askpass's stdout. + # 8. After the read completes, the FIFO is removed and the thread + # exits. + # + # Result: + # The password is transferred exactly once, synchronously, via a FIFO, + # without appearing on the command line, in files, or in the process + # list. + # + # Reason: + # It is about security. It minimizes password lifetime and exposure. + # Password never appears in a shell context, is transffered only once. + + # Prepare the password-fifo-thread thread = TempPasswordThread(self.password) env = self.env() env['ASKPASS_TEMP'] = thread.temp_file + # Start thread and write password to FIFO with thread.starter(): + + # build encfs command and provide "backintime-askpass" as + # password helper encfs = [self.mountproc, '--extpass=backintime-askpass'] if self.reverse: @@ -86,6 +115,9 @@ def _mount(self): encfs += [self.path, self.currentMountpoint] logger.debug('Call mount command: ' + ' '.join(encfs), self) + # Encfs aks backintime-askpass for the password. + # backintime-askpass will read the password from FIFO and provide + # it via return on stdout to the encfs process proc = subprocess.Popen( encfs, env=env, @@ -100,10 +132,10 @@ def _mount(self): if proc.returncode: raise MountException( '{}:\n\n{}\n\n{}'.format( - _("Unable to mount '{command}'") + _('Unable to mount '{command}'') .format(command=' '.join(encfs)), output, - f"Return code: {proc.returncode}", + f'Return code: {proc.returncode}', )) def init_backend(self): diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 73629197c..33fd738e2 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -1,4 +1,6 @@ # SPDX-FileCopyrightText: © 2017 Germar Reitze +# SPDX-FileCopyrightText: © 2025 David Wales (@daviewales) +# SPDX-FileCopyrightText: © 2025 Christian Buhtz # # SPDX-License-Identifier: GPL-2.0-or-later # diff --git a/common/password_ipc.py b/common/password_ipc.py index 5fd91a7f9..90490dd97 100644 --- a/common/password_ipc.py +++ b/common/password_ipc.py @@ -15,6 +15,7 @@ import logger + class FIFO: """Inter-process communication (IPC) with named pipes using the first-in, first-out principle (FIFO). @@ -32,6 +33,7 @@ def delfifo(self): """Remove named pipe file.""" try: os.remove(self.fifo) + # TODO: Catch FileNotFoundError only except: pass @@ -121,9 +123,10 @@ def isFifo(self): class TempPasswordThread(threading.Thread): - """ - in case BIT is not configured yet provide password through temp FIFO - to backintime-askpass. + """Provide password through temp FIFO. + + In case BIT is not configured yet this provides a password through temp + FIFO to backintime-askpass. """ def __init__(self, string): diff --git a/common/test/test_lint.py b/common/test/test_lint.py index 6ef60db62..d97d99803 100644 --- a/common/test/test_lint.py +++ b/common/test/test_lint.py @@ -51,6 +51,7 @@ 'daemon.py', 'event.py', 'encode.py', + 'encfstools.py', 'gocryptfstools.py', 'inhibitsuspend.py', 'languages.py', From fbf3b10ef456eb7c323a5e2b05c65eeca46ae8e7 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Mon, 5 Jan 2026 14:37:09 +0100 Subject: [PATCH 35/36] more doc and encfs empty initbackend --- common/encfstools.py | 71 ++++++++++++++++---------------------------- common/mount.py | 7 +++++ 2 files changed, 32 insertions(+), 46 deletions(-) diff --git a/common/encfstools.py b/common/encfstools.py index 1691ba02a..006217542 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -130,50 +130,20 @@ def _mount(self): self.backupConfig() if proc.returncode: + msg = _('Unable to mount "{command}"').format( + command=' '.join(encfs) + ) raise MountException( - '{}:\n\n{}\n\n{}'.format( - _('Unable to mount '{command}'') - .format(command=' '.join(encfs)), - output, - f'Return code: {proc.returncode}', - )) + f'{msg}:\n\n{output}\n\nReturn code: {proc.returncode}' + ) def init_backend(self): - """ - init the cipher path - """ - return # DEBUG - - if self.password is None: - self.password = self.config.password(self.parent, self.profile_id, self.mode) - logger.debug('Provide password through temp FIFO', self) - thread = TempPasswordThread(self.password) - env = os.environ.copy() - env['ASKPASS_TEMP'] = thread.temp_file - - with thread.starter(): - encfs = [self.mountproc, '--extpass=backintime-askpass'] - if self.reverse: - encfs += ['--reverse'] - encfs += ['--standard'] - encfs += [self.path, self.currentMountpoint] - logger.debug( - 'Call command to create EncFS config file: %s' - %' '.join(encfs), - self - ) + """Empty for Encfs because initialization happens implicite. - proc = subprocess.Popen(encfs, env = env, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) - output = proc.communicate()[0] - self.backupConfig() - if proc.returncode: - raise MountException( - _("Can't init encrypted path '{command}':\n\n{error}") - .format(command=' '.join(encfs), error=output) - ) + Init happens in mount() via "--standard" switch on encfs if + self.isConfigured() is False. + """ + return def preMountCheck(self, first_run=False): """Check what ever conditions must be given for the mount. @@ -281,25 +251,34 @@ def backupConfig(self): """ cfg = self.configFile() if not os.path.isfile(cfg): - logger.warning('No encfs config in %s. Skip backup of config file.' %cfg, self) + logger.warning( + f'No encfs config in {cfg}. Skip backup of config file.', self + ) return + backup_folder = self.config.encfsconfigBackupFolder(self.profile_id) tools.makeDirs(backup_folder) + old_backups = os.listdir(backup_folder) - old_backups.sort(reverse = True) + old_backups.sort(reverse=True) + if len(old_backups): last_backup = os.path.join(backup_folder, old_backups[0]) - #don't create a new backup if config hasn't changed + # Don't create a new backup if config hasn't changed if tools.md5sum(cfg) == \ tools.md5sum(last_backup): logger.debug('Encfs config did not change. Skip backup', self) return - new_backup_file = '.'.join((os.path.basename(cfg), datetime.now().strftime('%Y%m%d%H%M'))) + new_backup_file = '.'.join(( + os.path.basename(cfg), + datetime.now().strftime('%Y%m%d%H%M') + )) new_backup = os.path.join(backup_folder, new_backup_file) - logger.debug('Create backup of encfs config %s to %s' - %(cfg, new_backup), self) + logger.debug( + f'Create backup of encfs config {cfg} to {new_backup}', self + ) shutil.copy2(cfg, new_backup) diff --git a/common/mount.py b/common/mount.py index 253c86f99..5605c898a 100644 --- a/common/mount.py +++ b/common/mount.py @@ -130,6 +130,9 @@ class Mount: tmp_mount (bool): If ``True`` mount to a temporary destination. parent (QWidget): Parent widget for QDialogs or ``None`` if there is no parent. + + Dev note (buhtz, 2026-01): Rename that class into something like + MountOrchestrator or MountManager. """ def __init__(self, @@ -435,6 +438,7 @@ def init_backend(self, mode = None, **kwargs): **kwargs) return backend.init_backend() + class MountControl: """This is the low-level mount API. This should be subclassed by backends. @@ -473,6 +477,9 @@ class MountControl: ``ssh_encfs`` hash_collision (int): global value used to prevent hash collisions on mountpoints + + Dev note (buhtz, 2026-01): Rename that class into something like + MountController or MountBase """ def __init__(self, From e8249d63ae543d6e1608da74aca88d7969e64572 Mon Sep 17 00:00:00 2001 From: Christian Buhtz Date: Mon, 5 Jan 2026 15:03:22 +0100 Subject: [PATCH 36/36] pepification --- common/encfstools.py | 296 +++++++++++++++++++++++++++------------ common/gocryptfstools.py | 96 ++++++++----- common/test/test_lint.py | 4 +- 3 files changed, 272 insertions(+), 124 deletions(-) diff --git a/common/encfstools.py b/common/encfstools.py index 006217542..461f35d81 100644 --- a/common/encfstools.py +++ b/common/encfstools.py @@ -65,10 +65,6 @@ def _mount(self): self.password = self.config.password( self.parent, self.profile_id, self.mode) - # DEBUG - logger.debug( - f'Provide password through temp FIFO {self.password=}', self) - # Dev note (2026-01, buhtz): # Password flow overview: # @@ -231,11 +227,18 @@ def checkVersion(self): """ logger.debug('Check version', self) if self.reverse: - proc = subprocess.Popen(['encfs', '--version'], - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) + proc = subprocess.Popen( + [ + 'encfs', + '--version' + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + output = proc.communicate()[0] + m = re.search(r'(\d\.\d\.\d)', output) if m and Version(m.group(1)) <= Version('1.7.2'): logger.debug('Wrong encfs version %s' % m.group(1), self) @@ -244,10 +247,10 @@ def checkVersion(self): 'option --reverse. Please update encfs.') def backupConfig(self): - """ - create a backup of encfs config file into local config folder - so in cases of the config file get deleted or corrupt user can restore - it from there + """Create a backup of encfs config file into local config folder. + + In cases of the config file get deleted or corrupt user can restore + it from there. """ cfg = self.configFile() if not os.path.isfile(cfg): @@ -288,7 +291,15 @@ class EncFS_SSH(EncFS_mount): Mount / with encfs --reverse. rsync will then sync the encrypted view on / to the remote path """ - def __init__(self, cfg = None, profile_id = None, mode = None, parent = None,*args, **kwargs): + + def __init__( + self, + cfg=None, + profile_id=None, + mode=None, + parent=None, + *args, **kwargs + ): self.config = cfg or config.Config() self.profile_id = profile_id or self.config.currentProfile() self.mode = mode @@ -299,8 +310,13 @@ def __init__(self, cfg = None, profile_id = None, mode = None, parent = None,*ar self.args = args self.kwargs = kwargs - self.ssh = sshtools.SSH(*self.args, symlink = False, **self.splitKwargs('ssh')) - self.rev_root = EncFS_mount(*self.args, symlink = False, **self.splitKwargs('encfs_reverse')) + self.ssh = sshtools.SSH( + *self.args, symlink=False, **self.splitKwargs('ssh') + ) + self.rev_root = EncFS_mount( + *self.args, symlink=False, **self.splitKwargs('encfs_reverse') + ) + super(EncFS_SSH, self).__init__(*self.args, **self.splitKwargs('encfs')) def mount(self, *args, **kwargs): @@ -310,49 +326,74 @@ def mount(self, *args, **kwargs): """ logger.debug('Mount sshfs', self) self.ssh.mount(*args, **kwargs) - #mount fsroot with encfs --reverse first. - #If the config does not exist already this will make sure - #the new created config works with --reverse + # mount fsroot with encfs --reverse first. + # If the config does not exist already this will make sure + # the new created config works with --reverse + if not os.path.isfile(self.configFile()): - #encfs >= 1.8.0 changed behavior when ENCFS6_CONFIG environ variable - #file does not exist. It will not create a new one anymore but just fail. - #As encfs would create the config in /.encfs6.xml (which will most likely fail) - #we need to mount a temp folder with reverse first and copy the config when done. - logger.debug('Mount temp directory with encfs --reverse to create a new encfs config', self) + # encfs >= 1.8.0 changed behavior when ENCFS6_CONFIG environ + # variable file does not exist. It will not create a new one + # anymore but just fail. As encfs would create the config in + # /.encfs6.xml (which will most likely fail) we need to mount a + # temp folder with reverse first and copy the config when done. + + # logger.debug( + # 'Mount temp directory with encfs --reverse to create a new ' + # 'encfs config', + # self + # ) + with tempfile.TemporaryDirectory() as src: tmp_kwargs = self.splitKwargs('encfs_reverse') tmp_kwargs['path'] = src tmp_kwargs['config_path'] = src - tmp_mount = EncFS_mount(*self.args, symlink = False, **tmp_kwargs) + + tmp_mount = EncFS_mount( + *self.args, symlink=False, **tmp_kwargs) tmp_mount.mount(*args, **kwargs) tmp_mount.umount() + cfg = tmp_mount.configFile() + if os.path.isfile(cfg): - logger.debug('Copy new encfs config %s to its original place %s' %(cfg, self.ssh.currentMountpoint), self) + logger.debug( + f'Copy new encfs config {cfg} to its original place ' + f'{self.ssh.currentMountpoint}', + self + ) shutil.copy2(cfg, self.ssh.currentMountpoint) + else: - logger.error('New encfs config %s not found' %cfg, self) - logger.debug('Mount local filesystem root with encfs --reverse', self) + logger.error(f'New encfs config {cfg} not found', self) + + # logger.debug('Mount local filesystem root with encfs --reverse', self) self.rev_root.mount(*args, **kwargs) - logger.debug('Mount encfs', self) + # logger.debug('Mount encfs', self) kwargs['check'] = False + ret = super(EncFS_SSH, self).mount(*args, **kwargs) + self.config.ENCODE = Encode(self) + return ret def umount(self, *args, **kwargs): + """Close 'encfsctl encode' process and set config.ENCODE back to the + dummy class. Call umount for encfs, encfs --reverse and sshfs """ - close 'encfsctl encode' process and set config.ENCODE back to the dummy class. - call umount for encfs, encfs --reverse and sshfs - """ + self.config.ENCODE.close() self.config.ENCODE = encode.Bounce() - logger.debug('Unmount encfs', self) + + # logger.debug('Unmount encfs', self) + super(EncFS_SSH, self).umount(*args, **kwargs) - logger.debug('Unmount local filesystem root mount encfs --reverse', self) + # logger.debug('Unmount local filesystem root mount encfs --reverse', self) + self.rev_root.umount(*args, **kwargs) - logger.debug('Unmount sshfs', self) + # logger.debug('Unmount sshfs', self) + self.ssh.umount(*args, **kwargs) def preMountCheck(self, *args, **kwargs): @@ -371,47 +412,74 @@ def splitKwargs(self, mode): split all given arguments for the desired mount class """ d = self.kwargs.copy() - d['cfg'] = self.config + d['cfg'] = self.config d['profile_id'] = self.profile_id - d['mode'] = self.mode - d['parent'] = self.parent + d['mode'] = self.mode + d['parent'] = self.parent + if mode == 'ssh': if 'path' in d: d.pop('path') + if 'ssh_path' in d: d['path'] = d.pop('ssh_path') + if 'ssh_password' in d: d['password'] = d.pop('ssh_password') else: - d['password'] = self.config.password(parent = self.parent, profile_id = self.profile_id, mode = self.mode) + d['password'] = self.config.password( + parent=self.parent, + profile_id=self.profile_id, + mode=self.mode + ) + if 'hash_id' in d: d.pop('hash_id') + if 'hash_id_2' in d: d['hash_id'] = d['hash_id_2'] + return d elif mode == 'encfs': d['path'] = self.ssh.currentMountpoint d['hash_id_1'] = self.rev_root.hash_id d['hash_id_2'] = self.ssh.hash_id + if 'encfs_password' in d: d['password'] = d.pop('encfs_password') + else: - d['password'] = self.config.password(parent = self.parent, profile_id = self.profile_id, mode = self.mode, pw_id = 2) + d['password'] = self.config.password( + parent=self.parent, + profile_id=self.profile_id, + mode=self.mode, + pw_id=2 + ) + return d elif mode == 'encfs_reverse': d['reverse'] = True d['path'] = '/' d['config_path'] = self.ssh.currentMountpoint + if 'encfs_password' in d: d['password'] = d.pop('encfs_password') else: - d['password'] = self.config.password(parent = self.parent, profile_id = self.profile_id, mode = self.mode, pw_id = 2) + d['password'] = self.config.password( + parent=self.parent, + profile_id=self.profile_id, + mode=self.mode, + pw_id=2 + ) + if 'hash_id' in d: d.pop('hash_id') + if 'hash_id_1' in d: d['hash_id'] = d['hash_id_1'] + return d @@ -431,7 +499,7 @@ def __init__(self, encfs): if not self.remote_path[-1] == os.sep: self.remote_path += os.sep - #precompile some regular expressions + # Precompile some regular expressions self.re_asterisk = re.compile(r'\*') self.re_separate_asterisk = re.compile(r'(.*?)(\*+)(.*)') @@ -446,15 +514,16 @@ def startProcess(self): env = self.encfs.env() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - logger.debug('start \'encfsctl encode\' process', self) encfsctl = ['encfsctl', 'encode', '--extpass=backintime-askpass', '/'] - logger.debug('Call command: %s' - %' '.join(encfsctl), - self) - self.p = subprocess.Popen(encfsctl, env = env, bufsize = 0, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - universal_newlines = True) + logger.debug(f'Call command: {encfsctl}', self) + self.p = subprocess.Popen( + encfsctl, + env=env, + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + universal_newlines=True + ) def path(self, path): """ @@ -462,16 +531,22 @@ def path(self, path): """ if not 'p' in vars(self): self.startProcess() + if not self.p.returncode is None: - logger.warning('\'encfsctl encode\' process terminated. Restarting.', self) + logger.warning( + "'encfsctl encode' process terminated. Restarting.", self + ) del self.p + self.startProcess() + self.p.stdin.write(path + '\n') ret = self.p.stdout.readline().strip('\n') + if not len(ret) and len(path): - logger.debug('Failed to encode %s. Got empty string' - %path, self) + logger.debug(f'Failed to encode {path}. Got empty string', self) raise EncodeValueError() + return ret def exclude(self, path): @@ -491,32 +566,43 @@ def exclude(self, path): path_ = path[:] while True: - #search for foo/*, foo/*/bar, */bar or **/bar - #but not foo* or foo/*bar + # Search for foo/*, foo/*/bar, */bar or **/bar + # but not foo* or foo/*bar m = self.re_separate_asterisk.search(path_) + if m is None: return None + if m.group(1): if not m.group(1).endswith(os.sep): return None enc = os.path.join(enc, self.path(m.group(1))) + enc = os.path.join(enc, m.group(2)) + if m.group(3): if not m.group(3).startswith(os.sep): return None + m1 = self.re_asterisk.search(m.group(3)) + if m1 is None: enc = os.path.join(enc, self.path(m.group(3))) break + else: path_ = m.group(3) continue + else: break + else: enc = self.path(path) + if os.path.isabs(path): return os.path.join(os.sep, enc) + return enc def include(self, path): @@ -530,6 +616,7 @@ def remote(self, path): encode the path on remote host starting from backintime/host/user/... """ enc_path = self.path(path[len(self.remote_path):]) + return os.path.join(self.remote_path, enc_path) def close(self): @@ -537,7 +624,7 @@ def close(self): stop encfsctl process """ if 'p' in vars(self) and self.p.returncode is None: - logger.debug('stop \'encfsctl encode\' process', self) + logger.debug("stop 'encfsctl encode' process", self) self.p.communicate() @@ -551,10 +638,10 @@ def __init__(self, cfg, string=True): self.mode = cfg.snapshotsMode() if self.mode == 'local_encfs': - self.password = cfg.password(pw_id = 1) + self.password = cfg.password(pw_id=1) elif self.mode == 'ssh_encfs': - self.password = cfg.password(pw_id = 2) + self.password = cfg.password(pw_id=2) self.encfs = cfg.SNAPSHOT_MODES[self.mode][0](cfg) self.remote_path = cfg.sshSnapshotsPath() @@ -572,19 +659,21 @@ def __init__(self, cfg, string=True): takeSnapshot = _('Take snapshot') \ .replace('Schnappschuss', '(?:Schnappschuss|Snapshot)') - #precompile some regular expressions host, _post, user, path, _cipher = cfg.sshHostUserPortPathCipher() - #replace: --exclude"" or --include"" + + # replace: --exclude"" or --include"" self.re_include_exclude = re.compile( r'(--(?:ex|in)clude=")(.*?)(")') # codespell-ignore - #replace: 'USER@HOST:"PATH"' - self.re_remote_path = re.compile(r'(\'%s@%s:"%s)(.*?)("\')' %(user, host, path)) + # replace: 'USER@HOST:"PATH"' + self.re_remote_path = re.compile( + r'(\'%s@%s:"%s)(.*?)("\')' % (user, host, path) + ) - #replace: --link-dest="../../" - self.re_link_dest = re.compile(r'(--link-dest="\.\./\.\./)(.*?)(")') + # replace: --link-dest="../../" + self.re_link_dest = re.compile(r'(--link-dest="\.\./\.\./)(.*?)(")') - #search for: [C] + # search for: [C] self.re_change = re.compile(r'(^\[C\] .{11} )(.*)') #search for: [I] Take snapshot (rsync: BACKINTIME: ) @@ -609,22 +698,24 @@ def __init__(self, cfg, string=True): pattern = [] pattern.append(r' rsync: readlink_stat\(".*?mountpoint/') pattern.append(r' rsync: send_files failed to open ".*?mountpoint/') + if self.remote_path == './': pattern.append(r' rsync: recv_generator: failed to stat "/home/[^/]*/') pattern.append(r' rsync: recv_generator: mkdir "/home/[^/]*/') else: pattern.append(r' rsync: recv_generator: failed to stat ".*?{}'.format(self.remote_path)) pattern.append(r' rsync: recv_generator: mkdir ".*?{}'.format(self.remote_path)) + pattern.append(r' rsync: .*?".*?mountpoint/') self.re_error = re.compile(r'(^(?:\[E\] )?Error:(?:%s))(.*?)(".*)' % '|'.join(pattern)) - #search for: [I] ssh USER@HOST cp -aRl "PATH"* "PATH" + # search for: [I] ssh USER@HOST cp -aRl "PATH"* "PATH" self.re_info_cp= re.compile(r'(^\[I\] .*? cp -aRl "%s/)(.*?)("\* "%s/)(.*?)(")' % (path, path)) - #search for all chars except * + # search for all chars except * self.re_all_except_asterisk = re.compile(r'[^\*]+') - #search for: -> + # search for: -> self.re_all_except_arrow = re.compile(r'(.*?)((?: [-=]> )+)(.*)') #skip: [I] Take snapshot (rsync: sending incremental file list) @@ -643,10 +734,8 @@ def __init__(self, cfg, string=True): self.re_skip = re.compile(r'^(?:\[I\] )?%s \(rsync: (%s)' % (takeSnapshot, '|'.join(pattern))) self.string = string - if string: - self.newline = '\n' - else: - self.newline = b'\n' + + self.newline = '\n' if string else b'\n' def __del__(self): self.close() @@ -658,17 +747,25 @@ def startProcess(self): thread = TempPasswordThread(self.password) env = os.environ.copy() env['ASKPASS_TEMP'] = thread.temp_file + with thread.starter(): - logger.debug('start \'encfsctl decode\' process', self) - encfsctl = ['encfsctl', 'decode', '--extpass=backintime-askpass', self.encfs.path] - logger.debug('Call command: %s' - %' '.join(encfsctl), - self) - self.p = subprocess.Popen(encfsctl, env = env, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - universal_newlines = self.string, #return string (if True) or bytes - bufsize = 0) + encfsctl = [ + 'encfsctl', + 'decode', + '--extpass=backintime-askpass', + self.encfs.path + ] + logger.debug(f'Call command: {encfsctl}', self) + + self.p = subprocess.Popen( + encfsctl, + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # return string (if True) or bytes + universal_newlines=self.string, + bufsize=0 + ) def path(self, path): """ @@ -679,20 +776,27 @@ def path(self, path): assert isinstance(path, str), 'path is not str type: %s' % path else: assert isinstance(path, bytes), 'path is not bytes type: %s' % path + if not 'p' in vars(self): self.startProcess() + if not self.p.returncode is None: - logger.warning('\'encfsctl decode\' process terminated. Restarting.', self) + logger.warning( + "'encfsctl decode' process terminated. Restarting.", self + ) + del self.p self.startProcess() + self.p.stdin.write(path + self.newline) ret = self.p.stdout.readline() ret = ret.strip(self.newline) + if ret: return ret + return path - #TODO: rename this, 'list' is corrupting sphinx doc def list(self, list_): """ decode a list of paths @@ -700,37 +804,44 @@ def list(self, list_): output = [] for path in list_: output.append(self.path(path)) + return output def log(self, line): """ decode paths in takesnapshot.log """ - #rsync cmd + # rsync cmd if line.startswith('[I] rsync') or line.startswith('[I] nocache rsync'): line = self.re_include_exclude.sub(self.replace, line) line = self.re_remote_path.sub(self.replace, line) line = self.re_link_dest.sub(self.replace, line) return line - #[C] Change lines + + # [C] Change lines m = self.re_change.match(line) if not m is None: return m.group(1) + self.pathWithArrow(m.group(2)) - #[I] Information lines + + # [I] Information lines m = self.re_skip.match(line) if not m is None: return line + m = self.re_info.match(line) if not m is None: return m.group(1) + self.pathWithArrow(m.group(2)) + m.group(3) - #[E] Error lines + + # [E] Error lines m = self.re_error.match(line) if not m is None: return m.group(1) + self.path(m.group(2)) + m.group(3) - #cp cmd + + # cp cmd m = self.re_info_cp.match(line) if not m is None: return m.group(1) + self.path(m.group(2)) + m.group(3) + self.path(m.group(4)) + m.group(5) + return line def replace(self, m): @@ -738,8 +849,10 @@ def replace(self, m): return decoded string for re.sub """ decrypt = self.re_all_except_asterisk.sub(self.pathMatch, m.group(2)) + if os.path.isabs(m.group(2)): decrypt = os.path.join(os.sep, decrypt) + return m.group(1) + decrypt + m.group(3) def pathMatch(self, m): @@ -749,12 +862,14 @@ def pathMatch(self, m): return self.path(m.group(0)) def pathWithArrow(self, path): - """ - rsync print symlinks like 'dest -> src'. This will decode both and also normal paths + """rsync print symlinks like 'dest -> src'. This will decode both and + also normal paths """ m = self.re_all_except_arrow.match(path) + if not m is None: return self.path(m.group(1)) + m.group(2) + self.path(m.group(3)) + else: return self.path(path) @@ -766,6 +881,7 @@ def remote(self, path): remote_path = self.remote_path.encode() dec_path = self.path(path[len(remote_path):]) + return os.path.join(remote_path, dec_path) def close(self): diff --git a/common/gocryptfstools.py b/common/gocryptfstools.py index 33fd738e2..43326ee4a 100644 --- a/common/gocryptfstools.py +++ b/common/gocryptfstools.py @@ -20,6 +20,7 @@ class GocryptfsMount(MountControl): """ """ + def __init__(self, *args, **kwargs): super(GocryptfsMount, self).__init__(*args, **kwargs) @@ -28,9 +29,11 @@ def __init__(self, *args, **kwargs): self.reverse = None self.config_path = None - self.setattrKwargs('path', self.config.localGocryptfsPath(self.profile_id), **kwargs) + self.setattrKwargs( + 'path', self.config.localGocryptfsPath(self.profile_id), **kwargs + ) self.setattrKwargs('reverse', False, **kwargs) - self.setattrKwargs('password', None, store = False, **kwargs) + self.setattrKwargs('password', None, store=False, **kwargs) self.setattrKwargs('config_path', None, **kwargs) self.setDefaultArgs() @@ -44,30 +47,46 @@ def _mount(self): mount the service """ if self.password is None: - self.password = self.config.password(self.parent, self.profile_id, self.mode) - logger.debug('Provide password through temp FIFO', self) + self.password = self.config.password( + self.parent, self.profile_id, self.mode + ) thread = TempPasswordThread(self.password) env = os.environ.copy() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass', '-quiet'] + gocryptfs = [ + self.mountproc, + '-extpass', + 'backintime-askpass', + '-quiet' + ] if self.reverse: gocryptfs += ['-reverse'] - gocryptfs += [self.path, self.currentMountpoint] - logger.debug('Call mount command: %s' - %' '.join(gocryptfs), - self) - - proc = subprocess.Popen(gocryptfs, env = env, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) + + gocryptfs += [ + self.path, + self.currentMountpoint + ] + + logger.debug(f'Call mount command: {gocryptfs}', self) + + proc = subprocess.Popen( + gocryptfs, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) output = proc.communicate()[0] - #### self.backupConfig() + if proc.returncode: - raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ - % {'command': ' '.join(gocryptfs), 'error': output}) + msg = _('Unable to mount "{command}"').format( + command=' '.join(gocryptfs) + ) + raise MountException( + f'{msg}:\n\n{output}\n\nReturn code: {proc.returncode}' + ) def init_backend(self): """ @@ -76,35 +95,46 @@ def init_backend(self): if self.password is None: self.password = self.config.password( self.parent, self.profile_id, self.mode) - logger.debug(f'Provide password through temp FIFO {self.password=}', self) + # Dev note: See docstring in EncFS_mount._mount() for detailed + # description about the password thing. thread = TempPasswordThread(self.password) env = os.environ.copy() env['ASKPASS_TEMP'] = thread.temp_file with thread.starter(): - gocryptfs = [self.mountproc, '-extpass', 'backintime-askpass'] + gocryptfs = [ + self.mountproc, + '-extpass', + 'backintime-askpass'] + gocryptfs.append('-init') + gocryptfs.append(self.path) + logger.debug( - 'Call command to create gocryptfs config file: %s' - %' '.join(gocryptfs), - self + f'Call command to create gocryptfs config file: {gocryptfs}', + self ) - proc = subprocess.Popen(gocryptfs, env = env, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT, - universal_newlines = True) + proc = subprocess.Popen( + gocryptfs, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True) + output = proc.communicate()[0] - #### self.backupConfig() + if proc.returncode: + msg = _('Unable to init encrypted path "{command}"').format( + command=' '.join(gocryptfs) + ) raise MountException( - _("Can't init encrypted path '{command}':\n\n{error}") - .format(command=' '.join(gocryptfs), error=output) + f'{msg}:\n\n{output}\n\nReturn code: {proc.returncode}' ) - def preMountCheck(self, first_run = False): + def preMountCheck(self, first_run=False): """ check what ever conditions must be given for the mount """ @@ -130,8 +160,10 @@ def isConfigured(self): """ conf = self.configFile() ret = os.path.exists(conf) + if ret: - logger.debug('Found gocryptfs config file in {}'.format(conf), self) + logger.debug(f'Found gocryptfs config file in {conf}', self) else: - logger.debug('No config in {}'.format(conf), self) + logger.debug(f'No config in {conf}', self) + return ret diff --git a/common/test/test_lint.py b/common/test/test_lint.py index d97d99803..2220f84a0 100644 --- a/common/test/test_lint.py +++ b/common/test/test_lint.py @@ -51,8 +51,8 @@ 'daemon.py', 'event.py', 'encode.py', - 'encfstools.py', - 'gocryptfstools.py', + # 'encfstools.py', + # 'gocryptfstools.py', 'inhibitsuspend.py', 'languages.py', # 'mount.py',