Added AsyncFileEncrypted

This commit is contained in:
sfc-gh-tclinkenbeard 2021-01-22 21:47:36 -08:00
parent 1615977695
commit 88bc157bd0
11 changed files with 383 additions and 29 deletions

View File

@ -0,0 +1,254 @@
/*
* AsyncFileEncrypted.actor.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2018 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:
static auto getFirstBlockIV(const std::string& filename) {
StreamCipher::IV iv;
auto hash = XXH3_128bits(filename.c_str(), filename.size());
auto high = reinterpret_cast<unsigned char*>(&hash.high64);
auto low = reinterpret_cast<unsigned char*>(&hash.low64);
std::copy(high, high + 8, &iv[0]);
std::copy(low, low + 6, &iv[8]);
iv[14] = iv[15] = 0; // last 16 bits identify block
return iv;
}
ACTOR static Future<Standalone<StringRef>> readBlock(AsyncFileEncrypted* self, uint16_t block) {
state Arena arena;
state unsigned char* encrypted = new (arena) unsigned char[FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE];
int bytes = wait(
self->file->read(encrypted, FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE, FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE * block));
DecryptionStreamCipher decryptor(AsyncFileEncrypted::getKey(), self->getIV(block));
auto decrypted = decryptor.decrypt(encrypted, bytes, arena);
return Standalone<StringRef>(decrypted, arena);
}
ACTOR static Future<int> read(AsyncFileEncrypted* self, void* data, int length, int offset) {
state const uint16_t firstBlock = offset / FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE;
state const uint16_t lastBlock = (offset + length - 1) / FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE;
state uint16_t block;
state unsigned char* output = reinterpret_cast<unsigned char*>(data);
state int bytesRead = 0;
for (block = firstBlock; block <= lastBlock; ++block) {
state StringRef plaintext;
auto it = self->readBuffers.find(block);
if (it != self->readBuffers.end()) {
plaintext = it->second;
} else {
Standalone<StringRef> _plaintext = wait(readBlock(self, block));
ASSERT(_plaintext.size() == FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE);
if (self->readBuffers.size() == FLOW_KNOBS->MAX_DECRYPTED_BLOCKS) {
// TODO: Improve eviction policy
self->readBuffers.erase(self->readBuffers.begin());
}
self->readBuffers[block] = _plaintext;
plaintext = _plaintext;
}
ASSERT(plaintext.size() == FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE);
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();
}
std::copy(start, end, output);
output += (end - start);
bytesRead += (end - start);
}
return bytesRead;
}
ACTOR static Future<Void> write(AsyncFileEncrypted* self, void const* data, int length, int64_t offset) {
ASSERT(self->canWrite);
ASSERT(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(self->currentBlock < std::numeric_limits<uint16_t>::max());
++self->currentBlock;
self->encryptor = std::make_unique<EncryptionStreamCipher>(AsyncFileEncrypted::getKey(), self->getIV(self->currentBlock));
}
}
return Void();
}
ACTOR static Future<Void> sync(AsyncFileEncrypted* self) {
ASSERT(self->canWrite);
wait(self->writeLastBlockToFile());
wait(self->file->sync());
return Void();
}
ACTOR static Future<Void> initializeKey(Reference<IAsyncFile> keyFile, int64_t offset) {
ASSERT(!AsyncFileEncrypted::key.present());
AsyncFileEncrypted::key = StreamCipher::Key{};
state int keySize = AsyncFileEncrypted::key.get().size();
if (g_network->isSimulated()) {
generateRandomData(AsyncFileEncrypted::key.get().data(), keySize);
return Void();
} else {
int bytesRead = wait(keyFile->read(AsyncFileEncrypted::key.get().data(), keySize, offset));
ASSERT(bytesRead == keySize);
return Void();
}
}
ACTOR static Future<Void> zeroRange(AsyncFileEncrypted* self, int64_t offset, int64_t length) {
// TODO: Could optimize this
Arena arena;
auto zeroes = new (arena) unsigned char[length];
memset(zeroes, 0, length);
wait(self->write(zeroes, length, offset));
return Void();
}
};
AsyncFileEncrypted::AsyncFileEncrypted(Reference<IAsyncFile> file, bool canWrite)
: file(file), canWrite(canWrite), currentBlock(0) {
firstBlockIV = AsyncFileEncryptedImpl::getFirstBlockIV(file->getFilename());
if (canWrite) {
encryptor = std::make_unique<EncryptionStreamCipher>(AsyncFileEncrypted::getKey(), 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(this, data, length, offset);
}
Future<Void> AsyncFileEncrypted::write(void const* data, int length, int64_t offset) {
return AsyncFileEncryptedImpl::write(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(false); // TODO: Not yet implemented
return Void();
}
Future<Void> AsyncFileEncrypted::sync() {
return AsyncFileEncryptedImpl::sync(this);
}
Future<Void> AsyncFileEncrypted::flush() {
return Void();
}
Future<int64_t> AsyncFileEncrypted::size() const {
return currentBlock * FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE + offsetInBlock;
}
std::string AsyncFileEncrypted::getFilename() const {
return file->getFilename();
}
Future<Void> AsyncFileEncrypted::readZeroCopy(void** data, int* length, int64_t offset) {
ASSERT(false); // Not implemented
return Void();
}
void AsyncFileEncrypted::releaseZeroCopy(void* data, int length, int64_t offset) {
ASSERT(false); // Not implemented
}
int64_t AsyncFileEncrypted::debugFD() const {
return 0;
}
StreamCipher::IV AsyncFileEncrypted::getIV(uint16_t block) const {
auto iv = firstBlockIV;
iv[14] = block / 256;
iv[15] = block % 256;
return iv;
}
Future<Void> AsyncFileEncrypted::writeLastBlockToFile() {
return file->write(&writeBuffer[0], offsetInBlock, currentBlock * FLOW_KNOBS->ENCRYPTION_BLOCK_SIZE);
}
Optional<StreamCipher::Key> AsyncFileEncrypted::key;
StreamCipher::Key AsyncFileEncrypted::getKey() {
return key.get();
}
Future<Void> AsyncFileEncrypted::initializeKey(const Reference<IAsyncFile>& keyFile, int64_t offset) {
return AsyncFileEncryptedImpl::initializeKey(keyFile, offset);
}
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);
generateRandomData(&writeBuffer.front(), bytes);
state std::vector<unsigned char> readBuffer(bytes, 0);
ASSERT(g_network->isSimulated());
wait(AsyncFileEncrypted::initializeKey(Reference<IAsyncFile>{}));
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("/tmp/test", flags, 0600));
state int bytesWritten = 0;
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;
state int chunkSize;
while (bytesRead < bytes) {
chunkSize = std::min(deterministicRandom()->randomInt(0, 100), bytes - bytesRead);
int bytesReadInChunk = wait(file->read(&readBuffer[bytesRead], chunkSize, bytesRead));
ASSERT(bytesReadInChunk == chunkSize);
bytesRead += bytesReadInChunk;
}
ASSERT(writeBuffer == readBuffer);
return Void();
}

View File

@ -0,0 +1,72 @@
/*
* AsyncFileEncrypted.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2018 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.
*/
#ifndef __FDBRPC_ASYNC_FILE_ENCRYPTED_H__
#define __FDBRPC_ASYNC_FILE_ENCRYPTED_H__
#include "fdbrpc/IAsyncFile.h"
#include "flow/FastRef.h"
#include "flow/flow.h"
#include "flow/IRandom.h"
#include "flow/StreamCipher.h"
#include <array>
/*
* Append-only file encrypted using AES-128-GCM.
* */
class AsyncFileEncrypted : public IAsyncFile, public ReferenceCounted<AsyncFileEncrypted> {
Reference<IAsyncFile> file;
StreamCipher::IV firstBlockIV;
StreamCipher::IV getIV(uint16_t block) const;
bool canWrite;
Future<Void> writeLastBlockToFile();
friend class AsyncFileEncryptedImpl;
static Optional<StreamCipher::Key> key;
static StreamCipher::Key getKey();
// Reading:
std::map<uint16_t, Standalone<StringRef>> readBuffers;
// Writing (append only):
std::unique_ptr<EncryptionStreamCipher> encryptor;
uint16_t currentBlock{ 0 };
int offsetInBlock{ 0 };
std::vector<unsigned char> writeBuffer;
public:
AsyncFileEncrypted(Reference<IAsyncFile>, bool canWrite);
void addref() override;
void delref() override;
Future<int> read(void* data, int length, int64_t offset) override;
Future<Void> write(void const* data, int length, int64_t offset) override;
Future<Void> zeroRange(int64_t offset, int64_t length) override;
Future<Void> truncate(int64_t size) override;
Future<Void> sync() override;
Future<Void> flush() override;
Future<int64_t> size() const override;
std::string getFilename() const override;
Future<Void> readZeroCopy(void** data, int* length, int64_t offset) override;
void releaseZeroCopy(void* data, int length, int64_t offset) override;
int64_t debugFD() const override;
static Future<Void> initializeKey(const Reference<IAsyncFile>& keyFile, int64_t offset = 0);
};
#endif

