/***************************************************************************** * Author: Valient Gough * ***************************************************************************** * Copyright (c) 2004, Valient Gough * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MACFileIO.h" #include "easylogging++.h" #include #include #include #include #include "BlockFileIO.h" #include "Cipher.h" #include "Error.h" #include "FileIO.h" #include "FileUtils.h" #include "MemoryPool.h" #include "i18n.h" using namespace std; namespace encfs { // Version 1.0 worked on blocks of size (blockSize + headerSize). // That is, it took [blockSize] worth of user data and added headers. // Version 2.0 takes [blockSize - headerSize] worth of user data and writes // [blockSize] bytes. That way the size going into the crypto engine is // valid from what was selected based on the crypto module allowed ranges! // Version 2.1 allows per-block rand bytes to be used without enabling MAC. // // The information about MACFileIO currently does not make its way into the // configuration file, so there is no easy way to make this backward // compatible, except at a high level by checking a revision number for the // filesystem... // static Interface MACFileIO_iface("FileIO/MAC", 2, 1, 0); int dataBlockSize(const FSConfigPtr &cfg) { return cfg->config->blockSize - cfg->config->blockMACBytes - cfg->config->blockMACRandBytes; } MACFileIO::MACFileIO(std::shared_ptr _base, const FSConfigPtr &cfg) : BlockFileIO(dataBlockSize(cfg), cfg), base(std::move(_base)), cipher(cfg->cipher), key(cfg->key), macBytes(cfg->config->blockMACBytes), randBytes(cfg->config->blockMACRandBytes), warnOnly(cfg->opts->forceDecode) { rAssert(macBytes >= 0 && macBytes <= 8); rAssert(randBytes >= 0); VLOG(1) << "fs block size = " << cfg->config->blockSize << ", macBytes = " << cfg->config->blockMACBytes << ", randBytes = " << cfg->config->blockMACRandBytes; } MACFileIO::~MACFileIO() = default; Interface MACFileIO::interface() const { return MACFileIO_iface; } int MACFileIO::open(int flags) { return base->open(flags); } void MACFileIO::setFileName(const char *fileName) { base->setFileName(fileName); } const char *MACFileIO::getFileName() const { return base->getFileName(); } bool MACFileIO::setIV(uint64_t iv) { return base->setIV(iv); } inline static off_t roundUpDivide(off_t numerator, int denominator) { // integer arithmetic always rounds down, so we can round up by adding // enough so that any value other then a multiple of denominator gets // rouned to the next highest value. return (numerator + denominator - 1) / denominator; } // Convert from a location in the raw file to a location when MAC headers are // interleved with the data. // So, if the filesystem stores/encrypts [blockSize] bytes per block, then // [blockSize - headerSize] of those bytes will contain user-supplied data, // and the rest ([headerSize]) will contain the MAC header for this block. // Example, offset points to second block (of user-data) // offset = blockSize - headerSize // ... blockNum = 1 // ... partialBlock = 0 // ... adjLoc = 1 * blockSize static off_t locWithHeader(off_t offset, int blockSize, int headerSize) { off_t blockNum = roundUpDivide(offset, blockSize - headerSize); return offset + blockNum * headerSize; } // convert from a given location in the stream containing headers, and return a // location in the user-data stream (which doesn't contain MAC headers).. // The output value will always be less then the input value, because the // headers are stored at the beginning of the block, so even the first data is // offset by the size of the header. static off_t locWithoutHeader(off_t offset, int blockSize, int headerSize) { off_t blockNum = roundUpDivide(offset, blockSize); return offset - blockNum * headerSize; } int MACFileIO::getAttr(struct stat *stbuf) const { int res = base->getAttr(stbuf); if (res == 0 && S_ISREG(stbuf->st_mode)) { // have to adjust size field.. int headerSize = macBytes + randBytes; int bs = blockSize() + headerSize; stbuf->st_size = locWithoutHeader(stbuf->st_size, bs, headerSize); } return res; } off_t MACFileIO::getSize() const { // adjust the size to hide the header overhead we tack on.. int headerSize = macBytes + randBytes; int bs = blockSize() + headerSize; off_t size = base->getSize(); if (size > 0) { size = locWithoutHeader(size, bs, headerSize); } return size; } ssize_t MACFileIO::readOneBlock(const IORequest &req) const { int headerSize = macBytes + randBytes; int bs = blockSize() + headerSize; // ok, should clearly fit into an int MemBlock mb = MemoryPool::allocate(bs); IORequest tmp; tmp.offset = locWithHeader(req.offset, bs, headerSize); tmp.data = mb.data; tmp.dataLen = headerSize + req.dataLen; // get the data from the base FileIO layer ssize_t readSize = base->read(tmp); // don't store zeros if configured for zero-block pass-through bool skipBlock = true; if (_allowHoles) { for (int i = 0; i < readSize; ++i) { if (tmp.data[i] != 0) { skipBlock = false; break; } } } else if (macBytes > 0) { skipBlock = false; } if (readSize > headerSize) { if (!skipBlock) { // At this point the data has been decoded. So, compute the MAC of // the block and check against the checksum stored in the header.. uint64_t mac = cipher->MAC_64(tmp.data + macBytes, readSize - macBytes, key); // Constant time comparision to prevent timing attacks unsigned char fail = 0; for (int i = 0; i < macBytes; ++i, mac >>= 8) { int test = mac & 0xff; int stored = tmp.data[i]; fail |= (test ^ stored); } if (fail > 0) { // uh oh.. long blockNum = req.offset / bs; RLOG(WARNING) << "MAC comparison failure in block " << blockNum; if (!warnOnly) { MemoryPool::release(mb); return -EBADMSG; } } } // now copy the data to the output buffer readSize -= headerSize; memcpy(req.data, tmp.data + headerSize, readSize); } else { VLOG(1) << "readSize " << readSize << " at offset " << req.offset; if (readSize > 0) { readSize = 0; } } MemoryPool::release(mb); return readSize; } ssize_t MACFileIO::writeOneBlock(const IORequest &req) { int headerSize = macBytes + randBytes; int bs = blockSize() + headerSize; // ok, should clearly fit into an int // we have the unencrypted data, so we need to attach a header to it. MemBlock mb = MemoryPool::allocate(bs); IORequest newReq; newReq.offset = locWithHeader(req.offset, bs, headerSize); newReq.data = mb.data; newReq.dataLen = headerSize + req.dataLen; memset(newReq.data, 0, headerSize); memcpy(newReq.data + headerSize, req.data, req.dataLen); if (randBytes > 0) { if (!cipher->randomize(newReq.data + macBytes, randBytes, false)) { return -EBADMSG; } } if (macBytes > 0) { // compute the mac (which includes the random data) and fill it in uint64_t mac = cipher->MAC_64(newReq.data + macBytes, req.dataLen + randBytes, key); for (int i = 0; i < macBytes; ++i) { newReq.data[i] = mac & 0xff; mac >>= 8; } } // now, we can let the next level have it.. ssize_t writeSize = base->write(newReq); MemoryPool::release(mb); return writeSize; } int MACFileIO::truncate(off_t size) { int headerSize = macBytes + randBytes; int bs = blockSize() + headerSize; // ok, should clearly fit into an int int res = BlockFileIO::truncateBase(size, nullptr); if (res == 0) { res = base->truncate(locWithHeader(size, bs, headerSize)); } return res; } bool MACFileIO::isWritable() const { return base->isWritable(); } } // namespace encfs