Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add local gocryptfs support #1897

Open
wants to merge 30 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8437df7
test gocryptfs on travis
Germar Jul 22, 2017
48267cd
fix backintime-askpass not found
Germar Jul 22, 2017
85b8dde
fix gocryptfs stay in foreground
Germar Jul 22, 2017
c481e11
reenable all tests
Germar Jul 22, 2017
91975a4
set gocryptfs to be quiet
Germar Jul 22, 2017
ca7d7c6
typo
Germar Jul 22, 2017
c5711fc
fix settings
Germar Jul 22, 2017
3af5aeb
reenable piping gocryptfs output as the bug is fixed in https://githu…
Germar Jul 24, 2017
e73cd76
test gocryptfs on travis
Germar Jul 22, 2017
01138d1
fix gocryptfs stay in foreground
Germar Jul 22, 2017
171a7d9
reenable all tests
Germar Jul 22, 2017
ee4df40
set gocryptfs to be quiet
Germar Jul 22, 2017
7ce058e
typo
Germar Jul 22, 2017
765045e
reenable piping gocryptfs output as the bug is fixed in https://githu…
Germar Jul 24, 2017
e490e50
integrate gocrypt into Settings
Germar Apr 14, 2017
3177815
Use gocryptfs from packagemanager in Travis
daviewales Oct 15, 2024
bb09aea
Update common/gocryptfstools.py
daviewales Oct 18, 2024
1b78200
Remove explicit gettext definition
daviewales Oct 18, 2024
9bf5c04
Remove explicit gettext definition
daviewales Oct 18, 2024
ec83c46
Fix some pylint warnings
daviewales Oct 18, 2024
2ee5529
Disable pylint duplicate check for encfs module
daviewales Oct 18, 2024
0d8c5f3
Fix undefined variable
daviewales Oct 18, 2024
82cafeb
Add more gocryptfs details
daviewales Nov 29, 2024
d9a92b6
Standardise GocryptfsMount class name
daviewales Nov 29, 2024
a6f55f6
Ensure GUI elements appear for local_gocryptfs mode
daviewales Nov 29, 2024
a740e0e
fix errorHandler
buhtz Nov 30, 2024
1ca3175
Fix typo in docstring
daviewales Dec 4, 2024
8dab4d9
Rename init to init_backend
daviewales Jan 8, 2025
cf8a23a
Slightly improve code style
daviewales Jan 15, 2025
0ef4740
Add init_backend method to match gocryptfs updates
daviewales Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 21 additions & 4 deletions common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import logger
import sshtools
import encfstools
import gocryptfstools
import password
import pluginmanager
import schedule
Expand Down Expand Up @@ -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
),
Expand All @@ -281,7 +282,13 @@ def __init__(self, config_path=None, data_path=None):
_('SSH encrypted'),
_('SSH private key'),
_('Encryption')
)
),
'local_gocryptfs':(
gocryptfstools.GocryptfsMount,
_('Local encrypted') + ' (gocryptfs)',
_('Encryption'),
False
),
}

self.SSH_CIPHERS = {
Expand Down Expand Up @@ -382,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:
Expand Down Expand Up @@ -420,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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1558,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)
Expand Down
45 changes: 41 additions & 4 deletions common/encfstools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -55,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():
Expand All @@ -82,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.

Expand Down Expand Up @@ -109,6 +145,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:
Expand Down Expand Up @@ -361,7 +398,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():
Expand Down Expand Up @@ -593,7 +630,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():
Expand Down
133 changes: 133 additions & 0 deletions common/gocryptfstools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# SPDX-FileCopyrightText: © 2017 Germar Reitze
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# 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
# <https://spdx.org/licenses/GPL-2.0-or-later.html>.

import os
import subprocess

import logger
from password_ipc import TempPasswordThread
from mount import MountControl
from exceptions import MountException


class GocryptfsMount(MountControl):
"""
"""
def __init__(self, *args, **kwargs):
super(GocryptfsMount, 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)
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', '-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)
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})

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():
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}':\n\n{error}")
.format(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
71 changes: 67 additions & 4 deletions common/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_backend(self, mode = None, **kwargs):
"""
High-level init. Run :py:func:`MountControl.init_backend` to initiate
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_backend 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_backend()

class MountControl:
"""This is the low-level mount API. This should be subclassed by backends.

Expand Down Expand Up @@ -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):
"""
Expand Down
Loading