Add basic AES-XTS client-side encryption support

This commit is contained in:
Vitaliy Filippov
2026-04-22 01:41:28 +03:00
parent ced3cc3de9
commit e64d8c8c61
20 changed files with 856 additions and 21 deletions
+36
View File
@@ -2124,3 +2124,39 @@ jobs:
echo ""
done
test_write_encrypted:
runs-on: ubuntu-latest
needs: build
container: ${{env.TEST_IMAGE}}:${{github.sha}}
steps:
- name: Run test
id: test
timeout-minutes: 3
run: /root/vitastor/tests/test_write_encrypted.sh
- name: Print logs
if: always() && steps.test.outcome == 'failure'
run: |
for i in /root/vitastor/testdata/*.log /root/vitastor/testdata/*.txt; do
echo "-------- $i --------"
cat $i
echo ""
done
test_write_encrypted_ec:
runs-on: ubuntu-latest
needs: build
container: ${{env.TEST_IMAGE}}:${{github.sha}}
steps:
- name: Run test
id: test
timeout-minutes: 3
run: SCHEME=ec /root/vitastor/tests/test_write_encrypted.sh
- name: Print logs
if: always() && steps.test.outcome == 'failure'
run: |
for i in /root/vitastor/testdata/*.log /root/vitastor/testdata/*.txt; do
echo "-------- $i --------"
cat $i
echo ""
done
+1 -1
View File
@@ -74,7 +74,7 @@ if (RDMACM_LIBRARIES)
add_definitions(-DWITH_RDMACM)
endif (RDMACM_LIBRARIES)
find_package(OpenSSL)
find_package(OpenSSL REQUIRED)
if (OPENSSL_FOUND)
add_definitions(-DWITH_OPENSSL)
endif (OPENSSL_FOUND)
+4 -3
View File
@@ -13,10 +13,10 @@ if (RDMACM_LIBRARIES)
endif (RDMACM_LIBRARIES)
add_library(vitastor_common STATIC
../util/epoll_manager.cpp etcd_state_client.cpp messenger.cpp ../util/addr_util.cpp
msgr_stop.cpp msgr_op.cpp msgr_send.cpp msgr_receive.cpp ../util/ringloop.cpp ../../json11/json11.cpp
msgr_encrypt.cpp msgr_stop.cpp msgr_op.cpp msgr_send.cpp msgr_receive.cpp ../util/ringloop.cpp ../../json11/json11.cpp
http_client.cpp osd_ops.cpp pg_states.cpp ../util/timerfd_manager.cpp ../util/str_util.cpp ../util/json_util.cpp ${MSGR_RDMA} ${MSGR_RDMACM}
)
target_link_libraries(vitastor_common pthread)
target_link_libraries(vitastor_common pthread ${OPENSSL_LIBRARIES})
target_compile_options(vitastor_common PUBLIC -fPIC)
# libvitastor_client.so
@@ -99,9 +99,10 @@ endif (${WITH_QEMU})
add_executable(test_cluster_client
EXCLUDE_FROM_ALL
../test/test_cluster_client.cpp
pg_states.cpp osd_ops.cpp cluster_client.cpp cluster_client_list.cpp cluster_client_wb.cpp msgr_op.cpp ../test/mock/messenger.cpp msgr_stop.cpp
pg_states.cpp osd_ops.cpp cluster_client.cpp cluster_client_list.cpp cluster_client_wb.cpp msgr_op.cpp ../test/mock/messenger.cpp msgr_stop.cpp msgr_encrypt.cpp
etcd_state_client.cpp ../util/timerfd_manager.cpp ../util/addr_util.cpp ../util/str_util.cpp ../util/json_util.cpp ../../json11/json11.cpp
)
target_link_libraries(test_cluster_client ${OPENSSL_LIBRARIES})
target_compile_definitions(test_cluster_client PUBLIC -D__MOCK__)
target_include_directories(test_cluster_client BEFORE PUBLIC ${CMAKE_SOURCE_DIR}/src/test/mock)
add_dependencies(build_tests test_cluster_client)
+15 -2
View File
@@ -958,9 +958,22 @@ bool cluster_client_t::check_rw(cluster_op_t *op)
{
op->flags |= OP_IMMEDIATE_COMMIT;
}
auto ino_it = st_cli.inode_config.find(op->inode);
if (ino_it != st_cli.inode_config.end() && ino_it->second.enc)
{
// FIXME: Rework client API by adding open/close and cache inode information in the "FD"
op->enc = ino_it->second.enc;
if (!op->enc->bitmap_granularity)
{
op->enc->bitmap_granularity = pool_it->second.bitmap_granularity;
}
}
else
{
op->enc.reset();
}
if ((op->opcode == OSD_OP_WRITE || op->opcode == OSD_OP_DELETE) && !(op->flags & OSD_OP_IGNORE_READONLY))
{
auto ino_it = st_cli.inode_config.find(op->inode);
if (ino_it != st_cli.inode_config.end() && ino_it->second.readonly)
{
op->retval = -EROFS;
@@ -972,7 +985,6 @@ bool cluster_client_t::check_rw(cluster_op_t *op)
op->deoptimise_snapshot = false;
if (enable_writeback && (op->opcode == OSD_OP_READ || op->opcode == OSD_OP_READ_BITMAP || op->opcode == OSD_OP_READ_CHAIN_BITMAP))
{
auto ino_it = st_cli.inode_config.find(op->inode);
if (ino_it != st_cli.inode_config.end())
{
int chain_size = 0;
@@ -1435,6 +1447,7 @@ int cluster_client_t::try_send(cluster_op_t *op, int i, std::function<void(osd_o
? (uint8_t*)op->part_bitmaps + pg_bitmap_size*i : NULL),
.bitmap_len = (unsigned)(op->opcode == OSD_OP_READ || op->opcode == OSD_OP_READ_BITMAP || op->opcode == OSD_OP_READ_CHAIN_BITMAP
? pg_bitmap_size : 0),
.enc = op->enc,
.callback = cb ? cb : [this, part](osd_op_t *op_part)
{
handle_op_part(part);
+1
View File
@@ -71,6 +71,7 @@ protected:
cluster_op_t *prev = NULL, *next = NULL;
int prev_wait = 0;
uint64_t flush_id = 0;
std::shared_ptr<inode_enc_t> enc;
friend class cluster_client_t;
friend class writeback_cache_t;
};
+11
View File
@@ -1307,6 +1307,16 @@ void etcd_state_client_t::parse_state(const etcd_kv_t & kv)
else
parent_inode_num |= parent_pool_id << (64-POOL_ID_BITS);
}
std::shared_ptr<inode_enc_t> enc;
if (!value["enc_key"].string_value().empty())
{
std::vector<uint8_t> k = hexdecode(value["enc_key"].string_value());
if (k.size() == 512/8)
{
enc = std::make_shared<inode_enc_t>();
enc->key = std::move(k);
}
}
insert_inode_config((inode_config_t){
.num = inode_num,
.name = value["name"].string_value(),
@@ -1314,6 +1324,7 @@ void etcd_state_client_t::parse_state(const etcd_kv_t & kv)
.parent_id = parent_inode_num,
.readonly = value["readonly"].bool_value(),
.deleted = value["deleted"].bool_value(),
.enc = enc,
.meta = value["meta"],
.mod_revision = kv.mod_revision,
});
+10
View File
@@ -4,6 +4,7 @@
#pragma once
#include <set>
#include <memory>
#include "json11/json11.hpp"
#include "object_id.h"
@@ -75,6 +76,14 @@ struct pool_config_t
void *reshard_state = NULL;
};
struct inode_enc_t
{
int refs = 0;
std::vector<uint8_t> key;
// FIXME It may also contain snapshot chain and key information
uint32_t bitmap_granularity = 0;
};
struct inode_config_t
{
uint64_t num = 0;
@@ -83,6 +92,7 @@ struct inode_config_t
inode_t parent_id = 0;
bool readonly = false;
bool deleted = false;
std::shared_ptr<inode_enc_t> enc;
// Arbitrary metadata
json11::Json meta;
// Change revision of the metadata in etcd
+11
View File
@@ -295,6 +295,14 @@ osd_messenger_t::~osd_messenger_t()
rdmacm_evch = NULL;
}
#endif
for (auto encrypt_ctx: encrypt_ctx_pool)
{
destroy_aes_xts_encrypt(encrypt_ctx);
}
for (auto decrypt_ctx: decrypt_ctx_pool)
{
destroy_aes_xts_decrypt(decrypt_ctx);
}
}
void osd_messenger_t::parse_config(const json11::Json & config)
@@ -329,6 +337,9 @@ void osd_messenger_t::parse_config(const json11::Json & config)
if (!this->rdma_max_msg || this->rdma_max_msg > 128*1024*1024)
this->rdma_max_msg = 129*1024;
#endif
this->max_aes_xts_pool_size = config["max_aes_xts_pool_size"].uint64_value();
if (!this->max_aes_xts_pool_size)
this->max_aes_xts_pool_size = 256;
if (!osd_num)
this->iothread_count = (uint32_t)config["client_iothread_count"].uint64_value();
else
+19
View File
@@ -46,6 +46,11 @@ struct msgr_rdma_connection_t;
struct msgr_rdma_context_t;
#endif
struct op_aes_xts_encrypt_t;
struct op_aes_xts_decrypt_t;
void destroy_aes_xts_encrypt(op_aes_xts_encrypt_t *encrypt_ctx);
void destroy_aes_xts_decrypt(op_aes_xts_decrypt_t *decrypt_ctx);
struct osd_client_t
{
uint64_t client_id = 0;
@@ -80,6 +85,9 @@ struct osd_client_t
uint64_t read_op_id = 1;
bool check_sequencing = false;
bool enable_pg_locks = false;
op_aes_xts_decrypt_t *decrypt_ctx = NULL;
size_t read_op_inline_decrypt_pos = 0;
size_t read_op_inline_decrypt_in = 0;
// Incoming operations
std::vector<osd_op_t*> received_ops;
@@ -101,6 +109,7 @@ struct osd_client_t
size_t send_list_size = 0;
std::deque<osd_op_t*> send_free_ops;
std::vector<osd_op_t*> zc_free_list;
op_aes_xts_encrypt_t *encrypt_ctx = NULL;
~osd_client_t();
void cancel_ops();
@@ -192,6 +201,7 @@ protected:
bool use_sync_send_recv = false;
int min_zerocopy_send_size = DEFAULT_MIN_ZEROCOPY_SEND_SIZE;
int iothread_count = 0;
int max_aes_xts_pool_size = 256;
#ifdef WITH_RDMA
bool use_rdma = true;
@@ -215,6 +225,9 @@ protected:
// We don't use ringloop->set_immediate here because we may have no ringloop in client :)
std::deque<osd_op_t*> set_immediate_ops;
std::vector<op_aes_xts_encrypt_t*> encrypt_ctx_pool;
std::vector<op_aes_xts_decrypt_t*> decrypt_ctx_pool;
public:
timerfd_manager_t *tfd = NULL;
ring_loop_t *ringloop = NULL;
@@ -294,6 +307,12 @@ protected:
void handle_finished_op(osd_client_t *cl);
void handle_immediate_ops();
bool op_encrypted_copy_data_to(osd_client_t* cl, uint8_t *buf, size_t len, size_t from, size_t & done);
bool op_decrypted_copy_data_from(osd_client_t* cl, uint8_t *buf, size_t len, size_t from, size_t & done);
void op_decrypt_start(osd_client_t* cl);
void op_decrypt_inline(osd_client_t* cl);
void op_decrypt_free(osd_client_t* cl);
#ifdef WITH_RDMA
void try_send_rdma(osd_client_t *cl);
int try_send_rdma_copy(osd_client_t *cl, uint8_t *dst, int dst_len);
+457
View File
@@ -0,0 +1,457 @@
// Copyright (c) Vitaliy Filippov, 2026+
// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details)
#include <assert.h>
#include "etcd_state_client.h"
#include "messenger.h"
#include "msgr_encrypt.h"
op_aes_xts_encrypt_t::op_aes_xts_encrypt_t()
{
#ifdef WITH_OPENSSL
if (!(ctx = EVP_CIPHER_CTX_new()))
{
ERR_print_errors_fp(stderr);
abort();
}
EVP_CIPHER_CTX_set_padding(ctx, 0);
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_xts(), NULL, NULL, NULL) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
#else
fprintf(stderr, "Error: Vitastor is built without encryption support\n");
abort();
#endif
}
op_aes_xts_encrypt_t::~op_aes_xts_encrypt_t()
{
assert(!encrypted);
#ifdef WITH_OPENSSL
EVP_CIPHER_CTX_free(ctx);
#endif
if (tmp)
free(tmp);
}
void op_aes_xts_encrypt_t::start(const uint8_t *key, uint64_t start_offset, size_t block_size)
{
assert(!encrypted);
this->start_offset = start_offset;
this->key = key;
this->block_size = block_size;
this->offset = 0;
this->encrypted = false;
this->tmp_pos = 0;
if (tmp && tmp_size != block_size)
{
free(tmp);
tmp = NULL;
tmp_size = 0;
}
#ifdef WITH_OPENSSL
if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, NULL) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
#endif
}
void op_aes_xts_encrypt_t::encrypt_block(uint8_t *in, uint8_t *out)
{
#ifdef WITH_OPENSSL
uint8_t iv[16] = { 0 };
*((uint64_t*)iv) = start_offset + offset - offset%block_size;
if (EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, iv) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
int actual_out = 0;
if (EVP_EncryptUpdate(ctx, out, &actual_out, in, block_size) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
assert(actual_out == block_size);
#endif
}
// FIXME: Copy-paste
void op_aes_xts_encrypt_t::update(uint8_t *in, size_t max_in, uint8_t *out, size_t max_out, size_t & done_in, size_t & done_out)
{
// Fucking AES-XTS implementations (all of them) don't have streaming support,
// crafting IV to resume encryption is slow, so we have to accumulate a full block
// and encrypt it at once :-(
// And then we have to support consuming it in parts because it's simpler for the
// higher layers.
if (encrypted)
{
// Copy accumulated and encrypted output
assert(tmp);
if (max_out > block_size - tmp_pos)
max_out = block_size - tmp_pos;
memcpy(out, tmp + tmp_pos, max_out);
done_out += max_out;
tmp_pos += max_out;
if (tmp_pos >= block_size)
encrypted = false;
}
else if (max_in < block_size - offset%block_size)
{
// Just accumulate input
if (!tmp)
{
tmp = (uint8_t*)malloc_or_die(block_size);
tmp_size = block_size;
}
memcpy(tmp + offset%block_size, in, max_in);
done_in += max_in;
offset += max_in;
}
else if (max_out < block_size)
{
// Accumulate and encrypt input in <tmp>, then copy part of it to <out>
if (!tmp)
{
tmp = (uint8_t*)malloc_or_die(block_size);
tmp_size = block_size;
}
max_in = block_size - offset%block_size;
memcpy(tmp + offset%block_size, in, max_in);
encrypt_block(tmp, tmp);
encrypted = true;
memcpy(out, tmp, max_out);
tmp_pos = max_out;
done_in += max_in;
offset += max_in;
done_out += max_out;
}
else if (!(offset%block_size))
{
// Full block - simplest case
encrypt_block(in, out);
done_in += block_size;
offset += block_size;
done_out += block_size;
}
else
{
// Accumulate input and encrypt directly to <output>
assert(tmp);
max_in = block_size - offset%block_size;
memcpy(tmp + offset%block_size, in, max_in);
encrypt_block(tmp, out);
done_in += max_in;
offset += max_in;
done_out += block_size;
}
}
void destroy_aes_xts_encrypt(op_aes_xts_encrypt_t *encrypt_ctx)
{
delete encrypt_ctx;
}
op_aes_xts_decrypt_t::op_aes_xts_decrypt_t()
{
#ifdef WITH_OPENSSL
if (!(ctx = EVP_CIPHER_CTX_new()))
{
ERR_print_errors_fp(stderr);
abort();
}
EVP_CIPHER_CTX_set_padding(ctx, 0);
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_xts(), NULL, NULL, NULL) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
#else
fprintf(stderr, "Error: Vitastor is built without encryption support\n");
abort();
#endif
}
op_aes_xts_decrypt_t::~op_aes_xts_decrypt_t()
{
assert(!decrypted);
#ifdef WITH_OPENSSL
EVP_CIPHER_CTX_free(ctx);
#endif
if (tmp)
free(tmp);
}
void op_aes_xts_decrypt_t::start(const uint8_t *key, uint64_t start_offset, size_t block_size)
{
assert(!decrypted);
this->start_offset = start_offset;
this->key = key;
this->block_size = block_size;
this->offset = 0;
this->tmp_pos = 0;
if (tmp && tmp_size != block_size)
{
free(tmp);
tmp = NULL;
tmp_size = 0;
}
#ifdef WITH_OPENSSL
if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, NULL) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
#endif
}
void op_aes_xts_decrypt_t::decrypt_block(uint8_t *in, uint8_t *out)
{
#ifdef WITH_OPENSSL
uint8_t iv[16] = { 0 };
*((uint64_t*)iv) = start_offset + offset - offset%block_size;
if (EVP_DecryptInit_ex(ctx, NULL, NULL, NULL, iv) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
int actual_out = 0;
if (EVP_DecryptUpdate(ctx, out, &actual_out, in, block_size) != 1)
{
ERR_print_errors_fp(stderr);
abort();
}
assert(actual_out == block_size);
#endif
}
void op_aes_xts_decrypt_t::update(uint8_t *in, size_t max_in, uint8_t *out, size_t max_out, size_t & done_in, size_t & done_out)
{
// Fucking AES-XTS implementations (all of them) don't have streaming support,
// crafting IV to resume decryption is slow, so we have to accumulate a full block
// and decrypt it at once :-(
// And then we have to support consuming it in parts because clients sometimes need
// fragmented output.
if (decrypted)
{
// Copy accumulated and decrypted output
assert(tmp);
if (max_out > block_size - tmp_pos)
max_out = block_size - tmp_pos;
memcpy(out, tmp + tmp_pos, max_out);
done_out += max_out;
tmp_pos += max_out;
if (tmp_pos >= block_size)
decrypted = false;
}
else if (max_in < block_size - offset%block_size)
{
// Just accumulate input
if (!tmp)
{
tmp = (uint8_t*)malloc_or_die(block_size);
tmp_size = block_size;
}
memcpy(tmp + offset%block_size, in, max_in);
done_in += max_in;
offset += max_in;
}
else if (max_out < block_size)
{
// Accumulate and decrypt input in <tmp>, then copy part of it to <out>
if (!tmp)
{
tmp = (uint8_t*)malloc_or_die(block_size);
tmp_size = block_size;
}
max_in = block_size - offset%block_size;
memcpy(tmp + offset%block_size, in, max_in);
decrypt_block(tmp, tmp);
decrypted = true;
memcpy(out, tmp, max_out);
tmp_pos = max_out;
done_in += max_in;
offset += max_in;
done_out += max_out;
}
else if (!(offset%block_size))
{
// Full block - simplest case
decrypt_block(in, out);
done_in += block_size;
offset += block_size;
done_out += block_size;
}
else
{
// Accumulate input and decrypt directly to <output>
assert(tmp);
max_in = block_size - offset%block_size;
memcpy(tmp + offset%block_size, in, max_in);
decrypt_block(tmp, out);
done_in += max_in;
offset += max_in;
done_out += block_size;
}
}
void destroy_aes_xts_decrypt(op_aes_xts_decrypt_t *decrypt_ctx)
{
delete decrypt_ctx;
}
bool osd_messenger_t::op_encrypted_copy_data_to(osd_client_t* cl, uint8_t *enc_buf, size_t enc_len, size_t from, size_t & done)
{
auto op = cl->write_op;
auto & op_pos = cl->write_op_pos;
assert(op->req.hdr.opcode == OSD_OP_WRITE);
if (!from)
{
if (!cl->encrypt_ctx)
{
if (encrypt_ctx_pool.size())
{
cl->encrypt_ctx = encrypt_ctx_pool.back();
encrypt_ctx_pool.pop_back();
}
else
cl->encrypt_ctx = new op_aes_xts_encrypt_t();
}
assert(op->enc->key.size() == 512/8);
cl->encrypt_ctx->start(op->enc->key.data(), op->req.rw.offset, op->enc->bitmap_granularity);
}
for (int i = 0; i < op->iov.count; i++)
{
uint8_t *plain = (uint8_t*)op->iov.buf[i].iov_base;
size_t plain_len = op->iov.buf[i].iov_len;
while (from < plain_len || cl->encrypt_ctx->has_buffered())
{
if (done >= enc_len)
return false;
size_t done_in = 0;
size_t done_out = 0;
cl->encrypt_ctx->update(plain+from, plain_len-from, enc_buf+done, enc_len-done, done_in, done_out);
done += done_out;
op_pos += done_in;
from += done_in;
}
from -= plain_len;
}
if (cl->encrypt_ctx)
{
if (encrypt_ctx_pool.size() > max_aes_xts_pool_size)
delete cl->encrypt_ctx;
else
encrypt_ctx_pool.push_back(cl->encrypt_ctx);
cl->encrypt_ctx = NULL;
}
return true;
}
bool osd_messenger_t::op_decrypted_copy_data_from(osd_client_t* cl, uint8_t *enc_buf, size_t enc_len, size_t from, size_t & done)
{
op_decrypt_start(cl);
auto op = cl->read_op;
assert(op->req.hdr.opcode == OSD_OP_READ);
for (int i = 0; i < op->iov.count; i++)
{
uint8_t *plain = (uint8_t*)op->iov.buf[i].iov_base;
size_t plain_len = op->iov.buf[i].iov_len;
while (from < plain_len)
{
if (done >= enc_len)
return false;
size_t done_in = 0;
size_t done_out = 0;
cl->decrypt_ctx->update(enc_buf+done, enc_len-done, plain+from, plain_len-from, done_in, done_out);
done += done_in;
cl->read_op_pos += done_out;
cl->read_op_inline_decrypt_in += done_in;
from += done_out;
if (!done_out)
return false;
}
from -= plain_len;
}
op_decrypt_free(cl);
return true;
}
void osd_messenger_t::op_decrypt_start(osd_client_t* cl)
{
if (!cl->decrypt_ctx)
{
if (decrypt_ctx_pool.size())
{
cl->decrypt_ctx = decrypt_ctx_pool.back();
decrypt_ctx_pool.pop_back();
}
else
cl->decrypt_ctx = new op_aes_xts_decrypt_t();
assert(cl->read_op->enc->key.size() == 512/8);
cl->decrypt_ctx->start(cl->read_op->enc->key.data(), cl->read_op->req.rw.offset, cl->read_op->enc->bitmap_granularity);
}
}
void osd_messenger_t::op_decrypt_inline(osd_client_t* cl)
{
op_decrypt_start(cl);
osd_op_t *op = cl->read_op;
size_t from_in = cl->read_op_inline_decrypt_in;
int i = 0;
while (i < op->iov.count && from_in >= op->iov.buf[i].iov_len)
{
from_in -= op->iov.buf[i].iov_len;
i++;
}
size_t from_out = cl->read_op_inline_decrypt_pos - OSD_PACKET_SIZE - op->reply.rw.bitmap_len;
int j = 0;
while (j < op->iov.count && from_out >= op->iov.buf[j].iov_len)
{
from_out -= op->iov.buf[j].iov_len;
j++;
}
while (i < op->iov.count && j < op->iov.count)
{
uint8_t *in = (uint8_t*)op->iov.buf[i].iov_base + from_in;
size_t in_len = op->iov.buf[i].iov_len - from_in;
uint8_t *out = (uint8_t*)op->iov.buf[j].iov_base + from_out;
size_t out_len = op->iov.buf[j].iov_len - from_out;
size_t done_in = 0;
size_t done_out = 0;
cl->decrypt_ctx->update(in, in_len, out, out_len, done_in, done_out);
if (done_in >= in_len)
{
i++;
from_in = 0;
}
else
from_in += done_in;
if (done_out >= out_len)
{
j++;
from_out = 0;
}
else
from_out += done_out;
}
assert(j >= op->iov.count);
op_decrypt_free(cl);
}
void osd_messenger_t::op_decrypt_free(osd_client_t* cl)
{
if (cl->decrypt_ctx)
{
if (decrypt_ctx_pool.size() > max_aes_xts_pool_size)
delete cl->decrypt_ctx;
else
decrypt_ctx_pool.push_back(cl->decrypt_ctx);
cl->decrypt_ctx = NULL;
}
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (c) Vitaliy Filippov, 2026+
// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details)
#include <stdint.h>
// WITH_OPENSSL is left to possibly support other crypto libraries
#ifdef WITH_OPENSSL
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#endif
class op_aes_xts_encrypt_t
{
#ifdef WITH_OPENSSL
EVP_CIPHER_CTX *ctx = NULL;
#endif
uint64_t start_offset = 0;
const uint8_t *key = NULL;
size_t offset = 0;
size_t block_size = 0;
uint8_t *tmp = NULL;
size_t tmp_size = 0;
size_t tmp_pos = 0;
bool encrypted = false;
void encrypt_block(uint8_t *in, uint8_t *out);
public:
op_aes_xts_encrypt_t();
~op_aes_xts_encrypt_t();
inline bool has_buffered() { return encrypted; };
void start(const uint8_t *key, uint64_t start_offset, size_t block_size);
void update(uint8_t *in, size_t max_in, uint8_t *out, size_t max_out, size_t & done_in, size_t & done_out);
};
void destroy_aes_xts_encrypt(op_aes_xts_encrypt_t *encrypt_ctx);
class op_aes_xts_decrypt_t
{
#ifdef WITH_OPENSSL
EVP_CIPHER_CTX *ctx = NULL;
#endif
uint64_t start_offset = 0;
const uint8_t *key = NULL;
size_t offset = 0;
size_t block_size = 0;
uint8_t *tmp = NULL;
size_t tmp_size = 0;
size_t tmp_pos = 0;
bool decrypted = false;
void decrypt_block(uint8_t *in, uint8_t *out);
public:
op_aes_xts_decrypt_t();
~op_aes_xts_decrypt_t();
inline bool has_buffered() { return decrypted; };
void start(const uint8_t *key, uint64_t start_offset, size_t block_size);
void update(uint8_t *in, size_t max_in, uint8_t *out, size_t max_out, size_t & done_in, size_t & done_out);
};
void destroy_aes_xts_decrypt(op_aes_xts_decrypt_t *decrypt_ctx);
+4
View File
@@ -23,6 +23,10 @@ osd_op_t::~osd_op_t()
// So we don't reuse it, but free it every time
free(buf);
}
if (enc_buf)
{
free(enc_buf);
}
}
bool osd_op_t::is_recovery_related()
+6
View File
@@ -3,6 +3,8 @@
#pragma once
#include <memory>
#include <sys/uio.h>
#include <stdint.h>
#include <stdio.h>
@@ -152,6 +154,8 @@ struct blockstore_op_t;
struct osd_primary_op_data_t;
struct inode_enc_t;
struct __attribute__((visibility("default"))) osd_op_t
{
timespec tv_begin = { 0 }, tv_end = { 0 };
@@ -167,6 +171,8 @@ struct __attribute__((visibility("default"))) osd_op_t
unsigned bmp_data = 0;
void *bitmap_buf = NULL;
void *rmw_buf = NULL;
std::shared_ptr<inode_enc_t> enc;
uint8_t *enc_buf = NULL;
osd_primary_op_data_t* op_data = NULL;
std::function<void(osd_op_t*)> callback;
-3
View File
@@ -733,9 +733,6 @@ void osd_messenger_t::handle_rdma_events(msgr_rdma_context_t *rdma_context)
}
if (!is_send)
{
// Reset OSD ping state - client is obviously alive
cl->ping_time_remaining = 0;
cl->idle_time_remaining = osd_idle_timeout;
rc->cur_recv--;
if (!handle_read_buffer(cl, rc->recv_buffers[rc->next_recv_buf], wc[i].byte_len))
{
+24 -2
View File
@@ -194,6 +194,8 @@ bool osd_messenger_t::handle_read_buffer(osd_client_t *cl, uint8_t *curbuf, size
cl->read_op->op_type = OSD_OP_IN;
cl->read_op_pos = 0;
cl->read_op_size = 0;
cl->read_op_inline_decrypt_in = 0;
cl->read_op_inline_decrypt_pos = (size_t)-1;
}
if (cl->read_op_pos < OSD_PACKET_SIZE)
{
@@ -452,9 +454,17 @@ size_t osd_messenger_t::op_copy_from(osd_client_t *cl, uint8_t *src, size_t src_
}
if (op->reply.hdr.retval > 0)
{
for (int i = 0; i < op->iov.count; i++)
if (!op_read_buf((uint8_t*)op->iov.buf[i].iov_base, op->iov.buf[i].iov_len))
if (op->enc)
{
if (!op_decrypted_copy_data_from(cl, src, src_len, from, done))
return done;
}
else
{
for (int i = 0; i < op->iov.count; i++)
if (!op_read_buf((uint8_t*)op->iov.buf[i].iov_base, op->iov.buf[i].iov_len))
return done;
}
}
}
else if (op->reply.hdr.opcode == OSD_OP_SEC_LIST && op->reply.hdr.retval > 0)
@@ -555,6 +565,12 @@ size_t osd_messenger_t::op_get_read_buffers(osd_client_t *cl, std::vector<iovec>
}
if (op->reply.hdr.retval > 0)
{
if (op->enc)
{
cl->read_op_inline_decrypt_pos = cl->read_op_pos;
cl->read_op_pos = cl->read_op_inline_decrypt_in + OSD_PACKET_SIZE + op->reply.rw.bitmap_len;
from = cl->read_op_inline_decrypt_in;
}
for (int i = 0; i < op->iov.count; i++)
if (!op_read_buf((uint8_t*)op->iov.buf[i].iov_base, op->iov.buf[i].iov_len))
return done;
@@ -590,6 +606,12 @@ void osd_messenger_t::handle_finished_op(osd_client_t *cl)
}
else
{
// Inline decryption
if (cl->read_op_inline_decrypt_pos != (size_t)-1)
{
op_decrypt_inline(cl);
cl->read_op_inline_decrypt_pos = (size_t)-1;
}
// Measure subop (outbound op) latency
timespec tv_end;
clock_gettime(CLOCK_REALTIME, &tv_end);
+32 -10
View File
@@ -5,12 +5,6 @@
#include <limits.h>
#include <sys/epoll.h>
#ifdef WITH_OPENSSL
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#endif
#include "messenger.h"
void osd_messenger_t::outbox_push(osd_op_t *cur_op)
@@ -376,10 +370,20 @@ size_t osd_messenger_t::op_copy_to(osd_client_t *cl, uint8_t *dst, size_t dst_le
// Operation data
if (op_has_data(cl->write_op))
{
for (int i = 0; i < cl->write_op->iov.count; i++)
if (cl->write_op->enc)
{
if (!op_write_buf((uint8_t*)cl->write_op->iov.buf[i].iov_base, cl->write_op->iov.buf[i].iov_len))
if (!op_encrypted_copy_data_to(cl, dst, dst_len, from, done))
{
return done;
}
}
else
{
for (int i = 0; i < cl->write_op->iov.count; i++)
{
if (!op_write_buf((uint8_t*)cl->write_op->iov.buf[i].iov_base, cl->write_op->iov.buf[i].iov_len))
return done;
}
}
}
cl->write_op = NULL;
@@ -411,10 +415,28 @@ void osd_messenger_t::op_get_write_buffers(osd_client_t *cl, std::vector<iovec>
// Operation data
if (op_has_data(cl->write_op))
{
for (int i = 0; i < cl->write_op->iov.count; i++)
if (cl->write_op->enc)
{
if (!op_write_buf((uint8_t*)cl->write_op->iov.buf[i].iov_base, cl->write_op->iov.buf[i].iov_len))
if (lst.size() >= IOV_MAX)
return;
// No way except to allocate a temporary buffer and encrypt data to it
assert(cl->write_op->req.hdr.opcode == OSD_OP_WRITE);
size_t remsize = cl->write_op->req.rw.len - from + (from % 16);
assert(remsize > 0);
assert(!cl->write_op->enc_buf);
cl->write_op->enc_buf = (uint8_t*)malloc_or_die(remsize);
size_t done = 0;
bool end = op_encrypted_copy_data_to(cl, cl->write_op->enc_buf, remsize, from, done);
assert(end);
lst.push_back((iovec){ .iov_base = cl->write_op->enc_buf, .iov_len = remsize });
}
else
{
for (int i = 0; i < cl->write_op->iov.count; i++)
{
if (!op_write_buf((uint8_t*)cl->write_op->iov.buf[i].iov_base, cl->write_op->iov.buf[i].iov_len))
return;
}
}
}
cl->write_op = NULL;
+16
View File
@@ -79,6 +79,22 @@ void osd_messenger_t::stop_client(uint64_t client_id, bool force_delete)
fprintf(stderr, "[OSD %ju] Stopping client %ju (regular client)\n", osd_num, client_id);
}
}
if (cl->encrypt_ctx)
{
if (encrypt_ctx_pool.size() > max_aes_xts_pool_size)
destroy_aes_xts_encrypt(cl->encrypt_ctx);
else
encrypt_ctx_pool.push_back(cl->encrypt_ctx);
cl->encrypt_ctx = NULL;
}
if (cl->decrypt_ctx)
{
if (decrypt_ctx_pool.size() > max_aes_xts_pool_size)
destroy_aes_xts_decrypt(cl->decrypt_ctx);
else
decrypt_ctx_pool.push_back(cl->decrypt_ctx);
cl->decrypt_ctx = NULL;
}
// First set state to STOPPED so another stop_client() call doesn't try to free it again
cl->refs++;
int prev_state = cl->peer_state;
+122
View File
@@ -1,10 +1,15 @@
// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
#ifdef WITH_OPENSSL
#include <openssl/rand.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "cluster_client_impl.h"
#include "msgr_encrypt.h"
void configure_single_pg_pool(cluster_client_t *cli)
{
@@ -550,11 +555,128 @@ void test_writeback_merge()
printf("[ok] writeback merge test\n");
}
#ifdef WITH_OPENSSL
void test_msgr_encrypt()
{
const size_t sz = 1048576;
uint8_t *src = (uint8_t*)malloc_or_die(sz);
for (size_t i = 0; i < sz; i++)
src[i] = (i*0x1001) % 256;
uint8_t *crypt = (uint8_t*)malloc_or_die(sz);
uint8_t *decrypt = (uint8_t*)malloc_or_die(sz);
uint8_t *crypt2 = (uint8_t*)malloc_or_die(sz);
uint8_t *key = (uint8_t*)malloc_or_die(64);
RAND_bytes(key, 64);
// Basic encrypt+decrypt and also get reference data
auto enc = new op_aes_xts_encrypt_t();
enc->start(key, 4096 * 113, 4096);
size_t in_pos = 0;
size_t out_pos = 0;
while (out_pos < sz)
{
enc->update(src+in_pos, sz-in_pos, crypt+out_pos, sz-out_pos, in_pos, out_pos);
}
auto dec = new op_aes_xts_decrypt_t();
dec->start(key, 4096 * 113, 4096);
in_pos = out_pos = 0;
while (out_pos < sz)
{
dec->update(crypt+in_pos, sz-in_pos, decrypt+out_pos, sz-out_pos, in_pos, out_pos);
}
assert(memcmp(src, decrypt, sz) == 0);
// Insufficient output encrypt
printf("...insufficient output encrypt\n");
enc->start(key, 4096 * 114, 4096);
in_pos = out_pos = 0;
enc->update(src+4096, 4096, crypt2, 4095, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 4095);
enc->update(src+4096+in_pos, 4096-in_pos, crypt2+out_pos, 4096-out_pos, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 4096);
assert(memcmp(crypt2, crypt+4096, 4096) == 0);
// Fragmented encrypt
printf("...fragmented encrypt\n");
enc->start(key, 4096 * 114, 4096);
in_pos = out_pos = 0;
enc->update(src+4096, 2000, crypt2, 4095, in_pos, out_pos);
assert(in_pos == 2000);
assert(out_pos == 0);
enc->update(src+4096+in_pos, 4096-in_pos, crypt2+out_pos, 4096-out_pos, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 4096);
assert(memcmp(crypt2, crypt+4096, 4096) == 0);
// Fragmented decrypt
// Input: 1000 + 2000 + 3000 + 2192, output: 500 + 3000 + 1000 + 3000 + 692
printf("...fragmented decrypt\n");
dec->start(key, 4096 * 114, 4096);
in_pos = out_pos = 0;
dec->update(crypt+4096, 1000, decrypt, 500, in_pos, out_pos);
assert(in_pos == 1000);
assert(out_pos == 0);
dec->update(crypt+4096+1000, 2000, decrypt, 500, in_pos, out_pos);
assert(in_pos == 3000);
assert(out_pos == 0);
dec->update(crypt+4096+3000, 3000, decrypt, 500, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 500);
dec->update(crypt+4096+in_pos, 6000-in_pos, decrypt+out_pos, 3000, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 3500);
dec->update(crypt+4096+in_pos, 6000-in_pos, decrypt+out_pos, 1000, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 4096);
dec->update(crypt+4096+in_pos, 6000-in_pos, decrypt+out_pos, 4500-out_pos, in_pos, out_pos);
assert(in_pos == 6000);
assert(out_pos == 4096);
dec->update(crypt+4096+in_pos, 8192-in_pos, decrypt+out_pos, 4500-out_pos, in_pos, out_pos);
assert(in_pos == 8192);
assert(out_pos == 4500);
dec->update(crypt+4096+in_pos, 8192-in_pos, decrypt+out_pos, 7500-out_pos, in_pos, out_pos);
assert(in_pos == 8192);
assert(out_pos == 7500);
dec->update(crypt+4096+in_pos, 8192-in_pos, decrypt+out_pos, 8192-out_pos, in_pos, out_pos);
assert(in_pos == 8192);
assert(out_pos == 8192);
assert(memcmp(decrypt, src+4096, 8192) == 0);
// Extra size decrypt
// Input: 8192, output: 4096
printf("...extra size decrypt\n");
dec->start(key, 4096 * 114, 4096);
in_pos = out_pos = 0;
dec->update(crypt+4096, 8192, decrypt, 4096, in_pos, out_pos);
assert(in_pos == 4096);
assert(out_pos == 4096);
assert(memcmp(decrypt, src+4096, 4096) == 0);
delete dec;
delete enc;
free(key);
free(crypt2);
free(decrypt);
free(crypt);
free(src);
printf("[ok] msgr aes-xts encryption test\n");
}
#endif
int main(int narg, char *args[])
{
test1();
test2();
test_writeback();
test_writeback_merge();
#ifdef WITH_OPENSSL
test_msgr_encrypt();
#endif
return 0;
}
+3
View File
@@ -159,3 +159,6 @@ TEST_NAME=old_csum_4k OLD=1 OSD_ARGS="--data_csum_type crc32c" OFFSET_ARGS=
./test_nfs.sh
./test_nfs_unaligned_append.sh
./test_write_encrypted.sh
SCHEME=ec ./test_write_encrypted.sh
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash -ex
. `dirname $0`/run_3osds.sh
# Basic AES-XTS encryption test
$ETCDCTL put /vitastor/config/inode/1/1 '{"name":"testimg","size":'$((128*1024*1024))',"enc_key":"'$(openssl rand -hex 64)'"}'
$VITASTOR_FIO -bs=1M -direct=1 -iodepth=4 -mirror_file=./testdata/bin/mirror.bin -end_fsync=1 -rw=write -image=testimg
$VITASTOR_FIO -bs=4k -direct=1 -iodepth=16 -serialize_overlap=1 -mirror_file=./testdata/bin/mirror.bin -verify=md5 -end_fsync=1 -rw=randwrite -image=testimg -runtime=10
$VITASTOR_CLI dd iimg=testimg of=./testdata/bin/read.bin
diff ./testdata/bin/read.bin ./testdata/bin/mirror.bin
$VITASTOR_CLI dd iimg=testimg of=./testdata/bin/read.bin bs=4k iodepth=32
diff ./testdata/bin/read.bin ./testdata/bin/mirror.bin
format_green OK