233 lines
9.5 KiB
C++
233 lines
9.5 KiB
C++
#include <blockstore/interface/BlockStore2.h>
|
|
#include "IntegrityBlockStore2.h"
|
|
#include "KnownBlockVersions.h"
|
|
#include <cpp-utils/data/SerializationHelper.h>
|
|
#include <cpp-utils/process/SignalCatcher.h>
|
|
|
|
using cpputils::Data;
|
|
using cpputils::unique_ref;
|
|
using cpputils::serialize;
|
|
using cpputils::deserialize;
|
|
using cpputils::SignalCatcher;
|
|
using std::string;
|
|
using boost::optional;
|
|
using boost::none;
|
|
using namespace cpputils::logging;
|
|
|
|
namespace blockstore {
|
|
namespace integrity {
|
|
|
|
#ifndef CRYFS_NO_COMPATIBILITY
|
|
constexpr uint16_t IntegrityBlockStore2::FORMAT_VERSION_HEADER_OLD;
|
|
#endif
|
|
constexpr uint16_t IntegrityBlockStore2::FORMAT_VERSION_HEADER;
|
|
constexpr uint64_t IntegrityBlockStore2::VERSION_ZERO;
|
|
constexpr unsigned int IntegrityBlockStore2::ID_HEADER_OFFSET;
|
|
constexpr unsigned int IntegrityBlockStore2::CLIENTID_HEADER_OFFSET;
|
|
constexpr unsigned int IntegrityBlockStore2::VERSION_HEADER_OFFSET;
|
|
constexpr unsigned int IntegrityBlockStore2::HEADER_LENGTH;
|
|
|
|
Data IntegrityBlockStore2::_prependHeaderToData(const BlockId& blockId, uint32_t myClientId, uint64_t version, const Data &data) {
|
|
static_assert(HEADER_LENGTH == sizeof(FORMAT_VERSION_HEADER) + BlockId::BINARY_LENGTH + sizeof(myClientId) + sizeof(version), "Wrong header length");
|
|
Data result(data.size() + HEADER_LENGTH);
|
|
serialize<uint16_t>(result.dataOffset(0), FORMAT_VERSION_HEADER);
|
|
blockId.ToBinary(result.dataOffset(ID_HEADER_OFFSET));
|
|
serialize<uint32_t>(result.dataOffset(CLIENTID_HEADER_OFFSET), myClientId);
|
|
serialize<uint64_t>(result.dataOffset(VERSION_HEADER_OFFSET), version);
|
|
std::memcpy(result.dataOffset(HEADER_LENGTH), data.data(), data.size());
|
|
return result;
|
|
}
|
|
|
|
bool IntegrityBlockStore2::_checkHeader(const BlockId &blockId, const Data &data) const {
|
|
_checkFormatHeader(data);
|
|
return _checkIdHeader(blockId, data) && _checkVersionHeader(blockId, data);
|
|
}
|
|
|
|
void IntegrityBlockStore2::_checkFormatHeader(const Data &data) const {
|
|
if (FORMAT_VERSION_HEADER != _readFormatHeader(data)) {
|
|
throw std::runtime_error("The versioned block has the wrong format. Was it created with a newer version of CryFS?");
|
|
}
|
|
}
|
|
|
|
bool IntegrityBlockStore2::_checkVersionHeader(const BlockId &blockId, const Data &data) const {
|
|
uint32_t clientId = _readClientId(data);
|
|
uint64_t version = _readVersion(data);
|
|
|
|
if(!_knownBlockVersions.checkAndUpdateVersion(clientId, blockId, version)) {
|
|
integrityViolationDetected("The block version number is too low. Did an attacker try to roll back the block or to re-introduce a deleted block?");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IntegrityBlockStore2::_checkIdHeader(const BlockId &expectedBlockId, const Data &data) const {
|
|
// The obvious reason for this is to prevent adversaries from renaming blocks, but storing the block id in this way also
|
|
// makes the authenticated cipher more robust, see https://libsodium.gitbook.io/doc/secret-key_cryptography/aead#robustness
|
|
BlockId actualBlockId = _readBlockId(data);
|
|
if (expectedBlockId != actualBlockId) {
|
|
integrityViolationDetected("The block id is wrong. Did an attacker try to rename some blocks?");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint16_t IntegrityBlockStore2::_readFormatHeader(const Data &data) {
|
|
return deserialize<uint16_t>(data.data());
|
|
}
|
|
|
|
uint32_t IntegrityBlockStore2::_readClientId(const Data &data) {
|
|
return deserialize<uint32_t>(data.dataOffset(CLIENTID_HEADER_OFFSET));
|
|
}
|
|
|
|
BlockId IntegrityBlockStore2::_readBlockId(const Data &data) {
|
|
return BlockId::FromBinary(data.dataOffset(ID_HEADER_OFFSET));
|
|
}
|
|
|
|
uint64_t IntegrityBlockStore2::_readVersion(const Data &data) {
|
|
return deserialize<uint64_t>(data.dataOffset(VERSION_HEADER_OFFSET));
|
|
}
|
|
|
|
Data IntegrityBlockStore2::_removeHeader(const Data &data) {
|
|
return data.copyAndRemovePrefix(HEADER_LENGTH);
|
|
}
|
|
|
|
void IntegrityBlockStore2::integrityViolationDetected(const string &reason) const {
|
|
if (_allowIntegrityViolations) {
|
|
LOG(WARN, "Integrity violation (but integrity checks are disabled): {}", reason);
|
|
return;
|
|
}
|
|
_knownBlockVersions.setIntegrityViolationOnPreviousRun(true);
|
|
_onIntegrityViolation();
|
|
}
|
|
|
|
IntegrityBlockStore2::IntegrityBlockStore2(unique_ref<BlockStore2> baseBlockStore, const boost::filesystem::path &integrityFilePath, uint32_t myClientId, bool allowIntegrityViolations, bool missingBlockIsIntegrityViolation, std::function<void ()> onIntegrityViolation)
|
|
: _baseBlockStore(std::move(baseBlockStore)), _knownBlockVersions(integrityFilePath, myClientId), _allowIntegrityViolations(allowIntegrityViolations), _missingBlockIsIntegrityViolation(missingBlockIsIntegrityViolation), _onIntegrityViolation(std::move(onIntegrityViolation)) {
|
|
if (_knownBlockVersions.integrityViolationOnPreviousRun()) {
|
|
throw IntegrityViolationOnPreviousRun(_knownBlockVersions.path());
|
|
}
|
|
}
|
|
|
|
bool IntegrityBlockStore2::tryCreate(const BlockId &blockId, const Data &data) {
|
|
uint64_t version = _knownBlockVersions.incrementVersion(blockId);
|
|
Data dataWithHeader = _prependHeaderToData(blockId, _knownBlockVersions.myClientId(), version, data);
|
|
return _baseBlockStore->tryCreate(blockId, dataWithHeader);
|
|
}
|
|
|
|
bool IntegrityBlockStore2::remove(const BlockId &blockId) {
|
|
_knownBlockVersions.markBlockAsDeleted(blockId);
|
|
return _baseBlockStore->remove(blockId);
|
|
}
|
|
|
|
optional<Data> IntegrityBlockStore2::load(const BlockId &blockId) const {
|
|
auto loaded = _baseBlockStore->load(blockId);
|
|
if (none == loaded) {
|
|
if (_missingBlockIsIntegrityViolation && _knownBlockVersions.blockShouldExist(blockId)) {
|
|
integrityViolationDetected("A block that should exist wasn't found. Did an attacker delete it?");
|
|
}
|
|
return optional<Data>(none);
|
|
}
|
|
#ifndef CRYFS_NO_COMPATIBILITY
|
|
if (FORMAT_VERSION_HEADER_OLD == _readFormatHeader(*loaded)) {
|
|
Data migrated = _migrateBlock(blockId, *loaded);
|
|
if (!_checkHeader(blockId, migrated) && !_allowIntegrityViolations) {
|
|
return optional<Data>(none);
|
|
}
|
|
Data content = _removeHeader(migrated);
|
|
const_cast<IntegrityBlockStore2*>(this)->store(blockId, content);
|
|
return optional<Data>(_removeHeader(migrated));
|
|
}
|
|
#endif
|
|
if (!_checkHeader(blockId, *loaded) && !_allowIntegrityViolations) {
|
|
return optional<Data>(none);
|
|
}
|
|
return optional<Data>(_removeHeader(*loaded));
|
|
}
|
|
|
|
#ifndef CRYFS_NO_COMPATIBILITY
|
|
Data IntegrityBlockStore2::_migrateBlock(const BlockId &blockId, const Data &data) {
|
|
Data migrated(data.size() + BlockId::BINARY_LENGTH);
|
|
serialize<uint16_t>(migrated.dataOffset(0), FORMAT_VERSION_HEADER);
|
|
blockId.ToBinary(migrated.dataOffset(ID_HEADER_OFFSET));
|
|
std::memcpy(migrated.dataOffset(ID_HEADER_OFFSET + BlockId::BINARY_LENGTH), data.dataOffset(sizeof(FORMAT_VERSION_HEADER)), data.size() - sizeof(FORMAT_VERSION_HEADER));
|
|
ASSERT(migrated.size() == sizeof(FORMAT_VERSION_HEADER) + BlockId::BINARY_LENGTH + (data.size() - sizeof(FORMAT_VERSION_HEADER)), "Wrong offset computation");
|
|
return migrated;
|
|
}
|
|
#endif
|
|
|
|
void IntegrityBlockStore2::store(const BlockId &blockId, const Data &data) {
|
|
uint64_t version = _knownBlockVersions.incrementVersion(blockId);
|
|
Data dataWithHeader = _prependHeaderToData(blockId, _knownBlockVersions.myClientId(), version, data);
|
|
return _baseBlockStore->store(blockId, dataWithHeader);
|
|
}
|
|
|
|
uint64_t IntegrityBlockStore2::numBlocks() const {
|
|
return _baseBlockStore->numBlocks();
|
|
}
|
|
|
|
uint64_t IntegrityBlockStore2::estimateNumFreeBytes() const {
|
|
return _baseBlockStore->estimateNumFreeBytes();
|
|
}
|
|
|
|
uint64_t IntegrityBlockStore2::blockSizeFromPhysicalBlockSize(uint64_t blockSize) const {
|
|
uint64_t baseBlockSize = _baseBlockStore->blockSizeFromPhysicalBlockSize(blockSize);
|
|
if (baseBlockSize <= HEADER_LENGTH) {
|
|
return 0;
|
|
}
|
|
return baseBlockSize - HEADER_LENGTH;
|
|
}
|
|
|
|
void IntegrityBlockStore2::forEachBlock(std::function<void (const BlockId &)> callback) const {
|
|
if (!_missingBlockIsIntegrityViolation) {
|
|
return _baseBlockStore->forEachBlock(std::move(callback));
|
|
}
|
|
|
|
std::unordered_set<blockstore::BlockId> existingBlocks = _knownBlockVersions.existingBlocks();
|
|
_baseBlockStore->forEachBlock([&existingBlocks, callback] (const BlockId &blockId) {
|
|
callback(blockId);
|
|
|
|
auto found = existingBlocks.find(blockId);
|
|
if (found != existingBlocks.end()) {
|
|
existingBlocks.erase(found);
|
|
}
|
|
});
|
|
if (!existingBlocks.empty()) {
|
|
integrityViolationDetected("A block that should have existed wasn't found.");
|
|
}
|
|
}
|
|
|
|
#ifndef CRYFS_NO_COMPATIBILITY
|
|
void IntegrityBlockStore2::migrateFromBlockstoreWithoutVersionNumbers(BlockStore2 *baseBlockStore, const boost::filesystem::path &integrityFilePath, uint32_t myClientId) {
|
|
SignalCatcher signalCatcher;
|
|
|
|
KnownBlockVersions knownBlockVersions(integrityFilePath, myClientId);
|
|
baseBlockStore->forEachBlock([&] (const BlockId &blockId) {
|
|
if (signalCatcher.signal_occurred()) {
|
|
throw std::runtime_error("Caught signal");
|
|
}
|
|
migrateBlockFromBlockstoreWithoutVersionNumbers(baseBlockStore, blockId, &knownBlockVersions);
|
|
});
|
|
}
|
|
|
|
void IntegrityBlockStore2::migrateBlockFromBlockstoreWithoutVersionNumbers(blockstore::BlockStore2* baseBlockStore, const blockstore::BlockId& blockId, KnownBlockVersions *knownBlockVersions) {
|
|
auto data_ = baseBlockStore->load(blockId);
|
|
if (data_ == boost::none) {
|
|
LOG(WARN, "Block not found, but was returned from forEachBlock before");
|
|
return;
|
|
}
|
|
if (0 != _readFormatHeader(*data_)) {
|
|
// already migrated
|
|
return;
|
|
}
|
|
|
|
uint64_t version = knownBlockVersions->incrementVersion(blockId);
|
|
cpputils::Data data = std::move(*data_);
|
|
cpputils::Data dataWithHeader = _prependHeaderToData(blockId, knownBlockVersions->myClientId(), version, data);
|
|
baseBlockStore->store(blockId, dataWithHeader);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
}
|