View File

@ -8,6 +8,8 @@ set(FDBRPC_SRCS
AsyncFileCached.actor.cpp
AsyncFileNonDurable.actor.cpp
AsyncFileWriteChecker.cpp
AsyncFileEncrypted.actor.cpp
AsyncFileEncrypted.h
FailureMonitor.actor.cpp
FlowTransport.actor.cpp
genericactors.actor.h

View File

@ -35,21 +35,25 @@ public:
virtual ~IAsyncFile();
// Pass these to g_network->open to get an IAsyncFile
enum {
// Implementation relies on the low bits being the same as the SQLite flags (this is validated by a static_assert there)
OPEN_READONLY = 0x1,
OPEN_READWRITE = 0x2,
OPEN_CREATE = 0x4,
OPEN_EXCLUSIVE = 0x10,
// Implementation relies on the low bits being the same as the SQLite flags (this is validated by a
// static_assert there)
OPEN_READONLY = 0x1,
OPEN_READWRITE = 0x2,
OPEN_CREATE = 0x4,
OPEN_EXCLUSIVE = 0x10,
// Further flag values are arbitrary bits
OPEN_UNBUFFERED = 0x10000,
OPEN_UNCACHED = 0x20000,
OPEN_LOCK = 0x40000,
OPEN_ATOMIC_WRITE_AND_CREATE = 0x80000, // A temporary file is opened, and on the first call to sync() it is atomically renamed to the given filename
OPEN_LARGE_PAGES = 0x100000,
OPEN_NO_AIO = 0x200000, // Don't use AsyncFileKAIO or similar implementations that rely on filesystem support for AIO
OPEN_CACHED_READ_ONLY = 0x400000 // AsyncFileCached opens files read/write even if you specify read only
};
OPEN_UNBUFFERED = 0x10000,
OPEN_UNCACHED = 0x20000,
OPEN_LOCK = 0x40000,
OPEN_ATOMIC_WRITE_AND_CREATE = 0x80000, // A temporary file is opened, and on the first call to sync() it is
// atomically renamed to the given filename
OPEN_LARGE_PAGES = 0x100000,
OPEN_NO_AIO =
0x200000, // Don't use AsyncFileKAIO or similar implementations that rely on filesystem support for AIO
OPEN_CACHED_READ_ONLY = 0x400000, // AsyncFileCached opens files read/write even if you specify read only
OPEN_ENCRYPTED = 0x800000 // File is encrypted using AES-128-GCM (must be either read-only or write-only)
};
virtual void addref() = 0;
virtual void delref() = 0;

