diff --git a/cmake/autogenerated_versions.txt b/cmake/autogenerated_versions.txt index ea5027262049..e985ffaa60f9 100644 --- a/cmake/autogenerated_versions.txt +++ b/cmake/autogenerated_versions.txt @@ -7,8 +7,8 @@ SET(VERSION_MAJOR 25) SET(VERSION_MINOR 8) SET(VERSION_PATCH 12) SET(VERSION_GITHASH fa393206741c830da77b8f1bcf18c753161932c8) -SET(VERSION_DESCRIBE v25.8.12.20000.altinityantalya) -SET(VERSION_STRING 25.8.12.20000.altinityantalya) +SET(VERSION_DESCRIBE v25.8.12.20000.altinitytest) +SET(VERSION_STRING 25.8.12.20000.altinitytest) # end of autochange # This is the 'base' tweak of the version, build scripts will diff --git a/src/Functions/FunctionsAES.cpp b/src/Functions/FunctionsAES.cpp index 8bdca8bcd361..c19c5467e9fe 100644 --- a/src/Functions/FunctionsAES.cpp +++ b/src/Functions/FunctionsAES.cpp @@ -9,6 +9,9 @@ #include #include +#include +#include +#include namespace DB { @@ -39,11 +42,47 @@ StringRef foldEncryptionKeyInMySQLCompatitableMode(size_t cipher_key_size, Strin const EVP_CIPHER * getCipherByName(StringRef cipher_name) { - // NOTE: cipher obtained not via EVP_CIPHER_fetch() would cause extra work on each context reset - // with EVP_CIPHER_CTX_reset() or EVP_EncryptInit_ex(), but using EVP_CIPHER_fetch() - // causes data race, so we stick to the slower but safer alternative here. + using CipherGetter = const EVP_CIPHER *(*)(); + struct Entry + { + std::string_view name; + CipherGetter getter; + }; + + /// Fast path to avoid OBJ/namemap lookups in EVP_get_cipherbyname(). + /// Statically linked OpenSSL provides direct getters for common AES modes. + static constexpr std::array fast_paths{{ + {"aes-128-cbc", &EVP_aes_128_cbc}, + {"aes-192-cbc", &EVP_aes_192_cbc}, + {"aes-256-cbc", &EVP_aes_256_cbc}, + {"aes-128-ecb", &EVP_aes_128_ecb}, + {"aes-192-ecb", &EVP_aes_192_ecb}, + {"aes-256-ecb", &EVP_aes_256_ecb}, + {"aes-128-cfb", &EVP_aes_128_cfb}, + {"aes-192-cfb", &EVP_aes_192_cfb}, + {"aes-256-cfb", &EVP_aes_256_cfb}, + {"aes-128-cfb8", &EVP_aes_128_cfb8}, + {"aes-192-cfb8", &EVP_aes_192_cfb8}, + {"aes-256-cfb8", &EVP_aes_256_cfb8}, + {"aes-128-cfb1", &EVP_aes_128_cfb1}, + {"aes-192-cfb1", &EVP_aes_192_cfb1}, + {"aes-256-cfb1", &EVP_aes_256_cfb1}, + {"aes-128-ofb", &EVP_aes_128_ofb}, + {"aes-192-ofb", &EVP_aes_192_ofb}, + {"aes-256-ofb", &EVP_aes_256_ofb}, + {"aes-128-ctr", &EVP_aes_128_ctr}, + {"aes-192-ctr", &EVP_aes_192_ctr}, + {"aes-256-ctr", &EVP_aes_256_ctr}, + }}; + + for (const auto & entry : fast_paths) + { + if (cipher_name.size == entry.name.size() + && std::memcmp(cipher_name.data, entry.name.data(), entry.name.size()) == 0) + return entry.getter(); + } - /// We need zero-terminated string here: + /// Fallback keeps compatibility with custom providers/algorithms. return EVP_get_cipherbyname(cipher_name.toString().c_str()); } diff --git a/src/Functions/FunctionsAES.h b/src/Functions/FunctionsAES.h index d65f1d1e32fb..c41b31ecd915 100644 --- a/src/Functions/FunctionsAES.h +++ b/src/Functions/FunctionsAES.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -274,6 +275,17 @@ class FunctionEncrypt : public IFunction const auto block_size = static_cast(EVP_CIPHER_block_size(evp_cipher)); const auto key_size = static_cast(EVP_CIPHER_key_length(evp_cipher)); [[maybe_unused]] const auto iv_size = static_cast(EVP_CIPHER_iv_length(evp_cipher)); + std::string zero_iv_buffer; + auto getIVPtr = [&](const StringRef & iv_value) -> const unsigned char * + { + if (iv_size > 0 && iv_value.size == 0) + { + if (zero_iv_buffer.size() != iv_size) + zero_iv_buffer.assign(iv_size, '\0'); + return reinterpret_cast(zero_iv_buffer.data()); + } + return reinterpret_cast(iv_value.data); + }; const auto tag_size = 16; // https://tools.ietf.org/html/rfc5116#section-5.1 auto encrypted_result_column = ColumnString::create(); @@ -300,17 +312,114 @@ class FunctionEncrypt : public IFunction KeyHolder key_holder; + const bool key_is_constant = isColumnConst(*key_column); + const bool iv_is_constant = iv_column && isColumnConst(*iv_column); + /// Avoid re-initializing cipher with the same key/IV on every row (expensive with OpenSSL 3). + const bool can_reuse_context = (mode != CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant && (!iv_column || iv_is_constant); + /// Reuse only key schedule even if IV changes per row (still cheaper than full init). + const bool can_reuse_key_schedule = (mode != CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant; + /// GCM: reuse key schedule, still re-set IVLEN/IV per row. + const bool can_reuse_gcm_key = (mode == CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant; + + StringRef const_key_value{}; + StringRef const_iv_value{}; + if (can_reuse_key_schedule || can_reuse_gcm_key) + { + const_key_value = key_holder.setKey(key_size, key_column->getDataAt(0)); + if constexpr (mode != CipherMode::MySQLCompatibility) + { + if (const_key_value.size != key_size) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid key size {} != expected size {}", const_key_value.size, key_size); + } + + if (iv_column && iv_is_constant) + { + const_iv_value = iv_column->getDataAt(0); + if (const_iv_value.size == 0) + const_iv_value.data = nullptr; + + validateIV(const_iv_value, iv_size); + } + + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { + if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(const_key_value.data), + nullptr) != 1) + onError("EVP_EncryptInit_ex"); + } + else + { + if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(const_key_value.data), + getIVPtr(const_iv_value)) != 1) + onError("EVP_EncryptInit_ex"); + } + } + for (size_t row_idx = 0; row_idx < input_rows_count; ++row_idx) { - const auto key_value = key_holder.setKey(key_size, key_column->getDataAt(row_idx)); + StringRef key_value; auto iv_value = StringRef{}; - if (iv_column) + + if (can_reuse_context) { - iv_value = iv_column->getDataAt(row_idx); + key_value = const_key_value; + iv_value = const_iv_value; + + /// Reset context to the fresh state but keep the already prepared key schedule. + if (row_idx != 0 && EVP_EncryptInit_ex(evp_ctx, nullptr, nullptr, nullptr, + getIVPtr(iv_value)) != 1) + onError("EVP_EncryptInit_ex"); + } + else if (can_reuse_gcm_key) + { + key_value = const_key_value; + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + if (iv_value.size == 0) + iv_value.data = nullptr; + } - /// If the length is zero (empty string is passed) it should be treat as no IV. if (iv_value.size == 0) - iv_value.data = nullptr; + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid IV size {} != expected size {}", iv_value.size, iv_size); + + if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) + onError("EVP_CIPHER_CTX_ctrl"); + + if (EVP_EncryptInit_ex(evp_ctx, nullptr, nullptr, + nullptr, + reinterpret_cast(iv_value.data)) != 1) + onError("EVP_EncryptInit_ex"); + } + else if (can_reuse_key_schedule) + { + key_value = const_key_value; + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + if (iv_value.size == 0) + iv_value.data = nullptr; + } + + validateIV(iv_value, iv_size); + + if (EVP_EncryptInit_ex(evp_ctx, nullptr, nullptr, nullptr, + getIVPtr(iv_value)) != 1) + onError("EVP_EncryptInit_ex"); + } + else + { + key_value = key_holder.setKey(key_size, key_column->getDataAt(row_idx)); + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + + /// If the length is zero (empty string is passed) it should be treat as no IV. + if (iv_value.size == 0) + iv_value.data = nullptr; + } } const StringRef input_value = input_column->getDataAt(row_idx); @@ -332,22 +441,37 @@ class FunctionEncrypt : public IFunction // Avoid extra work on empty ciphertext/plaintext for some ciphers if (!(input_value.size == 0 && block_size == 1 && mode != CipherMode::RFC5116_AEAD_AES_GCM)) { - // 1: Init CTX - if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + if (!can_reuse_context && !can_reuse_key_schedule && !can_reuse_gcm_key) { - // 1.a.1: Init CTX with custom IV length and optionally with AAD - if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, nullptr, nullptr) != 1) - onError("EVP_EncryptInit_ex"); + // 1: Init CTX + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { + // 1.a.1: Init CTX with custom IV length + if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, nullptr, nullptr) != 1) + onError("EVP_EncryptInit_ex"); - if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) - onError("EVP_CIPHER_CTX_ctrl"); + if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) + onError("EVP_CIPHER_CTX_ctrl"); - if (EVP_EncryptInit_ex(evp_ctx, nullptr, nullptr, - reinterpret_cast(key_value.data), - reinterpret_cast(iv_value.data)) != 1) - onError("EVP_EncryptInit_ex"); + if (EVP_EncryptInit_ex(evp_ctx, nullptr, nullptr, + reinterpret_cast(key_value.data), + getIVPtr(iv_value)) != 1) + onError("EVP_EncryptInit_ex"); + } + else + { + // 1.b: Init CTX + validateIV(iv_value, iv_size); + + if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(key_value.data), + getIVPtr(iv_value)) != 1) + onError("EVP_EncryptInit_ex"); + } + } - // 1.a.2 Set AAD + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { if (aad_column) { const auto aad_data = aad_column->getDataAt(row_idx); @@ -357,16 +481,6 @@ class FunctionEncrypt : public IFunction onError("EVP_EncryptUpdate"); } } - else - { - // 1.b: Init CTX - validateIV(iv_value, iv_size); - - if (EVP_EncryptInit_ex(evp_ctx, evp_cipher, nullptr, - reinterpret_cast(key_value.data), - reinterpret_cast(iv_value.data)) != 1) - onError("EVP_EncryptInit_ex"); - } int output_len = 0; // 2: Feed the data to the cipher @@ -548,6 +662,17 @@ class FunctionDecrypt : public IFunction [[maybe_unused]] const auto block_size = static_cast(EVP_CIPHER_block_size(evp_cipher)); [[maybe_unused]] const auto iv_size = static_cast(EVP_CIPHER_iv_length(evp_cipher)); + std::string zero_iv_buffer; + auto getIVPtr = [&](const StringRef & iv_value) -> const unsigned char * + { + if (iv_size > 0 && iv_value.size == 0) + { + if (zero_iv_buffer.size() != iv_size) + zero_iv_buffer.assign(iv_size, '\0'); + return reinterpret_cast(zero_iv_buffer.data()); + } + return reinterpret_cast(iv_value.data); + }; const size_t key_size = static_cast(EVP_CIPHER_key_length(evp_cipher)); static constexpr size_t tag_size = 16; // https://tools.ietf.org/html/rfc5116#section-5.1 @@ -582,18 +707,119 @@ class FunctionDecrypt : public IFunction auto * decrypted = decrypted_result_column_data.data(); KeyHolder key_holder; + const bool key_is_constant = isColumnConst(*key_column); + const bool iv_is_constant = iv_column && isColumnConst(*iv_column); + /// Avoid re-initializing cipher with the same key/IV on every row (expensive with OpenSSL 3). + const bool can_reuse_context = (mode != CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant && (!iv_column || iv_is_constant); + /// Reuse only key schedule even if IV changes per row. + const bool can_reuse_key_schedule = (mode != CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant; + const bool can_reuse_gcm_key = (mode == CipherMode::RFC5116_AEAD_AES_GCM) && key_is_constant; + + StringRef const_key_value{}; + StringRef const_iv_value{}; + if (can_reuse_key_schedule || can_reuse_gcm_key) + { + const_key_value = key_holder.setKey(key_size, key_column->getDataAt(0)); + if constexpr (mode != CipherMode::MySQLCompatibility) + { + if (const_key_value.size != key_size) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid key size {} != expected size {}", const_key_value.size, key_size); + } + + if (iv_column && iv_is_constant) + { + const_iv_value = iv_column->getDataAt(0); + + /// If the length is zero (empty string is passed) it should be treat as no IV. + if (const_iv_value.size == 0) + const_iv_value.data = nullptr; + + validateIV(const_iv_value, iv_size); + } + + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { + if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(const_key_value.data), + nullptr) != 1) + onError("EVP_DecryptInit_ex"); + } + else + { + if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(const_key_value.data), + getIVPtr(const_iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + } + for (size_t row_idx = 0; row_idx < input_rows_count; ++row_idx) { // 0: prepare key if required - auto key_value = key_holder.setKey(key_size, key_column->getDataAt(row_idx)); + auto key_value = StringRef{}; auto iv_value = StringRef{}; - if (iv_column) + + if (can_reuse_context) { - iv_value = iv_column->getDataAt(row_idx); + key_value = const_key_value; + iv_value = const_iv_value; + + /// Reset context but keep already computed key schedule. + if (row_idx != 0 && EVP_DecryptInit_ex(evp_ctx, nullptr, nullptr, nullptr, + getIVPtr(iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + else if (can_reuse_gcm_key) + { + key_value = const_key_value; + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + + /// If the length is zero (empty string is passed) it should be treat as no IV. + if (iv_value.size == 0) + iv_value.data = nullptr; + } - /// If the length is zero (empty string is passed) it should be treat as no IV. if (iv_value.size == 0) - iv_value.data = nullptr; + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid IV size {} != expected size {}", iv_value.size, iv_size); + + if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) + onError("EVP_CIPHER_CTX_ctrl"); + + if (EVP_DecryptInit_ex(evp_ctx, nullptr, nullptr, nullptr, + getIVPtr(iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + else if (can_reuse_key_schedule) + { + key_value = const_key_value; + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + + /// If the length is zero (empty string is passed) it should be treat as no IV. + if (iv_value.size == 0) + iv_value.data = nullptr; + } + + validateIV(iv_value, iv_size); + + if (EVP_DecryptInit_ex(evp_ctx, nullptr, nullptr, nullptr, + getIVPtr(iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + else + { + key_value = key_holder.setKey(key_size, key_column->getDataAt(row_idx)); + if (iv_column) + { + iv_value = iv_column->getDataAt(row_idx); + + /// If the length is zero (empty string is passed) it should be treat as no IV. + if (iv_value.size == 0) + iv_value.data = nullptr; + } } auto input_value = input_column->getDataAt(row_idx); @@ -631,23 +857,39 @@ class FunctionDecrypt : public IFunction /// This makes sense for default implementation for NULLs. if (input_value.size > 0) { - // 1: Init CTX - if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + if (!can_reuse_context && !can_reuse_key_schedule && !can_reuse_gcm_key) { - if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, nullptr, nullptr) != 1) - onError("EVP_DecryptInit_ex"); + // 1: Init CTX + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { + if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, nullptr, nullptr) != 1) + onError("EVP_DecryptInit_ex"); - // 1.a.1 : Set custom IV length - if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) - onError("EVP_CIPHER_CTX_ctrl"); + // 1.a.1 : Set custom IV length + if (EVP_CIPHER_CTX_ctrl(evp_ctx, EVP_CTRL_AEAD_SET_IVLEN, safe_cast(iv_value.size), nullptr) != 1) + onError("EVP_CIPHER_CTX_ctrl"); + + // 1.a.1 : Init CTX with key and IV + if (EVP_DecryptInit_ex(evp_ctx, nullptr, nullptr, + reinterpret_cast(key_value.data), + getIVPtr(iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + else + { + // 1.b: Init CTX + validateIV(iv_value, iv_size); - // 1.a.1 : Init CTX with key and IV - if (EVP_DecryptInit_ex(evp_ctx, nullptr, nullptr, - reinterpret_cast(key_value.data), - reinterpret_cast(iv_value.data)) != 1) - onError("EVP_DecryptInit_ex"); + if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, + reinterpret_cast(key_value.data), + getIVPtr(iv_value)) != 1) + onError("EVP_DecryptInit_ex"); + } + } - // 1.a.2: Set AAD if present + // 2: Feed the data to the cipher + if constexpr (mode == CipherMode::RFC5116_AEAD_AES_GCM) + { if (aad_column) { StringRef aad_data = aad_column->getDataAt(row_idx); @@ -657,18 +899,7 @@ class FunctionDecrypt : public IFunction onError("EVP_DecryptUpdate"); } } - else - { - // 1.b: Init CTX - validateIV(iv_value, iv_size); - if (EVP_DecryptInit_ex(evp_ctx, evp_cipher, nullptr, - reinterpret_cast(key_value.data), - reinterpret_cast(iv_value.data)) != 1) - onError("EVP_DecryptInit_ex"); - } - - // 2: Feed the data to the cipher int output_len = 0; if (EVP_DecryptUpdate(evp_ctx, reinterpret_cast(decrypted), &output_len, diff --git a/tests/broken_tests.yaml b/tests/broken_tests.yaml index cda3d3091d8c..4ebae2c285ba 100644 --- a/tests/broken_tests.yaml +++ b/tests/broken_tests.yaml @@ -60,6 +60,10 @@ - name: 00024_random_counters reason: INVESTIGATE - random timeout message: Timeout! Killing process group +- name: 03211_nested_json_merges + reason: KNOWN - Unstable upstream +- name: 03644_object_storage_correlated_subqueries + reason: KNOWN - Unstable in upstream 25.8. - name: test_storage_s3_queue/test_5.py::test_migration[1-s3queue_] reason: KNOWN - Sometimes fails due to test order message: 'Failed: Timeout >900.0s' @@ -168,8 +172,7 @@ reason: 'KNOWN: Occasional timeout' message: docker_compose_iceberg_hms_catalog.yml', '--verbose', 'up', '-d']' timed out after 300 seconds - name: test_keeper_memory_soft_limit/test.py::test_soft_limit_create - reason: 'INVESTIGATE: Out of Memory' - message: 'Coordination error: Out of Memory.' + reason: 'FIXME: Unstable in current version. Fixed in recent upstream.' # Regex rules should be ordered from most specific to least specific. # regex: true applies to name, message, and not_message fields, but not to reason or check_types fields.