Implement a VersionCountingBlockStore that checks that version numbers are nondecreasing. Currently, the block store is not used yet and the state is not stored over different runs of CryFS. This comes with future commits.

This commit is contained in:
Sebastian Messmer 2016-06-21 15:07:06 -07:00
parent da6390646e
commit d7f547dd47
11 changed files with 542 additions and 0 deletions

View File

@ -27,6 +27,9 @@ set(SOURCES
implementations/caching/cache/QueueMap.cpp
implementations/caching/CachedBlock.cpp
implementations/caching/NewBlock.cpp
implementations/versioncounting/VersionCountingBlock.cpp
implementations/versioncounting/VersionCountingBlockStore.cpp
implementations/versioncounting/KnownBlockVersions.cpp
)
add_library(${PROJECT_NAME} STATIC ${SOURCES})

View File

@ -0,0 +1,32 @@
#include "KnownBlockVersions.h"
namespace blockstore {
namespace versioncounting {
KnownBlockVersions::KnownBlockVersions()
:_knownVersions() {
}
bool KnownBlockVersions::checkAndUpdateVersion(const Key &key, uint64_t version) {
auto found = _knownVersions.find(key);
if (found == _knownVersions.end()) {
_knownVersions.emplace(key, version);
return true;
}
if (found->second > version) {
return false;
}
found->second = version;
return true;
}
void KnownBlockVersions::updateVersion(const Key &key, uint64_t version) {
if (!checkAndUpdateVersion(key, version)) {
throw std::logic_error("Tried to decrease block version");
}
}
}
}

View File

@ -0,0 +1,30 @@
#pragma once
#ifndef MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_KNOWNBLOCKVERSIONS_H_
#define MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_KNOWNBLOCKVERSIONS_H_
#include <cpp-utils/macros.h>
#include <blockstore/utils/Key.h>
namespace blockstore {
namespace versioncounting {
class KnownBlockVersions final {
public:
KnownBlockVersions();
KnownBlockVersions(KnownBlockVersions &&rhs) = default;
__attribute__((warn_unused_result))
bool checkAndUpdateVersion(const Key &key, uint64_t version);
void updateVersion(const Key &key, uint64_t version);
private:
std::unordered_map<Key, uint64_t> _knownVersions;
DISALLOW_COPY_AND_ASSIGN(KnownBlockVersions);
};
}
}
#endif

View File

@ -0,0 +1,9 @@
#include "VersionCountingBlock.h"
namespace blockstore {
namespace versioncounting {
constexpr unsigned int VersionCountingBlock::HEADER_LENGTH;
constexpr uint16_t VersionCountingBlock::FORMAT_VERSION_HEADER;
constexpr uint64_t VersionCountingBlock::VERSION_ZERO;
}
}

View File

