1 /**
2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module dsnapshot.backend.crypt;
7 
8 import logger = std.experimental.logger;
9 import std.algorithm : map, filter;
10 import std.array : empty;
11 import std.process : Pid;
12 
13 import sumtype;
14 
15 import dsnapshot.backend;
16 import dsnapshot.config;
17 import dsnapshot.exception;
18 import dsnapshot.layout : Layout;
19 import dsnapshot.process;
20 import dsnapshot.types;
21 
22 @safe:
23 
24 final class PlainText : CryptBackend {
25     override void open(const string decrypted) {
26     }
27 
28     override void close() {
29     }
30 
31     override bool supportHardLinks() {
32         return true;
33     }
34 
35     override bool supportRemoteEncryption() {
36         return true;
37     }
38 }
39 
40 /**
41  *
42  * encfs is started as a daemon with a keepalive timer so it automatically
43  * closes when it has been unused for some time. This is because it can take
44  * some time both for encfs to open the encrypted data and to finish writing
45  * all changes to it.
46  *
47  * Other designs that has been tried where:
48  *
49  * # open and close as fast as possible
50  * Directly after opening dsnapshot started writing data to the decrypted path
51  * and then close it.  If the writing of the data where faster than encfs took
52  * to open the encrypted data it would result in everything being lost.
53  *
54  * # encryp only the snapshot
55  * This fails because while we are working on a new snapshot it has the
56  * trailing prefix -in-progress.  It is then renamed, by removing the prefix,
57  * when it is done. This somehow breaks encfs in mysterious ways.
58  */
59 final class EncFs : CryptBackend {
60     import core.sys.posix.sys.stat : stat_t, stat;
61     import std.string : toStringz;
62     import std.typecons : Nullable;
63 
64     Path encrypted;
65     Path decrypted;
66     string[] mountCmd;
67     string[] mountFuseOpts;
68     string[] unmountCmd;
69     string[] unmountFuseOpts;
70     string config;
71     string passwd;
72     Nullable!stat_t decryptBeforeOpen;
73 
74     this(const string config, const string passwd, const string encrypted,
75             const string[] mountCmd, const string[] mountFuseOpts,
76             const string[] unmountCmd, const string[] unmountFuseOpts) {
77         if (encrypted.empty)
78             throw new CryptException(null, CryptException.Kind.noEncryptedSrc);
79 
80         this.config = config.dup;
81         this.passwd = passwd.dup;
82         this.encrypted = Path(encrypted);
83         this.mountCmd = mountCmd.dup;
84         this.mountFuseOpts = mountFuseOpts.dup;
85         this.unmountCmd = unmountCmd.dup;
86         this.unmountFuseOpts = unmountFuseOpts.dup;
87     }
88 
89     override void open(const string decrypted) {
90         import std.file : exists;
91 
92         this.decrypted = decrypted.Path;
93 
94         if (isEncryptedOpen) {
95             throw new CryptException(null, CryptException.Kind.errorWhenOpening);
96         }
97         if (!exists(this.encrypted.toString)) {
98             throw new CryptException("encrypted path do not exist: " ~ this.encrypted.toString,
99                     CryptException.Kind.errorWhenOpening);
100         }
101         if (!exists(this.decrypted.toString)) {
102             throw new CryptException("decrypted path do not exist: " ~ this.decrypted.toString,
103                     CryptException.Kind.errorWhenOpening);
104         }
105 
106         {
107             stat_t st = void;
108             () @trusted { stat(this.decrypted.toString.toStringz, &st); }();
109             decryptBeforeOpen = st;
110         }
111 
112         string[] cmd = mountCmd;
113         if (!config.empty) {
114             cmd ~= ["-c", config];
115         }
116         if (!passwd.empty) {
117             // ugly hack
118             // hide the password in an environnment variable so it isn't visible in the logs.
119             cmd ~= ["--extpass", "echo $DSNAPSHOT_ENCFS_PWD"];
120         }
121 
122         cmd ~= this.encrypted.toString;
123         cmd ~= this.decrypted.toString;
124 
125         if (!mountFuseOpts.empty) {
126             cmd ~= "--";
127             cmd ~= mountFuseOpts;
128         }
129 
130         try {
131             string[string] env = ["DSNAPSHOT_ENCFS_PWD" : passwd];
132             spawnProcessLog!(Yes.throwOnFailure)(cmd, env).wait;
133         } catch (Exception e) {
134             decryptBeforeOpen.nullify;
135             logger.warning(e.msg);
136             throw new CryptException("encfs failed", CryptException.Kind.errorWhenOpening);
137         }
138     }
139 
140     override void close() @trusted {
141         if (!isEncryptedOpen)
142             return;
143 
144         string[] cmd = unmountCmd;
145         cmd ~= decrypted.toString;
146         if (!unmountFuseOpts.empty) {
147             cmd ~= "--";
148             cmd ~= unmountFuseOpts;
149         }
150 
151         try {
152             spawnProcessLog!(Yes.throwOnFailure)(cmd).wait;
153             decryptBeforeOpen.nullify;
154         } catch (ProcessException e) {
155             logger.error(e.msg);
156             logger.info("Exit code: ", e.exitCode);
157             logger.error("Failed closing the decrypted endpoint ", decrypted.toString);
158             throw new CryptException("Unable to close decrypted " ~ this.decrypted.toString,
159                     CryptException.Kind.errorWhenClosing);
160         }
161     }
162 
163     override bool supportHardLinks() @safe pure nothrow const @nogc {
164         return false;
165     }
166 
167     override bool supportRemoteEncryption() @safe pure nothrow const @nogc {
168         return false;
169     }
170 
171     private bool isEncryptedOpen() @safe nothrow {
172         if (decryptBeforeOpen.isNull)
173             return false;
174 
175         stat_t st = void;
176         () @trusted { stat(decrypted.toString.toStringz, &st); }();
177 
178         if (st.st_dev == decryptBeforeOpen.get.st_dev && st.st_ino == decryptBeforeOpen.get.st_ino)
179             return false;
180         return true;
181     }
182 }