View File

@ -33,6 +33,7 @@
#include "fdbrpc/AsyncFileCached.actor.h"
#include "fdbrpc/AsyncFileEIO.actor.h"
#include "fdbrpc/AsyncFileEncrypted.h"
#include "fdbrpc/AsyncFileWinASIO.actor.h"
#include "fdbrpc/AsyncFileKAIO.actor.h"
#include "flow/AsioReactor.h"
@ -69,7 +70,11 @@ Future<Reference<class IAsyncFile>> Net2FileSystem::open(const std::string& file
#endif
f = Net2AsyncFile::open(filename, flags, mode, static_cast<boost::asio::io_service*> ((void*) g_network->global(INetwork::enASIOService)));
if(FLOW_KNOBS->PAGE_WRITE_CHECKSUM_HISTORY > 0)
f = map(f, [=](Reference<IAsyncFile> r) { return Reference<IAsyncFile>(new AsyncFileWriteChecker(r)); });
f = map(f, [](Reference<IAsyncFile> r) { return Reference<IAsyncFile>(new AsyncFileWriteChecker(r)); });
if (flags & IAsyncFile::OPEN_ENCRYPTED)
f = map(f, [flags](Reference<IAsyncFile> r) {
return Reference<IAsyncFile>(new AsyncFileEncrypted(r, flags & IAsyncFile::OPEN_READWRITE));
});
return f;
}