@ -0,0 +1,186 @@
#pragma once
#ifndef MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_VERSIONCOUNTINGBLOCK_H_
#define MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_VERSIONCOUNTINGBLOCK_H_
#include "../../interface/Block.h"
#include <cpp-utils/data/Data.h>
#include "../../interface/BlockStore.h"
#include "KnownBlockVersions.h"
#include <cpp-utils/macros.h>
#include <memory>
#include <iostream>
#include <boost/optional.hpp>
#include <cpp-utils/crypto/symmetric/Cipher.h>
#include <cpp-utils/assert/assert.h>
#include <cpp-utils/data/DataUtils.h>
#include <mutex>
#include <cpp-utils/logging/logging.h>
namespace blockstore {
namespace versioncounting {
class VersionCountingBlockStore;
// TODO Is an implementation that doesn't keep an in-memory copy but just passes through write() calls to the underlying block store (including a write call to the version number each time) faster?
class VersionCountingBlock final: public Block {
public:
static boost::optional<cpputils::unique_ref<VersionCountingBlock>> TryCreateNew(BlockStore *baseBlockStore, const Key &key, cpputils::Data data, KnownBlockVersions *knownBlockVersions);
static boost::optional<cpputils::unique_ref<VersionCountingBlock>> TryLoad(cpputils::unique_ref<Block> baseBlock, KnownBlockVersions *knownBlockVersions);
static uint64_t blockSizeFromPhysicalBlockSize(uint64_t blockSize);
//TODO Storing key twice (in parent class and in object pointed to). Once would be enough.
VersionCountingBlock(cpputils::unique_ref<Block> baseBlock, cpputils::Data dataWithHeader, uint64_t version, KnownBlockVersions *knownBlockVersions);
~VersionCountingBlock();
const void *data() const override;
void write(const void *source, uint64_t offset, uint64_t count) override;
void flush() override;
size_t size() const override;
void resize(size_t newSize) override;
cpputils::unique_ref<Block> releaseBlock();
private:
KnownBlockVersions *_knownBlockVersions;
cpputils::unique_ref<Block> _baseBlock;
cpputils::Data _dataWithHeader;
uint64_t _version;
bool _dataChanged;
void _storeToBaseBlock();
static cpputils::Data _prependHeaderToData(uint64_t version, cpputils::Data data);
static void _checkFormatHeader(const cpputils::Data &data);
static uint64_t _readVersion(const cpputils::Data &data);
static bool _versionIsNondecreasing(const Key &key, uint64_t version, KnownBlockVersions *knownBlockVersions);
// This header is prepended to blocks to allow future versions to have compatibility.
static constexpr uint16_t FORMAT_VERSION_HEADER = 0;
static constexpr uint64_t VERSION_ZERO = 0;
static constexpr unsigned int HEADER_LENGTH = sizeof(FORMAT_VERSION_HEADER) + sizeof(VERSION_ZERO);
std::mutex _mutex;
DISALLOW_COPY_AND_ASSIGN(VersionCountingBlock);
};
inline boost::optional<cpputils::unique_ref<VersionCountingBlock>> VersionCountingBlock::TryCreateNew(BlockStore *baseBlockStore, const Key &key, cpputils::Data data, KnownBlockVersions *knownBlockVersions) {
cpputils::Data dataWithHeader = _prependHeaderToData(VERSION_ZERO, std::move(data));
auto baseBlock = baseBlockStore->tryCreate(key, dataWithHeader.copy()); // TODO Copy necessary?
if (baseBlock == boost::none) {
//TODO Test this code branch
return boost::none;
}
knownBlockVersions->updateVersion(key, VERSION_ZERO);
return cpputils::make_unique_ref<VersionCountingBlock>(std::move(*baseBlock), std::move(dataWithHeader), VERSION_ZERO, knownBlockVersions);
}
inline cpputils::Data VersionCountingBlock::_prependHeaderToData(const uint64_t version, cpputils::Data data) {
static_assert(HEADER_LENGTH == sizeof(FORMAT_VERSION_HEADER) + sizeof(version), "Wrong header length");
cpputils::Data result(data.size() + HEADER_LENGTH);
std::memcpy(result.dataOffset(0), &FORMAT_VERSION_HEADER, sizeof(FORMAT_VERSION_HEADER));
std::memcpy(result.dataOffset(sizeof(FORMAT_VERSION_HEADER)), &version, sizeof(version));
std::memcpy((uint8_t*)result.dataOffset(HEADER_LENGTH), data.data(), data.size());
return result;
}
inline boost::optional<cpputils::unique_ref<VersionCountingBlock>> VersionCountingBlock::TryLoad(cpputils::unique_ref<Block> baseBlock, KnownBlockVersions *knownBlockVersions) {
cpputils::Data data(baseBlock->size());
std::memcpy(data.data(), baseBlock->data(), data.size());
_checkFormatHeader(data);
uint64_t version = _readVersion(data);
if(!_versionIsNondecreasing(baseBlock->key(), version, knownBlockVersions)) {
//The stored key in the block data is incorrect - an attacker might have exchanged the contents with the encrypted data from a different block
cpputils::logging::LOG(cpputils::logging::WARN) << "Decrypting block " << baseBlock->key().ToString() << " failed due to wrong version number. Was the block rolled back by an attacker?";
return boost::none;
}
return cpputils::make_unique_ref<VersionCountingBlock>(std::move(baseBlock), std::move(data), version, knownBlockVersions);
}
inline void VersionCountingBlock::_checkFormatHeader(const cpputils::Data &data) {
if (*reinterpret_cast<decltype(FORMAT_VERSION_HEADER)*>(data.data()) != FORMAT_VERSION_HEADER) {
throw std::runtime_error("The versioned block has the wrong format. Was it created with a newer version of CryFS?");
}
}
inline uint64_t VersionCountingBlock::_readVersion(const cpputils::Data &data) {
uint64_t version;
std::memcpy(&version, data.dataOffset(sizeof(FORMAT_VERSION_HEADER)), sizeof(version));
return version;
}
inline bool VersionCountingBlock::_versionIsNondecreasing(const Key &key, uint64_t version, KnownBlockVersions *knownBlockVersions) {
return knownBlockVersions->checkAndUpdateVersion(key, version);
}
inline VersionCountingBlock::VersionCountingBlock(cpputils::unique_ref<Block> baseBlock, cpputils::Data dataWithHeader, uint64_t version, KnownBlockVersions *knownBlockVersions)
:Block(baseBlock->key()),
_knownBlockVersions(knownBlockVersions),
_baseBlock(std::move(baseBlock)),
_dataWithHeader(std::move(dataWithHeader)),
_version(version),
_dataChanged(false),
_mutex() {
}
inline VersionCountingBlock::~VersionCountingBlock() {
std::unique_lock<std::mutex> lock(_mutex);
_storeToBaseBlock();
}
inline const void *VersionCountingBlock::data() const {
return (uint8_t*)_dataWithHeader.data() + HEADER_LENGTH;
}
inline void VersionCountingBlock::write(const void *source, uint64_t offset, uint64_t count) {
ASSERT(offset <= size() && offset + count <= size(), "Write outside of valid area"); //Also check offset < size() because of possible overflow in the addition
std::memcpy((uint8_t*)_dataWithHeader.data()+HEADER_LENGTH+offset, source, count);
_dataChanged = true;
}
inline void VersionCountingBlock::flush() {
std::unique_lock<std::mutex> lock(_mutex);
_storeToBaseBlock();
return _baseBlock->flush();
}
inline size_t VersionCountingBlock::size() const {
return _dataWithHeader.size() - HEADER_LENGTH;
}
inline void VersionCountingBlock::resize(size_t newSize) {
_dataWithHeader = cpputils::DataUtils::resize(std::move(_dataWithHeader), newSize + HEADER_LENGTH);
_dataChanged = true;
}
inline void VersionCountingBlock::_storeToBaseBlock() {
if (_dataChanged) {
++_version;
std::memcpy(_dataWithHeader.dataOffset(sizeof(FORMAT_VERSION_HEADER)), &_version, sizeof(_version));
_baseBlock->write(_dataWithHeader.data(), 0, _dataWithHeader.size());
_knownBlockVersions->updateVersion(key(), _version);
_dataChanged = false;
}
}
inline cpputils::unique_ref<Block> VersionCountingBlock::releaseBlock() {
std::unique_lock<std::mutex> lock(_mutex);
_storeToBaseBlock();
return std::move(_baseBlock);
}
inline uint64_t VersionCountingBlock::blockSizeFromPhysicalBlockSize(uint64_t blockSize) {
if (blockSize <= HEADER_LENGTH) {
return 0;
}
return blockSize - HEADER_LENGTH;
}
}
}
#endif

