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 }