Merge pull request #34 from rfjakob/reverse-iv

reverse: Implement unique IV derived from the inode number
This commit is contained in:
Valient Gough 2014-11-24 21:00:50 -08:00
commit 89513f273a
13 changed files with 515 additions and 54 deletions

View File

@ -27,6 +27,8 @@
#include "i18n.h" #include "i18n.h"
#include "FileUtils.h"
template <typename Type> template <typename Type>
inline Type min(Type A, Type B) { inline Type min(Type A, Type B) {
return (B < A) ? B : A; return (B < A) ? B : A;
@ -41,6 +43,7 @@ BlockFileIO::BlockFileIO(int blockSize, const FSConfigPtr &cfg)
: _blockSize(blockSize), _allowHoles(cfg->config->allowHoles) { : _blockSize(blockSize), _allowHoles(cfg->config->allowHoles) {
rAssert(_blockSize > 1); rAssert(_blockSize > 1);
_cache.data = new unsigned char[_blockSize]; _cache.data = new unsigned char[_blockSize];
_noCache = cfg->opts->noCache;
} }
BlockFileIO::~BlockFileIO() { BlockFileIO::~BlockFileIO() {
@ -48,13 +51,27 @@ BlockFileIO::~BlockFileIO() {
delete[] _cache.data; delete[] _cache.data;
} }
/**
* Serve a read request for the size of one block or less,
* at block-aligned offsets.
* Always requests full blocks form the lower layer, truncates the
* returned data as neccessary.
*/
ssize_t BlockFileIO::cacheReadOneBlock(const IORequest &req) const { ssize_t BlockFileIO::cacheReadOneBlock(const IORequest &req) const {
// we can satisfy the request even if _cache.dataLen is too short, because
// we always request a full block during reads.. rAssert(req.dataLen <= _blockSize);
if ((req.offset == _cache.offset) && (_cache.dataLen != 0)) { rAssert(req.offset % _blockSize == 0);
/* we can satisfy the request even if _cache.dataLen is too short, because
* we always request a full block during reads. This just means we are
* in the last block of a file, which may be smaller than the blocksize.
* For reverse encryption, the cache must not be used at all, because
* the lower file may have changed behind our back. */
if ( (_noCache == false) && (req.offset == _cache.offset) &&
(_cache.dataLen != 0)) {
// satisfy request from cache // satisfy request from cache
int len = req.dataLen; int len = req.dataLen;
if (_cache.dataLen < len) len = _cache.dataLen; if (_cache.dataLen < len) len = _cache.dataLen; // Don't read past EOF
memcpy(req.data, _cache.data, len); memcpy(req.data, _cache.data, len);
return len; return len;
} else { } else {
@ -88,6 +105,13 @@ bool BlockFileIO::cacheWriteOneBlock(const IORequest &req) {
return ok; return ok;
} }
/**
* Serve a read request of arbitrary size at an arbitrary offset.
* Stitches together multiple blocks to serve large requests, drops
* data from the front of the first block if the request is not aligned.
* Always requests aligned data of the size of one block or less from the
* lower layer.
*/
ssize_t BlockFileIO::read(const IORequest &req) const { ssize_t BlockFileIO::read(const IORequest &req) const {
rAssert(_blockSize != 0); rAssert(_blockSize != 0);
@ -97,7 +121,7 @@ ssize_t BlockFileIO::read(const IORequest &req) const {
if (partialOffset == 0 && req.dataLen <= _blockSize) { if (partialOffset == 0 && req.dataLen <= _blockSize) {
// read completely within a single block -- can be handled as-is by // read completely within a single block -- can be handled as-is by
// readOneBloc(). // readOneBlock().
return cacheReadOneBlock(req); return cacheReadOneBlock(req);
} else { } else {
size_t size = req.dataLen; size_t size = req.dataLen;

View File

@ -57,6 +57,7 @@ class BlockFileIO : public FileIO {
int _blockSize; int _blockSize;
bool _allowHoles; bool _allowHoles;
bool _noCache;
// cache last block for speed... // cache last block for speed...
mutable IORequest _cache; mutable IORequest _cache;

View File

@ -184,7 +184,11 @@ int BlockNameIO::decodeName(const char *encodedName, int length, uint64_t *iv,
int decodedStreamLen = decLen256 - 2; int decodedStreamLen = decLen256 - 2;
// don't bother trying to decode files which are too small // don't bother trying to decode files which are too small
if (decodedStreamLen < _bs) throw ERROR("Filename too small to decode"); if (decodedStreamLen < _bs)
{
rDebug("Rejecting filename '%s'", encodedName);
throw ERROR("Filename too small to decode");
}
BUFFER_INIT(tmpBuf, 32, (unsigned int)length); BUFFER_INIT(tmpBuf, 32, (unsigned int)length);

View File

@ -28,6 +28,9 @@
#include <fcntl.h> #include <fcntl.h>
#include <cerrno> #include <cerrno>
#include <string.h>
#include <openssl/sha.h>
/* /*
- Version 2:0 adds support for a per-file initialization vector with a - Version 2:0 adds support for a per-file initialization vector with a
@ -128,25 +131,55 @@ bool CipherFileIO::setIV(uint64_t iv) {
return base->setIV(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 { int CipherFileIO::getAttr(struct stat *stbuf) const {
// stat() the backing file
int res = base->getAttr(stbuf); int res = base->getAttr(stbuf);
// adjust size if we have a file header // adjust size if we have a file header
if ((res == 0) && haveHeader && S_ISREG(stbuf->st_mode) && if ((res == 0) && haveHeader && S_ISREG(stbuf->st_mode) &&
(stbuf->st_size > 0)) { (stbuf->st_size > 0)) {
rAssert(stbuf->st_size >= HEADER_SIZE); if(!fsConfig->reverseEncryption)
stbuf->st_size -= HEADER_SIZE; {
/* 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; 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 CipherFileIO::getSize() const {
off_t size = base->getSize(); off_t size = base->getSize();
// No check on S_ISREG here -- don't call getSize over getAttr unless this // No check on S_ISREG here -- don't call getSize over getAttr unless this
// is a normal file! // is a normal file!
if (haveHeader && size > 0) { if (haveHeader && size > 0) {
rAssert(size >= HEADER_SIZE); if(!fsConfig->reverseEncryption)
size -= HEADER_SIZE; {
rAssert(size >= HEADER_SIZE);
size -= HEADER_SIZE;
}
else
{
size += HEADER_SIZE;
}
} }
return size; return size;
} }
@ -233,15 +266,71 @@ bool CipherFileIO::writeHeader() {
return true; return true;
} }
/**
* Generate the file IV header bytes for reverse mode
* (truncated SHA1 hash of the inode number)
*
* The kernel guarantees that the inode number is unique for one file
* system. SHA1 spreads out the values over the whole 64-bit space.
* Without this step, the XOR with the block number (see readOneBlock)
* may lead to duplicate IVs.
* SSL_Cipher::setIVec does an additional HMAC before using
* the IV. This guarantees unpredictability and prevents watermarking
* attacks.
*/
void CipherFileIO::generateReverseHeader(unsigned char* headerBuf) {
struct stat stbuf;
int res = getAttr(&stbuf);
rAssert( res == 0 );
ino_t ino = stbuf.st_ino;
rAssert( ino != 0 );
rDebug("generating reverse file IV header from ino=%lu", (unsigned long)ino);
// Serialize the inode number into inoBuf
unsigned char inoBuf[sizeof(ino_t)];
for (unsigned int i = 0; i < sizeof(ino_t); ++i) {
inoBuf[i] = (unsigned char)(ino & 0xff);
ino >>= 8;
}
/* Take the SHA1 hash of the inode number so the values are spread out
* over the whole 64-bit space. Otherwise, the XOR with the block number
* may lead to duplicate IVs (see readOneBlock) */
unsigned char md[20];
SHA1(inoBuf, sizeof(ino), md);
rAssert( HEADER_SIZE <= 20 );
memcpy(headerBuf, md, HEADER_SIZE);
// Save the IV in fileIV for internal use
fileIV = 0;
for (int i = 0; i < HEADER_SIZE; ++i) {
fileIV = (fileIV << 8) | (uint64_t)headerBuf[i];
}
rDebug("fileIV=%" PRIx64, fileIV);
// Encrypt externally-visible header
cipher->streamEncode(headerBuf, 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 { ssize_t CipherFileIO::readOneBlock(const IORequest &req) const {
// read raw data, then decipher it..
int bs = blockSize(); int bs = blockSize();
off_t blockNum = req.offset / bs; off_t blockNum = req.offset / bs;
ssize_t readSize = 0; ssize_t readSize = 0;
IORequest tmpReq = req; 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); readSize = base->read(tmpReq);
bool ok; bool ok;
@ -250,6 +339,7 @@ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const {
const_cast<CipherFileIO *>(this)->initHeader(); const_cast<CipherFileIO *>(this)->initHeader();
if (readSize != bs) { if (readSize != bs) {
rDebug("streamRead(data, %d, IV)", (int)readSize);
ok = streamRead(tmpReq.data, (int)readSize, blockNum ^ fileIV); ok = streamRead(tmpReq.data, (int)readSize, blockNum ^ fileIV);
} else { } else {
ok = blockRead(tmpReq.data, (int)readSize, blockNum ^ fileIV); ok = blockRead(tmpReq.data, (int)readSize, blockNum ^ fileIV);
@ -267,6 +357,12 @@ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const {
} }
bool CipherFileIO::writeOneBlock(const IORequest &req) { 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(); int bs = blockSize();
off_t blockNum = req.offset / bs; off_t blockNum = req.offset / bs;
@ -296,6 +392,7 @@ bool CipherFileIO::writeOneBlock(const IORequest &req) {
bool CipherFileIO::blockWrite(unsigned char *buf, int size, bool CipherFileIO::blockWrite(unsigned char *buf, int size,
uint64_t _iv64) const { uint64_t _iv64) const {
rDebug("Called blockWrite");
if (!fsConfig->reverseEncryption) if (!fsConfig->reverseEncryption)
return cipher->blockEncode(buf, size, _iv64, key); return cipher->blockEncode(buf, size, _iv64, key);
else else
@ -304,6 +401,7 @@ bool CipherFileIO::blockWrite(unsigned char *buf, int size,
bool CipherFileIO::streamWrite(unsigned char *buf, int size, bool CipherFileIO::streamWrite(unsigned char *buf, int size,
uint64_t _iv64) const { uint64_t _iv64) const {
rDebug("Called streamWrite");
if (!fsConfig->reverseEncryption) if (!fsConfig->reverseEncryption)
return cipher->streamEncode(buf, size, _iv64, key); return cipher->streamEncode(buf, size, _iv64, key);
else else
@ -359,4 +457,70 @@ int CipherFileIO::truncate(off_t size) {
return res; 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<CipherFileIO *>(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(); } bool CipherFileIO::isWritable() const { return base->isWritable(); }

View File

@ -57,6 +57,7 @@ class CipherFileIO : public BlockFileIO {
private: private:
virtual ssize_t readOneBlock(const IORequest &req) const; virtual ssize_t readOneBlock(const IORequest &req) const;
virtual bool writeOneBlock(const IORequest &req); virtual bool writeOneBlock(const IORequest &req);
virtual void generateReverseHeader(unsigned char* data);
void initHeader(); void initHeader();
bool writeHeader(); bool writeHeader();
@ -65,6 +66,8 @@ class CipherFileIO : public BlockFileIO {
bool blockWrite(unsigned char *buf, int size, uint64_t iv64) const; bool blockWrite(unsigned char *buf, int size, uint64_t iv64) const;
bool streamWrite(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<FileIO> base; shared_ptr<FileIO> base;
FSConfigPtr fsConfig; FSConfigPtr fsConfig;

View File

@ -99,6 +99,11 @@ static int V5SubVersionDefault = 0;
// 20080813 was really made on 20080413 -- typo on date.. // 20080813 was really made on 20080413 -- typo on date..
// const int V6SubVersion = 20080813; // switch to v6/XML, add allowHoles option // const int V6SubVersion = 20080813; // switch to v6/XML, add allowHoles option
// const int V6SubVersion = 20080816; // add salt and iteration count // const int V6SubVersion = 20080816; // add salt and iteration count
/*
* In boost 1.42+, serial numbers change to 8 bit, which means the date
* numbering scheme does not work any longer.
* boost-versioning.h implements a workaround that sets the version to
* 20 for boost 1.42+. */
const int V6SubVersion = 20100713; // add version field for boost 1.42+ const int V6SubVersion = 20100713; // add version field for boost 1.42+
struct ConfigInfo { struct ConfigInfo {
@ -111,6 +116,7 @@ struct ConfigInfo {
int currentSubVersion; int currentSubVersion;
int defaultSubVersion; int defaultSubVersion;
} ConfigFileMapping[] = { } ConfigFileMapping[] = {
// current format
{".encfs6.xml", Config_V6, "ENCFS6_CONFIG", readV6Config, writeV6Config, {".encfs6.xml", Config_V6, "ENCFS6_CONFIG", readV6Config, writeV6Config,
V6SubVersion, 0}, V6SubVersion, 0},
// backward compatible support for older versions // backward compatible support for older versions
@ -321,6 +327,9 @@ bool userAllowMkdir(int promptno, const char *path, mode_t mode) {
} }
} }
/**
* Load config file by calling the load function on the filename
*/
ConfigType readConfig_load(ConfigInfo *nm, const char *path, ConfigType readConfig_load(ConfigInfo *nm, const char *path,
const shared_ptr<EncFSConfig> &config) { const shared_ptr<EncFSConfig> &config) {
if (nm->loadFunc) { if (nm->loadFunc) {
@ -334,8 +343,8 @@ ConfigType readConfig_load(ConfigInfo *nm, const char *path,
err.log(_RLWarningChannel); err.log(_RLWarningChannel);
} }
rError(_("Found config file %s, but failed to load"), path); rError(_("Found config file %s, but failed to load - exiting"), path);
return Config_None; exit(1);
} else { } else {
// No load function - must be an unsupported type.. // No load function - must be an unsupported type..
config->cfgType = nm->type; config->cfgType = nm->type;
@ -343,6 +352,10 @@ ConfigType readConfig_load(ConfigInfo *nm, const char *path,
} }
} }
/**
* Try to locate the config file
* Tries the most recent format first, then looks for older versions
*/
ConfigType readConfig(const string &rootDir, ConfigType readConfig(const string &rootDir,
const shared_ptr<EncFSConfig> &config) { const shared_ptr<EncFSConfig> &config) {
ConfigInfo *nm = ConfigFileMapping; ConfigInfo *nm = ConfigFileMapping;
@ -350,7 +363,13 @@ ConfigType readConfig(const string &rootDir,
// allow environment variable to override default config path // allow environment variable to override default config path
if (nm->environmentOverride != NULL) { if (nm->environmentOverride != NULL) {
char *envFile = getenv(nm->environmentOverride); char *envFile = getenv(nm->environmentOverride);
if (envFile != NULL) return readConfig_load(nm, envFile, config); if (envFile != NULL) {
if (! fileExists(envFile)) {
rError("fatal: config file specified by environment does not exist: %s", envFile);
exit(1);
}
return readConfig_load(nm, envFile, config);
}
} }
// the standard place to look is in the root directory // the standard place to look is in the root directory
string path = rootDir + nm->fileName; string path = rootDir + nm->fileName;
@ -363,6 +382,10 @@ ConfigType readConfig(const string &rootDir,
return Config_None; return Config_None;
} }
/**
* Read config file in current "V6" XML format, normally named ".encfs6.xml"
* This format is in use since Apr 13, 2008 (commit 6d081f5c)
*/
bool readV6Config(const char *configFile, const shared_ptr<EncFSConfig> &config, bool readV6Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
ConfigInfo *info) { ConfigInfo *info) {
(void)info; (void)info;
@ -385,6 +408,10 @@ bool readV6Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
} }
} }
/**
* Read config file in deprecated "V5" format, normally named ".encfs5"
* This format has been used before Apr 13, 2008
*/
bool readV5Config(const char *configFile, const shared_ptr<EncFSConfig> &config, bool readV5Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
ConfigInfo *info) { ConfigInfo *info) {
bool ok = false; bool ok = false;
@ -437,6 +464,10 @@ bool readV5Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
return ok; return ok;
} }
/**
* Read config file in deprecated "V4" format, normally named ".encfs4"
* This format has been used before Jan 7, 2008
*/
bool readV4Config(const char *configFile, const shared_ptr<EncFSConfig> &config, bool readV4Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
ConfigInfo *info) { ConfigInfo *info) {
bool ok = false; bool ok = false;
@ -576,6 +607,9 @@ static Cipher::CipherAlgorithm findCipherAlgorithm(const char *name,
return result; return result;
} }
/**
* Ask the user which cipher to use
*/
static Cipher::CipherAlgorithm selectCipherAlgorithm() { static Cipher::CipherAlgorithm selectCipherAlgorithm() {
for (;;) { for (;;) {
// figure out what cipher they want to use.. // figure out what cipher they want to use..
@ -639,6 +673,9 @@ static Cipher::CipherAlgorithm selectCipherAlgorithm() {
} }
} }
/**
* Ask the user which encoding to use for file names
*/
static Interface selectNameCoding() { static Interface selectNameCoding() {
for (;;) { for (;;) {
// figure out what cipher they want to use.. // figure out what cipher they want to use..
@ -676,6 +713,9 @@ static Interface selectNameCoding() {
} }
} }
/**
* Ask the user which key size to use
*/
static int selectKeySize(const Cipher::CipherAlgorithm &alg) { static int selectKeySize(const Cipher::CipherAlgorithm &alg) {
if (alg.keyLength.min() == alg.keyLength.max()) { if (alg.keyLength.min() == alg.keyLength.max()) {
cout << autosprintf(_("Using key size of %i bits"), alg.keyLength.min()) cout << autosprintf(_("Using key size of %i bits"), alg.keyLength.min())
@ -726,6 +766,9 @@ static int selectKeySize(const Cipher::CipherAlgorithm &alg) {
return keySize; return keySize;
} }
/**
* Ask the user which block size to use
*/
static int selectBlockSize(const Cipher::CipherAlgorithm &alg) { static int selectBlockSize(const Cipher::CipherAlgorithm &alg) {
if (alg.blockSize.min() == alg.blockSize.max()) { if (alg.blockSize.min() == alg.blockSize.max()) {
cout << autosprintf( cout << autosprintf(
@ -777,6 +820,9 @@ static bool boolDefaultNo(const char *prompt) {
return false; return false;
} }
/**
* Ask the user whether to enable block MAC and random header bytes
*/
static void selectBlockMAC(int *macBytes, int *macRandBytes) { static void selectBlockMAC(int *macBytes, int *macRandBytes) {
// xgroup(setup) // xgroup(setup)
bool addMAC = boolDefaultNo( bool addMAC = boolDefaultNo(
@ -827,6 +873,9 @@ static bool boolDefaultYes(const char *prompt) {
return true; return true;
} }
/**
* Ask the user if per-file unique IVs should be used
*/
static bool selectUniqueIV() { static bool selectUniqueIV() {
// xgroup(setup) // xgroup(setup)
return boolDefaultYes( return boolDefaultYes(
@ -836,6 +885,9 @@ static bool selectUniqueIV() {
"which rely on block-aligned file io for performance.")); "which rely on block-aligned file io for performance."));
} }
/**
* Ask the user if the filename IV should depend on the complete path
*/
static bool selectChainedIV() { static bool selectChainedIV() {
// xgroup(setup) // xgroup(setup)
return boolDefaultYes( return boolDefaultYes(
@ -844,6 +896,9 @@ static bool selectChainedIV() {
"rather then encoding each path element individually.")); "rather then encoding each path element individually."));
} }
/**
* Ask the user if the file IV should depend on the file path
*/
static bool selectExternalChainedIV() { static bool selectExternalChainedIV() {
// xgroup(setup) // xgroup(setup)
return boolDefaultNo( return boolDefaultNo(
@ -855,6 +910,9 @@ static bool selectExternalChainedIV() {
"in the filesystem.")); "in the filesystem."));
} }
/**
* Ask the user if file holes should be passed through
*/
static bool selectZeroBlockPassThrough() { static bool selectZeroBlockPassThrough() {
// xgroup(setup) // xgroup(setup)
return boolDefaultYes( return boolDefaultYes(
@ -895,16 +953,17 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &opts) {
cout << "\n"; cout << "\n";
} }
int keySize = 0; // documented in ...
int blockSize = 0; int keySize = 0; // selectKeySize()
Cipher::CipherAlgorithm alg; int blockSize = 0; // selectBlockSize()
Interface nameIOIface; Cipher::CipherAlgorithm alg; // selectCipherAlgorithm()
int blockMACBytes = 0; Interface nameIOIface; // selectNameCoding()
int blockMACRandBytes = 0; int blockMACBytes = 0; // selectBlockMAC()
bool uniqueIV = false; int blockMACRandBytes = 0; // selectBlockMAC()
bool chainedIV = false; bool uniqueIV = false; // selectUniqueIV()
bool externalIV = false; bool chainedIV = false; // selectChainedIV()
bool allowHoles = true; bool externalIV = false; // selectExternalChainedIV()
bool allowHoles = true; // selectZeroBlockPassThrough()
long desiredKDFDuration = NormalKDFDuration; long desiredKDFDuration = NormalKDFDuration;
if (reverseEncryption) { if (reverseEncryption) {
@ -949,11 +1008,11 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &opts) {
blockMACBytes = 0; blockMACBytes = 0;
externalIV = false; externalIV = false;
nameIOIface = BlockNameIO::CurrentInterface(); nameIOIface = BlockNameIO::CurrentInterface();
uniqueIV = true;
if (reverseEncryption) { if (reverseEncryption) {
cout << _("--reverse specified, not using unique/chained IV") << "\n"; cout << _("--reverse specified, not using chained IV") << "\n";
} else { } else {
uniqueIV = true;
chainedIV = true; chainedIV = true;
} }
} }
@ -1421,7 +1480,7 @@ RootPtr initFS(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &opts) {
if (readConfig(opts->rootDir, config) != Config_None) { if (readConfig(opts->rootDir, config) != Config_None) {
if (opts->reverseEncryption) { if (opts->reverseEncryption) {
if (config->blockMACBytes != 0 || config->blockMACRandBytes != 0 || if (config->blockMACBytes != 0 || config->blockMACRandBytes != 0 ||
config->uniqueIV || config->externalIVChaining || config->externalIVChaining ||
config->chainedNameIV) { config->chainedNameIV) {
cout cout
<< _("The configuration loaded is not compatible with --reverse\n"); << _("The configuration loaded is not compatible with --reverse\n");

View File

@ -76,6 +76,11 @@ struct EncFS_Opts {
bool reverseEncryption; // Reverse encryption bool reverseEncryption; // Reverse encryption
bool noCache; /* Disable block cache (in EncFS) and stat cache (in kernel).
* This is needed if the backing files may be modified
* behind the back of EncFS (for example, in reverse mode).
* See main.cpp for a longer explaination. */
ConfigMode configMode; ConfigMode configMode;
EncFS_Opts() { EncFS_Opts() {
@ -90,6 +95,7 @@ struct EncFS_Opts {
ownerCreate = false; ownerCreate = false;
reverseEncryption = false; reverseEncryption = false;
configMode = Config_Prompt; configMode = Config_Prompt;
noCache = false;
} }
}; };

View File

@ -49,14 +49,14 @@ using namespace rel;
using namespace rlog; using namespace rlog;
const int MAX_KEYLENGTH = 32; // in bytes (256 bit) const int MAX_KEYLENGTH = 32; // in bytes (256 bit)
const int MAX_IVLENGTH = 16; const int MAX_IVLENGTH = 16; // 128 bit (AES block size, Blowfish has 64)
const int KEY_CHECKSUM_BYTES = 4; const int KEY_CHECKSUM_BYTES = 4;
#ifndef MIN #ifndef MIN
inline int MIN(int a, int b) { return (a < b) ? a : b; } inline int MIN(int a, int b) { return (a < b) ? a : b; }
#endif #endif
/* /**
This produces the same result as OpenSSL's EVP_BytesToKey. The difference This produces the same result as OpenSSL's EVP_BytesToKey. The difference
is that here we can explicitly specify the key size, instead of relying on is that here we can explicitly specify the key size, instead of relying on
the state of EVP_CIPHER struct. EVP_BytesToKey will only produce 128 bit the state of EVP_CIPHER struct. EVP_BytesToKey will only produce 128 bit
@ -348,7 +348,7 @@ SSL_Cipher::~SSL_Cipher() {}
Interface SSL_Cipher::interface() const { return realIface; } Interface SSL_Cipher::interface() const { return realIface; }
/* /**
create a key from the password. create a key from the password.
Use SHA to distribute entropy from the password into the key. Use SHA to distribute entropy from the password into the key.
@ -413,7 +413,7 @@ CipherKey SSL_Cipher::newKey(const char *password, int passwdLength) {
return key; return key;
} }
/* /**
Create a random key. Create a random key.
We use the OpenSSL library to generate random bytes, then take the hash of We use the OpenSSL library to generate random bytes, then take the hash of
those bytes to use as the key. those bytes to use as the key.
@ -447,7 +447,7 @@ CipherKey SSL_Cipher::newRandomKey() {
return key; return key;
} }
/* /**
compute a 64-bit check value for the data using HMAC. compute a 64-bit check value for the data using HMAC.
*/ */
static uint64_t _checksum_64(SSLKey *key, const unsigned char *data, static uint64_t _checksum_64(SSLKey *key, const unsigned char *data,
@ -487,6 +487,11 @@ static uint64_t _checksum_64(SSLKey *key, const unsigned char *data,
return value; return value;
} }
/**
* Write "len" bytes of random data into "buf"
*
* See "man 3 RAND_bytes" for the effect of strongRandom
*/
bool SSL_Cipher::randomize(unsigned char *buf, int len, bool SSL_Cipher::randomize(unsigned char *buf, int len,
bool strongRandom) const { bool strongRandom) const {
// to avoid warnings of uninitialized data from valgrind // to avoid warnings of uninitialized data from valgrind
@ -604,6 +609,22 @@ int SSL_Cipher::cipherBlockSize() const {
return EVP_CIPHER_block_size(_blockCipher); return EVP_CIPHER_block_size(_blockCipher);
} }
/**
* Generate the initialization vector that will actually be used for
* AES/Blowfish encryption and decryption in {stream,block}{Encode,Decode}
*
* It is derived from
* 1) a "seed" value that is passed from the higher layer, for the default
* configuration it is "block_number XOR per_file_IV_header" from
* CipherFileIO
* 2) The IV that is used for encrypting the master key, "IVData(key)"
* 3) The master key
* using
* ivec = HMAC(master_key, IVData(key) CONCAT seed)
*
* As an HMAC is unpredictable as long as the key is secret, the only
* requirement for "seed" is that is must be unique.
*/
void SSL_Cipher::setIVec(unsigned char *ivec, uint64_t seed, void SSL_Cipher::setIVec(unsigned char *ivec, uint64_t seed,
const shared_ptr<SSLKey> &key) const { const shared_ptr<SSLKey> &key) const {
if (iface.current() >= 3) { if (iface.current() >= 3) {
@ -695,7 +716,7 @@ static void unshuffleBytes(unsigned char *buf, int size) {
for (int i = size - 1; i; --i) buf[i] ^= buf[i - 1]; for (int i = size - 1; i; --i) buf[i] ^= buf[i - 1];
} }
/* Partial blocks are encoded with a stream cipher. We make multiple passes on /** Partial blocks are encoded with a stream cipher. We make multiple passes on
the data to ensure that the ends of the data depend on each other. the data to ensure that the ends of the data depend on each other.
*/ */
bool SSL_Cipher::streamEncode(unsigned char *buf, int size, uint64_t iv64, bool SSL_Cipher::streamEncode(unsigned char *buf, int size, uint64_t iv64,

View File

@ -93,7 +93,7 @@ static int withCipherPath(const char *opName, const char *path,
} else if (!passReturnCode) } else if (!passReturnCode)
res = ESUCCESS; res = ESUCCESS;
} catch (rlog::Error &err) { } catch (rlog::Error &err) {
rError("error caught in %s", opName); rError("withCipherPath: error caught in %s: %s", opName, err.message());
err.log(_RLWarningChannel); err.log(_RLWarningChannel);
} }
return res; return res;
@ -123,7 +123,7 @@ static int withFileNode(const char *opName, const char *path,
if (res < 0) rInfo("%s error: %s", opName, strerror(-res)); if (res < 0) rInfo("%s error: %s", opName, strerror(-res));
} catch (rlog::Error &err) { } catch (rlog::Error &err) {
rError("error caught in %s", opName); rError("withFileNode: error caught in %s: %s", opName, err.message());
err.log(_RLWarningChannel); err.log(_RLWarningChannel);
} }
return res; return res;

View File

@ -212,6 +212,7 @@ static bool processArgs(int argc, char *argv[],
// {"single-thread", 0, 0, 's'}, // single-threaded mode // {"single-thread", 0, 0, 's'}, // single-threaded mode
{"stdinpass", 0, 0, 'S'}, // read password from stdin {"stdinpass", 0, 0, 'S'}, // read password from stdin
{"annotate", 0, 0, 513}, // Print annotation lines to stderr {"annotate", 0, 0, 513}, // Print annotation lines to stderr
{"nocache", 0, 0, 514}, // disable caching
{"verbose", 0, 0, 'v'}, // verbose mode {"verbose", 0, 0, 'v'}, // verbose mode
{"version", 0, 0, 'V'}, // version {"version", 0, 0, 'V'}, // version
{"reverse", 0, 0, 'r'}, // reverse encryption {"reverse", 0, 0, 'r'}, // reverse encryption
@ -272,8 +273,32 @@ static bool processArgs(int argc, char *argv[],
case 'D': case 'D':
out->opts->forceDecode = true; out->opts->forceDecode = true;
break; 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': case 'r':
out->opts->reverseEncryption = true; out->opts->reverseEncryption = true;
case 514:
/* Disable EncFS block cache
* Causes reverse grow tests to fail because short reads
* are returned */
out->opts->noCache = true;
/* Disable kernel stat() cache
* Causes reverse grow tests to fail because stale stat() data
* is returned */
PUSHARG("-oattr_timeout=0");
/* Disable kernel dentry cache
* Fallout unknown, disabling for safety */
PUSHARG("-oentry_timeout=0");
break; break;
case 'm': case 'm':
out->opts->mountOnDemand = true; out->opts->mountOnDemand = true;

76
tests/common.inc Normal file
View File

@ -0,0 +1,76 @@
# Helper function
# Get the MD5 sum of the file open at the filehandle
use Digest::MD5 qw(md5_hex);
sub md5fh
{
my $fh_orig = shift;
open(my $fh, "<&", $fh_orig); # Duplicate the file handle so the seek
seek($fh, 0, 0); # does not affect the caller
my $md5 = Digest::MD5->new->addfile($fh)->hexdigest;
close($fh);
return $md5;
}
# Get the file size from stat() (by file handle or name)
sub statSize
{
my $f = shift;
my @s = stat($f) or die("stat on '$f' failed");
return $s[7];
}
# Get the file size by read()ing the whole file
sub readSize
{
my $fh = shift;
seek($fh, 0, 0);
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
# Checks both stat() and read()
sub sizeVerify
{
my $ok = 1;
my $fh = shift;
my $s0 = shift;
$ss = statSize($fh);
if ($s0 != $ss) {
$ok = 0;
print("# stat size $ss, expected $s0\n");
}
$sr = readSize($fh);
if ($s0 != $sr) {
$ok = 0;
print("# read size $sr, expected $s0\n");
}
return $ok;
}
# Wait for a file to appear
use Time::HiRes qw(usleep);
sub waitForFile
{
my $file = shift;
my $timeout;
$timeout = shift or $timeout = 5;
for(my $i = $timeout*10; $i > 0; $i--)
{
-f $file and return 1;
usleep(100000); # 0.1 seconds
}
print "# timeout waiting for '$file' to appear\n";
return 0;
}
# As this file will be require()'d, it needs to return true
return 1;

View File

@ -7,7 +7,8 @@ use File::Path;
use File::Copy; use File::Copy;
use File::Temp; use File::Temp;
use IO::Handle; use IO::Handle;
use Digest::MD5 qw(md5_hex);
require("tests/common.inc");
my $tempDir = $ENV{'TMPDIR'} || "/tmp"; my $tempDir = $ENV{'TMPDIR'} || "/tmp";
@ -190,7 +191,7 @@ sub truncate
sub fileCreation sub fileCreation
{ {
# create a file # create a file
qx(df -ah > "$crypt/df.txt"); qx(df -ah > "$crypt/df.txt" 2> /dev/null);
ok( -f "$crypt/df.txt", "file created" ); ok( -f "$crypt/df.txt", "file created" );
# ensure there is an encrypted version. # ensure there is an encrypted version.
@ -268,17 +269,6 @@ sub encName
return $enc; return $enc;
} }
# Helper function
# Get the MD5 sum of the file open at the filehandle
sub md5fh
{
my $fh_orig = shift;
open(my $fh, "<&", $fh_orig); # Duplicate the file handle so the seek
seek($fh, 0, 0); # does not affect the caller
return Digest::MD5->new->addfile($fh)->hexdigest;
close($fh);
}
# Test symlinks & hardlinks # Test symlinks & hardlinks
sub links sub links
{ {

View File

@ -2,9 +2,13 @@
# Test EncFS --reverse mode # Test EncFS --reverse mode
use warnings;
use Test::More qw( no_plan ); use Test::More qw( no_plan );
use File::Path; use File::Path;
use File::Temp; use File::Temp;
use IO::Handle;
require("tests/common.inc");
my $tempDir = $ENV{'TMPDIR'} || "/tmp"; my $tempDir = $ENV{'TMPDIR'} || "/tmp";
@ -29,7 +33,7 @@ sub cleanup
{ {
system("fusermount -u $decrypted"); system("fusermount -u $decrypted");
system("fusermount -u $ciphertext"); system("fusermount -u $ciphertext");
our $workingDir;
rmtree($workingDir); rmtree($workingDir);
ok( ! -d $workingDir, "working dir removed"); ok( ! -d $workingDir, "working dir removed");
} }
@ -40,11 +44,23 @@ sub cleanup
# Directory structure: plain -[encrypt]-> ciphertext -[decrypt]-> decrypted # Directory structure: plain -[encrypt]-> ciphertext -[decrypt]-> decrypted
sub mount sub mount
{ {
my $r=system("./encfs/encfs --extpass=\"echo test\" --standard $plain $ciphertext --reverse > /dev/null"); system("./encfs/encfs --extpass=\"echo test\" --standard $plain $ciphertext --reverse");
ok($r == 0, "mounted ciphertext file system"); ok(waitForFile("$plain/.encfs6.xml"), "plain .encfs6.xml exists") or BAIL_OUT("'$plain/.encfs6.xml'");
my $e = encName(".encfs6.xml");
ok(waitForFile("$ciphertext/$e"), "encrypted .encfs6.xml exists") or BAIL_OUT("'$ciphertext/$e'");
system("ENCFS6_CONFIG=$plain/.encfs6.xml ./encfs/encfs --nocache --extpass=\"echo test\" $ciphertext $decrypted");
ok(waitForFile("$decrypted/.encfs6.xml"), "decrypted .encfs6.xml exists") or BAIL_OUT("'$decrypted/.encfs6.xml'");
}
$r=system("ENCFS6_CONFIG=$plain/.encfs6.xml ./encfs/encfs --extpass=\"echo test\" $ciphertext $decrypted"); # Helper function
ok($r == 0, "mounted decrypting file system"); #
# Get encrypted name for file
sub encName
{
my $name = shift;
my $enc = qx(ENCFS6_CONFIG=$plain/.encfs6.xml ./encfs/encfsctl encode --extpass="echo test" $ciphertext $name);
chomp($enc);
return $enc;
} }
# Copy a directory tree and verify that the decrypted data is identical # Copy a directory tree and verify that the decrypted data is identical
@ -58,9 +74,81 @@ sub copy_test
ok(! -f "$decrypted/encfs.cpp", "file deleted"); ok(! -f "$decrypted/encfs.cpp", "file deleted");
} }
# Create symlinks and verify they are correctly decrypted
# Parameter: symlink target
sub symlink_test
{
my $target = shift(@_);
symlink($target, "$plain/symlink");
ok( readlink("$decrypted/symlink") eq "$target", "symlink to '$target'");
unlink("$plain/symlink");
}
# Grow a file from 0 to x kB and
# * check the ciphertext length is correct (stat + read)
# * 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");
# 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;
my $max = 9000;
for($i=5; $i < $max; $i += 5)
{
print($pfh "abcde") 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;
if(md5fh($vfh) ne md5fh($dfh))
{
$ok = 0;
print("# content is different, unified diff:\n");
system("diff -u $plain/grow $decrypted/grow");
}
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(); newWorkingDir();
mount(); mount();
# Actual tests
grow();
largeRead();
copy_test(); 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
# Umount and delete files
cleanup(); cleanup();