diff --git a/encfs/DirNode.cpp b/encfs/DirNode.cpp index 908dded..7d08b77 100644 --- a/encfs/DirNode.cpp +++ b/encfs/DirNode.cpp @@ -275,25 +275,43 @@ string DirNode::rootDirectory() { return string(rootDir, 0, rootDir.length() - 1); } +/** + * Encrypt a plain-text file path to the ciphertext path with the + * ciphertext root directory name prefixed. + * + * Example: + * $ encfs -f -v cipher plain + * $ cd plain + * $ touch foobar + * cipherPath: /foobar encoded to cipher/NKAKsn2APtmquuKPoF4QRPxS + */ string DirNode::cipherPath(const char *plaintextPath) { return rootDir + naming->encodePath(plaintextPath); } +/** + * Same as cipherPath(), but does not prefix the ciphertext root directory + */ string DirNode::cipherPathWithoutRoot(const char *plaintextPath) { return naming->encodePath(plaintextPath); } +/** + * Return the decrypted version of cipherPath + * + * In reverse mode, returns the encrypted version of cipherPath + */ string DirNode::plainPath(const char *cipherPath_) { try { - if (!strncmp(cipherPath_, rootDir.c_str(), rootDir.length())) { - return naming->decodePath(cipherPath_ + rootDir.length()); - } - // Handle special absolute path encodings. - char mark = fsConfig->reverseEncryption ? '/' : '+'; + char mark = '+'; + string prefix = "/"; + if (fsConfig->reverseEncryption) { + mark = '/'; + prefix = "+"; + } if (cipherPath_[0] == mark) { - return string(fsConfig->reverseEncryption ? "+" : "/") + - naming->decodeName(cipherPath_ + 1, strlen(cipherPath_ + 1)); + return prefix + naming->decodeName(cipherPath_ + 1, strlen(cipherPath_ + 1)); } // Default. diff --git a/encfs/FSConfig.h b/encfs/FSConfig.h index 4e82856..5cc8fe7 100644 --- a/encfs/FSConfig.h +++ b/encfs/FSConfig.h @@ -41,6 +41,9 @@ struct EncFS_Opts; class Cipher; class NameIO; +/** + * Persistent configuration (stored in config file .encfs6.xml) + */ struct EncFSConfig { ConfigType cfgType; diff --git a/encfs/FileUtils.cpp b/encfs/FileUtils.cpp index 1c563bf..ccfaf0b 100644 --- a/encfs/FileUtils.cpp +++ b/encfs/FileUtils.cpp @@ -967,7 +967,6 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr &opts) { long desiredKDFDuration = NormalKDFDuration; if (reverseEncryption) { - uniqueIV = false; chainedIV = false; externalIV = false; blockMACBytes = 0; @@ -976,7 +975,7 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr &opts) { if (configMode == Config_Paranoia || answer[0] == 'p') { if (reverseEncryption) { - rError(_("Paranoia configuration not supported for --reverse")); + rError(_("Paranoia configuration not supported for reverse encryption")); return rootInfo; } @@ -1011,7 +1010,7 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr &opts) { uniqueIV = true; if (reverseEncryption) { - cout << _("--reverse specified, not using chained IV") << "\n"; + cout << _("reverse encryption - chained IV disabled") << "\n"; } else { chainedIV = true; } @@ -1035,7 +1034,13 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr &opts) { blockSize = selectBlockSize(alg); nameIOIface = selectNameCoding(); if (reverseEncryption) { - cout << _("--reverse specified, not using unique/chained IV") << "\n"; + cout << _("reverse encryption - chained IV and MAC disabled") << "\n"; + uniqueIV = selectUniqueIV(); + /* Reverse mounts are read-only by default (set in main.cpp). + * If uniqueIV is off, writing can be allowed, because there + * is no header that could be overwritten */ + if (uniqueIV == false) + opts->readOnly = false; } else { chainedIV = selectChainedIV(); uniqueIV = selectUniqueIV(); diff --git a/encfs/FileUtils.h b/encfs/FileUtils.h index 85ae23b..0cedb14 100644 --- a/encfs/FileUtils.h +++ b/encfs/FileUtils.h @@ -58,6 +58,11 @@ typedef shared_ptr RootPtr; enum ConfigMode { Config_Prompt, Config_Standard, Config_Paranoia }; +/** + * EncFS_Opts stores internal settings + * + * See struct EncFS_Args (main.cpp) for the parsed command line arguments + */ struct EncFS_Opts { std::string rootDir; bool createIfNotFound; // create filesystem if not found @@ -81,6 +86,8 @@ struct EncFS_Opts { * behind the back of EncFS (for example, in reverse mode). * See main.cpp for a longer explaination. */ + bool readOnly; // Mount read-only + ConfigMode configMode; EncFS_Opts() { @@ -96,6 +103,7 @@ struct EncFS_Opts { reverseEncryption = false; configMode = Config_Prompt; noCache = false; + readOnly = false; } }; diff --git a/encfs/encfs.cpp b/encfs/encfs.cpp index 9ae1ce1..42beac4 100644 --- a/encfs/encfs.cpp +++ b/encfs/encfs.cpp @@ -70,6 +70,18 @@ static EncFS_Context *context() { return (EncFS_Context *)fuse_get_context()->private_data; } +/** + * Helper function - determine if the filesystem is read-only + * Optionally takes a pointer to the EncFS_Context, will get it from FUSE + * if the argument is NULL. + */ +static bool isReadOnly(EncFS_Context *ctx) { + if (ctx == NULL) + ctx = (EncFS_Context *)fuse_get_context()->private_data; + + return ctx->opts->readOnly; +} + // helper function -- apply a functor to a cipher path, given the plain path static int withCipherPath(const char *opName, const char *path, function op, @@ -130,11 +142,10 @@ static int withFileNode(const char *opName, const char *path, } /* - The rLog messages below always print out encrypted filenames, not + The rLog messages below always prints out encrypted filenames, not plaintext. The reason is so that it isn't possible to leak information about the encrypted data through rlog interfaces. - The purpose of this layer of code is to take the FUSE request and dispatch to the internal interfaces. Any marshaling of arguments and return types can be done here. @@ -215,6 +226,8 @@ int encfs_getdir(const char *path, fuse_dirh_t h, fuse_dirfil_t filler) { int encfs_mknod(const char *path, mode_t mode, dev_t rdev) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -255,6 +268,8 @@ int encfs_mkdir(const char *path, mode_t mode) { fuse_context *fctx = fuse_get_context(); EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -287,6 +302,8 @@ int encfs_mkdir(const char *path, mode_t mode) { int encfs_unlink(const char *path) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -307,6 +324,7 @@ int _do_rmdir(EncFS_Context *, const string &cipherPath) { } int encfs_rmdir(const char *path) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("rmdir", path, bind(_do_rmdir, _1, _2)); } @@ -343,17 +361,22 @@ int encfs_readlink(const char *path, char *buf, size_t size) { bind(_do_readlink, _1, _2, buf, size)); } -int encfs_symlink(const char *from, const char *to) { +/** + * Create a symbolic link pointing to "to" named "from" + */ +int encfs_symlink(const char *to, const char *from) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; try { + string fromCName = FSRoot->cipherPath(from); // allow fully qualified names in symbolic links. - string fromCName = FSRoot->relativeCipherPath(from); - string toCName = FSRoot->cipherPath(to); + string toCName = FSRoot->relativeCipherPath(to); rLog(Info, "symlink %s -> %s", fromCName.c_str(), toCName.c_str()); @@ -366,7 +389,7 @@ int encfs_symlink(const char *from, const char *to) { olduid = setfsuid(context->uid); oldgid = setfsgid(context->gid); } - res = ::symlink(fromCName.c_str(), toCName.c_str()); + res = ::symlink(toCName.c_str(), fromCName.c_str()); if (olduid >= 0) setfsuid(olduid); if (oldgid >= 0) setfsgid(oldgid); @@ -384,6 +407,8 @@ int encfs_symlink(const char *from, const char *to) { int encfs_link(const char *from, const char *to) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -400,6 +425,8 @@ int encfs_link(const char *from, const char *to) { int encfs_rename(const char *from, const char *to) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx)) return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -418,6 +445,7 @@ int _do_chmod(EncFS_Context *, const string &cipherPath, mode_t mode) { } int encfs_chmod(const char *path, mode_t mode) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("chmod", path, bind(_do_chmod, _1, _2, mode)); } @@ -427,16 +455,19 @@ int _do_chown(EncFS_Context *, const string &cyName, uid_t u, gid_t g) { } int encfs_chown(const char *path, uid_t uid, gid_t gid) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("chown", path, bind(_do_chown, _1, _2, uid, gid)); } int _do_truncate(FileNode *fnode, off_t size) { return fnode->truncate(size); } int encfs_truncate(const char *path, off_t size) { + if (isReadOnly(NULL)) return -EROFS; return withFileNode("truncate", path, NULL, bind(_do_truncate, _1, size)); } int encfs_ftruncate(const char *path, off_t size, struct fuse_file_info *fi) { + if (isReadOnly(NULL)) return -EROFS; return withFileNode("ftruncate", path, fi, bind(_do_truncate, _1, size)); } @@ -446,6 +477,7 @@ int _do_utime(EncFS_Context *, const string &cyName, struct utimbuf *buf) { } int encfs_utime(const char *path, struct utimbuf *buf) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("utime", path, bind(_do_utime, _1, _2, buf)); } @@ -462,12 +494,16 @@ int _do_utimens(EncFS_Context *, const string &cyName, } int encfs_utimens(const char *path, const struct timespec ts[2]) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("utimens", path, bind(_do_utimens, _1, _2, ts)); } int encfs_open(const char *path, struct fuse_file_info *file) { EncFS_Context *ctx = context(); + if (isReadOnly(ctx) && (file->flags & O_WRONLY || file->flags & O_RDWR)) + return -EROFS; + int res = -EIO; shared_ptr FSRoot = ctx->getRoot(&res); if (!FSRoot) return res; @@ -508,6 +544,7 @@ int _do_flush(FileNode *fnode) { return res; } +// Called on each close() of a file descriptor int encfs_flush(const char *path, struct fuse_file_info *fi) { return withFileNode("flush", path, fi, bind(_do_flush, _1)); } @@ -545,6 +582,7 @@ int _do_fsync(FileNode *fnode, int dataSync) { } int encfs_fsync(const char *path, int dataSync, struct fuse_file_info *file) { + if (isReadOnly(NULL)) return -EROFS; return withFileNode("fsync", path, file, bind(_do_fsync, _1, dataSync)); } @@ -557,6 +595,7 @@ int _do_write(FileNode *fnode, unsigned char *ptr, size_t size, off_t offset) { int encfs_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *file) { + if (isReadOnly(NULL)) return -EROFS; return withFileNode("write", path, file, bind(_do_write, _1, (unsigned char *)buf, size, offset)); } @@ -595,6 +634,7 @@ int _do_setxattr(EncFS_Context *, const string &cyName, const char *name, } int encfs_setxattr(const char *path, const char *name, const char *value, size_t size, int flags, uint32_t position) { + if (isReadOnly(NULL)) return -EROFS; (void)flags; return withCipherPath("setxattr", path, bind(_do_setxattr, _1, _2, name, value, size, position)); @@ -606,6 +646,7 @@ int _do_setxattr(EncFS_Context *, const string &cyName, const char *name, } int encfs_setxattr(const char *path, const char *name, const char *value, size_t size, int flags) { + if (isReadOnly(NULL)) return -EROFS; return withCipherPath("setxattr", path, bind(_do_setxattr, _1, _2, name, value, size, flags)); } @@ -663,6 +704,8 @@ int _do_removexattr(EncFS_Context *, const string &cyName, const char *name) { } int encfs_removexattr(const char *path, const char *name) { + if (isReadOnly(NULL)) return -EROFS; + return withCipherPath("removexattr", path, bind(_do_removexattr, _1, _2, name)); } diff --git a/encfs/main.cpp b/encfs/main.cpp index 1af861a..728517f 100644 --- a/encfs/main.cpp +++ b/encfs/main.cpp @@ -65,6 +65,12 @@ using gnu::autosprintf; // Maximum number of arguments that we're going to pass on to fuse. Doesn't // affect how many arguments we can handle, just how many we can pass on.. const int MaxFuseArgs = 32; +/** + * EncFS_Args stores the parsed command-line arguments + * + * See also: struct EncFS_Opts (FileUtils.h), stores internal settings that are + * derived from the arguments + */ struct EncFS_Args { string mountPoint; // where to make filesystem visible bool isDaemon; // true == spawn in background, log to syslog @@ -273,20 +279,23 @@ static bool processArgs(int argc, char *argv[], case 'D': out->opts->forceDecode = true; break; - /* By default, the kernel caches file metadata for one second. - * This is fine for EncFS' normal mode, but for --reverse, this - * means that the encrypted view will be up to one second out of - * date. - * Quoting Goswin von Brederlow: - * "Caching only works correctly if you implement a disk based - * filesystem, one where only the fuse process can alter - * metadata and all access goes only through fuse. Any overlay - * filesystem where something can change the underlying - * filesystem without going through fuse can run into - * inconsistencies." - * Enabling reverse automatically enables noCache. */ case 'r': out->opts->reverseEncryption = true; + /* Reverse encryption does not support writing unless uniqueIV + * is disabled (expert mode) */ + out->opts->readOnly = true; + /* By default, the kernel caches file metadata for one second. + * This is fine for EncFS' normal mode, but for --reverse, this + * means that the encrypted view will be up to one second out of + * date. + * Quoting Goswin von Brederlow: + * "Caching only works correctly if you implement a disk based + * filesystem, one where only the fuse process can alter + * metadata and all access goes only through fuse. Any overlay + * filesystem where something can change the underlying + * filesystem without going through fuse can run into + * inconsistencies." + * Enabling reverse automatically enables noCache */ case 514: /* Disable EncFS block cache * Causes reverse grow tests to fail because short reads diff --git a/tests/common.inc b/tests/common.inc index 9effbd2..d997114 100644 --- a/tests/common.inc +++ b/tests/common.inc @@ -72,5 +72,26 @@ sub waitForFile return 0; } +# writeZeroes($filename, $size) +# Write zeroes of size $size to file $filename +sub writeZeroes +{ + my $filename = shift; + my $size = shift; + open(my $fh, ">", $filename); + my $bs = 4096; # 4 KiB + my $block = "\0" x $bs; + my $remain; + for($remain = $size; $remain >= $bs; $remain -= $bs) + { + print($fh $block) or BAIL_OUT("Could not write to $filename: $!"); + } + if($remain > 0) + { + $block = "\0" x $remain; + print($fh $block) or BAIL_OUT("Could not write to $filename: $!"); + } +} + # As this file will be require()'d, it needs to return true return 1; diff --git a/tests/normal.pl b/tests/normal.pl index 2badb01..572ef03 100644 --- a/tests/normal.pl +++ b/tests/normal.pl @@ -2,7 +2,7 @@ # Test EncFS normal and paranoid mode -use Test::More qw( no_plan ); +use Test::More tests => 101; use File::Path; use File::Copy; use File::Temp; @@ -88,7 +88,7 @@ sub corruption sub internalModification { $ofile = "$workingDir/crypt-internal-$$"; - qx(dd if=/dev/urandom of=$ofile bs=2k count=2 2> /dev/null); + writeZeroes($ofile, 2*1024); ok(copy($ofile, "$crypt/internal"), "copying crypt-internal file"); open(my $out1, "+<", "$crypt/internal"); @@ -289,7 +289,7 @@ sub links is( readlink("$crypt/data-rel"), "data", "read rel symlink"); SKIP: { - skip "No hardlink support" unless $hardlinkTests; + skip "No hardlink support", 2 unless $hardlinkTests; ok( link("$crypt/data", "$crypt/data.2"), "hard link"); checkContents("$crypt/data.2", $contents, "hardlink read"); @@ -306,6 +306,7 @@ sub mount mkdir($raw) || BAIL_OUT("Could not create $raw: $!"); mkdir($crypt) || BAIL_OUT("Could not create $crypt: $!"); + delete $ENV{"ENCFS6_CONFIG"}; qx(./encfs/encfs --extpass="echo test" $args $raw $crypt); ok( -f "$raw/.encfs6.xml", "created control file"); diff --git a/tests/reverse.pl b/tests/reverse.pl index cc3f3af..2c073e8 100755 --- a/tests/reverse.pl +++ b/tests/reverse.pl @@ -3,10 +3,11 @@ # Test EncFS --reverse mode use warnings; -use Test::More qw( no_plan ); +use Test::More tests => 26; use File::Path; use File::Temp; use IO::Handle; +use Errno qw(EROFS); require("tests/common.inc"); @@ -44,6 +45,7 @@ sub cleanup # Directory structure: plain -[encrypt]-> ciphertext -[decrypt]-> decrypted sub mount { + delete $ENV{"ENCFS6_CONFIG"}; system("./encfs/encfs --extpass=\"echo test\" --standard $plain $ciphertext --reverse"); ok(waitForFile("$plain/.encfs6.xml"), "plain .encfs6.xml exists") or BAIL_OUT("'$plain/.encfs6.xml'"); my $e = encName(".encfs6.xml"); @@ -68,7 +70,6 @@ sub copy_test { ok(system("cp -a encfs $plain")==0, "copying files to plain"); ok(system("diff -r -q $plain $decrypted")==0, "decrypted files are identical"); - ok(-f "$plain/encfs/encfs.cpp", "file exists"); unlink("$plain/encfs/encfs.cpp"); ok(! -f "$decrypted/encfs.cpp", "file deleted"); @@ -78,9 +79,11 @@ sub copy_test # Parameter: symlink target sub symlink_test { - my $target = shift(@_); + my $target = shift; symlink($target, "$plain/symlink"); - ok( readlink("$decrypted/symlink") eq "$target", "symlink to '$target'"); + $dec = readlink("$decrypted/symlink"); + ok( $dec eq $target, "symlink to '$target'") or + print("# (original) $target' != '$dec' (decrypted)\n"); unlink("$plain/symlink"); } @@ -129,7 +132,7 @@ sub grow { } sub largeRead { - system("dd if=/dev/zero of=$plain/largeRead bs=1M count=1 2> /dev/null"); + writeZeroes("$plain/largeRead", 1024*1024); # ciphertext file name my $cname = encName("largeRead"); # cfh ... ciphertext file handle @@ -137,6 +140,32 @@ sub largeRead { ok(sizeVerify($cfh, 1024*1024+8), "1M file size"); } +# Check that the reverse mount is read-only +# (writing is not supported in reverse mode because of the added +# complexity and the marginal use case) +sub writesDenied { + $fn = "$plain/writesDenied"; + writeZeroes($fn, 1024); + my $efn = $ciphertext . "/" . encName("writesDenied"); + open(my $fh, ">", $efn); + if( ok( $! == EROFS, "open for write denied, EROFS")) { + ok( 1, "writing denied, filehandle not open"); + } + else { + print($fh "foo"); + ok( $! == EROFS, "writing denied, EROFS"); + } + $target = $ciphertext . "/" . encName("writesDenied2"); + rename($efn, $target); + ok( $! == EROFS, "rename denied, EROFS") or die(); + unlink($efn); + ok( $! == EROFS, "unlink denied, EROFS"); + utime(undef, undef, $efn) ; + ok( $! == EROFS, "touch denied, EROFS"); + truncate($efn, 10); + ok( $! == EROFS, "truncate denied, EROFS"); +} + # Setup mounts newWorkingDir(); mount(); @@ -149,6 +178,8 @@ symlink_test("/"); # absolute symlink_test("foo"); # relative symlink_test("/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/15/17/18"); # long symlink_test("!ยง\$%&/()\\<>#+="); # special characters +symlink_test("$plain/foo"); +writesDenied(); # Umount and delete files cleanup();