File: encfstools.py

package info (click to toggle)
backintime 1.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 10,424 kB
  • sloc: python: 27,312; sh: 886; makefile: 174; xml: 62
file content (893 lines) | stat: -rw-r--r-- 30,465 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
# SPDX-FileCopyrightText: © 2012-2022 Germar Reitze
# SPDX-FileCopyrightText: © 2012-2022 Taylor Raack
# SPDX-FileCopyrightText: © 2025 David Wales (@daviewales)
# SPDX-FileCopyrightText: © 2025 Christian Buhtz <c.buhtz@posteo.jp>
#
# 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 re
import shutil
import tempfile
from datetime import datetime
from packaging.version import Version
import config
import encode
import password
from password_ipc import TempPasswordThread
import tools
import sshtools
import logger
from mount import MountControl
from exceptions import MountException, EncodeValueError


class EncFS_mount(MountControl):
    """Mount encrypted paths with encfs."""

    def __init__(self, *args, **kwargs):
        # logger.debug("EncFS_mount.init() :: {args=} {kwargs=}")  # DEBUG

        # init MountControl
        super(EncFS_mount, self).__init__(*args, **kwargs)

        # Workaround for some linters.
        self.path = None
        self.reverse = None
        self.config_path = None

        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('hash_id_1', None, **kwargs)
        self.setattrKwargs('hash_id_2', None, **kwargs)

        self.setDefaultArgs()

        # pylint: disable=duplicate-code
        self.mountproc = 'encfs'
        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)

        # 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:
                encfs += ['--reverse']

            if not self.isConfigured():
                encfs += ['--standard']

            encfs += [self.path, self.currentMountpoint]
            logger.debug('Call mount command: ' + ' '.join(encfs), self)

            # Encfs ask 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,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True
            )
            output = proc.communicate()[0]

            self.backupConfig()

            if proc.returncode:
                msg = _('Unable to mount "{command}"').format(
                    command=' '.join(encfs)
                )
                raise MountException(
                    f'{msg}:\n\n{output}\n\nReturn code: {proc.returncode}'
                )

    def init_backend(self):
        """Empty for Encfs because initialization happens implicit.

        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.

        Raises: Several exceptions.
        """
        self.checkFuse()

        if first_run:
            self.checkVersion()

        return True

    def env(self):
        """
        return environment with encfs configfile
        """
        env = os.environ.copy()
        cfg = self.configFile()
        if os.path.isfile(cfg):
            env['ENCFS6_CONFIG'] = cfg
        return env

    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:
            cfg = os.path.join(self.config_path, f)
        return cfg

    def isConfigured(self):
        """
        check if encfs config file exist. If not and if we are in settingsdialog
        ask for password confirmation. _mount will then create a new config
        """
        cfg = self.configFile()

        if os.path.isfile(cfg):
            logger.debug(f'Found EncFS config in {cfg}', self)
            return True

        logger.debug(f'No EncFS config in {cfg}', self)
        msg = _('Configuration for the encrypted directory not found.')

        if not self.tmp_mount:
            raise MountException(msg)

        question = '{}\n{}'.format(
            msg,
            _('Create a new encrypted directory?')
        )

        if not self.config.askQuestion(question):
            # TODO
            # This string can appear in a "critical" message dialog.
            # Let us know the steps to reproduce that behavior.
            raise MountException(_('Cancel'))

        pw = password.Password(self.config)
        password_confirm = pw.passwordFromUser(
            self.parent,
            prompt=_('Please re-enter the EncFS password to confirm.'))

        if self.password == password_confirm:
            return False

        raise MountException(
            _('The EncFS passwords do not match.'))

    def checkVersion(self):
        """Check encfs version.
        1.7.2 had a bug with --reverse that will create corrupt files

        Dev note (buhtz, 2025-06): EncFS itself is scheduled for removal.

        Dev note (buhtz, 2024-05): Looking at upstream it seems that the 1.7.2
        release was widthdrawn. The release before and after are from the year
        2010. In consequence this code is definitely out dated and a candidate
        for removal.
        """
        logger.debug('Check version', self)
        if self.reverse:
            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)
                raise MountException(
                        'encfs version 1.7.2 and before has a bug with '
                        'option --reverse. Please update encfs.')

    def backupConfig(self):
        """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):
            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)

        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
            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 = os.path.join(backup_folder, new_backup_file)
        logger.debug(
            f'Create backup of encfs config {cfg} to {new_backup}', self
        )
        shutil.copy2(cfg, new_backup)


class EncFS_SSH(EncFS_mount):
    """
    Mount encrypted remote path with sshfs and encfs.
    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
    ):
        self.config = cfg or config.Config()
        self.profile_id = profile_id or self.config.currentProfile()
        self.mode = mode
        if self.mode is None:
            self.mode = self.config.snapshotsMode(self.profile_id)

        self.parent = parent
        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')
        )

        super(EncFS_SSH, self).__init__(*self.args, **self.splitKwargs('encfs'))

    def mount(self, *args, **kwargs):
        """
        call mount for sshfs, encfs --reverse and encfs
        register 'encfsctl encode' in config.ENCODE
        """
        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

        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
            # )

            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.mount(*args, **kwargs)
                tmp_mount.umount()

                cfg = tmp_mount.configFile()

                if os.path.isfile(cfg):
                    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(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)
        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
        """

        self.config.ENCODE.close()
        self.config.ENCODE = encode.Bounce()

        # logger.debug('Unmount encfs', self)

        super(EncFS_SSH, self).umount(*args, **kwargs)
        # logger.debug('Unmount local filesystem root mount encfs --reverse', self)

        self.rev_root.umount(*args, **kwargs)
        # logger.debug('Unmount sshfs', self)

        self.ssh.umount(*args, **kwargs)

    def preMountCheck(self, *args, **kwargs):
        """Call preMountCheck for sshfs, encfs --reverse and encfs.
        """
        if (self.ssh.preMountCheck(*args, **kwargs)
                and self.rev_root.preMountCheck(*args, **kwargs)
                and super(EncFS_SSH, self).preMountCheck(*args, **kwargs)):

            # Dev note (buhtz, 2024-09): Seems unnecessary. No one checks this
            # return value.
            return True

    def splitKwargs(self, mode):
        """
        split all given arguments for the desired mount class
        """
        d = self.kwargs.copy()
        d['cfg'] = self.config
        d['profile_id'] = self.profile_id
        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
                )

            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
                )

            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
                )

            if 'hash_id' in d:
                d.pop('hash_id')

            if 'hash_id_1' in d:
                d['hash_id'] = d['hash_id_1']

            return d


class Encode:
    """
    encode path with encfsctl.
    ENCFS_SSH will replace config.ENCODE with this
    """

    def __init__(self, encfs):
        self.encfs = encfs
        self.password = self.encfs.password
        self.chroot = self.encfs.rev_root.currentMountpoint
        if not self.chroot[-1] == os.sep:
            self.chroot += os.sep
        self.remote_path = self.encfs.ssh.path
        if not self.remote_path[-1] == os.sep:
            self.remote_path += os.sep

        # Precompile some regular expressions
        self.re_asterisk = re.compile(r'\*')
        self.re_separate_asterisk = re.compile(r'(.*?)(\*+)(.*)')

    def __del__(self):
        self.close()

    def startProcess(self):
        """
        start 'encfsctl encode' process in pipe mode.
        """
        thread = TempPasswordThread(self.password)
        env = self.encfs.env()
        env['ASKPASS_TEMP'] = thread.temp_file
        with thread.starter():
            encfsctl = ['encfsctl', 'encode', '--extpass=backintime-askpass', '/']
            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):
        """
        write plain path to encfsctl stdin and read encrypted path from stdout
        """
        if not 'p' in vars(self):
            self.startProcess()

        if not self.p.returncode is None:
            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(f'Failed to encode {path}. Got empty string', self)
            raise EncodeValueError()

        return ret

    def exclude(self, path):
        """
        encrypt paths for snapshots.takeSnapshot exclude list.
        After encoding the path a wildcard would not match anymore
        so all paths with wildcards are ignored. Only single and double asterisk
        that will match a full file or folder name will work.
        """
        if tools.patternHasNotEncryptableWildcard(path):
            return None

        enc = ''
        m = self.re_asterisk.search(path)

        if not m is None:
            path_ = path[:]

            while True:
                # 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):
        """
        encrypt paths for snapshots.takeSnapshot include list.
        """
        return os.path.join(os.sep, self.path(path))

    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):
        """
        stop encfsctl process
        """
        if 'p' in vars(self) and self.p.returncode is None:
            logger.debug("stop 'encfsctl encode' process", self)
            self.p.communicate()


class Decode:
    """
    decode path with encfsctl.
    """

    def __init__(self, cfg, string=True):
        self.config = cfg
        self.mode = cfg.snapshotsMode()

        if self.mode == 'local_encfs':
            self.password = cfg.password(pw_id=1)

        elif self.mode == 'ssh_encfs':
            self.password = cfg.password(pw_id=2)

        self.encfs = cfg.SNAPSHOT_MODES[self.mode][0](cfg)
        self.remote_path = cfg.sshSnapshotsPath()

        if not self.remote_path:
            self.remote_path = './'

        if not self.remote_path[-1] == os.sep:
            self.remote_path += os.sep

        # German translation changed from Snapshot to Schnappschuss.
        # Catch both variants otherwise old logs wouldn't get decoded.
        # Warning (2023-11): Do not modify the source string.
        # See #1559 for details.
        takeSnapshot = _('Take snapshot') \
            .replace('Schnappschuss', '(?:Schnappschuss|Snapshot)')

        host, _post, user, path, _cipher = cfg.sshHostUserPortPathCipher()

        # replace: --exclude"<crypted_path>" or --include"<crypted_path>"
        self.re_include_exclude = re.compile(
            r'(--(?:ex|in)clude=")(.*?)(")')  # codespell-ignore

        # replace: 'USER@HOST:"PATH<crypted_path>"'
        self.re_remote_path = re.compile(
            r'(\'%s@%s:"%s)(.*?)("\')' % (user, host, path)
        )

        # replace: --link-dest="../../<crypted_path>"
        self.re_link_dest = re.compile(r'(--link-dest="\.\./\.\./)(.*?)(")')

        # search for: [C] <f+++++++++ <crypted_path>
        self.re_change = re.compile(r'(^\[C\] .{11} )(.*)')

        #search for: [I] Take snapshot (rsync: BACKINTIME: <f+++++++++ <crypted_path>)
        #            [I] Take snapshot (rsync: deleting <crypted_path>)
        #            [I] Take snapshot (rsync: rsync: readlink_stat("...mountpoint/<crypted_path>")
        #            [I] Take snapshot (rsync: rsync: send_files failed to open "...mountpoint/<crypted_path>": Permission denied (13))
        #            [I] Take snapshot (rsync: file has vanished: "...mountpoint/<crypted_path>")
        #            [I] Take snapshot (rsync: <crypted_path>)
        pattern = []
        pattern.append(r' BACKINTIME: .{11} ')
        pattern.append(r' deleting ')
        pattern.append(r' rsync: readlink_stat\(".*?mountpoint/')
        pattern.append(r' rsync: send_files failed to open ".*?mountpoint/')
        pattern.append(r' file has vanished: ".*?mountpoint/')
        pattern.append(r' ')
        self.re_info = re.compile(r'(^(?:\[I\] )?%s \(rsync:(?:%s))(.*?)(\).*|".*)' % (takeSnapshot, '|'.join(pattern)))

        #search for: [E] Error: rsync readlink_stat("...mountpoint/<crypted_path>")
        #            [E] Error: rsync: send_files failed to open "...mountpoint/<crypted_path>": Permission denied (13)
        #            [E] Error: rsync: recv_generator: failed to stat "<remote_path>/<crypted_path>": File name too long (36)
        #            [E] Error: rsync: recv_generator: mkdir "<remote_path>/<crypted_path>": File name too long (36)
        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<crypted_path>"* "PATH<crypted_path>"
        self.re_info_cp= re.compile(r'(^\[I\] .*? cp -aRl "%s/)(.*?)("\* "%s/)(.*?)(")' % (path, path))

        # search for all chars except *
        self.re_all_except_asterisk = re.compile(r'[^\*]+')

        # search for: <crypted_path> -> <crypted_path>
        self.re_all_except_arrow = re.compile(r'(.*?)((?: [-=]> )+)(.*)')

        #skip: [I] Take snapshot (rsync: sending incremental file list)
        #      [I] Take snapshot (rsync: building file list ... done)
        #      [I] Take snapshot (rsync: sent 26569703 bytes  received 239616 bytes  85244.26 bytes/sec)
        #      [I] Take snapshot (rsync: total size is 9130263449  speedup is 340.56)
        #      [I] Take snapshot (rsync: rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1070) [sender=3.0.9])
        #      [I] Take snapshot (rsync: rsync warning: some files vanished before they could be transferred (code 24) at main.c(1070) [sender=3.0.9])
        pattern = []
        pattern.append(r'sending incremental file list')
        pattern.append(r'building file list ... done')
        pattern.append(r'sent .*? received')
        pattern.append(r'total size is .*? speedup is')
        pattern.append(r'rsync error: some files/attrs were not transferred')
        pattern.append(r'rsync warning: some files vanished before they could be transferred')
        self.re_skip = re.compile(r'^(?:\[I\] )?%s \(rsync: (%s)' % (takeSnapshot, '|'.join(pattern)))

        self.string = string

        self.newline = '\n' if string else b'\n'

    def __del__(self):
        self.close()

    def startProcess(self):
        """
        start 'encfsctl decode' process in pipe mode.
        """
        thread = TempPasswordThread(self.password)
        env = os.environ.copy()
        env['ASKPASS_TEMP'] = thread.temp_file

        with thread.starter():
            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):
        """
        write encrypted path to encfsctl stdin and read plain path from stdout
        if stdout is empty (most likely because there was an error) return crypt path
        """
        if self.string:
            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
            )

            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

    def list(self, list_):
        """
        decode a list of paths
        """
        output = []
        for path in list_:
            output.append(self.path(path))

        return output

    def log(self, line):
        """
        decode paths in takesnapshot.log
        """
        # 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
        m = self.re_change.match(line)
        if not m is None:
            return m.group(1) + self.pathWithArrow(m.group(2))

        # [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
        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
        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):
        """
        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):
        """
        return decoded path of a match object
        """
        return self.path(m.group(0))

    def pathWithArrow(self, path):
        """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)

    def remote(self, path):
        """
        decode the path on remote host starting from backintime/host/user/...
        """
        assert isinstance(path, bytes), 'path is not bytes type: %s' % 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):
        """
        stop encfsctl process
        """
        if 'p' in vars(self) and self.p.returncode is None:
            logger.debug('stop \'encfsctl decode\' process', self)
            self.p.communicate()