View File

@ -0,0 +1 @@
#include "VersionCountingBlockStore.h"

View File

@ -0,0 +1,82 @@
#pragma once
#ifndef MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_VERSIONCOUNTINGBLOCKSTORE_H_
#define MESSMER_BLOCKSTORE_IMPLEMENTATIONS_VERSIONCOUNTING_VERSIONCOUNTINGBLOCKSTORE_H_
#include "../../interface/BlockStore.h"
#include <cpp-utils/macros.h>
#include <cpp-utils/pointer/cast.h>
#include "VersionCountingBlock.h"
#include "KnownBlockVersions.h"
#include <iostream>
namespace blockstore {
namespace versioncounting {
class VersionCountingBlockStore final: public BlockStore {
public:
VersionCountingBlockStore(cpputils::unique_ref<BlockStore> baseBlockStore, KnownBlockVersions knownBlockVersions);
Key createKey() override;
boost::optional<cpputils::unique_ref<Block>> tryCreate(const Key &key, cpputils::Data data) override;
boost::optional<cpputils::unique_ref<Block>> load(const Key &key) override;
void remove(cpputils::unique_ref<Block> block) override;
uint64_t numBlocks() const override;
uint64_t estimateNumFreeBytes() const override;
uint64_t blockSizeFromPhysicalBlockSize(uint64_t blockSize) const override;
private:
cpputils::unique_ref<BlockStore> _baseBlockStore;
KnownBlockVersions _knownBlockVersions;
DISALLOW_COPY_AND_ASSIGN(VersionCountingBlockStore);
};
inline VersionCountingBlockStore::VersionCountingBlockStore(cpputils::unique_ref<BlockStore> baseBlockStore, KnownBlockVersions knownBlockVersions)
: _baseBlockStore(std::move(baseBlockStore)), _knownBlockVersions(std::move(knownBlockVersions)) {
}
inline Key VersionCountingBlockStore::createKey() {
return _baseBlockStore->createKey();
}
inline boost::optional<cpputils::unique_ref<Block>> VersionCountingBlockStore::tryCreate(const Key &key, cpputils::Data data) {
//TODO Easier implementation? This is only so complicated because of the cast VersionCountingBlock -> Block
auto result = VersionCountingBlock::TryCreateNew(_baseBlockStore.get(), key, std::move(data), &_knownBlockVersions);
if (result == boost::none) {
return boost::none;
}
return cpputils::unique_ref<Block>(std::move(*result));
}
inline boost::optional<cpputils::unique_ref<Block>> VersionCountingBlockStore::load(const Key &key) {
auto block = _baseBlockStore->load(key);
if (block == boost::none) {
return boost::none;
}
return boost::optional<cpputils::unique_ref<Block>>(VersionCountingBlock::TryLoad(std::move(*block), &_knownBlockVersions));
}
inline void VersionCountingBlockStore::remove(cpputils::unique_ref<Block> block) {
auto versionCountingBlock = cpputils::dynamic_pointer_move<VersionCountingBlock>(block);
ASSERT(versionCountingBlock != boost::none, "Block is not an VersionCountingBlock");
auto baseBlock = (*versionCountingBlock)->releaseBlock();
return _baseBlockStore->remove(std::move(baseBlock));
}
inline uint64_t VersionCountingBlockStore::numBlocks() const {
return _baseBlockStore->numBlocks();
}
inline uint64_t VersionCountingBlockStore::estimateNumFreeBytes() const {
return _baseBlockStore->estimateNumFreeBytes();
}
inline uint64_t VersionCountingBlockStore::blockSizeFromPhysicalBlockSize(uint64_t blockSize) const {
return VersionCountingBlock::blockSizeFromPhysicalBlockSize(_baseBlockStore->blockSizeFromPhysicalBlockSize(blockSize));
}
}
}
#endif

