diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 6dd1d132..d57a045a 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -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 + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3ea077ea..8c10a42c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index e3958fb9..61670a10 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -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 msgr_iothread.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 @@ -96,10 +96,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 ${LIBURING_LIBRARIES}) +target_link_libraries(test_cluster_client ${LIBURING_LIBRARIES} ${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) diff --git a/src/client/cluster_client.cpp b/src/client/cluster_client.cpp index 9ee812e1..1e2347f3 100644 --- a/src/client/cluster_client.cpp +++ b/src/client/cluster_client.cpp @@ -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; @@ -1442,6 +1454,7 @@ int cluster_client_t::try_send(cluster_op_t *op, int i, std::functionpart_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); diff --git a/src/client/cluster_client.h b/src/client/cluster_client.h index 88e0393b..97545ea6 100644 --- a/src/client/cluster_client.h +++ b/src/client/cluster_client.h @@ -71,6 +71,7 @@ protected: cluster_op_t *prev = NULL, *next = NULL; int prev_wait = 0; uint64_t flush_id = 0; + std::shared_ptr enc; friend class cluster_client_t; friend class writeback_cache_t; }; diff --git a/src/client/etcd_state_client.cpp b/src/client/etcd_state_client.cpp index d7cc98c8..8111f7a4 100644 --- a/src/client/etcd_state_client.cpp +++ b/src/client/etcd_state_client.cpp @@ -1305,6 +1305,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 enc; + if (!value["enc_key"].string_value().empty()) + { + std::vector k = hexdecode(value["enc_key"].string_value()); + if (k.size() == 512/8) + { + enc = std::make_shared(); + enc->key = std::move(k); + } + } insert_inode_config((inode_config_t){ .num = inode_num, .name = value["name"].string_value(), @@ -1312,6 +1322,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, }); diff --git a/src/client/etcd_state_client.h b/src/client/etcd_state_client.h index 41ef7fd0..a08619f0 100644 --- a/src/client/etcd_state_client.h +++ b/src/client/etcd_state_client.h @@ -4,6 +4,7 @@ #pragma once #include +#include #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 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 enc; // Arbitrary metadata json11::Json meta; // Change revision of the metadata in etcd diff --git a/src/client/messenger.cpp b/src/client/messenger.cpp index 00e46ccc..ac204ca8 100644 --- a/src/client/messenger.cpp +++ b/src/client/messenger.cpp @@ -184,6 +184,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) @@ -218,6 +226,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 diff --git a/src/client/messenger.h b/src/client/messenger.h index ebf5b335..2bb64121 100644 --- a/src/client/messenger.h +++ b/src/client/messenger.h @@ -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 received_ops; @@ -101,6 +109,7 @@ struct osd_client_t size_t send_list_size = 0; std::deque send_free_ops; std::vector zc_free_list; + op_aes_xts_encrypt_t *encrypt_ctx = NULL; ~osd_client_t(); void cancel_ops(); @@ -156,6 +165,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; @@ -179,6 +189,9 @@ protected: // We don't use ringloop->set_immediate here because we may have no ringloop in client :) std::deque set_immediate_ops; + std::vector encrypt_ctx_pool; + std::vector decrypt_ctx_pool; + public: timerfd_manager_t *tfd = NULL; ring_loop_i *ringloop = NULL; @@ -260,6 +273,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); diff --git a/src/client/msgr_encrypt.cpp b/src/client/msgr_encrypt.cpp new file mode 100644 index 00000000..3fc563ff --- /dev/null +++ b/src/client/msgr_encrypt.cpp @@ -0,0 +1,457 @@ +// Copyright (c) Vitaliy Filippov, 2026+ +// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details) + +#include + +#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 , then copy part of it to + 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 + 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 , then copy part of it to + 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 + 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; + } +} diff --git a/src/client/msgr_encrypt.h b/src/client/msgr_encrypt.h new file mode 100644 index 00000000..9283ffc8 --- /dev/null +++ b/src/client/msgr_encrypt.h @@ -0,0 +1,65 @@ +// Copyright (c) Vitaliy Filippov, 2026+ +// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details) + +#include + +// WITH_OPENSSL is left to possibly support other crypto libraries +#ifdef WITH_OPENSSL +#include +#include +#include +#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); diff --git a/src/client/msgr_op.cpp b/src/client/msgr_op.cpp index 84dd0a0f..d5e56383 100644 --- a/src/client/msgr_op.cpp +++ b/src/client/msgr_op.cpp @@ -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() diff --git a/src/client/msgr_op.h b/src/client/msgr_op.h index ce8c2869..916095a3 100644 --- a/src/client/msgr_op.h +++ b/src/client/msgr_op.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include #include @@ -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 }; @@ -168,6 +172,8 @@ struct __attribute__((visibility("default"))) osd_op_t unsigned bmp_data = 0; void *bitmap_buf = NULL; void *rmw_buf = NULL; + std::shared_ptr enc; + uint8_t *enc_buf = NULL; osd_primary_op_data_t* op_data = NULL; std::function callback; diff --git a/src/client/msgr_rdma.cpp b/src/client/msgr_rdma.cpp index c01ac554..082443d6 100644 --- a/src/client/msgr_rdma.cpp +++ b/src/client/msgr_rdma.cpp @@ -735,9 +735,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)) { diff --git a/src/client/msgr_receive.cpp b/src/client/msgr_receive.cpp index 0de35aa1..26e420d3 100644 --- a/src/client/msgr_receive.cpp +++ b/src/client/msgr_receive.cpp @@ -195,6 +195,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) { @@ -453,9 +455,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) @@ -556,6 +566,12 @@ size_t osd_messenger_t::op_get_read_buffers(osd_client_t *cl, std::vector } 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; @@ -591,6 +607,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); diff --git a/src/client/msgr_send.cpp b/src/client/msgr_send.cpp index e8853d95..5585b460 100644 --- a/src/client/msgr_send.cpp +++ b/src/client/msgr_send.cpp @@ -5,12 +5,6 @@ #include #include -#ifdef WITH_OPENSSL -#include -#include -#include -#endif - #include "messenger.h" #include "msgr_iothread.h" @@ -377,10 +371,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; @@ -412,10 +416,28 @@ void osd_messenger_t::op_get_write_buffers(osd_client_t *cl, std::vector // 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; diff --git a/src/client/msgr_stop.cpp b/src/client/msgr_stop.cpp index 7cc77271..6af4a7cf 100644 --- a/src/client/msgr_stop.cpp +++ b/src/client/msgr_stop.cpp @@ -76,6 +76,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; diff --git a/src/test/test_cluster_client.cpp b/src/test/test_cluster_client.cpp index b8de60e4..3ccf2edb 100644 --- a/src/test/test_cluster_client.cpp +++ b/src/test/test_cluster_client.cpp @@ -1,10 +1,15 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) +#ifdef WITH_OPENSSL +#include +#endif + #include #include #include #include "cluster_client_impl.h" +#include "msgr_encrypt.h" class cluster_client_test_t { @@ -559,11 +564,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; } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 26acb994..9340eb42 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -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 diff --git a/tests/test_write_encrypted.sh b/tests/test_write_encrypted.sh new file mode 100755 index 00000000..cb82a2c0 --- /dev/null +++ b/tests/test_write_encrypted.sh @@ -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