View File

@ -31,6 +31,7 @@
#include "flow/Util.h"
#include "fdbrpc/IAsyncFile.h"
#include "fdbrpc/AsyncFileCached.actor.h"
#include "fdbrpc/AsyncFileEncrypted.h"
#include "fdbrpc/AsyncFileNonDurable.actor.h"
#include "flow/crc32c.h"
#include "fdbrpc/TraceFileIO.h"
@ -2060,7 +2061,11 @@ Future<Reference<class IAsyncFile>> Sim2FileSystem::open(const std::string& file
}
Future<Reference<IAsyncFile>> f = AsyncFileDetachable::open( machineCache[actualFilename] );
if(FLOW_KNOBS->PAGE_WRITE_CHECKSUM_HISTORY > 0)
f = map(f, [=](Reference<IAsyncFile> r) { return Reference<IAsyncFile>(new AsyncFileWriteChecker(r)); });
f = map(f, [](Reference<IAsyncFile> r) { return Reference<IAsyncFile>(new AsyncFileWriteChecker(r)); });
if (flags & IAsyncFile::OPEN_ENCRYPTED)
f = map(f, [flags](Reference<IAsyncFile> r) {
return Reference<IAsyncFile>(new AsyncFileEncrypted(r, flags & IAsyncFile::OPEN_READWRITE));
});
return f;
}
else

View File

@ -132,8 +132,7 @@ target_link_libraries(flow PRIVATE ${FLOW_LIBS})
if(USE_VALGRIND)
target_link_libraries(flow PUBLIC Valgrind)
endif()
# TODO(atn34) Re-enable TLS for OPEN_FOR_IDE build once #2201 is resolved
if(NOT WITH_TLS OR OPEN_FOR_IDE)
if(NOT WITH_TLS)
target_compile_definitions(flow PUBLIC TLS_DISABLED)
else()
target_link_libraries(flow PUBLIC OpenSSL::SSL)

View File