View File

@ -34,6 +34,9 @@ set(SOURCES
implementations/caching/cache/CacheTest_RaceCondition.cpp
implementations/caching/cache/PeriodicTaskTest.cpp
implementations/caching/cache/QueueMapTest_Peek.cpp
implementations/versioncounting/KnownBlockVersionsTest.cpp
implementations/versioncounting/VersionCountingBlockStoreTest_Generic.cpp
implementations/versioncounting/VersionCountingBlockStoreTest_Specific.cpp
)
add_executable(${PROJECT_NAME} ${SOURCES})

View File

@ -0,0 +1,77 @@
#include <gtest/gtest.h>
#include <blockstore/implementations/versioncounting/KnownBlockVersions.h>
using blockstore::versioncounting::KnownBlockVersions;
class KnownBlockVersionsTest : public ::testing::Test {
public:
blockstore::Key key = blockstore::Key::FromString("1491BB4932A389EE14BC7090AC772972");
KnownBlockVersions testobj;
};
TEST_F(KnownBlockVersionsTest, update_newEntry_zero) {
testobj.updateVersion(key, 0);
}
TEST_F(KnownBlockVersionsTest, update_newEntry_nonzero) {
testobj.updateVersion(key, 100);
}
TEST_F(KnownBlockVersionsTest, update_existingEntry_equal_zero) {
testobj.updateVersion(key, 0);
testobj.updateVersion(key, 0);
}
TEST_F(KnownBlockVersionsTest, update_existingEntry_equal_nonzero) {
testobj.updateVersion(key, 100);
testobj.updateVersion(key, 100);
}
TEST_F(KnownBlockVersionsTest, update_existingEntry_nonequal) {
testobj.updateVersion(key, 100);
testobj.updateVersion(key, 101);
}
TEST_F(KnownBlockVersionsTest, update_existingEntry_invalid) {
testobj.updateVersion(key, 100);
EXPECT_ANY_THROW(
testobj.updateVersion(key, 99);
);
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_newEntry_zero) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 0));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_newEntry_nonzero) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_existingEntry_equal_zero) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 0));
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 0));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_existingEntry_equal_nonzero) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_existingEntry_nonequal) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 101));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_existingEntry_invalid) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
EXPECT_FALSE(testobj.checkAndUpdateVersion(key, 99));
}
TEST_F(KnownBlockVersionsTest, checkAndUpdate_existingEntry_invalidDoesntModifyEntry) {
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
EXPECT_FALSE(testobj.checkAndUpdateVersion(key, 99));
EXPECT_FALSE(testobj.checkAndUpdateVersion(key, 99));
EXPECT_TRUE(testobj.checkAndUpdateVersion(key, 100));
}

