mirror of
https://github.com/vgough/encfs.git
synced 2024-12-27 09:08:48 +01:00
Merge pull request #34 from rfjakob/reverse-iv
reverse: Implement unique IV derived from the inode number
This commit is contained in:
commit
89513f273a
@ -27,6 +27,8 @@
|
||||
|
||||
#include "i18n.h"
|
||||
|
||||
#include "FileUtils.h"
|
||||
|
||||
template <typename Type>
|
||||
inline Type min(Type A, Type B) {
|
||||
return (B < A) ? B : A;
|
||||
@ -41,6 +43,7 @@ BlockFileIO::BlockFileIO(int blockSize, const FSConfigPtr &cfg)
|
||||
: _blockSize(blockSize), _allowHoles(cfg->config->allowHoles) {
|
||||
rAssert(_blockSize > 1);
|
||||
_cache.data = new unsigned char[_blockSize];
|
||||
_noCache = cfg->opts->noCache;
|
||||
}
|
||||
|
||||
BlockFileIO::~BlockFileIO() {
|
||||
@ -48,13 +51,27 @@ BlockFileIO::~BlockFileIO() {
|
||||
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 {
|
||||
// we can satisfy the request even if _cache.dataLen is too short, because
|
||||
// we always request a full block during reads..
|
||||
if ((req.offset == _cache.offset) && (_cache.dataLen != 0)) {
|
||||
|
||||
rAssert(req.dataLen <= _blockSize);
|
||||
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
|
||||
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);
|
||||
return len;
|
||||
} else {
|
||||
@ -88,6 +105,13 @@ bool BlockFileIO::cacheWriteOneBlock(const IORequest &req) {
|
||||
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 {
|
||||
rAssert(_blockSize != 0);
|
||||
|
||||
@ -97,7 +121,7 @@ ssize_t BlockFileIO::read(const IORequest &req) const {
|
||||
|
||||
if (partialOffset == 0 && req.dataLen <= _blockSize) {
|
||||
// read completely within a single block -- can be handled as-is by
|
||||
// readOneBloc().
|
||||
// readOneBlock().
|
||||
return cacheReadOneBlock(req);
|
||||
} else {
|
||||
size_t size = req.dataLen;
|
||||
|
@ -57,6 +57,7 @@ class BlockFileIO : public FileIO {
|
||||
|
||||
int _blockSize;
|
||||
bool _allowHoles;
|
||||
bool _noCache;
|
||||
|
||||
// cache last block for speed...
|
||||
mutable IORequest _cache;
|
||||
|
@ -184,7 +184,11 @@ int BlockNameIO::decodeName(const char *encodedName, int length, uint64_t *iv,
|
||||
int decodedStreamLen = decLen256 - 2;
|
||||
|
||||
// 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);
|
||||
|
||||
|
@ -28,6 +28,9 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <cerrno>
|
||||
#include <string.h>
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
/*
|
||||
- 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +266,71 @@ bool CipherFileIO::writeHeader() {
|
||||
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 {
|
||||
// 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 +339,7 @@ ssize_t CipherFileIO::readOneBlock(const IORequest &req) const {
|
||||
const_cast<CipherFileIO *>(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 +357,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 +392,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 +401,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 +457,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<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(); }
|
||||
|
@ -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<FileIO> base;
|
||||
|
||||
FSConfigPtr fsConfig;
|
||||
|
@ -99,6 +99,11 @@ static int V5SubVersionDefault = 0;
|
||||
// 20080813 was really made on 20080413 -- typo on date..
|
||||
// const int V6SubVersion = 20080813; // switch to v6/XML, add allowHoles option
|
||||
// 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+
|
||||
|
||||
struct ConfigInfo {
|
||||
@ -111,6 +116,7 @@ struct ConfigInfo {
|
||||
int currentSubVersion;
|
||||
int defaultSubVersion;
|
||||
} ConfigFileMapping[] = {
|
||||
// current format
|
||||
{".encfs6.xml", Config_V6, "ENCFS6_CONFIG", readV6Config, writeV6Config,
|
||||
V6SubVersion, 0},
|
||||
// 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,
|
||||
const shared_ptr<EncFSConfig> &config) {
|
||||
if (nm->loadFunc) {
|
||||
@ -334,8 +343,8 @@ ConfigType readConfig_load(ConfigInfo *nm, const char *path,
|
||||
err.log(_RLWarningChannel);
|
||||
}
|
||||
|
||||
rError(_("Found config file %s, but failed to load"), path);
|
||||
return Config_None;
|
||||
rError(_("Found config file %s, but failed to load - exiting"), path);
|
||||
exit(1);
|
||||
} else {
|
||||
// No load function - must be an unsupported 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,
|
||||
const shared_ptr<EncFSConfig> &config) {
|
||||
ConfigInfo *nm = ConfigFileMapping;
|
||||
@ -350,7 +363,13 @@ ConfigType readConfig(const string &rootDir,
|
||||
// allow environment variable to override default config path
|
||||
if (nm->environmentOverride != NULL) {
|
||||
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
|
||||
string path = rootDir + nm->fileName;
|
||||
@ -363,6 +382,10 @@ ConfigType readConfig(const string &rootDir,
|
||||
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,
|
||||
ConfigInfo *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,
|
||||
ConfigInfo *info) {
|
||||
bool ok = false;
|
||||
@ -437,6 +464,10 @@ bool readV5Config(const char *configFile, const shared_ptr<EncFSConfig> &config,
|
||||
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,
|
||||
ConfigInfo *info) {
|
||||
bool ok = false;
|
||||
@ -576,6 +607,9 @@ static Cipher::CipherAlgorithm findCipherAlgorithm(const char *name,
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user which cipher to use
|
||||
*/
|
||||
static Cipher::CipherAlgorithm selectCipherAlgorithm() {
|
||||
for (;;) {
|
||||
// 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() {
|
||||
for (;;) {
|
||||
// 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) {
|
||||
if (alg.keyLength.min() == alg.keyLength.max()) {
|
||||
cout << autosprintf(_("Using key size of %i bits"), alg.keyLength.min())
|
||||
@ -726,6 +766,9 @@ static int selectKeySize(const Cipher::CipherAlgorithm &alg) {
|
||||
return keySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user which block size to use
|
||||
*/
|
||||
static int selectBlockSize(const Cipher::CipherAlgorithm &alg) {
|
||||
if (alg.blockSize.min() == alg.blockSize.max()) {
|
||||
cout << autosprintf(
|
||||
@ -777,6 +820,9 @@ static bool boolDefaultNo(const char *prompt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user whether to enable block MAC and random header bytes
|
||||
*/
|
||||
static void selectBlockMAC(int *macBytes, int *macRandBytes) {
|
||||
// xgroup(setup)
|
||||
bool addMAC = boolDefaultNo(
|
||||
@ -827,6 +873,9 @@ static bool boolDefaultYes(const char *prompt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user if per-file unique IVs should be used
|
||||
*/
|
||||
static bool selectUniqueIV() {
|
||||
// xgroup(setup)
|
||||
return boolDefaultYes(
|
||||
@ -836,6 +885,9 @@ static bool selectUniqueIV() {
|
||||
"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() {
|
||||
// xgroup(setup)
|
||||
return boolDefaultYes(
|
||||
@ -844,6 +896,9 @@ static bool selectChainedIV() {
|
||||
"rather then encoding each path element individually."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user if the file IV should depend on the file path
|
||||
*/
|
||||
static bool selectExternalChainedIV() {
|
||||
// xgroup(setup)
|
||||
return boolDefaultNo(
|
||||
@ -855,6 +910,9 @@ static bool selectExternalChainedIV() {
|
||||
"in the filesystem."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user if file holes should be passed through
|
||||
*/
|
||||
static bool selectZeroBlockPassThrough() {
|
||||
// xgroup(setup)
|
||||
return boolDefaultYes(
|
||||
@ -895,16 +953,17 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &opts) {
|
||||
cout << "\n";
|
||||
}
|
||||
|
||||
int keySize = 0;
|
||||
int blockSize = 0;
|
||||
Cipher::CipherAlgorithm alg;
|
||||
Interface nameIOIface;
|
||||
int blockMACBytes = 0;
|
||||
int blockMACRandBytes = 0;
|
||||
bool uniqueIV = false;
|
||||
bool chainedIV = false;
|
||||
bool externalIV = false;
|
||||
bool allowHoles = true;
|
||||
// documented in ...
|
||||
int keySize = 0; // selectKeySize()
|
||||
int blockSize = 0; // selectBlockSize()
|
||||
Cipher::CipherAlgorithm alg; // selectCipherAlgorithm()
|
||||
Interface nameIOIface; // selectNameCoding()
|
||||
int blockMACBytes = 0; // selectBlockMAC()
|
||||
int blockMACRandBytes = 0; // selectBlockMAC()
|
||||
bool uniqueIV = false; // selectUniqueIV()
|
||||
bool chainedIV = false; // selectChainedIV()
|
||||
bool externalIV = false; // selectExternalChainedIV()
|
||||
bool allowHoles = true; // selectZeroBlockPassThrough()
|
||||
long desiredKDFDuration = NormalKDFDuration;
|
||||
|
||||
if (reverseEncryption) {
|
||||
@ -949,11 +1008,11 @@ RootPtr createV6Config(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &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;
|
||||
}
|
||||
}
|
||||
@ -1421,7 +1480,7 @@ RootPtr initFS(EncFS_Context *ctx, const shared_ptr<EncFS_Opts> &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");
|
||||
|
@ -76,6 +76,11 @@ struct EncFS_Opts {
|
||||
|
||||
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;
|
||||
|
||||
EncFS_Opts() {
|
||||
@ -90,6 +95,7 @@ struct EncFS_Opts {
|
||||
ownerCreate = false;
|
||||
reverseEncryption = false;
|
||||
configMode = Config_Prompt;
|
||||
noCache = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -49,14 +49,14 @@ using namespace rel;
|
||||
using namespace rlog;
|
||||
|
||||
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;
|
||||
|
||||
#ifndef MIN
|
||||
inline int MIN(int a, int b) { return (a < b) ? a : b; }
|
||||
#endif
|
||||
|
||||
/*
|
||||
/**
|
||||
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
|
||||
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; }
|
||||
|
||||
/*
|
||||
/**
|
||||
create a key from the password.
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
Create a random key.
|
||||
We use the OpenSSL library to generate random bytes, then take the hash of
|
||||
those bytes to use as the key.
|
||||
@ -447,7 +447,7 @@ CipherKey SSL_Cipher::newRandomKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
compute a 64-bit check value for the data using HMAC.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 strongRandom) const {
|
||||
// to avoid warnings of uninitialized data from valgrind
|
||||
@ -604,6 +609,22 @@ int SSL_Cipher::cipherBlockSize() const {
|
||||
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,
|
||||
const shared_ptr<SSLKey> &key) const {
|
||||
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];
|
||||
}
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
bool SSL_Cipher::streamEncode(unsigned char *buf, int size, uint64_t iv64,
|
||||
|
@ -93,7 +93,7 @@ static int withCipherPath(const char *opName, const char *path,
|
||||
} else if (!passReturnCode)
|
||||
res = ESUCCESS;
|
||||
} catch (rlog::Error &err) {
|
||||
rError("error caught in %s", opName);
|
||||
rError("withCipherPath: error caught in %s: %s", opName, err.message());
|
||||
err.log(_RLWarningChannel);
|
||||
}
|
||||
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));
|
||||
} catch (rlog::Error &err) {
|
||||
rError("error caught in %s", opName);
|
||||
rError("withFileNode: error caught in %s: %s", opName, err.message());
|
||||
err.log(_RLWarningChannel);
|
||||
}
|
||||
return res;
|
||||
|
@ -212,6 +212,7 @@ static bool processArgs(int argc, char *argv[],
|
||||
// {"single-thread", 0, 0, 's'}, // single-threaded mode
|
||||
{"stdinpass", 0, 0, 'S'}, // read password from stdin
|
||||
{"annotate", 0, 0, 513}, // Print annotation lines to stderr
|
||||
{"nocache", 0, 0, 514}, // disable caching
|
||||
{"verbose", 0, 0, 'v'}, // verbose mode
|
||||
{"version", 0, 0, 'V'}, // version
|
||||
{"reverse", 0, 0, 'r'}, // reverse encryption
|
||||
@ -272,8 +273,32 @@ 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;
|
||||
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;
|
||||
case 'm':
|
||||
out->opts->mountOnDemand = true;
|
||||
|
76
tests/common.inc
Normal file
76
tests/common.inc
Normal 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;
|
@ -7,7 +7,8 @@ use File::Path;
|
||||
use File::Copy;
|
||||
use File::Temp;
|
||||
use IO::Handle;
|
||||
use Digest::MD5 qw(md5_hex);
|
||||
|
||||
require("tests/common.inc");
|
||||
|
||||
my $tempDir = $ENV{'TMPDIR'} || "/tmp";
|
||||
|
||||
@ -190,7 +191,7 @@ sub truncate
|
||||
sub fileCreation
|
||||
{
|
||||
# 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" );
|
||||
|
||||
# ensure there is an encrypted version.
|
||||
@ -268,17 +269,6 @@ sub encName
|
||||
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
|
||||
sub links
|
||||
{
|
||||
|
@ -2,9 +2,13 @@
|
||||
|
||||
# Test EncFS --reverse mode
|
||||
|
||||
use warnings;
|
||||
use Test::More qw( no_plan );
|
||||
use File::Path;
|
||||
use File::Temp;
|
||||
use IO::Handle;
|
||||
|
||||
require("tests/common.inc");
|
||||
|
||||
my $tempDir = $ENV{'TMPDIR'} || "/tmp";
|
||||
|
||||
@ -29,7 +33,7 @@ sub cleanup
|
||||
{
|
||||
system("fusermount -u $decrypted");
|
||||
system("fusermount -u $ciphertext");
|
||||
|
||||
our $workingDir;
|
||||
rmtree($workingDir);
|
||||
ok( ! -d $workingDir, "working dir removed");
|
||||
}
|
||||
@ -40,11 +44,23 @@ sub cleanup
|
||||
# Directory structure: plain -[encrypt]-> ciphertext -[decrypt]-> decrypted
|
||||
sub mount
|
||||
{
|
||||
my $r=system("./encfs/encfs --extpass=\"echo test\" --standard $plain $ciphertext --reverse > /dev/null");
|
||||
ok($r == 0, "mounted ciphertext file system");
|
||||
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");
|
||||
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");
|
||||
ok($r == 0, "mounted decrypting file system");
|
||||
# Helper function
|
||||
#
|
||||
# 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
|
||||
@ -58,9 +74,81 @@ sub copy_test
|
||||
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();
|
||||
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
|
||||
|
||||
# Umount and delete files
|
||||
cleanup();
|
||||
|
Loading…
Reference in New Issue
Block a user