diff --git a/encfs/CipherFileIO.cpp b/encfs/CipherFileIO.cpp index 1f9401a..924b562 100644 --- a/encfs/CipherFileIO.cpp +++ b/encfs/CipherFileIO.cpp @@ -28,6 +28,7 @@ #include #include +#include /* - Version 2:0 adds support for a per-file initialization vector with a @@ -128,25 +129,55 @@ bool CipherFileIO::setIV(uint64_t iv) { return base->setIV(iv); } +/** + * Get file attributes (FUSE-speak for "stat()") for an upper file + * Upper file = file we present to the user via FUSE + * Backing file = file that is actually on disk + */ int CipherFileIO::getAttr(struct stat *stbuf) const { + + // stat() the backing file int res = base->getAttr(stbuf); + // adjust size if we have a file header if ((res == 0) && haveHeader && S_ISREG(stbuf->st_mode) && (stbuf->st_size > 0)) { - rAssert(stbuf->st_size >= HEADER_SIZE); - stbuf->st_size -= HEADER_SIZE; + if(!fsConfig->reverseEncryption) + { + /* In normal mode, the upper file (plaintext) is smaller + * than the backing ciphertext file */ + rAssert(stbuf->st_size >= HEADER_SIZE); + stbuf->st_size -= HEADER_SIZE; + } + else + { + /* In reverse mode, the upper file (ciphertext) is larger than + * the backing plaintext file */ + stbuf->st_size += HEADER_SIZE; + } } return res; } +/** + * Get the size for an upper file + * See getAttr() for an explaination of the reverse handling + */ off_t CipherFileIO::getSize() const { off_t size = base->getSize(); // No check on S_ISREG here -- don't call getSize over getAttr unless this // is a normal file! if (haveHeader && size > 0) { - rAssert(size >= HEADER_SIZE); - size -= HEADER_SIZE; + if(!fsConfig->reverseEncryption) + { + rAssert(size >= HEADER_SIZE); + size -= HEADER_SIZE; + } + else + { + size += HEADER_SIZE; + } } return size; } @@ -233,15 +264,43 @@ bool CipherFileIO::writeHeader() { return true; } +/** + * Generate the file IV header bytes in reverse mode + */ +void CipherFileIO::generateReverseHeader(unsigned char* buf) { + rDebug("generating file IV header (reverse mode)"); + + // TODO derive from inode + for (int i = 0; i < HEADER_SIZE; ++i) { + buf[i]=i; + } + + // Save plain-text IV + fileIV = 0; + for (int i = 0; i < HEADER_SIZE; ++i) { + fileIV = (fileIV << 8) | (uint64_t)buf[i]; + } + + // Encrypt externally-visible header + cipher->streamEncode(buf, HEADER_SIZE, externalIV, key); +} + +/** + * Read block from backing ciphertext file, decrypt it (normal mode) + * or + * Read block from backing plaintext file, then encrypt it (reverse mode) + */ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const { - // read raw data, then decipher it.. int bs = blockSize(); off_t blockNum = req.offset / bs; ssize_t readSize = 0; IORequest tmpReq = req; - if (haveHeader) tmpReq.offset += HEADER_SIZE; + // adjust offset if we have a file header + if (haveHeader && !fsConfig->reverseEncryption) { + tmpReq.offset += HEADER_SIZE; + } readSize = base->read(tmpReq); bool ok; @@ -250,6 +309,7 @@ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const { const_cast(this)->initHeader(); if (readSize != bs) { + rDebug("streamRead(data, %d, IV)", (int)readSize); ok = streamRead(tmpReq.data, (int)readSize, blockNum ^ fileIV); } else { ok = blockRead(tmpReq.data, (int)readSize, blockNum ^ fileIV); @@ -267,6 +327,12 @@ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const { } bool CipherFileIO::writeOneBlock(const IORequest &req) { + + if (haveHeader && fsConfig->reverseEncryption) { + rDebug("writing to a reverse mount with per-file IVs is not implemented"); + return -EROFS; + } + int bs = blockSize(); off_t blockNum = req.offset / bs; @@ -296,6 +362,7 @@ bool CipherFileIO::writeOneBlock(const IORequest &req) { bool CipherFileIO::blockWrite(unsigned char *buf, int size, uint64_t _iv64) const { + rDebug("Called blockWrite"); if (!fsConfig->reverseEncryption) return cipher->blockEncode(buf, size, _iv64, key); else @@ -304,6 +371,7 @@ bool CipherFileIO::blockWrite(unsigned char *buf, int size, bool CipherFileIO::streamWrite(unsigned char *buf, int size, uint64_t _iv64) const { + rDebug("Called streamWrite"); if (!fsConfig->reverseEncryption) return cipher->streamEncode(buf, size, _iv64, key); else @@ -359,4 +427,70 @@ int CipherFileIO::truncate(off_t size) { return res; } +/** + * Handle reads for reverse mode with uniqueIV + */ +ssize_t CipherFileIO::read(const IORequest &origReq) const { + + /* if reverse mode is not active with uniqueIV, + * the read request is handled by the base class */ + if ( !(fsConfig->reverseEncryption && haveHeader) ) { + rDebug("relaying request to base class: offset=%d, dataLen=%d", origReq.offset, origReq.dataLen); + return BlockFileIO::read(origReq); + } + + rDebug("handling reverse unique IV read: offset=%d, dataLen=%d", origReq.offset, origReq.dataLen); + + // generate the file IV header + // this is needed in any case - without IV the file cannot be decoded + unsigned char headerBuf[HEADER_SIZE]; + const_cast(this)->generateReverseHeader(headerBuf); + + // Copy the request so we can modify it without affecting the caller + IORequest req = origReq; + + /* An offset x in the ciphertext file maps to x-8 in the + * plain text file. Values below zero are the header. */ + req.offset -= HEADER_SIZE; + + int headerBytes = 0; // number of header bytes to add + + /* The request contains (a part of) the header, so we prefix that part + * to the data. */ + if (req.offset < 0) { + headerBytes = -req.offset; + if ( req.dataLen < headerBytes ) + headerBytes = req.dataLen; // only up to the number of bytes requested + rDebug("Adding %d header bytes", headerBytes); + + // copy the header bytes into the data + int headerOffset = HEADER_SIZE - headerBytes; + memcpy(req.data, &headerBuf[headerOffset], headerBytes); + + // the read does not want data beyond the header + if ( headerBytes == req.dataLen) + return headerBytes; + + /* The rest of the request will be read from the backing file. + * As we have already generated n=headerBytes bytes, the request is + * shifted by headerBytes */ + req.offset += headerBytes; + rAssert( req.offset == 0 ); + req.data += headerBytes; + req.dataLen -= headerBytes; + } + + // read the payload + ssize_t readBytes = BlockFileIO::read(req); + rDebug("read %ld bytes from backing file", (long)readBytes); + if ( readBytes < 0) + return readBytes; // Return error code + else + { + ssize_t sum = headerBytes + readBytes; + rDebug("returning sum=%ld", (long)sum); + return sum; + } +} + bool CipherFileIO::isWritable() const { return base->isWritable(); } diff --git a/encfs/CipherFileIO.h b/encfs/CipherFileIO.h index 8012ae1..4042e29 100644 --- a/encfs/CipherFileIO.h +++ b/encfs/CipherFileIO.h @@ -57,6 +57,7 @@ class CipherFileIO : public BlockFileIO { private: virtual ssize_t readOneBlock(const IORequest &req) const; virtual bool writeOneBlock(const IORequest &req); + virtual void generateReverseHeader(unsigned char* data); void initHeader(); bool writeHeader(); @@ -65,6 +66,8 @@ class CipherFileIO : public BlockFileIO { bool blockWrite(unsigned char *buf, int size, uint64_t iv64) const; bool streamWrite(unsigned char *buf, int size, uint64_t iv64) const; + ssize_t read(const IORequest &req) const; + shared_ptr base; FSConfigPtr fsConfig; diff --git a/encfs/FileUtils.cpp b/encfs/FileUtils.cpp index c3538e0..1c563bf 100644 --- a/encfs/FileUtils.cpp +++ b/encfs/FileUtils.cpp @@ -1008,11 +1008,11 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr &opts) { blockMACBytes = 0; externalIV = false; nameIOIface = BlockNameIO::CurrentInterface(); + uniqueIV = true; if (reverseEncryption) { - cout << _("--reverse specified, not using unique/chained IV") << "\n"; + cout << _("--reverse specified, not using chained IV") << "\n"; } else { - uniqueIV = true; chainedIV = true; } } @@ -1480,7 +1480,7 @@ RootPtr initFS(EncFS_Context *ctx, const shared_ptr &opts) { if (readConfig(opts->rootDir, config) != Config_None) { if (opts->reverseEncryption) { if (config->blockMACBytes != 0 || config->blockMACRandBytes != 0 || - config->uniqueIV || config->externalIVChaining || + config->externalIVChaining || config->chainedNameIV) { cout << _("The configuration loaded is not compatible with --reverse\n"); diff --git a/tests/common.inc b/tests/common.inc index ea3b93c..b78e6a5 100644 --- a/tests/common.inc +++ b/tests/common.inc @@ -23,9 +23,16 @@ sub readSize { my $fh = shift; seek($fh, 0, 0); - my $s = read($fh, my $data, 10240); - $data = 0; - return $s; + my $block = 4*1024; + my $s; + my $data; + my $sum = read($fh, $data, $block); + while ( $s = read($fh, $data, $block) ) + { + $sum += $s; + } + $data = ""; + return $sum; } # Verify that the size of the file passed by filehandle matches the target size s0 diff --git a/tests/reverse.pl b/tests/reverse.pl index 2e7500d..de010cf 100755 --- a/tests/reverse.pl +++ b/tests/reverse.pl @@ -89,43 +89,59 @@ sub symlink_test # * check that the decrypted length is correct (stat + read) # * check that plaintext and decrypted are identical sub grow { - # pfh ... plaintext file handle - open(my $pfh, ">", "$plain/grow"); - # vfh ... verification file handle - open(my $vfh, "<", "$plain/grow"); - $pfh->autoflush; - # ciphertext file name - my $cname = encName("grow"); - # cfh ... ciphertext file handle - ok(open(my $cfh, "<", "$ciphertext/$cname"), "open ciphertext grow file"); - # dfh ... decrypted file handle - ok(open(my $dfh, "<", "$decrypted/grow"), "open decrypted grow file"); + # pfh ... plaintext file handle + open(my $pfh, ">", "$plain/grow"); + # vfh ... verification file handle + open(my $vfh, "<", "$plain/grow"); + $pfh->autoflush; + # ciphertext file name + my $cname = encName("grow"); + # cfh ... ciphertext file handle + ok(open(my $cfh, "<", "$ciphertext/$cname"), "open ciphertext grow file"); + # dfh ... decrypted file handle + ok(open(my $dfh, "<", "$decrypted/grow"), "open decrypted grow file"); - # csz ... ciphertext size - ok(sizeVerify($cfh, 0), "ciphertext of empty file is empty"); - ok(sizeVerify($dfh, 0), "decrypted empty file is empty"); + # csz ... ciphertext size + ok(sizeVerify($cfh, 0), "ciphertext of empty file is empty"); + ok(sizeVerify($dfh, 0), "decrypted empty file is empty"); - my $ok = 1; - for($i=1; $i < 20; $i++) - { - print($pfh "w") or die("write failed"); - # autoflush should make sure the write goes to the kernel - # immediately. Just to be sure, check it here. - sizeVerify($vfh, $i) or die("unexpected plain file size"); - sizeVerify($cfh, $i) or $ok = 0; + my $ok = 1; + my $max = 9000; + for($i=5; $i < $max; $i += 5) + { + print($pfh "wwwww") or die("write failed"); + # autoflush should make sure the write goes to the kernel + # immediately. Just to be sure, check it here. + sizeVerify($vfh, $i) or die("unexpected plain file size"); + sizeVerify($cfh, $i+8) or $ok = 0; sizeVerify($dfh, $i) or $ok = 0; - } - ok($ok, "ciphertext and decrypted size of file grown to $i bytes"); + + last unless $ok; + } + ok($ok, "ciphertext and decrypted size of file grown to $i bytes"); } +sub largeRead { + system("dd if=/dev/zero of=$plain/largeRead bs=1M count=1 2> /dev/null"); + # ciphertext file name + my $cname = encName("largeRead"); + # cfh ... ciphertext file handle + ok(open(my $cfh, "<", "$ciphertext/$cname"), "open ciphertext largeRead file"); + ok(sizeVerify($cfh, 1024*1024+8), "1M file size"); +} + +# Setup mounts newWorkingDir(); mount(); +# Actual tests +grow(); +largeRead(); copy_test(); 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 -grow(); +# Umount and delete files cleanup();