From 664624092795f5bfcca3bba8caaf6ea965c595ee Mon Sep 17 00:00:00 2001 From: Yura Sorokin Date: Thu, 6 Aug 2026 01:58:23 +0200 Subject: [PATCH] PBS-39 feature: Add binlog encryption config and keyring support (part 6) https://perconadev.atlassian.net/browse/PBS-39 'binsrv::storage::generate_binlog_encryption_record()' method reworked with proper 'opensslpp' cryptography primitives. Removed all remainders of fake key generation / encryption. Implemented proper binlog data encryption before writing: instead of calls to 'backend_->write_data_to_stream()' we now have a new method 'storage::write_data_to_stream()' that decides whether encryption is needed or not. Currently we stick to the strategy of never holding file keys in memory in plaintext. Instead, 'binsrv::storage::storage::binlog_encryption_record' holds them in encrypted form and we temporarily restore them on each write. This may cause some additional CPU time spent on decryption but it is expected to be insignificant in comparison to IO operations. 'opensslpp::cipher_context' extended with one new method 'extract_updated_iv()' that helps to identify the value of the updated IV (original IV + block counter) after some calls to the 'update()' method have been made. This function helps with resuming streaming encryption (for CTR modes, for instance). 'opensslpp::cipher_context' extended with one more static method 'create_with_offset()' that helps to create a context that would have internal state identical as if it had already processed 'offset' bytes. Again, this function helps with streaming operations. This commits also adds a list of TODO items that suggest to add more diagnostics for key / key combinations validation. Added new 'CipherContextUpdatedIVCTR' boost test case to the 'cipher_context.cpp' (BOOST_TEST_MODULE CipherContextTests) module that checks that 'opensslpp::cipher_context::create_with_offset()' method produces expected results. --- CMakeLists.txt | 3 +- src/binsrv/encryption_config.cpp | 2 + src/binsrv/keyring_record_collection.cpp | 8 + src/binsrv/storage.cpp | 277 ++++++++++++----------- src/binsrv/storage.hpp | 5 + src/opensslpp/cipher_context.cpp | 111 +++++++-- src/opensslpp/cipher_context.hpp | 11 + tests/cipher_context_test.cpp | 48 ++++ 8 files changed, 318 insertions(+), 147 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 308d93b..bede84e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,7 +184,7 @@ add_library(lib_opensslpp STATIC ${opensslpp_source_files}) target_link_libraries(lib_opensslpp PRIVATE binlog_server_compiler_flags - OpenSSL::Crypto + Boost::headers OpenSSL::Crypto ) # it is not possible to propagate CXX_EXTENSIONS and CXX_STANDARD_REQUIRED # via interface library (binlog_server_compiler_flags) @@ -594,6 +594,7 @@ target_link_libraries(binlog_server binsrv::lib_gtids binsrv::lib_events binsrv::lib_models + binsrv::lib_opensslpp Boost::headers Boost::json Boost::url aws-cpp-sdk-s3-crt ) diff --git a/src/binsrv/encryption_config.cpp b/src/binsrv/encryption_config.cpp index daf0596..bcf6972 100644 --- a/src/binsrv/encryption_config.cpp +++ b/src/binsrv/encryption_config.cpp @@ -28,6 +28,8 @@ void encryption_config::validate() const { util::exception_location().raise( "error validating storage encryption config: unsupported format"); } + // TODO: make sure that data encryption cipher is supported by OpenSSL + // and has CTR mode } } // namespace binsrv diff --git a/src/binsrv/keyring_record_collection.cpp b/src/binsrv/keyring_record_collection.cpp index 609ac3c..c715662 100644 --- a/src/binsrv/keyring_record_collection.cpp +++ b/src/binsrv/keyring_record_collection.cpp @@ -62,6 +62,14 @@ void keyring_record_collection::validate() const { util::exception_location().raise( "unsupported keyring record collection version"); } + // TODO: make sure that all the keys have unique IDs + + // TODO: make sure that all keys have ciphers known in OpenSSL + // (currently only ECB, CBC, CTR, GCM modes are supported by + // opensslpp::cipher_context) + + // TODO: make sure that all keys have data_hex values of the correct length + // that matches with the cipher } [[nodiscard]] std::string keyring_record_collection::get_description() const { diff --git a/src/binsrv/storage.cpp b/src/binsrv/storage.cpp index 768f5cf..c8c0285 100644 --- a/src/binsrv/storage.cpp +++ b/src/binsrv/storage.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -32,8 +31,6 @@ #include #include -#include - #include "binsrv/basic_keyring.hpp" #include "binsrv/basic_storage_backend.hpp" #include "binsrv/binlog_file_metadata.hpp" @@ -54,8 +51,10 @@ #include "binsrv/models/binlog_file_encryption_record.hpp" +#include "opensslpp/cipher_context.hpp" +#include "opensslpp/crypto_rng.hpp" + #include "util/byte_span.hpp" -#include "util/conversion_helpers.hpp" #include "util/ctime_timestamp.hpp" #include "util/exception_location_helpers.hpp" @@ -147,8 +146,12 @@ storage::storage(const storage_config &config, util::exception_location().raise( "keyring does not contain the specified KEK ID"); } - // TODO: validate that the length of the KEK in the keyring record match - // the length specified in the cipher name + // TODO: make sure that random file keys (of length that corresponds to the + // active data cipher) can be encrypted with the active KEK - + // for instance, if active data cipher is AES-192-CRT (key length 24 + // bytes), then the active KEK cannot be of ECB or CBC mode as these + // ciphers can only encrypt data of length that is a multiple of the + // block size (16 bytes) active_kek_id_ = kek_id; active_data_cipher_ = encryption_config->get<"cipher">(); } @@ -529,9 +532,11 @@ void storage::update_last_checkpoint_info() { [[nodiscard]] open_binlog_status storage::open_new_binlog_file_internal( const events::composite_binlog_name &binlog_name) { + + auto encryption_record{generate_binlog_encryption_record()}; // writing the magic binlog footprint only if this is a newly // created file - backend_->write_data_to_stream(events::magic_binlog_payload); + write_data_to_stream(events::magic_binlog_payload, encryption_record, 0ULL); gtids::optional_gtid_set previous_binlog_gtids{}; gtids::optional_gtid_set added_binlog_gtids{}; @@ -544,7 +549,7 @@ void storage::update_last_checkpoint_info() { binlog_name, events::magic_binlog_offset, std::move(previous_binlog_gtids), std::move(added_binlog_gtids), util::ctime_timestamp_range{}, events::seq_no_t{}, - generate_binlog_encryption_record()); + std::move(encryption_record)); save_binlog_metadata(get_current_binlog_record()); save_binlog_index(); return open_binlog_status::created; @@ -558,7 +563,9 @@ storage::open_existing_binlog_file_internal(std::uint64_t open_stream_offset) { : open_binlog_status::opened_with_data_present; } assert(open_stream_offset == 0ULL); - backend_->write_data_to_stream(events::magic_binlog_payload); + + write_data_to_stream(events::magic_binlog_payload, + get_current_binlog_record().encryption, 0ULL); get_current_binlog_record().size = events::magic_binlog_offset; return open_binlog_status::opened_empty; } @@ -573,7 +580,9 @@ void storage::flush_event_buffer_internal() { last_transaction_boundary_position_in_event_buffer_}; // writing bytes from // the beginning of the event buffer - backend_->write_data_to_stream(transactions_data); + write_data_to_stream(transactions_data, + get_current_binlog_record().encryption, + get_current_binlog_record().size); get_current_binlog_record().size += last_transaction_boundary_position_in_event_buffer_; if (is_in_gtid_replication_mode()) { @@ -851,145 +860,149 @@ storage::generate_binlog_encryption_record() const { return std::nullopt; } + // we identify the KEK record in the keyring by the active KEK ID, + // specified in the main configuration file + // ('' parameter) const auto &keyring_record{keyring_->get_key(active_kek_id_)}; + + // identifying the the cipher name and the key data from the + // keyring record - this data will be used to encrypt random file + // keys generated for new binlog data files const auto &kek_cipher{keyring_record.get<"cipher">()}; - const auto &kek_data{keyring_record.get<"data_hex">().get_data()}; - - // identify initialization vector length based on the cipher - // TODO: rework with proper OpenSSL "EVP_CIPHER_iv_length()" call - const auto iv_length_helper{[](const std::string &cipher) -> std::size_t { - const auto normalized_cipher{boost::algorithm::to_upper_copy(cipher)}; - - static constexpr std::size_t no_iv_length{0U}; - static constexpr std::size_t generic_iv_length{16U}; - static constexpr std::size_t gcm_iv_length{12U}; - if (normalized_cipher.ends_with("-ECB")) { - return no_iv_length; - } - if (normalized_cipher.ends_with("-GCM")) { - return gcm_iv_length; - } - if (normalized_cipher.ends_with("-CBC") || - normalized_cipher.ends_with("-CTR")) { - return generic_iv_length; - } - util::exception_location().raise( - "unsupported cipher mode: " + cipher); - }}; - - // identify the length of the key based on the cipher - // TODO: rework with proper OpenSSL "EVP_CIPHER_key_length()" call - const auto key_length_helper{[](const std::string &cipher) -> std::size_t { - static constexpr std::size_t x_128_key_length{16U}; - static constexpr std::size_t x_192_key_length{24U}; - static constexpr std::size_t x_256_key_length{32U}; - if (cipher.find("-128-") != std::string::npos) { - return x_128_key_length; - } - if (cipher.find("-192-") != std::string::npos) { - return x_192_key_length; - } - if (cipher.find("-256-") != std::string::npos) { - return x_256_key_length; - } - util::exception_location().raise( - "unsupported cipher length: " + cipher); - }}; - // identify the length of the tag based on the cipher - // TODO: rework with proper OpenSSL call - const auto tag_length_helper{[](const std::string &cipher) -> std::size_t { - const auto normalized_cipher{boost::algorithm::to_upper_copy(cipher)}; - static constexpr std::size_t no_tag_length{0U}; - static constexpr std::size_t gcm_tag_length{16U}; - - if (normalized_cipher.ends_with("-GCM")) { - return gcm_tag_length; - } - if (normalized_cipher.ends_with("-ECB") || - normalized_cipher.ends_with("-CBC") || - normalized_cipher.ends_with("-CTR")) { - return no_tag_length; - } - util::exception_location().raise( - "unsupported cipher mode: " + cipher); - }}; - - // generating a random blob - // TODO: rework with proper OpenSSL RAND_bytes() call - const auto random_blob_helper{ - [](std::size_t length) -> util::hex_value_storage { - static std::random_device rd_instance; - util::hex_value_storage result(length); - std::ranges::generate(result, []() { - return util::from_underlying( - static_cast(rd_instance())); - }); - return result; - }}; - - const auto iv_length_for_file_key_encryption{iv_length_helper(kek_cipher)}; + const auto &kek{keyring_record.get<"data_hex">().get_data()}; + + // identify the size of the IV that will be used for file key + // encryption based on the KEK cipher; if the KEK cipher is in ECB mode, then + // the IV is not used and its size will be 0 + const auto iv_size_for_file_key_encryption{ + opensslpp::cipher_context::get_iv_size_in_bytes(kek_cipher)}; util::optional_hex_value_storage iv_for_file_key_encryption{}; util::const_byte_span iv_for_file_key_encryption_v{}; - if (iv_length_for_file_key_encryption != 0U) { - iv_for_file_key_encryption = - random_blob_helper(iv_length_for_file_key_encryption); + if (iv_size_for_file_key_encryption != 0U) { + // generating random IV for file key encryption + iv_for_file_key_encryption.emplace(iv_size_for_file_key_encryption); + opensslpp::crypto_rng::generate(*iv_for_file_key_encryption); iv_for_file_key_encryption_v = *iv_for_file_key_encryption; } - const auto file_key_length{key_length_helper(kek_cipher)}; - const auto file_key_data{random_blob_helper(file_key_length)}; - - // TODO: implement encryption using OpenSSL EVP interface - const auto encrypt_helper{ - [&tag_length_helper]( - const std::string &cipher, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - [[maybe_unused]] util::const_byte_span key_data, - [[maybe_unused]] util::const_byte_span iv_data, - util::const_byte_span data) - -> std::pair { - util::optional_hex_value_storage tag{}; - const std::size_t tag_size{tag_length_helper(cipher)}; - if (tag_size != 0U) { - tag = util::hex_value_storage(tag_size); - std::ranges::generate( - *tag, [counter{static_cast(0U)}]() mutable { - return util::from_underlying(counter++); - }); - } - util::hex_value_storage encrypted_data(std::size(data)); - std::ranges::transform( - data, std::begin(encrypted_data), [](std::byte element) { - return std::byte{ - static_cast(util::to_underlying(element) + 1U)}; - }); - - return {encrypted_data, tag}; - }}; - - const auto file_key_encryption_result{encrypt_helper( - kek_cipher, kek_data, iv_for_file_key_encryption_v, file_key_data)}; + // identify the size of the file key based on the active data cipher + const auto file_key_size{ + opensslpp::cipher_context::get_key_size_in_bytes(active_data_cipher_)}; - const auto iv_length_for_data_encryption{ - iv_length_helper(active_data_cipher_)}; - util::hex_value_storage iv_for_data_encryption{ - random_blob_helper(iv_length_for_data_encryption)}; + // generating random file key + util::hex_value_storage file_key{file_key_size}; + opensslpp::crypto_rng::generate(file_key); + + // creating an encryption context with the KEK cipher, the KEK, and + // the IV for file key encryption + opensslpp::cipher_context file_key_encryption_context{ + opensslpp::cipher_context_mode_type::encryption, kek_cipher, kek, + iv_for_file_key_encryption_v}; + + // identify the size of the file key encryption tag from the encryption + // (should be non-zero only for GCM modes) + const auto file_key_encryption_tag_size{ + file_key_encryption_context.get_tag_size_in_bytes()}; + // provisioning the optional storage for the file key encryption tag + util::optional_hex_value_storage tag_of_file_key_encryption{}; + util::byte_span tag_of_file_key_encryption_v{}; + if (file_key_encryption_tag_size != 0U) { + tag_of_file_key_encryption.emplace(file_key_encryption_tag_size); + tag_of_file_key_encryption_v = *tag_of_file_key_encryption; + } - util::hex_value_storage dummy_data{}; - const auto data_encryption_result{encrypt_helper( - active_data_cipher_, file_key_data, iv_for_data_encryption, dummy_data)}; + // performing the file key encryption and finalizing the tag (if any) + util::hex_value_storage file_key_encrypted_with_kek{file_key_size}; + file_key_encryption_context.update(file_key, file_key_encrypted_with_kek); + file_key_encryption_context.finalize(tag_of_file_key_encryption_v); + // identifying the size of the IV that will be used for data encryption based + // on the active data cipher + const auto iv_length_for_data_encryption{ + opensslpp::cipher_context::get_iv_size_in_bytes(active_data_cipher_)}; + // generating random IV for file data encryption + util::hex_value_storage iv_for_data_encryption{iv_length_for_data_encryption}; + opensslpp::crypto_rng::generate(iv_for_data_encryption); + + // the tag of data encryption will be generated during the actual data + // encryption binlog_encryption_record encryption_record{ .kek_id = active_kek_id_, - .file_key_encrypted_with_kek = file_key_encryption_result.first, + .file_key_encrypted_with_kek = file_key_encrypted_with_kek, .iv_for_file_key_encryption = iv_for_file_key_encryption, - .tag_of_file_key_encryption = file_key_encryption_result.second, + .tag_of_file_key_encryption = tag_of_file_key_encryption, .data_cipher = active_data_cipher_, .iv_for_data_encryption = iv_for_data_encryption, - .tag_of_data_encryption = data_encryption_result.second}; + .tag_of_data_encryption = {}}; return encryption_record; } +void storage::write_data_to_stream( + util::const_byte_span data, + const optional_binlog_encryption_record &encryption_record, + std::uint64_t offset) { + if (!encryption_record.has_value()) { + // an early return when no encryption is needed + backend_->write_data_to_stream(data); + return; + } + + // as for security reasons our intent is to not store file keys in plaintext + // permanently, we need to decrypt the file key with the KEK before we can + // use it for data encryption. + + const auto &keyring_record{keyring_->get_key(encryption_record->kek_id)}; + + const auto &kek_cipher{keyring_record.get<"cipher">()}; + const auto &kek{keyring_record.get<"data_hex">().get_data()}; + + util::const_byte_span iv_for_file_key_encryption_v{}; + if (encryption_record->iv_for_file_key_encryption.has_value()) { + iv_for_file_key_encryption_v = + *encryption_record->iv_for_file_key_encryption; + }; + util::const_byte_span tag_of_file_key_encryption_v{}; + if (encryption_record->tag_of_file_key_encryption.has_value()) { + tag_of_file_key_encryption_v = + *encryption_record->tag_of_file_key_encryption; + } + + // creating a context for the file key decryption + opensslpp::cipher_context file_key_decryption_context{ + opensslpp::cipher_context_mode_type::decryption, kek_cipher, kek, + iv_for_file_key_encryption_v, tag_of_file_key_encryption_v}; + util::hex_value_storage file_key_decrypted{ + std::size(encryption_record->file_key_encrypted_with_kek)}; + file_key_decryption_context.update( + encryption_record->file_key_encrypted_with_kek, file_key_decrypted); + file_key_decryption_context.finalize(); + + // creating an context for data encryption with the data cipher, the file + // key (decrypted previously), and the IV for data encryption + + auto data_encryption_context{opensslpp::cipher_context::create_with_offset( + offset, opensslpp::cipher_context_mode_type::encryption, + encryption_record->data_cipher, file_key_decrypted, + encryption_record->iv_for_data_encryption)}; + + util::optional_hex_value_storage tag_of_data_encryption{}; + util::byte_span tag_of_data_encryption_v{}; + const auto data_encryption_tag_size{ + data_encryption_context.get_tag_size_in_bytes()}; + if (data_encryption_tag_size != 0U) { + tag_of_data_encryption.emplace(data_encryption_tag_size); + tag_of_data_encryption_v = *tag_of_data_encryption; + } + + util::hex_value_storage encrypted_data{std::size(data)}; + data_encryption_context.update(data, encrypted_data); + data_encryption_context.finalize(tag_of_data_encryption_v); + + backend_->write_data_to_stream(encrypted_data); + + // TODO: update file data encryption tag here, if one day we decide to + // support GCM mode for file data encryption +} + } // namespace binsrv diff --git a/src/binsrv/storage.hpp b/src/binsrv/storage.hpp index 1939ea3..174fef0 100644 --- a/src/binsrv/storage.hpp +++ b/src/binsrv/storage.hpp @@ -288,6 +288,11 @@ class [[nodiscard]] storage { [[nodiscard]] optional_binlog_encryption_record generate_binlog_encryption_record() const; + + void write_data_to_stream( + util::const_byte_span data, + const optional_binlog_encryption_record &encryption_record, + std::uint64_t offset); }; } // namespace binsrv diff --git a/src/opensslpp/cipher_context.cpp b/src/opensslpp/cipher_context.cpp index fb23631..c50b4aa 100644 --- a/src/opensslpp/cipher_context.cpp +++ b/src/opensslpp/cipher_context.cpp @@ -15,13 +15,21 @@ #include "opensslpp/cipher_context.hpp" +#include #include #include #include +#include +#include #include #include #include +#include +#include + +#include + #include #include @@ -210,6 +218,28 @@ cipher_context::get_iv_size_in_bytes(const std::string &cipher_name) { native_helper::get_native_cipher_by_name(cipher_name)); } +void cipher_context::extract_updated_iv(util::byte_span ivec) { + assert(!is_empty()); + if (std::size(ivec) != get_iv_size_in_bytes()) { + util::exception_location().raise( + "in cipher context invalid buffer size for extracting updated iv"); + } + + if (!std::in_range(std::size(ivec))) { + util::exception_location().raise( + "in cipher context buffer size is out of range for extracting updated " + "iv"); + } + if (EVP_CIPHER_CTX_get_updated_iv( + native_helper::deimpl(impl_), + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::data(ivec)), + std::size(ivec)) == 0) { + util::exception_location().raise( + "cannot get updated iv for cipher context"); + } +} + void cipher_context::update(util::const_byte_span input, util::byte_span output) { assert(!is_empty()); @@ -233,26 +263,26 @@ void cipher_context::update(util::const_byte_span input, util::exception_location().raise( "in cipher context update input size is out of range"); } - const auto input_length_native{static_cast(std::size(input))}; - int output_length_native{0}; + const auto input_size_native{static_cast(std::size(input))}; + int output_size_native{0}; if (EVP_CipherUpdate( native_helper::deimpl(impl_), // context // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) reinterpret_cast(std::data(output)), // output - &output_length_native, // output length + &output_size_native, // output length // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) reinterpret_cast(std::data(input)), // input - input_length_native // input length + input_size_native // input length ) == 0) { util::exception_location().raise( "cannot update cipher context"); } - if (!std::in_range(output_length_native)) { + if (!std::in_range(output_size_native)) { util::exception_location().raise( "in cipher context update output size is out of range"); } - const auto output_length{static_cast(output_length_native)}; - if (output_length != std::size(output)) { + const auto output_size{static_cast(output_size_native)}; + if (output_size != std::size(output)) { util::exception_location().raise( "in cipher context update the actual output size does not match the " "expected output size"); @@ -280,33 +310,33 @@ void cipher_context::finalize(util::byte_span output_tag) { using fake_buffer_type = std::array; fake_buffer_type fake_buffer; - int output_length_native{0}; + int output_size_native{0}; if (EVP_CipherFinal_ex( native_helper::deimpl(impl_), // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) reinterpret_cast(std::data(fake_buffer)), - &output_length_native) == 0) { + &output_size_native) == 0) { util::exception_location().raise( "cannot finalize cipher context"); } - if (!std::in_range(output_length_native)) { + if (!std::in_range(output_size_native)) { util::exception_location().raise( "in cipher context finalize output size is out of range"); } - const auto output_length{static_cast(output_length_native)}; - if (output_length != 0U) { + const auto output_size{static_cast(output_size_native)}; + if (output_size != 0U) { util::exception_location().raise( "in cipher context finalize the actual output size is not zero"); } if (mode == cipher_context_mode_type::encryption) { - const auto tag_length_native{static_cast(std::size(output_tag))}; + const auto tag_size_native{static_cast(std::size(output_tag))}; void *const tag_ptr{ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) const_cast(static_cast(std::data(output_tag)))}; if (EVP_CIPHER_CTX_ctrl(native_helper::deimpl(impl_), // context EVP_CTRL_GCM_GET_TAG, // type - tag_length_native, // length + tag_size_native, // length tag_ptr // tag ) == 0) { util::exception_location().raise( @@ -316,4 +346,57 @@ void cipher_context::finalize(util::byte_span output_tag) { impl_.reset(); } +cipher_context cipher_context::create_with_offset( + std::uint64_t offset, cipher_context_mode_type mode, + const std::string &cipher_name, util::const_byte_span key, + util::const_byte_span ivec, util::const_byte_span tag) { + if (offset == 0ULL) { + // early return when offset is zero to avoid unnecessary copying + return cipher_context{mode, cipher_name, key, ivec, tag}; + } + + // TODO: check if this function is called for one of the CTR ciphers + if (std::size(ivec) < sizeof(std::uint64_t)) { + util::exception_location().raise( + "in cipher context creation with offset the size of the iv is too " + "small"); + } + + const std::byte *original_ivec_ptr{std::data(ivec)}; + std::advance(original_ivec_ptr, std::size(ivec) - sizeof(std::uint64_t)); + std::uint64_t counter{boost::endian::load_big_u64( + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(original_ivec_ptr))}; + + // this is true for most of the algorithms, including AES - + // we cannot use get_block_size_in_bytes() because it returns 1 for CTR and + // GCM + + // TODO: rework with calling get_block_size_in_bytes() with modified cipher + // name (change the mode part to "-ECB") + static constexpr std::uint64_t block_size{16ULL}; + counter += offset / block_size; + + using buffer_type = boost::container::static_vector< + std::byte, std::max(EVP_MAX_IV_LENGTH, EVP_MAX_BLOCK_LENGTH)>; + buffer_type modified_ivec{std::size(ivec)}; + auto *dest_ptr{std::data(modified_ivec)}; + + const auto *source_en{std::data(ivec)}; + std::advance(source_en, std::size(ivec) - sizeof(std::uint64_t)); + dest_ptr = std::copy(std::data(ivec), source_en, dest_ptr); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + boost::endian::store_big_u64(reinterpret_cast(dest_ptr), + counter); + + cipher_context result{mode, cipher_name, key, modified_ivec, tag}; + + buffer_type source_dummy_block{offset % block_size, + boost::container::default_init}; + buffer_type dest_dummy_block{offset % block_size}; + result.update(source_dummy_block, dest_dummy_block); + + return result; +} + } // namespace opensslpp diff --git a/src/opensslpp/cipher_context.hpp b/src/opensslpp/cipher_context.hpp index 1ad4645..469d2c4 100644 --- a/src/opensslpp/cipher_context.hpp +++ b/src/opensslpp/cipher_context.hpp @@ -78,10 +78,21 @@ class cipher_context { // there is no static version of get_tag_size_in_bytes() as tag size is a // dynamic property of the cipher context + // this is not a const method as underlying EVP_CIPHER_CTX_get_updated_iv() + // accepts non-const EVP_CIPHER_CTX pointer + void extract_updated_iv(util::byte_span ivec); + // TODO: implement void update_inplace(util::byte_span inoutput) void update(util::const_byte_span input, util::byte_span output); void finalize(util::byte_span output_tag = {}); + static cipher_context create_with_offset(std::uint64_t offset, + cipher_context_mode_type mode, + const std::string &cipher_name, + util::const_byte_span key, + util::const_byte_span ivec = {}, + util::const_byte_span tag = {}); + private: struct native_helper; struct impl_deleter { diff --git a/tests/cipher_context_test.cpp b/tests/cipher_context_test.cpp index bc18d84..a3475ea 100644 --- a/tests/cipher_context_test.cpp +++ b/tests/cipher_context_test.cpp @@ -170,6 +170,12 @@ BOOST_AUTO_TEST_CASE(CipherContextGetKeySizeStatic) { BOOST_CHECK_THROW(key_size = opensslpp::cipher_context::get_key_size_in_bytes( invalid_cipher_name), opensslpp::core_error); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-128-ECB") == + 16U); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-192-ECB") == + 24U); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-256-ECB") == + 32U); BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-128-CBC") == 16U); BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-192-CBC") == @@ -182,6 +188,12 @@ BOOST_AUTO_TEST_CASE(CipherContextGetKeySizeStatic) { 24U); BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-256-CTR") == 32U); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-128-GCM") == + 16U); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-192-GCM") == + 24U); + BOOST_CHECK(opensslpp::cipher_context::get_key_size_in_bytes("AES-256-GCM") == + 32U); } BOOST_AUTO_TEST_CASE(CipherContextGetIVSizeStatic) { @@ -622,3 +634,39 @@ BOOST_DATA_TEST_CASE(CipherContextRoundtripGCM, BOOST_CHECK(message == restored_message); } + +BOOST_DATA_TEST_CASE(CipherContextUpdatedIVCTR, + boost::unit_test::data::make(bit_lengths) * + boost::unit_test::data::make(stream_message_sizes), + bit_length, message_size) { + const std::string cipher_name{"AES-" + std::to_string(bit_length) + "-CTR"}; + + const std::size_t valid_key_size{ + opensslpp::cipher_context::get_key_size_in_bytes(cipher_name)}; + const std::size_t valid_ivec_size{ + opensslpp::cipher_context::get_iv_size_in_bytes(cipher_name)}; + + buffer_type key{valid_key_size}; + buffer_type ivec{valid_ivec_size}; + opensslpp::crypto_rng::generate(key); + opensslpp::crypto_rng::generate(ivec); + + buffer_type message{message_size}; + buffer_type encrypted_message{message_size}; + opensslpp::crypto_rng::generate(message); + + opensslpp::cipher_context encryption_context( + opensslpp::cipher_context_mode_type::encryption, cipher_name, key, ivec); + encryption_context.update(message, encrypted_message); + + buffer_type updated_ivec{valid_ivec_size}; + encryption_context.extract_updated_iv(updated_ivec); + + auto fast_encryption_context{opensslpp::cipher_context::create_with_offset( + std::size(message), opensslpp::cipher_context_mode_type::encryption, + cipher_name, key, ivec)}; + buffer_type fast_updated_ivec{valid_ivec_size}; + fast_encryption_context.extract_updated_iv(fast_updated_ivec); + + BOOST_CHECK(updated_ivec == fast_updated_ivec); +}