foundationdb/fdbrpc/AsyncFileEncrypted.actor.cpp

294 lines
11 KiB
C++

/*
* AsyncFileEncrypted.actor.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "fdbrpc/AsyncFileEncrypted.h"
#include "flow/StreamCipher.h"
#include "flow/UnitTest.h"
#include "flow/xxhash.h"
#include "flow/actorcompiler.h" // must be last include
class AsyncFileEncryptedImpl {
public:
// Determine the initialization for the first block of a file based on a hash of
// the filename.
static auto getFirstBlockIV(const std::string& filename) {
StreamCipher::IV iv;
auto salt = basename(filename);
auto pos = salt.find('.');
salt = salt.substr(0, pos);
auto hash = XXH3_128bits(salt.c_str(), salt.size());
auto pHigh = reinterpret_cast<unsigned char*>(&hash.high64);
auto pLow = reinterpret_cast<unsigned char*>(&hash.low64);
std::copy(pHigh, pHigh + 8, &iv[0]);
std::copy(pLow, pLow + 4, &iv[8]);
uint32_t blockZero = 0;
auto pBlock = reinterpret_cast<unsigned char*>(&blockZero);
std::copy(pBlock, pBlock + 4, &iv[12]);
return iv;
}
// Read a single block of size ENCRYPTION_BLOCK_SIZE bytes, and decrypt.
ACTOR static Future<Standalone<StringRef>> readBlock(AsyncFileEncrypted* self, uint32_t block) {
state Arena arena;
state unsigned char* encrypted = new (arena) unsigned char[FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE];
int bytes = wait(uncancellable(holdWhile(arena,
self->file->read(encrypted,
FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE,
FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE * block))));
StreamCipherKey const* cipherKey = StreamCipherKey::getGlobalCipherKey();
DecryptionStreamCipher decryptor(cipherKey, self->getIV(block));
auto decrypted = decryptor.decrypt(encrypted, bytes, arena);
return Standalone<StringRef>(decrypted, arena);
}
ACTOR static Future<int> read(Reference<AsyncFileEncrypted> self, void* data, int length, int64_t offset) {
state const uint32_t firstBlock = offset / FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE;
state const uint32_t lastBlock = (offset + length - 1) / FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE;
state uint32_t block;
state unsigned char* output = reinterpret_cast<unsigned char*>(data);
state int bytesRead = 0;
ASSERT(self->mode == AsyncFileEncrypted::Mode::READ_ONLY);
for (block = firstBlock; block <= lastBlock; ++block) {
state Standalone<StringRef> plaintext;
auto cachedBlock = self->readBuffers.get(block);
if (cachedBlock.present()) {
plaintext = cachedBlock.get();
} else {
wait(store(plaintext, readBlock(self.getPtr(), block)));
self->readBuffers.insert(block, plaintext);
}
auto start = (block == firstBlock) ? plaintext.begin() + (offset % FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE)
: plaintext.begin();
auto end = (block == lastBlock)
? plaintext.begin() + ((offset + length) % FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE)
: plaintext.end();
if ((offset + length) % FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE == 0) {
end = plaintext.end();
}
// The block could be short if it includes or is after the end of the file.
end = std::min(end, plaintext.end());
// If the start position is at or after the end of the block, the read is complete.
if (start == end || start >= plaintext.end()) {
break;
}
std::copy(start, end, output);
output += (end - start);
bytesRead += (end - start);
}
return bytesRead;
}
ACTOR static Future<Void> write(Reference<AsyncFileEncrypted> self, void const* data, int length, int64_t offset) {
ASSERT(self->mode == AsyncFileEncrypted::Mode::APPEND_ONLY);
// All writes must append to the end of the file:
ASSERT_EQ(offset, self->currentBlock * FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE + self->offsetInBlock);
state unsigned char const* input = reinterpret_cast<unsigned char const*>(data);
while (length > 0) {
const auto chunkSize = std::min(length, FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE - self->offsetInBlock);
Arena arena;
auto encrypted = self->encryptor->encrypt(input, chunkSize, arena);
std::copy(encrypted.begin(), encrypted.end(), &self->writeBuffer[self->offsetInBlock]);
offset += encrypted.size();
self->offsetInBlock += chunkSize;
length -= chunkSize;
input += chunkSize;
if (self->offsetInBlock == FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE) {
wait(self->writeLastBlockToFile());
self->offsetInBlock = 0;
ASSERT_LT(self->currentBlock, std::numeric_limits<uint32_t>::max());
++self->currentBlock;
self->encryptor = std::make_unique<EncryptionStreamCipher>(StreamCipherKey::getGlobalCipherKey(),
self->getIV(self->currentBlock));
}
}
return Void();
}
ACTOR static Future<Void> sync(Reference<AsyncFileEncrypted> self) {
ASSERT(self->mode == AsyncFileEncrypted::Mode::APPEND_ONLY);
wait(self->writeLastBlockToFile());
wait(self->file->sync());
return Void();
}
ACTOR static Future<Void> zeroRange(AsyncFileEncrypted* self, int64_t offset, int64_t length) {
ASSERT(self->mode == AsyncFileEncrypted::Mode::APPEND_ONLY);
// TODO: Could optimize this
Arena arena;
auto zeroes = new (arena) unsigned char[length];
memset(zeroes, 0, length);
wait(uncancellable(holdWhile(arena, self->write(zeroes, length, offset))));
return Void();
}
};
AsyncFileEncrypted::AsyncFileEncrypted(Reference<IAsyncFile> file, Mode mode)
: file(file), mode(mode), readBuffers(FLOW_KNOBS->MAX_DECRYPTED_BLOCKS), currentBlock(0) {
firstBlockIV = AsyncFileEncryptedImpl::getFirstBlockIV(file->getFilename());
if (mode == Mode::APPEND_ONLY) {
encryptor =
std::make_unique<EncryptionStreamCipher>(StreamCipherKey::getGlobalCipherKey(), getIV(currentBlock));
writeBuffer = std::vector<unsigned char>(FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE, 0);
}
}
void AsyncFileEncrypted::addref() {
ReferenceCounted<AsyncFileEncrypted>::addref();
}
void AsyncFileEncrypted::delref() {
ReferenceCounted<AsyncFileEncrypted>::delref();
}
Future<int> AsyncFileEncrypted::read(void* data, int length, int64_t offset) {
return AsyncFileEncryptedImpl::read(Reference<AsyncFileEncrypted>::addRef(this), data, length, offset);
}
Future<Void> AsyncFileEncrypted::write(void const* data, int length, int64_t offset) {
return AsyncFileEncryptedImpl::write(Reference<AsyncFileEncrypted>::addRef(this), data, length, offset);
}
Future<Void> AsyncFileEncrypted::zeroRange(int64_t offset, int64_t length) {
return AsyncFileEncryptedImpl::zeroRange(this, offset, length);
}
Future<Void> AsyncFileEncrypted::truncate(int64_t size) {
ASSERT(mode == Mode::APPEND_ONLY);
return file->truncate(size);
}
Future<Void> AsyncFileEncrypted::sync() {
ASSERT(mode == Mode::APPEND_ONLY);
return AsyncFileEncryptedImpl::sync(Reference<AsyncFileEncrypted>::addRef(this));
}
Future<Void> AsyncFileEncrypted::flush() {
ASSERT(mode == Mode::APPEND_ONLY);
return Void();
}
Future<int64_t> AsyncFileEncrypted::size() const {
ASSERT(mode == Mode::READ_ONLY);
return file->size();
}
std::string AsyncFileEncrypted::getFilename() const {
return file->getFilename();
}
Future<Void> AsyncFileEncrypted::readZeroCopy(void** data, int* length, int64_t offset) {
throw io_error();
return Void();
}
void AsyncFileEncrypted::releaseZeroCopy(void* data, int length, int64_t offset) {
throw io_error();
}
int64_t AsyncFileEncrypted::debugFD() const {
return file->debugFD();
}
StreamCipher::IV AsyncFileEncrypted::getIV(uint32_t block) const {
auto iv = firstBlockIV;
auto pBlock = reinterpret_cast<unsigned char*>(&block);
std::copy(pBlock, pBlock + 4, &iv[12]);
return iv;
}
Future<Void> AsyncFileEncrypted::writeLastBlockToFile() {
// The source buffer for the write is owned by *this so this must be kept alive by reference count until the write
// is finished.
return uncancellable(
holdWhile(Reference<AsyncFileEncrypted>::addRef(this),
file->write(&writeBuffer[0], offsetInBlock, currentBlock * FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE)));
}
size_t AsyncFileEncrypted::RandomCache::evict() {
ASSERT_EQ(vec.size(), maxSize);
auto index = deterministicRandom()->randomInt(0, maxSize);
hashMap.erase(vec[index]);
return index;
}
AsyncFileEncrypted::RandomCache::RandomCache(size_t maxSize) : maxSize(maxSize) {
vec.reserve(maxSize);
}
void AsyncFileEncrypted::RandomCache::insert(uint32_t block, const Standalone<StringRef>& value) {
auto [_, found] = hashMap.insert({ block, value });
if (found) {
return;
} else if (vec.size() < maxSize) {
vec.push_back(block);
} else {
auto index = evict();
vec[index] = block;
}
}
Optional<Standalone<StringRef>> AsyncFileEncrypted::RandomCache::get(uint32_t block) const {
auto it = hashMap.find(block);
if (it == hashMap.end()) {
return {};
} else {
return it->second;
}
}
// This test writes random data into an encrypted file in random increments,
// then reads this data back from the file in random increments, then confirms that
// the bytes read match the bytes written.
TEST_CASE("fdbrpc/AsyncFileEncrypted") {
state const int bytes = FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE * deterministicRandom()->randomInt(0, 1000);
state std::vector<unsigned char> writeBuffer(bytes, 0);
deterministicRandom()->randomBytes(&writeBuffer.front(), bytes);
state std::vector<unsigned char> readBuffer(bytes, 0);
ASSERT(g_network->isSimulated());
StreamCipherKey::initializeGlobalRandomTestKey();
int flags = IAsyncFile::OPEN_READWRITE | IAsyncFile::OPEN_CREATE | IAsyncFile::OPEN_ATOMIC_WRITE_AND_CREATE |
IAsyncFile::OPEN_UNBUFFERED | IAsyncFile::OPEN_ENCRYPTED | IAsyncFile::OPEN_UNCACHED |
IAsyncFile::OPEN_NO_AIO;
state Reference<IAsyncFile> file =
wait(IAsyncFileSystem::filesystem()->open(joinPath(params.getDataDir(), "test-encrypted-file"), flags, 0600));
state int bytesWritten = 0;
state int chunkSize;
while (bytesWritten < bytes) {
chunkSize = std::min(deterministicRandom()->randomInt(0, 100), bytes - bytesWritten);
wait(file->write(&writeBuffer[bytesWritten], chunkSize, bytesWritten));
bytesWritten += chunkSize;
}
wait(file->sync());
state int bytesRead = 0;
while (bytesRead < bytes) {
chunkSize = std::min(deterministicRandom()->randomInt(0, 100), bytes - bytesRead);
int bytesReadInChunk = wait(file->read(&readBuffer[bytesRead], chunkSize, bytesRead));
ASSERT_EQ(bytesReadInChunk, chunkSize);
bytesRead += bytesReadInChunk;
}
ASSERT(writeBuffer == readBuffer);
return Void();
}