@ -121,6 +121,10 @@ void FlowKnobs::initialize(bool randomize, bool isSimulated) {
init( EIO_MAX_PARALLELISM, 4 );
init( EIO_USE_ODIRECT, 0 );
//AsyncFileEncrypted
init( ENCRYPTION_BLOCK_SIZE, 4096 );
init( MAX_DECRYPTED_BLOCKS, 10 );
//AsyncFileKAIO
init( MAX_OUTSTANDING, 64 );
init( MIN_SUBMIT, 10 );

View File

@ -138,6 +138,10 @@ public:
int EIO_MAX_PARALLELISM;
int EIO_USE_ODIRECT;
// AsyncFileEncrypted
int ENCRYPTION_BLOCK_SIZE;
int MAX_DECRYPTED_BLOCKS;
//AsyncFileKAIO
int MAX_OUTSTANDING;
int MIN_SUBMIT;

View File

@ -24,8 +24,8 @@
EncryptionStreamCipher::EncryptionStreamCipher(const StreamCipher::Key& key, const StreamCipher::IV& iv)
: ctx(EVP_CIPHER_CTX_new()) {
EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr);
EVP_EncryptInit_ex(ctx, nullptr, nullptr, key, iv);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, iv.size(), nullptr);
EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data());
}
EncryptionStreamCipher::~EncryptionStreamCipher() {
@ -46,12 +46,12 @@ StringRef EncryptionStreamCipher::finish(Arena& arena) {
return StringRef(ciphertext, bytes);
}
DecryptionStreamCipher::DecryptionStreamCipher(unsigned char const* key, unsigned char const* iv)
DecryptionStreamCipher::DecryptionStreamCipher(const StreamCipher::Key& key, const StreamCipher::IV& iv)
: ctx(EVP_CIPHER_CTX_new()) {
EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr);
EVP_DecryptInit_ex(ctx, nullptr, nullptr, key, iv);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, iv.size(), nullptr);
EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data());
}
DecryptionStreamCipher::~DecryptionStreamCipher() {
@ -77,10 +77,10 @@ StringRef DecryptionStreamCipher::finish(Arena& arena) {
void forceLinkStreamCipherTests() {}
TEST_CASE("flow/StreamCipher") {
std::array<unsigned char, 16> key;
StreamCipher::Key key;
generateRandomData(key.data(), key.size());
std::array<unsigned char, 12> iv;
StreamCipher::IV iv;
generateRandomData(iv.data(), iv.size());
Arena arena;
@ -93,7 +93,7 @@ TEST_CASE("flow/StreamCipher") {
.detail("PlaintextSize", plaintext.size())
.detail("AESBlockSize", AES_BLOCK_SIZE);
{
EncryptionStreamCipher encryptor(key.data(), iv.data());
EncryptionStreamCipher encryptor(key, iv);
int index = 0;
int encryptedOffset = 0;
while (index < plaintext.size()) {
@ -113,7 +113,7 @@ TEST_CASE("flow/StreamCipher") {
}
{
DecryptionStreamCipher decryptor(key.data(), iv.data());
DecryptionStreamCipher decryptor(key, iv);
int index = 0;
int decryptedOffset = 0;
while (index < plaintext.size()) {

View File

@ -30,10 +30,15 @@
#include <string>
#include <vector>
namespace StreamCipher {
using Key = std::array<unsigned char, 16>;
using IV = std::array<unsigned char, 16>;
}; // namespace StreamCipher
class EncryptionStreamCipher final : NonCopyable, public ReferenceCounted<EncryptionStreamCipher> {
EVP_CIPHER_CTX* ctx;
public:
EncryptionStreamCipher(unsigned char const* key, unsigned char const* salt);
EncryptionStreamCipher(const StreamCipher::Key& key, const StreamCipher::IV& iv);
~EncryptionStreamCipher();
StringRef encrypt(unsigned char const* plaintext, int len, Arena&);
StringRef finish(Arena&);
@ -42,7 +47,7 @@ public:
class DecryptionStreamCipher final : NonCopyable, public ReferenceCounted<DecryptionStreamCipher> {
EVP_CIPHER_CTX* ctx;
public:
DecryptionStreamCipher(unsigned char const* key, unsigned char const* salt);
DecryptionStreamCipher(const StreamCipher::Key& key, const StreamCipher::IV& iv);
~DecryptionStreamCipher();
StringRef decrypt(unsigned char const* ciphertext, int len, Arena&);
StringRef finish(Arena&);