View File

@ -0,0 +1,25 @@
#include "blockstore/implementations/versioncounting/VersionCountingBlockStore.h"
#include "blockstore/implementations/testfake/FakeBlockStore.h"
#include "../../testutils/BlockStoreTest.h"
#include <gtest/gtest.h>
using ::testing::Test;
using blockstore::BlockStore;
using blockstore::versioncounting::VersionCountingBlockStore;
using blockstore::versioncounting::KnownBlockVersions;
using blockstore::testfake::FakeBlockStore;
using cpputils::Data;
using cpputils::DataFixture;
using cpputils::make_unique_ref;
using cpputils::unique_ref;
class VersionCountingBlockStoreTestFixture: public BlockStoreTestFixture {
public:
unique_ref<BlockStore> createBlockStore() override {
return make_unique_ref<VersionCountingBlockStore>(make_unique_ref<FakeBlockStore>(), KnownBlockVersions());
}
};
INSTANTIATE_TYPED_TEST_CASE_P(VersionCounting, BlockStoreTest, VersionCountingBlockStoreTestFixture);

View File

@ -0,0 +1,94 @@
#include <gtest/gtest.h>
#include "blockstore/implementations/versioncounting/VersionCountingBlockStore.h"
#include "blockstore/implementations/testfake/FakeBlockStore.h"
#include "blockstore/utils/BlockStoreUtils.h"
#include <cpp-utils/data/DataFixture.h>
using ::testing::Test;
using cpputils::DataFixture;
using cpputils::Data;
using cpputils::unique_ref;
using cpputils::make_unique_ref;
using blockstore::testfake::FakeBlockStore;
using namespace blockstore::versioncounting;
class VersionCountingBlockStoreTest: public Test {
public:
static constexpr unsigned int BLOCKSIZE = 1024;
VersionCountingBlockStoreTest():
baseBlockStore(new FakeBlockStore),
blockStore(make_unique_ref<VersionCountingBlockStore>(std::move(cpputils::nullcheck(std::unique_ptr<FakeBlockStore>(baseBlockStore)).value()), KnownBlockVersions())),
data(DataFixture::generate(BLOCKSIZE)) {
}
FakeBlockStore *baseBlockStore;
unique_ref<VersionCountingBlockStore> blockStore;
Data data;
blockstore::Key CreateBlockReturnKey() {
return CreateBlockReturnKey(data);
}
blockstore::Key CreateBlockReturnKey(const Data &initData) {
return blockStore->create(initData)->key();
}
Data loadBaseBlock(const blockstore::Key &key) {
auto block = baseBlockStore->load(key).value();
Data result(block->size());
std::memcpy(result.data(), block->data(), data.size());
return result;
}
void modifyBlock(const blockstore::Key &key) {
auto block = blockStore->load(key).value();
uint64_t data = 5;
block->write(&data, 0, sizeof(data));
}
void rollbackBaseBlock(const blockstore::Key &key, const Data &data) {
auto block = baseBlockStore->load(key).value();
block->resize(data.size());
block->write(data.data(), 0, data.size());
}
private:
DISALLOW_COPY_AND_ASSIGN(VersionCountingBlockStoreTest);
};
TEST_F(VersionCountingBlockStoreTest, DoesntAllowRollbacks) {
auto key = CreateBlockReturnKey();
Data oldBaseBlock = loadBaseBlock(key);
modifyBlock(key);
rollbackBaseBlock(key, oldBaseBlock);
EXPECT_EQ(boost::none, blockStore->load(key));
}
TEST_F(VersionCountingBlockStoreTest, PhysicalBlockSize_zerophysical) {
EXPECT_EQ(0u, blockStore->blockSizeFromPhysicalBlockSize(0));
}
TEST_F(VersionCountingBlockStoreTest, PhysicalBlockSize_zerovirtual) {
auto key = CreateBlockReturnKey(Data(0));
auto base = baseBlockStore->load(key).value();
EXPECT_EQ(0u, blockStore->blockSizeFromPhysicalBlockSize(base->size()));
}
TEST_F(VersionCountingBlockStoreTest, PhysicalBlockSize_negativeboundaries) {
// This tests that a potential if/else in blockSizeFromPhysicalBlockSize that catches negative values has the
// correct boundary set. We test the highest value that is negative and the smallest value that is positive.
auto physicalSizeForVirtualSizeZero = baseBlockStore->load(CreateBlockReturnKey(Data(0))).value()->size();
if (physicalSizeForVirtualSizeZero > 0) {
EXPECT_EQ(0u, blockStore->blockSizeFromPhysicalBlockSize(physicalSizeForVirtualSizeZero - 1));
}
EXPECT_EQ(0u, blockStore->blockSizeFromPhysicalBlockSize(physicalSizeForVirtualSizeZero));
EXPECT_EQ(1u, blockStore->blockSizeFromPhysicalBlockSize(physicalSizeForVirtualSizeZero + 1));
}
TEST_F(VersionCountingBlockStoreTest, PhysicalBlockSize_positive) {
auto key = CreateBlockReturnKey(Data(10*1024));
auto base = baseBlockStore->load(key).value();
EXPECT_EQ(10*1024u, blockStore->blockSizeFromPhysicalBlockSize(base->size()));
}