diff --git a/cpp/src/gandiva/CMakeLists.txt b/cpp/src/gandiva/CMakeLists.txt index aabe4ec8bf70..54cd9977e285 100644 --- a/cpp/src/gandiva/CMakeLists.txt +++ b/cpp/src/gandiva/CMakeLists.txt @@ -65,6 +65,11 @@ set(SRC_FILES engine.cc date_utils.cc encrypt_utils.cc + encrypt_utils_gcm.cc + encrypt_mode_dispatcher.cc + encrypt_utils_cbc.cc + encrypt_utils_common.cc + encrypt_utils_ecb.cc expr_decomposer.cc expr_validator.cc expression.cc @@ -259,6 +264,10 @@ add_gandiva_test(internals-test annotator_test.cc tree_expr_test.cc encrypt_utils_test.cc + encrypt_utils_gcm_test.cc + encrypt_utils_cbc_test.cc + encrypt_utils_ecb_test.cc + encrypt_utils_common_test.cc expr_decomposer_test.cc exported_funcs_registry_test.cc expression_registry_test.cc diff --git a/cpp/src/gandiva/encrypt_mode_dispatcher.cc b/cpp/src/gandiva/encrypt_mode_dispatcher.cc new file mode 100644 index 000000000000..edfd3288bc3c --- /dev/null +++ b/cpp/src/gandiva/encrypt_mode_dispatcher.cc @@ -0,0 +1,138 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_mode_dispatcher.h" +#include +#include +#include +#include +#include "arrow/util/string.h" +#include "gandiva/encrypt_utils_cbc.h" +#include "gandiva/encrypt_utils_ecb.h" +#include "gandiva/encrypt_utils_gcm.h" + +namespace gandiva { + +namespace { + +// Supported encryption modes +const std::vector SUPPORTED_MODES = { + AES_ECB_MODE, AES_ECB_PKCS7_MODE, AES_ECB_NONE_MODE, AES_CBC_MODE, + AES_CBC_PKCS7_MODE, AES_CBC_NONE_MODE, AES_GCM_MODE}; + +// Internal enum for dispatching to the right encryption implementation +enum class EncryptionMode { + ECB, + ECB_PKCS7, + ECB_NONE, + CBC, + CBC_PKCS7, + CBC_NONE, + GCM, + UNKNOWN +}; + +EncryptionMode ParseEncryptionMode(std::string_view mode_str) { + if (mode_str == AES_ECB_MODE) return EncryptionMode::ECB; + if (mode_str == AES_ECB_PKCS7_MODE) return EncryptionMode::ECB_PKCS7; + if (mode_str == AES_ECB_NONE_MODE) return EncryptionMode::ECB_NONE; + if (mode_str == AES_CBC_MODE) return EncryptionMode::CBC; + if (mode_str == AES_CBC_PKCS7_MODE) return EncryptionMode::CBC_PKCS7; + if (mode_str == AES_CBC_NONE_MODE) return EncryptionMode::CBC_NONE; + if (mode_str == AES_GCM_MODE) return EncryptionMode::GCM; + return EncryptionMode::UNKNOWN; +} + +} // namespace + +int32_t EncryptModeDispatcher::encrypt(const char* plaintext, int32_t plaintext_len, + const char* key, int32_t key_len, const char* mode, + int32_t mode_len, const char* iv, int32_t iv_len, + const char* aad, int32_t aad_len, + unsigned char* cipher) { + std::string mode_str = arrow::internal::AsciiToUpper(std::string_view(mode, mode_len)); + + switch (ParseEncryptionMode(mode_str)) { + case EncryptionMode::ECB: + case EncryptionMode::ECB_PKCS7: + // Shorthand AES-ECB and explicit AES-ECB-PKCS7 both use ECB with PKCS7 padding + return aes_encrypt_ecb(plaintext, plaintext_len, key, key_len, true, cipher); + case EncryptionMode::ECB_NONE: + // ECB without padding + return aes_encrypt_ecb(plaintext, plaintext_len, key, key_len, false, cipher); + case EncryptionMode::CBC: + case EncryptionMode::CBC_PKCS7: + // Shorthand AES-CBC and explicit AES-CBC-PKCS7 both use CBC with PKCS7 + return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len, iv, iv_len, true, + cipher); + case EncryptionMode::CBC_NONE: + // CBC without padding + return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len, iv, iv_len, false, + cipher); + case EncryptionMode::GCM: + return aes_encrypt_gcm(plaintext, plaintext_len, key, key_len, iv, iv_len, aad, + aad_len, cipher); + case EncryptionMode::UNKNOWN: + default: { + std::string modes_str = arrow::internal::JoinStrings(SUPPORTED_MODES, ", "); + std::ostringstream oss; + oss << "Unsupported encryption mode: " << mode_str + << ". Supported modes: " << modes_str; + throw std::runtime_error(oss.str()); + } + } +} + +int32_t EncryptModeDispatcher::decrypt(const char* ciphertext, int32_t ciphertext_len, + const char* key, int32_t key_len, const char* mode, + int32_t mode_len, const char* iv, int32_t iv_len, + const char* aad, int32_t aad_len, + unsigned char* plaintext) { + std::string mode_str = arrow::internal::AsciiToUpper(std::string_view(mode, mode_len)); + + switch (ParseEncryptionMode(mode_str)) { + case EncryptionMode::ECB: + case EncryptionMode::ECB_PKCS7: + // Shorthand AES-ECB and explicit AES-ECB-PKCS7 both use ECB with PKCS7 padding + return aes_decrypt_ecb(ciphertext, ciphertext_len, key, key_len, true, plaintext); + case EncryptionMode::ECB_NONE: + // ECB without padding + return aes_decrypt_ecb(ciphertext, ciphertext_len, key, key_len, false, plaintext); + case EncryptionMode::CBC: + case EncryptionMode::CBC_PKCS7: + // Shorthand AES-CBC and explicit AES-CBC-PKCS7 both use CBC with PKCS7 + return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len, iv, iv_len, true, + plaintext); + case EncryptionMode::CBC_NONE: + // CBC without padding + return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len, iv, iv_len, false, + plaintext); + case EncryptionMode::GCM: + return aes_decrypt_gcm(ciphertext, ciphertext_len, key, key_len, iv, iv_len, aad, + aad_len, plaintext); + case EncryptionMode::UNKNOWN: + default: { + std::string modes_str = arrow::internal::JoinStrings(SUPPORTED_MODES, ", "); + std::ostringstream oss; + oss << "Unsupported decryption mode: " << mode_str + << ". Supported modes: " << modes_str; + throw std::runtime_error(oss.str()); + } + } +} + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_mode_dispatcher.h b/cpp/src/gandiva/encrypt_mode_dispatcher.h new file mode 100644 index 000000000000..be292539bc13 --- /dev/null +++ b/cpp/src/gandiva/encrypt_mode_dispatcher.h @@ -0,0 +1,77 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include + +namespace gandiva { + +/** + * Dispatcher for AES encryption/decryption based on mode string. + * Routes calls to appropriate implementation. + */ +class EncryptModeDispatcher { + public: + /** + * Encrypt data using the specified mode + * + * @param plaintext The data to encrypt + * @param plaintext_len Length of plaintext in bytes + * @param key The encryption key + * @param key_len Length of key in bytes + * @param mode Mode string + * @param mode_len Length of mode string in bytes + * @param iv The initialization vector (optional, only for modes that support it). + * It is caller-managed: GCM needs a nonce that is unique per key and CBC an + * unpredictable IV. + * @param iv_len Length of the IV in bytes + * @param aad Additional authenticated data (optional, only used by GCM) + * @param aad_len Length of aad in bytes + * @param cipher Output buffer for encrypted data + * @return Length of encrypted data in bytes + * @throws std::runtime_error on encryption failure or unsupported mode + */ + static int32_t encrypt(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, const char* mode, int32_t mode_len, + const char* iv, int32_t iv_len, const char* aad, int32_t aad_len, + unsigned char* cipher); + + /** + * Decrypt data using the specified mode + * + * @param ciphertext The data to decrypt + * @param ciphertext_len Length of ciphertext in bytes + * @param key The decryption key + * @param key_len Length of key in bytes + * @param mode Mode string + * @param mode_len Length of mode string in bytes + * @param iv The initialization vector (optional, only for modes that support it) + * @param iv_len Length of the IV in bytes + * @param aad Additional authenticated data (optional, only used by GCM) + * @param aad_len Length of aad in bytes + * @param plaintext Output buffer for decrypted data + * @return Length of decrypted data in bytes + * @throws std::runtime_error on decryption failure or unsupported mode + */ + static int32_t decrypt(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, const char* mode, int32_t mode_len, + const char* iv, int32_t iv_len, const char* aad, int32_t aad_len, + unsigned char* plaintext); +}; + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_cbc.cc b/cpp/src/gandiva/encrypt_utils_cbc.cc new file mode 100644 index 000000000000..0688924928e5 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_cbc.cc @@ -0,0 +1,170 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_cbc.h" +#include +#include +#include +#include +#include +#include +#include "gandiva/encrypt_utils_common.h" + +namespace gandiva { + +namespace { + +const EVP_CIPHER* get_cbc_cipher_algo(int32_t key_length) { + switch (key_length) { + case 16: + return EVP_aes_128_cbc(); + case 24: + return EVP_aes_192_cbc(); + case 32: + return EVP_aes_256_cbc(); + default: { + std::ostringstream oss; + oss << "Unsupported key length for AES-CBC: " << key_length + << " bytes. Supported lengths: 16, 24, 32 bytes"; + throw std::runtime_error(oss.str()); + } + } +} + +} // namespace + +GANDIVA_EXPORT +int32_t aes_encrypt_cbc(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, bool use_padding, + unsigned char* cipher) { + // Validate IV length + if (iv_len != 16) { + std::ostringstream oss; + oss << "Invalid IV length for AES-CBC: " << iv_len + << " bytes. IV must be exactly 16 bytes"; + throw std::runtime_error(oss.str()); + } + + int32_t cipher_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_cbc_cipher_algo(key_len); + EVP_CIPHER_CTX* en_ctx = EVP_CIPHER_CTX_new(); + + if (!en_ctx) { + throw std::runtime_error("Could not create EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + if (!EVP_EncryptInit_ex(en_ctx, cipher_algo, nullptr, + reinterpret_cast(key), + reinterpret_cast(iv))) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not initialize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + int padding_flag = use_padding ? 1 : 0; + if (!EVP_CIPHER_CTX_set_padding(en_ctx, padding_flag)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not set padding mode for encryption: " + + get_openssl_error_string()); + } + + if (!EVP_EncryptUpdate(en_ctx, cipher, &len, + reinterpret_cast(plaintext), + plaintext_len)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not update EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + if (!EVP_EncryptFinal_ex(en_ctx, cipher + len, &len)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not finalize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + EVP_CIPHER_CTX_free(en_ctx); + return cipher_len; +} + +GANDIVA_EXPORT +int32_t aes_decrypt_cbc(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, bool use_padding, + unsigned char* plaintext) { + // Validate IV length + if (iv_len != 16) { + std::ostringstream oss; + oss << "Invalid IV length for AES-CBC: " << iv_len + << " bytes. IV must be exactly 16 bytes"; + throw std::runtime_error(oss.str()); + } + + int32_t plaintext_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_cbc_cipher_algo(key_len); + EVP_CIPHER_CTX* de_ctx = EVP_CIPHER_CTX_new(); + + if (!de_ctx) { + throw std::runtime_error("Could not create EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + if (!EVP_DecryptInit_ex(de_ctx, cipher_algo, nullptr, + reinterpret_cast(key), + reinterpret_cast(iv))) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not initialize EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + int padding_flag = use_padding ? 1 : 0; + if (!EVP_CIPHER_CTX_set_padding(de_ctx, padding_flag)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not set padding mode for decryption: " + + get_openssl_error_string()); + } + + if (!EVP_DecryptUpdate(de_ctx, plaintext, &len, + reinterpret_cast(ciphertext), + ciphertext_len)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not update EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + plaintext_len += len; + + if (!EVP_DecryptFinal_ex(de_ctx, plaintext + len, &len)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not finalize EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + plaintext_len += len; + + EVP_CIPHER_CTX_free(de_ctx); + return plaintext_len; +} + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_cbc.h b/cpp/src/gandiva/encrypt_utils_cbc.h new file mode 100644 index 000000000000..37ef7dbeac90 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_cbc.h @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include +#include +#include "gandiva/visibility.h" + +namespace gandiva { + +// CBC mode identifiers +constexpr const char* AES_CBC_MODE = "AES-CBC"; +constexpr const char* AES_CBC_PKCS7_MODE = "AES-CBC-PKCS7"; +constexpr const char* AES_CBC_NONE_MODE = "AES-CBC-NONE"; + +/** + * Encrypt data using AES-CBC algorithm with explicit padding mode + * + * @param plaintext The data to encrypt + * @param plaintext_len Length of plaintext in bytes + * @param key The encryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param iv The initialization vector (must be exactly 16 bytes). It should be + * unpredictable (e.g. random) and need not be secret; a fixed IV makes the output + * deterministic for equal plaintexts. + * @param iv_len Length of IV in bytes (must be 16) + * @param use_padding Whether to use PKCS7 padding (true) or no padding (false) + * @param cipher Output buffer for encrypted data + * @return Length of encrypted data in bytes + * @throws std::runtime_error on encryption failure or invalid parameters + */ +GANDIVA_EXPORT +int32_t aes_encrypt_cbc(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, bool use_padding, + unsigned char* cipher); + +/** + * Decrypt data using AES-CBC algorithm with explicit padding mode + * + * @param ciphertext The data to decrypt + * @param ciphertext_len Length of ciphertext in bytes + * @param key The decryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param iv The initialization vector used for encryption (must be exactly 16 bytes) + * @param iv_len Length of IV in bytes (must be 16) + * @param use_padding Whether to use PKCS7 padding (true) or no padding (false) + * @param plaintext Output buffer for decrypted data + * @return Length of decrypted data in bytes + * @throws std::runtime_error on decryption failure or invalid parameters + */ +GANDIVA_EXPORT +int32_t aes_decrypt_cbc(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, bool use_padding, + unsigned char* plaintext); + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_cbc_test.cc b/cpp/src/gandiva/encrypt_utils_cbc_test.cc new file mode 100644 index 000000000000..fcd7eceb535c --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_cbc_test.cc @@ -0,0 +1,200 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_cbc.h" + +#include +#include +#include +#include +#include + +// Test PKCS#7 padding with 16-byte key +namespace { + +std::string FromHex(const std::string& hex) { + std::string bytes; + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + bytes.push_back(static_cast(std::stoi(hex.substr(i, 2), nullptr, 16))); + } + return bytes; +} + +// NIST SP 800-38A, AES-128 key and the four-block plaintext shared by F.1 and F.2 +const char kKey[] = "2b7e151628aed2a6abf7158809cf4f3c"; +const char kPlaintext[] = + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710"; + +} // namespace + +TEST(TestAesCbcEncryptUtils, TestAesEncryptDecryptPkcs7_16) { + auto* key = "12345678abcdefgh"; + auto* iv = "1234567890123456"; + auto* to_encrypt = "some test string"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + int32_t cipher_len = gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, true, cipher); + + unsigned char decrypted[64]; + int32_t decrypted_len = + gandiva::aes_decrypt_cbc(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, true, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test PKCS#7 padding with 24-byte key +TEST(TestAesCbcEncryptUtils, TestAesEncryptDecryptPkcs7_24) { + auto* key = "12345678abcdefgh12345678"; + auto* iv = "1234567890123456"; + auto* to_encrypt = "some\ntest\nstring"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + int32_t cipher_len = gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, true, cipher); + + unsigned char decrypted[64]; + int32_t decrypted_len = + gandiva::aes_decrypt_cbc(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, true, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test PKCS#7 padding with 32-byte key +TEST(TestAesCbcEncryptUtils, TestAesEncryptDecryptPkcs7_32) { + auto* key = "12345678abcdefgh12345678abcdefgh"; + auto* iv = "1234567890123456"; + auto* to_encrypt = "New\ntest\nstring"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + int32_t cipher_len = gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, true, cipher); + + unsigned char decrypted[64]; + int32_t decrypted_len = + gandiva::aes_decrypt_cbc(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, true, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test no-padding mode with block-aligned data (16 bytes) +TEST(TestAesCbcEncryptUtils, TestAesEncryptDecryptNoPadding_16) { + auto* key = "12345678abcdefgh"; + auto* iv = "1234567890123456"; + auto* to_encrypt = "1234567890123456"; // Exactly 16 bytes + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + int32_t cipher_len = gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, false, cipher); + + unsigned char decrypted[64]; + int32_t decrypted_len = + gandiva::aes_decrypt_cbc(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, false, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test invalid IV length +TEST(TestAesCbcEncryptUtils, TestInvalidIVLength) { + auto* key = "12345678abcdefgh"; + auto* iv = "short"; // Too short + auto* to_encrypt = "test"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + try { + gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, iv, iv_len, true, + cipher); + FAIL() << "Expected std::runtime_error"; + } catch (const std::runtime_error& e) { + EXPECT_THAT(e.what(), testing::HasSubstr("Invalid IV length for AES-CBC")); + } +} + +// Test invalid key length +TEST(TestAesCbcEncryptUtils, TestInvalidKeyLength) { + auto* key = "short"; // Too short + auto* iv = "1234567890123456"; + auto* to_encrypt = "test"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[64]; + + try { + gandiva::aes_encrypt_cbc(to_encrypt, to_encrypt_len, key, key_len, iv, iv_len, true, + cipher); + FAIL() << "Expected std::runtime_error"; + } catch (const std::runtime_error& e) { + EXPECT_THAT(e.what(), testing::HasSubstr("Unsupported key length for AES-CBC")); + } +} + +// NIST SP 800-38A, F.2.1 (CBC-AES128.Encrypt) and F.2.2 (CBC-AES128.Decrypt) +TEST(TestAesCbcEncryptUtils, TestKnownAnswerVectors) { + const std::string key = FromHex(kKey); + const std::string iv = FromHex("000102030405060708090a0b0c0d0e0f"); + const std::string plain = FromHex(kPlaintext); + const std::string expected = FromHex( + "7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2" + "73bed6b8e3c1743b7116e69e222295163ff1caa1681fac09120eca307586e1a7"); + + std::string cipher(plain.size() + 16, '\0'); + const int32_t cipher_len = gandiva::aes_encrypt_cbc( + plain.data(), static_cast(plain.size()), key.data(), + static_cast(key.size()), iv.data(), static_cast(iv.size()), false, + reinterpret_cast(cipher.data())); + ASSERT_EQ(cipher_len, static_cast(expected.size())); + EXPECT_EQ(cipher.substr(0, cipher_len), expected); + + std::string decrypted(expected.size() + 16, '\0'); + const int32_t decrypted_len = gandiva::aes_decrypt_cbc( + expected.data(), static_cast(expected.size()), key.data(), + static_cast(key.size()), iv.data(), static_cast(iv.size()), false, + reinterpret_cast(decrypted.data())); + ASSERT_EQ(decrypted_len, static_cast(plain.size())); + EXPECT_EQ(decrypted.substr(0, decrypted_len), plain); +} diff --git a/cpp/src/gandiva/encrypt_utils_common.cc b/cpp/src/gandiva/encrypt_utils_common.cc new file mode 100644 index 000000000000..f2525a45f799 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_common.cc @@ -0,0 +1,44 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_common.h" +#include +#include +#include + +namespace gandiva { + +std::string get_openssl_error_string() { + std::string error_string; + char error_buffer[256]; + + // Loop through all errors in the queue + while (auto error_code = ERR_get_error()) { + if (!error_string.empty()) { + error_string += "; "; + } + ERR_error_string(error_code, error_buffer); + error_string += std::string(error_buffer); + } + + if (error_string.empty()) { + return "Unknown OpenSSL error"; + } + return error_string; +} + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_common.h b/cpp/src/gandiva/encrypt_utils_common.h new file mode 100644 index 000000000000..1ec33a6477a4 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_common.h @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include + +#include "gandiva/visibility.h" + +namespace gandiva { + +/// @brief Get a human-readable error string from OpenSSL's error queue. +/// @details Retrieves all errors from the OpenSSL error queue and concatenates them +/// with "; " as a separator. This ensures complete error information is +/// captured. +/// @return A string describing all OpenSSL errors in the queue, or "Unknown OpenSSL +/// error" +/// if no error is available. +GANDIVA_EXPORT +std::string get_openssl_error_string(); + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_common_test.cc b/cpp/src/gandiva/encrypt_utils_common_test.cc new file mode 100644 index 000000000000..d91fc9c7f169 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_common_test.cc @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_common.h" + +#include +#include +#include +#include +#include + +namespace { + +// ERR_raise() only exists since OpenSSL 3.0 +void RaiseOpenSSLError(int reason) { +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + ERR_raise(ERR_LIB_EVP, reason); +#else + ERR_put_error(ERR_LIB_EVP, 0, reason, __FILE__, __LINE__); +#endif +} + +} // namespace + +// Test that get_openssl_error_string returns "Unknown OpenSSL error" when queue is empty +TEST(TestOpenSSLErrorUtils, TestEmptyErrorQueue) { + // Clear any existing errors + ERR_clear_error(); + + std::string error_string = gandiva::get_openssl_error_string(); + + EXPECT_EQ(error_string, "Unknown OpenSSL error"); +} + +// Test that get_openssl_error_string captures a single error +TEST(TestOpenSSLErrorUtils, TestSingleError) { + // Clear any existing errors + ERR_clear_error(); + + // Add a single error to the queue + RaiseOpenSSLError(EVP_R_UNSUPPORTED_ALGORITHM); + + std::string error_string = gandiva::get_openssl_error_string(); + + // Verify that the error string is not empty and not the default message + EXPECT_NE(error_string, "Unknown OpenSSL error"); + EXPECT_GT(error_string.length(), 0); +} + +// Test that get_openssl_error_string captures multiple errors +TEST(TestOpenSSLErrorUtils, TestMultipleErrors) { + // Clear any existing errors + ERR_clear_error(); + + // Populate the OpenSSL error queue with multiple errors + RaiseOpenSSLError(EVP_R_UNSUPPORTED_ALGORITHM); + RaiseOpenSSLError(EVP_R_INVALID_KEY_LENGTH); + RaiseOpenSSLError(EVP_R_INVALID_OPERATION); + + // Call our function to get all errors + std::string error_string = gandiva::get_openssl_error_string(); + + // Verify that the error string is not empty + EXPECT_NE(error_string, "Unknown OpenSSL error"); + + // Verify that all errors are captured (they should be separated by "; ") + // The exact error messages depend on OpenSSL version, so we just check + // that we got multiple errors (indicated by the separator) + EXPECT_THAT(error_string, testing::HasSubstr(";")); + + // Verify the error string contains meaningful content (not just separators) + EXPECT_GT(error_string.length(), 10); +} + +// Test that error queue is properly drained after calling get_openssl_error_string +TEST(TestOpenSSLErrorUtils, TestErrorQueueDrained) { + // Clear any existing errors + ERR_clear_error(); + + // Add errors to the queue + RaiseOpenSSLError(EVP_R_UNSUPPORTED_ALGORITHM); + RaiseOpenSSLError(EVP_R_INVALID_KEY_LENGTH); + + // Call our function to get all errors + std::string error_string = gandiva::get_openssl_error_string(); + + // Verify we got errors + EXPECT_NE(error_string, "Unknown OpenSSL error"); + + // Now call it again - the queue should be empty + std::string second_call = gandiva::get_openssl_error_string(); + + EXPECT_EQ(second_call, "Unknown OpenSSL error"); +} diff --git a/cpp/src/gandiva/encrypt_utils_ecb.cc b/cpp/src/gandiva/encrypt_utils_ecb.cc new file mode 100644 index 000000000000..36911abd33ec --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_ecb.cc @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_ecb.h" +#include +#include +#include +#include +#include +#include "gandiva/encrypt_utils_common.h" + +namespace gandiva { + +namespace { + +const EVP_CIPHER* get_ecb_cipher_algo(int32_t key_length) { + switch (key_length) { + case 16: + return EVP_aes_128_ecb(); + case 24: + return EVP_aes_192_ecb(); + case 32: + return EVP_aes_256_ecb(); + default: { + std::ostringstream oss; + oss << "Unsupported key length for AES-ECB: " << key_length + << " bytes. Supported lengths: 16, 24, 32 bytes"; + throw std::runtime_error(oss.str()); + } + } +} + +} // namespace + +GANDIVA_EXPORT +int32_t aes_encrypt_ecb(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, bool use_padding, unsigned char* cipher) { + int32_t cipher_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_ecb_cipher_algo(key_len); + EVP_CIPHER_CTX* en_ctx = EVP_CIPHER_CTX_new(); + + if (!en_ctx) { + throw std::runtime_error("Could not create EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + if (!EVP_EncryptInit_ex(en_ctx, cipher_algo, nullptr, + reinterpret_cast(key), nullptr)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not initialize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + int padding_flag = use_padding ? 1 : 0; + if (!EVP_CIPHER_CTX_set_padding(en_ctx, padding_flag)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not set padding mode for encryption: " + + get_openssl_error_string()); + } + + if (!EVP_EncryptUpdate(en_ctx, cipher, &len, + reinterpret_cast(plaintext), + plaintext_len)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not update EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + if (!EVP_EncryptFinal_ex(en_ctx, cipher + len, &len)) { + EVP_CIPHER_CTX_free(en_ctx); + throw std::runtime_error("Could not finalize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + EVP_CIPHER_CTX_free(en_ctx); + return cipher_len; +} + +GANDIVA_EXPORT +int32_t aes_decrypt_ecb(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, bool use_padding, unsigned char* plaintext) { + int32_t plaintext_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_ecb_cipher_algo(key_len); + EVP_CIPHER_CTX* de_ctx = EVP_CIPHER_CTX_new(); + + if (!de_ctx) { + throw std::runtime_error("Could not create EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + if (!EVP_DecryptInit_ex(de_ctx, cipher_algo, nullptr, + reinterpret_cast(key), nullptr)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not initialize EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + int padding_flag = use_padding ? 1 : 0; + if (!EVP_CIPHER_CTX_set_padding(de_ctx, padding_flag)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not set padding mode for decryption: " + + get_openssl_error_string()); + } + + if (!EVP_DecryptUpdate(de_ctx, plaintext, &len, + reinterpret_cast(ciphertext), + ciphertext_len)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not update EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + plaintext_len += len; + + if (!EVP_DecryptFinal_ex(de_ctx, plaintext + len, &len)) { + EVP_CIPHER_CTX_free(de_ctx); + throw std::runtime_error("Could not finalize EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + plaintext_len += len; + + EVP_CIPHER_CTX_free(de_ctx); + return plaintext_len; +} + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_ecb.h b/cpp/src/gandiva/encrypt_utils_ecb.h new file mode 100644 index 000000000000..af4a7a7c85bd --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_ecb.h @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include +#include +#include "gandiva/visibility.h" + +namespace gandiva { + +// ECB mode identifiers +constexpr const char* AES_ECB_MODE = "AES-ECB"; +constexpr const char* AES_ECB_PKCS7_MODE = "AES-ECB-PKCS7"; +constexpr const char* AES_ECB_NONE_MODE = "AES-ECB-NONE"; + +/** + * Encrypt data using AES-ECB algorithm (legacy, insecure) + * + * WARNING: ECB mode is deterministic and should not be used for sensitive data. + * Use other encryption modes for better security. + * + * @param plaintext The data to encrypt + * @param plaintext_len Length of plaintext in bytes + * @param key The encryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param use_padding Whether to use PKCS7 padding (true) or no padding (false) + * @param cipher Output buffer for encrypted data + * @return Length of encrypted data in bytes + * @throws std::runtime_error on encryption failure + */ +GANDIVA_EXPORT +int32_t aes_encrypt_ecb(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, bool use_padding, unsigned char* cipher); + +/** + * Decrypt data using AES-ECB algorithm (legacy, insecure) + * + * WARNING: ECB mode is deterministic and should not be used for sensitive data. + * Use other encryption modes for better security. + * + * @param ciphertext The data to decrypt + * @param ciphertext_len Length of ciphertext in bytes + * @param key The decryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param use_padding Whether to use PKCS7 padding (true) or no padding (false) + * @param plaintext Output buffer for decrypted data + * @return Length of decrypted data in bytes + * @throws std::runtime_error on decryption failure + */ +GANDIVA_EXPORT +int32_t aes_decrypt_ecb(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, bool use_padding, unsigned char* plaintext); + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_ecb_test.cc b/cpp/src/gandiva/encrypt_utils_ecb_test.cc new file mode 100644 index 000000000000..bf1cbb741322 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_ecb_test.cc @@ -0,0 +1,169 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_ecb.h" + +#include +#include +#include +#include + +namespace { + +std::string FromHex(const std::string& hex) { + std::string bytes; + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + bytes.push_back(static_cast(std::stoi(hex.substr(i, 2), nullptr, 16))); + } + return bytes; +} + +// NIST SP 800-38A, AES-128 key and the four-block plaintext shared by F.1 and F.2 +const char kKey[] = "2b7e151628aed2a6abf7158809cf4f3c"; +const char kPlaintext[] = + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710"; + +} // namespace + +TEST(TestAesEcbEncryptUtils, TestAesEncryptDecrypt) { + // 16 bytes key + auto* key = "12345678abcdefgh"; + auto* to_encrypt = "some test string"; + + auto key_len = static_cast(strlen(reinterpret_cast(key))); + auto to_encrypt_len = + static_cast(strlen(reinterpret_cast(to_encrypt))); + unsigned char cipher_1[64]; + + int32_t cipher_1_len = + gandiva::aes_encrypt_ecb(to_encrypt, to_encrypt_len, key, key_len, true, cipher_1); + + unsigned char decrypted_1[64]; + int32_t decrypted_1_len = + gandiva::aes_decrypt_ecb(reinterpret_cast(cipher_1), cipher_1_len, key, + key_len, true, decrypted_1); + + EXPECT_EQ(std::string(reinterpret_cast(to_encrypt), to_encrypt_len), + std::string(reinterpret_cast(decrypted_1), decrypted_1_len)); + + // 24 bytes key + key = "12345678abcdefgh12345678"; + to_encrypt = "some\ntest\nstring"; + + key_len = static_cast(strlen(reinterpret_cast(key))); + to_encrypt_len = + static_cast(strlen(reinterpret_cast(to_encrypt))); + unsigned char cipher_2[64]; + + int32_t cipher_2_len = + gandiva::aes_encrypt_ecb(to_encrypt, to_encrypt_len, key, key_len, true, cipher_2); + + unsigned char decrypted_2[64]; + int32_t decrypted_2_len = + gandiva::aes_decrypt_ecb(reinterpret_cast(cipher_2), cipher_2_len, key, + key_len, true, decrypted_2); + + EXPECT_EQ(std::string(reinterpret_cast(to_encrypt), to_encrypt_len), + std::string(reinterpret_cast(decrypted_2), decrypted_2_len)); + + // 32 bytes key + key = "12345678abcdefgh12345678abcdefgh"; + to_encrypt = "New\ntest\nstring"; + + key_len = static_cast(strlen(reinterpret_cast(key))); + to_encrypt_len = + static_cast(strlen(reinterpret_cast(to_encrypt))); + unsigned char cipher_3[64]; + + int32_t cipher_3_len = + gandiva::aes_encrypt_ecb(to_encrypt, to_encrypt_len, key, key_len, true, cipher_3); + + unsigned char decrypted_3[64]; + int32_t decrypted_3_len = + gandiva::aes_decrypt_ecb(reinterpret_cast(cipher_3), cipher_3_len, key, + key_len, true, decrypted_3); + + EXPECT_EQ(std::string(reinterpret_cast(to_encrypt), to_encrypt_len), + std::string(reinterpret_cast(decrypted_3), decrypted_3_len)); + + // check exception + char cipher[64] = "JBB7oJAQuqhDCx01fvBRi8PcljW1+nbnOSMk+R0Sz7E=="; + int32_t cipher_len = + static_cast(strlen(reinterpret_cast(cipher))); + unsigned char plain_text[64]; + + key = "12345678abcdefgh12345678abcdefgh12345678abcdefgh12345678abcdefgh"; + to_encrypt = "New\ntest\nstring"; + + key_len = static_cast(strlen(reinterpret_cast(key))); + to_encrypt_len = + static_cast(strlen(reinterpret_cast(to_encrypt))); + unsigned char cipher_4[64]; + ASSERT_THROW( + { + gandiva::aes_encrypt_ecb(to_encrypt, to_encrypt_len, key, key_len, true, + cipher_4); + }, + std::runtime_error); + + ASSERT_THROW( + { gandiva::aes_decrypt_ecb(cipher, cipher_len, key, key_len, true, plain_text); }, + std::runtime_error); + + key = "12345678"; + to_encrypt = "New\ntest\nstring"; + + key_len = static_cast(strlen(reinterpret_cast(key))); + to_encrypt_len = + static_cast(strlen(reinterpret_cast(to_encrypt))); + unsigned char cipher_5[64]; + ASSERT_THROW( + { + gandiva::aes_encrypt_ecb(to_encrypt, to_encrypt_len, key, key_len, true, + cipher_5); + }, + std::runtime_error); + ASSERT_THROW( + { gandiva::aes_decrypt_ecb(cipher, cipher_len, key, key_len, true, plain_text); }, + std::runtime_error); +} + +// NIST SP 800-38A, F.1.1 (ECB-AES128.Encrypt) and F.1.2 (ECB-AES128.Decrypt) +TEST(TestAesEcbEncryptUtils, TestKnownAnswerVectors) { + const std::string key = FromHex(kKey); + const std::string plain = FromHex(kPlaintext); + const std::string expected = FromHex( + "3ad77bb40d7a3660a89ecaf32466ef97f5d3d58503b9699de785895a96fdbaaf" + "43b1cd7f598ece23881b00e3ed0306887b0c785e27e8ad3f8223207104725dd4"); + + std::string cipher(plain.size() + 16, '\0'); + const int32_t cipher_len = + gandiva::aes_encrypt_ecb(plain.data(), static_cast(plain.size()), + key.data(), static_cast(key.size()), false, + reinterpret_cast(cipher.data())); + ASSERT_EQ(cipher_len, static_cast(expected.size())); + EXPECT_EQ(cipher.substr(0, cipher_len), expected); + + std::string decrypted(expected.size() + 16, '\0'); + const int32_t decrypted_len = + gandiva::aes_decrypt_ecb(expected.data(), static_cast(expected.size()), + key.data(), static_cast(key.size()), false, + reinterpret_cast(decrypted.data())); + ASSERT_EQ(decrypted_len, static_cast(plain.size())); + EXPECT_EQ(decrypted.substr(0, decrypted_len), plain); +} diff --git a/cpp/src/gandiva/encrypt_utils_gcm.cc b/cpp/src/gandiva/encrypt_utils_gcm.cc new file mode 100644 index 000000000000..d660569a3080 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_gcm.cc @@ -0,0 +1,224 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_gcm.h" +#include +#include +#include +#include +#include +#include "gandiva/encrypt_utils_common.h" + +namespace gandiva { + +namespace { + +const EVP_CIPHER* get_gcm_cipher_algo(int32_t key_length) { + switch (key_length) { + case 16: + return EVP_aes_128_gcm(); + case 24: + return EVP_aes_192_gcm(); + case 32: + return EVP_aes_256_gcm(); + default: { + std::ostringstream oss; + oss << "Unsupported key length for AES-GCM: " << key_length + << " bytes. Supported lengths: 16, 24, 32 bytes"; + throw std::runtime_error(oss.str()); + } + } +} + +} // namespace + +GANDIVA_EXPORT +int32_t aes_encrypt_gcm(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, const char* aad, + int32_t aad_len, unsigned char* cipher) { + if (iv_len <= 0) { + throw std::runtime_error( + "Invalid IV length for AES-GCM: IV length must be greater than 0"); + } + + int32_t cipher_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_gcm_cipher_algo(key_len); + EVP_CIPHER_CTX* en_ctx = EVP_CIPHER_CTX_new(); + + if (!en_ctx) { + throw std::runtime_error("Could not create EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + try { + if (!EVP_EncryptInit_ex(en_ctx, cipher_algo, nullptr, nullptr, nullptr)) { + throw std::runtime_error( + "Could not initialize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + // The IV length must be set before the IV itself is supplied; otherwise OpenSSL + // consumes only the default 12 bytes of the IV and ignores the rest. + if (!EVP_CIPHER_CTX_ctrl(en_ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) { + throw std::runtime_error("Could not set GCM IV length: " + + get_openssl_error_string()); + } + + if (!EVP_EncryptInit_ex(en_ctx, nullptr, nullptr, + reinterpret_cast(key), + reinterpret_cast(iv))) { + throw std::runtime_error("Could not set key and IV for encryption: " + + get_openssl_error_string()); + } + + // Process AAD if provided + if (aad != nullptr && aad_len > 0) { + if (!EVP_EncryptUpdate(en_ctx, nullptr, &len, + reinterpret_cast(aad), aad_len)) { + throw std::runtime_error("Could not process AAD for encryption: " + + get_openssl_error_string()); + } + } + + // Encrypt plaintext + if (!EVP_EncryptUpdate(en_ctx, cipher, &len, + reinterpret_cast(plaintext), + plaintext_len)) { + throw std::runtime_error("Could not update EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + // Finalize encryption + if (!EVP_EncryptFinal_ex(en_ctx, cipher + len, &len)) { + throw std::runtime_error("Could not finalize EVP cipher context for encryption: " + + get_openssl_error_string()); + } + + cipher_len += len; + + // Get the authentication tag and append it to ciphertext + if (!EVP_CIPHER_CTX_ctrl(en_ctx, EVP_CTRL_GCM_GET_TAG, GCM_TAG_LENGTH, + cipher + cipher_len)) { + throw std::runtime_error("Could not get GCM authentication tag: " + + get_openssl_error_string()); + } + cipher_len += GCM_TAG_LENGTH; + } catch (...) { + EVP_CIPHER_CTX_free(en_ctx); + throw; + } + + EVP_CIPHER_CTX_free(en_ctx); + return cipher_len; +} + +GANDIVA_EXPORT +int32_t aes_decrypt_gcm(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, const char* aad, + int32_t aad_len, unsigned char* plaintext) { + if (iv_len <= 0) { + throw std::runtime_error( + "Invalid IV length for AES-GCM: IV length must be greater than 0"); + } + + if (ciphertext_len < GCM_TAG_LENGTH) { + throw std::runtime_error( + "Ciphertext too short for AES-GCM: must be at least 16 bytes for tag"); + } + + int32_t plaintext_len = 0; + int32_t len = 0; + // Resolve the algorithm first: it throws on an invalid key length and must not leak ctx + const EVP_CIPHER* cipher_algo = get_gcm_cipher_algo(key_len); + EVP_CIPHER_CTX* de_ctx = EVP_CIPHER_CTX_new(); + + if (!de_ctx) { + throw std::runtime_error("Could not create EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + try { + if (!EVP_DecryptInit_ex(de_ctx, cipher_algo, nullptr, nullptr, nullptr)) { + throw std::runtime_error( + "Could not initialize EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + // The IV length must be set before the IV itself is supplied (see encryption). + if (!EVP_CIPHER_CTX_ctrl(de_ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) { + throw std::runtime_error("Could not set GCM IV length: " + + get_openssl_error_string()); + } + + if (!EVP_DecryptInit_ex(de_ctx, nullptr, nullptr, + reinterpret_cast(key), + reinterpret_cast(iv))) { + throw std::runtime_error("Could not set key and IV for decryption: " + + get_openssl_error_string()); + } + + // Process AAD if provided + if (aad != nullptr && aad_len > 0) { + if (!EVP_DecryptUpdate(de_ctx, nullptr, &len, + reinterpret_cast(aad), aad_len)) { + throw std::runtime_error("Could not process AAD for decryption: " + + get_openssl_error_string()); + } + } + + // Extract tag from end of ciphertext + int32_t actual_ciphertext_len = ciphertext_len - GCM_TAG_LENGTH; + const unsigned char* tag = + reinterpret_cast(ciphertext + actual_ciphertext_len); + + // Set the authentication tag + if (!EVP_CIPHER_CTX_ctrl(de_ctx, EVP_CTRL_GCM_SET_TAG, GCM_TAG_LENGTH, + const_cast(tag))) { + throw std::runtime_error("Could not set GCM authentication tag: " + + get_openssl_error_string()); + } + + // Decrypt ciphertext + if (!EVP_DecryptUpdate(de_ctx, plaintext, &len, + reinterpret_cast(ciphertext), + actual_ciphertext_len)) { + throw std::runtime_error("Could not update EVP cipher context for decryption: " + + get_openssl_error_string()); + } + + plaintext_len += len; + + // Finalize decryption (this verifies the tag) + if (!EVP_DecryptFinal_ex(de_ctx, plaintext + len, &len)) { + throw std::runtime_error("GCM tag verification failed or decryption error: " + + get_openssl_error_string()); + } + plaintext_len += len; + } catch (...) { + EVP_CIPHER_CTX_free(de_ctx); + throw; + } + + EVP_CIPHER_CTX_free(de_ctx); + return plaintext_len; +} + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_gcm.h b/cpp/src/gandiva/encrypt_utils_gcm.h new file mode 100644 index 000000000000..2be309893eb6 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_gcm.h @@ -0,0 +1,76 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include +#include +#include "gandiva/visibility.h" + +namespace gandiva { + +// GCM mode identifier +constexpr const char* AES_GCM_MODE = "AES-GCM"; + +// GCM authentication tag length in bytes +constexpr int32_t GCM_TAG_LENGTH = 16; + +/** + * Encrypt data using AES-GCM algorithm + * + * @param plaintext The data to encrypt + * @param plaintext_len Length of plaintext in bytes + * @param key The encryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param iv The initialization vector (variable length, typically 12 bytes). It must + * be unique for every encryption under the same key: reusing a nonce with GCM + * breaks both confidentiality and authenticity. + * @param iv_len Length of IV in bytes + * @param aad Optional additional authenticated data (can be null) + * @param aad_len Length of AAD in bytes (0 if aad is null) + * @param cipher Output buffer for encrypted data (must be at least plaintext_len + 16 + * bytes) + * @return Length of encrypted data in bytes (plaintext_len + 16 for the tag) + * @throws std::runtime_error on encryption failure or invalid parameters + */ +GANDIVA_EXPORT +int32_t aes_encrypt_gcm(const char* plaintext, int32_t plaintext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, const char* aad, + int32_t aad_len, unsigned char* cipher); + +/** + * Decrypt data using AES-GCM algorithm + * + * @param ciphertext The data to decrypt (includes 16-byte authentication tag at the end) + * @param ciphertext_len Length of ciphertext in bytes (includes tag) + * @param key The decryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys) + * @param key_len Length of key in bytes + * @param iv The initialization vector used for encryption (variable length) + * @param iv_len Length of IV in bytes + * @param aad Optional additional authenticated data (can be null) + * @param aad_len Length of AAD in bytes (0 if aad is null) + * @param plaintext Output buffer for decrypted data + * @return Length of decrypted data in bytes (ciphertext_len - 16) + * @throws std::runtime_error on decryption failure, invalid parameters, or tag + * verification failure + */ +GANDIVA_EXPORT +int32_t aes_decrypt_gcm(const char* ciphertext, int32_t ciphertext_len, const char* key, + int32_t key_len, const char* iv, int32_t iv_len, const char* aad, + int32_t aad_len, unsigned char* plaintext); + +} // namespace gandiva diff --git a/cpp/src/gandiva/encrypt_utils_gcm_test.cc b/cpp/src/gandiva/encrypt_utils_gcm_test.cc new file mode 100644 index 000000000000..916340358b00 --- /dev/null +++ b/cpp/src/gandiva/encrypt_utils_gcm_test.cc @@ -0,0 +1,271 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gandiva/encrypt_utils_gcm.h" + +#include +#include + +#include +#include +#include + +// Test IV-only GCM with 16-byte key +TEST(TestAesGcmEncryptUtils, TestAesEncryptDecryptIvOnly_16) { + auto* key = "12345678abcdefgh"; + auto* iv = "123456789012"; // 12-byte IV + auto* to_encrypt = "some test string"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[128]; + + int32_t cipher_len = gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, nullptr, 0, cipher); + + // Ciphertext should be plaintext_len + 16 (tag) + EXPECT_EQ(cipher_len, to_encrypt_len + 16); + + unsigned char decrypted[128]; + int32_t decrypted_len = + gandiva::aes_decrypt_gcm(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, nullptr, 0, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test IV + AAD GCM with 16-byte key +TEST(TestAesGcmEncryptUtils, TestAesEncryptDecryptWithAad_16) { + auto* key = "12345678abcdefgh"; + auto* iv = "123456789012"; + auto* to_encrypt = "some test string"; + auto* aad = "additional authenticated data"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + auto aad_len = static_cast(strlen(aad)); + unsigned char cipher[128]; + + int32_t cipher_len = gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, aad, aad_len, cipher); + + EXPECT_EQ(cipher_len, to_encrypt_len + 16); + + unsigned char decrypted[128]; + int32_t decrypted_len = + gandiva::aes_decrypt_gcm(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, aad, aad_len, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test IV-only GCM with 24-byte key +TEST(TestAesGcmEncryptUtils, TestAesEncryptDecryptIvOnly_24) { + auto* key = "12345678abcdefgh12345678"; + auto* iv = "123456789012"; + auto* to_encrypt = "test data"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[128]; + + int32_t cipher_len = gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, nullptr, 0, cipher); + + unsigned char decrypted[128]; + int32_t decrypted_len = + gandiva::aes_decrypt_gcm(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, nullptr, 0, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test IV-only GCM with 32-byte key +TEST(TestAesGcmEncryptUtils, TestAesEncryptDecryptIvOnly_32) { + auto* key = "12345678abcdefgh12345678abcdefgh"; + auto* iv = "123456789012"; + auto* to_encrypt = "another test"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[128]; + + int32_t cipher_len = gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, nullptr, 0, cipher); + + unsigned char decrypted[128]; + int32_t decrypted_len = + gandiva::aes_decrypt_gcm(reinterpret_cast(cipher), cipher_len, key, + key_len, iv, iv_len, nullptr, 0, decrypted); + + EXPECT_EQ(std::string(to_encrypt, to_encrypt_len), + std::string(reinterpret_cast(decrypted), decrypted_len)); +} + +// Test tag verification failure +TEST(TestAesGcmEncryptUtils, TestTagVerificationFailure) { + auto* key = "12345678abcdefgh"; + auto* iv = "123456789012"; + auto* to_encrypt = "some test string"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[128]; + + int32_t cipher_len = gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, + iv, iv_len, nullptr, 0, cipher); + + // Corrupt the tag (last byte) + cipher[cipher_len - 1] ^= 0xFF; + + unsigned char decrypted[128]; + EXPECT_THROW(gandiva::aes_decrypt_gcm(reinterpret_cast(cipher), cipher_len, + key, key_len, iv, iv_len, nullptr, 0, decrypted), + std::runtime_error); +} + +// Test invalid IV length +TEST(TestAesGcmEncryptUtils, TestInvalidIvLength) { + auto* key = "12345678abcdefgh"; + auto* iv = ""; // Empty IV + auto* to_encrypt = "some test string"; + + auto key_len = static_cast(strlen(key)); + auto iv_len = static_cast(strlen(iv)); + auto to_encrypt_len = static_cast(strlen(to_encrypt)); + unsigned char cipher[128]; + + EXPECT_THROW(gandiva::aes_encrypt_gcm(to_encrypt, to_encrypt_len, key, key_len, iv, + iv_len, nullptr, 0, cipher), + std::runtime_error); +} + +namespace { + +std::string FromHex(const std::string& hex) { + std::string bytes; + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + bytes.push_back(static_cast(std::stoi(hex.substr(i, 2), nullptr, 16))); + } + return bytes; +} + +// Known-answer vectors from "The Galois/Counter Mode of Operation (GCM)" (McGrew and +// Viega), test cases 4-6. They share key, plaintext and AAD, and differ in IV length. +struct GcmVector { + const char* iv; + const char* ciphertext; + const char* tag; +}; + +const char kGcmKey[] = "feffe9928665731c6d6a8f9467308308"; +const char kGcmPlaintext[] = + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72" + "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"; +const char kGcmAad[] = "feedfacedeadbeeffeedfacedeadbeefabaddad2"; + +const GcmVector kGcmVectors[] = { + // 12-byte IV + {"cafebabefacedbaddecaf888", + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e" + "21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", + "5bc94fbc3221a5db94fae95ae7121a47"}, + // 8-byte IV + {"cafebabefacedbad", + "61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c7423" + "73806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598", + "3612d2e79e3b0785561be14aaca2fccb"}, + // 60-byte IV: only produces the right output if the whole IV is honoured + {"9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728" + "c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + "8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca7" + "01e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5", + "619cc5aefffe0bfa462af43c1699d050"}, +}; + +} // namespace + +// Interoperability: output must match published GCM vectors for every IV length +TEST(TestAesGcmEncryptUtils, TestKnownAnswerVectors) { + const std::string key = FromHex(kGcmKey); + const std::string plaintext = FromHex(kGcmPlaintext); + const std::string aad = FromHex(kGcmAad); + + for (const auto& vec : kGcmVectors) { + SCOPED_TRACE(std::string("IV: ") + vec.iv); + const std::string iv = FromHex(vec.iv); + const std::string expected = FromHex(vec.ciphertext) + FromHex(vec.tag); + + std::string cipher(plaintext.size() + gandiva::GCM_TAG_LENGTH, '\0'); + int32_t cipher_len = gandiva::aes_encrypt_gcm( + plaintext.data(), static_cast(plaintext.size()), key.data(), + static_cast(key.size()), iv.data(), static_cast(iv.size()), + aad.data(), static_cast(aad.size()), + reinterpret_cast(cipher.data())); + ASSERT_EQ(cipher_len, static_cast(expected.size())); + EXPECT_EQ(cipher, expected); + + std::string decrypted(expected.size(), '\0'); + int32_t decrypted_len = gandiva::aes_decrypt_gcm( + expected.data(), static_cast(expected.size()), key.data(), + static_cast(key.size()), iv.data(), static_cast(iv.size()), + aad.data(), static_cast(aad.size()), + reinterpret_cast(decrypted.data())); + ASSERT_EQ(decrypted_len, static_cast(plaintext.size())); + EXPECT_EQ(decrypted.substr(0, decrypted_len), plaintext); + } +} + +// Every byte of a long IV must influence the output (regression: IV bytes beyond the +// default 12 were silently ignored) +TEST(TestAesGcmEncryptUtils, TestLongIvIsFullyUsed) { + const std::string key = FromHex(kGcmKey); + const std::string plaintext = "some test string"; + std::string iv_a(16, 'a'); + std::string iv_b = iv_a; + iv_b[15] = 'b'; // differs only past the 12th byte + + auto encrypt = [&](const std::string& iv) { + std::string cipher(plaintext.size() + gandiva::GCM_TAG_LENGTH, '\0'); + gandiva::aes_encrypt_gcm(plaintext.data(), static_cast(plaintext.size()), + key.data(), static_cast(key.size()), iv.data(), + static_cast(iv.size()), nullptr, 0, + reinterpret_cast(cipher.data())); + return cipher; + }; + EXPECT_NE(encrypt(iv_a), encrypt(iv_b)); +} + +// An invalid key length must be reported as an error for both directions +TEST(TestAesGcmEncryptUtils, TestInvalidKeyLength) { + const std::string iv(12, 'i'); + unsigned char out[64]; + EXPECT_THROW( + gandiva::aes_encrypt_gcm("data", 4, "short", 5, iv.data(), 12, nullptr, 0, out), + std::runtime_error); + EXPECT_THROW(gandiva::aes_decrypt_gcm(std::string(32, 'x').data(), 32, "short", 5, + iv.data(), 12, nullptr, 0, out), + std::runtime_error); +} diff --git a/cpp/src/gandiva/function_registry_string.cc b/cpp/src/gandiva/function_registry_string.cc index 35938cb82908..08a82f43315e 100644 --- a/cpp/src/gandiva/function_registry_string.cc +++ b/cpp/src/gandiva/function_registry_string.cc @@ -509,6 +509,34 @@ std::vector GetStringFunctionRegistry() { kResultNullIfNull, "gdv_fn_aes_decrypt", NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + // encrypt/decrypt(data, key, mode[, iv[, aad]]): the optional arguments needed + // depend on the mode, see encrypt_mode_dispatcher.h + NativeFunction("encrypt", {}, DataTypeVector{binary(), binary(), utf8()}, binary(), + kResultNullIfNull, "gdv_fn_encrypt_dispatcher_3args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + + NativeFunction("decrypt", {}, DataTypeVector{binary(), binary(), utf8()}, binary(), + kResultNullIfNull, "gdv_fn_decrypt_dispatcher_3args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + + NativeFunction("encrypt", {}, DataTypeVector{binary(), binary(), utf8(), binary()}, + binary(), kResultNullIfNull, "gdv_fn_encrypt_dispatcher_4args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + + NativeFunction("decrypt", {}, DataTypeVector{binary(), binary(), utf8(), binary()}, + binary(), kResultNullIfNull, "gdv_fn_decrypt_dispatcher_4args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + + NativeFunction("encrypt", {}, + DataTypeVector{binary(), binary(), utf8(), binary(), binary()}, + binary(), kResultNullIfNull, "gdv_fn_encrypt_dispatcher_5args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + + NativeFunction("decrypt", {}, + DataTypeVector{binary(), binary(), utf8(), binary(), binary()}, + binary(), kResultNullIfNull, "gdv_fn_decrypt_dispatcher_5args", + NativeFunction::kNeedsContext | NativeFunction::kCanReturnErrors), + NativeFunction("mask_first_n", {}, DataTypeVector{utf8(), int32()}, utf8(), kResultNullIfNull, "gdv_mask_first_n_utf8_int32", NativeFunction::kNeedsContext), diff --git a/cpp/src/gandiva/gdv_function_stubs.cc b/cpp/src/gandiva/gdv_function_stubs.cc index 6b3e9935b017..e0cd2e0579d0 100644 --- a/cpp/src/gandiva/gdv_function_stubs.cc +++ b/cpp/src/gandiva/gdv_function_stubs.cc @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,7 @@ #include "arrow/util/double_conversion_internal.h" #include "arrow/util/value_parsing.h" +#include "gandiva/encrypt_mode_dispatcher.h" #include "gandiva/encrypt_utils.h" #include "gandiva/engine.h" #include "gandiva/exported_funcs.h" @@ -177,8 +179,10 @@ int32_t gdv_fn_populate_varlen_vector(int64_t context_ptr, int8_t* data_ptr, return -1; } - // append the new entry. - memcpy(buffer->mutable_data() + offset, entry_buf, entry_len); + // append the new entry (NULL entries have a null buffer and zero length). + if (entry_len > 0) { + memcpy(buffer->mutable_data() + offset, entry_buf, entry_len); + } // update offsets buffer. offsets[slot] = offset; @@ -909,6 +913,125 @@ const char* gdv_mask_show_last_n_utf8_int32(int64_t context, const char* data, return gdv_mask_first_n_utf8_int32(context, data, data_len, n_to_mask, out_len); } +// The 3- and 4-argument versions forward to the 5-argument one with the omitted +// optional arguments empty. +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_3args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, int32_t* out_len) { + return gdv_fn_encrypt_dispatcher_5args(context, data, data_len, key_data, key_data_len, + mode, mode_len, nullptr, 0, nullptr, 0, out_len); +} + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_3args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, int32_t* out_len) { + return gdv_fn_decrypt_dispatcher_5args(context, data, data_len, key_data, key_data_len, + mode, mode_len, nullptr, 0, nullptr, 0, out_len); +} + +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_4args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, int32_t* out_len) { + return gdv_fn_encrypt_dispatcher_5args(context, data, data_len, key_data, key_data_len, + mode, mode_len, iv_data, iv_data_len, nullptr, 0, + out_len); +} + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_4args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, int32_t* out_len) { + return gdv_fn_decrypt_dispatcher_5args(context, data, data_len, key_data, key_data_len, + mode, mode_len, iv_data, iv_data_len, nullptr, 0, + out_len); +} + +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_5args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, const char* aad, + int32_t aad_len, int32_t* out_len) { + try { + if (data_len < 0) { + throw std::runtime_error("ENCRYPT: data length can't be negative, got " + + std::to_string(data_len)); + } + // Room for the largest output: PKCS#7 padding adds up to one 16-byte block, and GCM + // appends a 16-byte tag. Unused for the no-padding modes. + constexpr int32_t kMaxExpansion = 16; + if (data_len > std::numeric_limits::max() - kMaxExpansion) { + throw std::runtime_error("ENCRYPT: data is too large to encrypt, got " + + std::to_string(data_len) + " bytes"); + } + auto* output = reinterpret_cast( + gdv_fn_context_arena_malloc(context, data_len + kMaxExpansion)); + if (output == nullptr) { + throw std::runtime_error("Memory allocation failed for encryption output"); + } + + int32_t cipher_len = gandiva::EncryptModeDispatcher::encrypt( + data, data_len, key_data, key_data_len, mode, mode_len, iv_data, iv_data_len, aad, + aad_len, output); + + *out_len = cipher_len; + return reinterpret_cast(output); + } catch (const std::runtime_error& e) { + gdv_fn_context_set_error_msg(context, e.what()); + *out_len = 0; + return nullptr; + } +} + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_5args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, const char* aad, + int32_t aad_len, int32_t* out_len) { + try { + if (data_len < 0) { + throw std::runtime_error("DECRYPT: data length can't be negative, got " + + std::to_string(data_len)); + } + // The plaintext is never longer than the ciphertext, but OpenSSL documents that + // EVP_DecryptUpdate may use up to `inl + block_size` bytes of output space, so size + // the buffer accordingly. This also covers empty input. + constexpr int32_t kBlockSize = 16; + if (data_len > std::numeric_limits::max() - kBlockSize) { + throw std::runtime_error("DECRYPT: data is too large to decrypt, got " + + std::to_string(data_len) + " bytes"); + } + auto* output = reinterpret_cast( + gdv_fn_context_arena_malloc(context, data_len + kBlockSize)); + if (output == nullptr) { + throw std::runtime_error("Memory allocation failed for decryption output"); + } + + int32_t plaintext_len = gandiva::EncryptModeDispatcher::decrypt( + data, data_len, key_data, key_data_len, mode, mode_len, iv_data, iv_data_len, aad, + aad_len, output); + + *out_len = plaintext_len; + return reinterpret_cast(output); + } catch (const std::runtime_error& e) { + gdv_fn_context_set_error_msg(context, e.what()); + *out_len = 0; + return nullptr; + } +} + ARROW_UNSUPPRESS_MISSING_DECLARATIONS_WARNING } @@ -1148,6 +1271,114 @@ arrow::Status ExportedStubFunctions::AddMappings(Engine* engine) const { types->i8_ptr_type() /*return_type*/, args, reinterpret_cast(gdv_fn_aes_decrypt)); + // gdv_fn_encrypt_dispatcher_3args (data, key, mode) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_encrypt_dispatcher_3args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_encrypt_dispatcher_3args)); + + // gdv_fn_decrypt_dispatcher_3args (data, key, mode) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_decrypt_dispatcher_3args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_decrypt_dispatcher_3args)); + + // gdv_fn_encrypt_dispatcher_4args (data, key, mode, iv) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i8_ptr_type(), // iv (binary string) + types->i32_type(), // iv_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_encrypt_dispatcher_4args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_encrypt_dispatcher_4args)); + + // gdv_fn_decrypt_dispatcher_4args (data, key, mode, iv) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i8_ptr_type(), // iv (binary string) + types->i32_type(), // iv_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_decrypt_dispatcher_4args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_decrypt_dispatcher_4args)); + + // gdv_fn_encrypt_dispatcher_5args (data, key, mode, iv, aad) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i8_ptr_type(), // iv (binary string) + types->i32_type(), // iv_length + types->i8_ptr_type(), // aad (binary string) + types->i32_type(), // aad_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_encrypt_dispatcher_5args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_encrypt_dispatcher_5args)); + + // gdv_fn_decrypt_dispatcher_5args (data, key, mode, iv, aad) + args = { + types->i64_type(), // context + types->i8_ptr_type(), // data + types->i32_type(), // data_length + types->i8_ptr_type(), // key_data + types->i32_type(), // key_data_length + types->i8_ptr_type(), // mode (binary string) + types->i32_type(), // mode_length + types->i8_ptr_type(), // iv (binary string) + types->i32_type(), // iv_length + types->i8_ptr_type(), // aad (binary string) + types->i32_type(), // aad_length + types->i32_ptr_type() // out_length + }; + + engine->AddGlobalMappingForFunc( + "gdv_fn_decrypt_dispatcher_5args", types->i8_ptr_type() /*return_type*/, args, + reinterpret_cast(gdv_fn_decrypt_dispatcher_5args)); + // gdv_mask_first_n and gdv_mask_last_n std::vector mask_args = { types->i64_type(), // context diff --git a/cpp/src/gandiva/gdv_function_stubs.h b/cpp/src/gandiva/gdv_function_stubs.h index 4113f261ad76..104ebc1740c4 100644 --- a/cpp/src/gandiva/gdv_function_stubs.h +++ b/cpp/src/gandiva/gdv_function_stubs.h @@ -198,6 +198,49 @@ const char* gdv_fn_aes_decrypt(int64_t context, const char* data, int32_t data_l const char* key_data, int32_t key_data_len, int32_t* out_len); +// Dispatchers behind encrypt/decrypt: data, key and mode, then an optional iv and aad +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_3args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, int32_t* out_len); + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_3args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, int32_t* out_len); + +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_4args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, int32_t* out_len); + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_4args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, int32_t* out_len); + +GANDIVA_EXPORT +const char* gdv_fn_encrypt_dispatcher_5args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, const char* aad, + int32_t aad_len, int32_t* out_len); + +GANDIVA_EXPORT +const char* gdv_fn_decrypt_dispatcher_5args(int64_t context, const char* data, + int32_t data_len, const char* key_data, + int32_t key_data_len, const char* mode, + int32_t mode_len, const char* iv_data, + int32_t iv_data_len, const char* aad, + int32_t aad_len, int32_t* out_len); + GANDIVA_EXPORT const char* gdv_mask_first_n_utf8_int32(int64_t context, const char* data, int32_t data_len, int32_t n_to_mask, diff --git a/cpp/src/gandiva/gdv_function_stubs_test.cc b/cpp/src/gandiva/gdv_function_stubs_test.cc index c0197eb815c8..a341bb8ef695 100644 --- a/cpp/src/gandiva/gdv_function_stubs_test.cc +++ b/cpp/src/gandiva/gdv_function_stubs_test.cc @@ -24,6 +24,9 @@ #include #include "arrow/util/logging.h" +#include "gandiva/encrypt_utils_cbc.h" +#include "gandiva/encrypt_utils_ecb.h" +#include "gandiva/encrypt_utils_gcm.h" #include "gandiva/execution_context.h" namespace gandiva { @@ -1621,4 +1624,419 @@ TEST(TestGdvFnStubs, TestAesEncryptDecryptValidation) { EXPECT_THAT(ctx.get_error(), ::testing::HasSubstr("invalid key length")); ctx.Reset(); } + +TEST(TestGdvFnStubs, TestEncryptDecryptEcbKey16) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &cipher_len); + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +TEST(TestGdvFnStubs, TestEncryptDecryptEcbKey24) { + gandiva::ExecutionContext ctx; + std::string key24 = "12345678abcdefgh12345678"; + auto key24_len = static_cast(key24.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key24.c_str(), + key24_len, mode.c_str(), mode_len, &cipher_len); + + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key24.c_str(), + key24_len, mode.c_str(), mode_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +TEST(TestGdvFnStubs, TestEncryptDecryptEcbKey32) { + gandiva::ExecutionContext ctx; + std::string key32 = "12345678abcdefgh12345678abcdefgh"; + auto key32_len = static_cast(key32.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key32.c_str(), + key32_len, mode.c_str(), mode_len, &cipher_len); + + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key32.c_str(), + key32_len, mode.c_str(), mode_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +TEST(TestGdvFnStubs, TestEncryptDecryptValidation) { + gandiva::ExecutionContext ctx; + std::string key33 = "12345678abcdefgh12345678abcdefghb"; + auto key33_len = static_cast(key33.length()); + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + std::string cipher = "12345678abcdefgh12345678abcdefghb"; + auto cipher_len = static_cast(cipher.length()); + + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key33.c_str(), + key33_len, mode.c_str(), mode_len, &cipher_len); + EXPECT_THAT(ctx.get_error(), + ::testing::HasSubstr("Unsupported key length for AES-ECB")); + ctx.Reset(); + + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher.c_str(), cipher_len, key33.c_str(), + key33_len, mode.c_str(), mode_len, &decrypted_len); + EXPECT_THAT(ctx.get_error(), + ::testing::HasSubstr("Unsupported key length for AES-ECB")); + ctx.Reset(); +} + +// Tests for new mode-aware AES functions +TEST(TestGdvFnStubs, TestAesEncryptDecryptModeEcb) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &decrypted_len); + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +TEST(TestGdvFnStubs, TestAesEncryptDecryptModeValidation) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string invalid_mode = "AES-INVALID"; + auto invalid_mode_len = static_cast(invalid_mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + // Test encrypt with invalid mode + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key16.c_str(), + key16_len, invalid_mode.c_str(), invalid_mode_len, + &cipher_len); + EXPECT_THAT(ctx.get_error(), ::testing::HasSubstr("Unsupported encryption mode")); + ctx.Reset(); + + // Test decrypt with invalid mode + std::string cipher = "12345678abcdefgh12345678abcdefgh"; + auto cipher_len_val = static_cast(cipher.length()); + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher.c_str(), cipher_len_val, key16.c_str(), + key16_len, invalid_mode.c_str(), invalid_mode_len, + &decrypted_len); + EXPECT_THAT(ctx.get_error(), ::testing::HasSubstr("Unsupported decryption mode")); + ctx.Reset(); +} + +// Tests for AES-GCM mode +TEST(TestGdvFnStubs, TestAesEncryptDecryptGcmIvOnly) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_GCM_MODE; + auto mode_len = static_cast(mode.length()); + std::string iv = "123456789012"; + auto iv_len = static_cast(iv.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = gdv_fn_encrypt_dispatcher_5args( + ctx_ptr, data.c_str(), data_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, nullptr, 0, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = gdv_fn_decrypt_dispatcher_5args( + ctx_ptr, cipher, cipher_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, nullptr, 0, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +TEST(TestGdvFnStubs, TestAesEncryptDecryptGcmWithAad) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_GCM_MODE; + auto mode_len = static_cast(mode.length()); + std::string iv = "123456789012"; + auto iv_len = static_cast(iv.length()); + std::string aad = "additional authenticated data"; + auto aad_len = static_cast(aad.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = gdv_fn_encrypt_dispatcher_5args( + ctx_ptr, data.c_str(), data_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, aad.c_str(), aad_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = gdv_fn_decrypt_dispatcher_5args( + ctx_ptr, cipher, cipher_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, aad.c_str(), aad_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Tests for shorthand mode: AES-ECB (defaults to PKCS7) +TEST(TestGdvFnStubs, TestAesEncryptDecryptShorthandEcb) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_MODE; // Shorthand mode + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Tests for explicit mode: AES-ECB-PKCS7 +TEST(TestGdvFnStubs, TestAesEncryptDecryptExplicitEcbPkcs7) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_ECB_PKCS7_MODE; // Explicit mode + auto mode_len = static_cast(mode.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = + gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.c_str(), data_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = + gdv_fn_decrypt_dispatcher_3args(ctx_ptr, cipher, cipher_len, key16.c_str(), + key16_len, mode.c_str(), mode_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Tests for shorthand mode: AES-CBC (defaults to PKCS7) +TEST(TestGdvFnStubs, TestAesEncryptDecryptShorthandCbc) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_CBC_MODE; // Shorthand mode + auto mode_len = static_cast(mode.length()); + std::string iv = "1234567890123456"; + auto iv_len = static_cast(iv.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = gdv_fn_encrypt_dispatcher_4args( + ctx_ptr, data.c_str(), data_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = gdv_fn_decrypt_dispatcher_4args( + ctx_ptr, cipher, cipher_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Tests for explicit mode: AES-CBC-PKCS7 +TEST(TestGdvFnStubs, TestAesEncryptDecryptExplicitCbcPkcs7) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + std::string data = "test string"; + auto data_len = static_cast(data.length()); + std::string mode = AES_CBC_PKCS7_MODE; // Explicit mode + auto mode_len = static_cast(mode.length()); + std::string iv = "1234567890123456"; + auto iv_len = static_cast(iv.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = gdv_fn_encrypt_dispatcher_4args( + ctx_ptr, data.c_str(), data_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = gdv_fn_decrypt_dispatcher_4args( + ctx_ptr, cipher, cipher_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Tests for explicit mode: AES-CBC-NONE (no padding) +TEST(TestGdvFnStubs, TestAesEncryptDecryptCbcNone) { + gandiva::ExecutionContext ctx; + std::string key16 = "12345678abcdefgh"; + auto key16_len = static_cast(key16.length()); + int32_t cipher_len = 0; + int32_t decrypted_len = 0; + // Use exactly 16 bytes (one block) for no-padding mode + std::string data = "1234567890123456"; + auto data_len = static_cast(data.length()); + std::string mode = AES_CBC_NONE_MODE; // No padding mode + auto mode_len = static_cast(mode.length()); + std::string iv = "1234567890123456"; + auto iv_len = static_cast(iv.length()); + int64_t ctx_ptr = reinterpret_cast(&ctx); + + const char* cipher = gdv_fn_encrypt_dispatcher_4args( + ctx_ptr, data.c_str(), data_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &cipher_len); + EXPECT_GT(cipher_len, 0); + + const char* decrypted_value = gdv_fn_decrypt_dispatcher_4args( + ctx_ptr, cipher, cipher_len, key16.c_str(), key16_len, mode.c_str(), mode_len, + iv.c_str(), iv_len, &decrypted_len); + + EXPECT_EQ(data, + std::string(reinterpret_cast(decrypted_value), decrypted_len)); +} + +// Block-aligned inputs make PKCS#7 padding append a whole extra block, and GCM appends a +// tag. Output buffers must be sized for that, otherwise a value written from the same +// context overwrites the neighbouring output. +TEST(TestGdvFnStubs, TestEncryptDecryptBlockAlignedInputs) { + const std::string key = "12345678abcdefgh"; + const auto key_len = static_cast(key.length()); + const std::string iv16(16, 'i'); + const std::string iv12(12, 'n'); + + struct ModeCase { + std::string mode; + const std::string* iv; + }; + const ModeCase cases[] = {{gandiva::AES_ECB_MODE, nullptr}, + {gandiva::AES_CBC_MODE, &iv16}, + {gandiva::AES_GCM_MODE, &iv12}}; + + for (const auto& mode_case : cases) { + for (int32_t size : {16, 32, 48, 8192}) { + SCOPED_TRACE(mode_case.mode + " size=" + std::to_string(size)); + gandiva::ExecutionContext ctx; + const auto ctx_ptr = reinterpret_cast(&ctx); + const auto mode_len = static_cast(mode_case.mode.length()); + const std::string first(size, 'a'); + const std::string second(size, 'b'); + + auto encrypt = [&](const std::string& data, int32_t* out_len) { + if (mode_case.iv == nullptr) { + return gdv_fn_encrypt_dispatcher_3args(ctx_ptr, data.data(), size, key.data(), + key_len, mode_case.mode.data(), mode_len, + out_len); + } + return gdv_fn_encrypt_dispatcher_4args( + ctx_ptr, data.data(), size, key.data(), key_len, mode_case.mode.data(), + mode_len, mode_case.iv->data(), static_cast(mode_case.iv->size()), + out_len); + }; + auto decrypt = [&](const std::string& cipher, int32_t* out_len) { + const auto cipher_len = static_cast(cipher.size()); + if (mode_case.iv == nullptr) { + return gdv_fn_decrypt_dispatcher_3args( + ctx_ptr, cipher.data(), cipher_len, key.data(), key_len, + mode_case.mode.data(), mode_len, out_len); + } + return gdv_fn_decrypt_dispatcher_4args( + ctx_ptr, cipher.data(), cipher_len, key.data(), key_len, + mode_case.mode.data(), mode_len, mode_case.iv->data(), + static_cast(mode_case.iv->size()), out_len); + }; + + int32_t first_len = 0; + const char* first_cipher_ptr = encrypt(first, &first_len); + ASSERT_NE(first_cipher_ptr, nullptr) << ctx.get_error(); + const std::string first_cipher(first_cipher_ptr, first_len); + + int32_t second_len = 0; + ASSERT_NE(encrypt(second, &second_len), nullptr) << ctx.get_error(); + + // The second encryption must not have clobbered the first ciphertext + EXPECT_EQ(first_cipher, std::string(first_cipher_ptr, first_len)); + + int32_t plain_len = 0; + const char* plain = decrypt(first_cipher, &plain_len); + ASSERT_NE(plain, nullptr) << ctx.get_error(); + EXPECT_EQ(first, std::string(plain, plain_len)); + } + } +} + } // namespace gandiva diff --git a/cpp/src/gandiva/tests/projector_test.cc b/cpp/src/gandiva/tests/projector_test.cc index 6e9450e1c650..456212709d15 100644 --- a/cpp/src/gandiva/tests/projector_test.cc +++ b/cpp/src/gandiva/tests/projector_test.cc @@ -2895,6 +2895,226 @@ TEST_F(TestProjector, TestAesEncryptDecrypt) { EXPECT_ARROW_ARRAY_EQUALS(array_data, outputs_de.at(0)); } +// Builds encrypt/decrypt over (data, key, iv, aad) binary columns with a literal mode. +static ExpressionPtr MakeEncryptDecryptExpr(const std::string& function, + const std::string& mode, int num_args, + const FieldVector& columns) { + NodeVector args = {TreeExprBuilder::MakeField(columns[0]), + TreeExprBuilder::MakeField(columns[1]), + TreeExprBuilder::MakeStringLiteral(mode)}; + if (num_args > 3) args.push_back(TreeExprBuilder::MakeField(columns[2])); + if (num_args > 4) args.push_back(TreeExprBuilder::MakeField(columns[3])); + auto node = TreeExprBuilder::MakeFunction(function, args, arrow::binary()); + return TreeExprBuilder::MakeExpression(node, field("res", arrow::binary())); +} + +TEST_F(TestProjector, TestEncryptDecryptModes) { + auto data_field = field("data", arrow::binary()); + auto key_field = field("key", arrow::binary()); + auto iv_field = field("iv", arrow::binary()); + auto aad_field = field("aad", arrow::binary()); + FieldVector columns = {data_field, key_field, iv_field, aad_field}; + auto schema = arrow::schema(columns); + + // Includes an empty value, a block-aligned value and a NULL + const std::vector plain = {"abc", "", "0123456789abcdef", "unused"}; + const std::vector plain_valid = {true, true, true, false}; + const std::string key = "12345678abcdefgh12345678abcdefgh"; + const std::string iv = "iv-0123456789abc"; // 16 bytes: valid for CBC and for GCM + const std::string aad = "associated data"; + const int num_records = static_cast(plain.size()); + + auto plain_array = MakeArrowArrayBinary(plain, plain_valid); + auto key_array = MakeArrowArrayBinary({key, key, key, key}, {true, true, true, true}); + auto iv_array = MakeArrowArrayBinary({iv, iv, iv, iv}, {true, true, true, true}); + auto aad_array = MakeArrowArrayBinary({aad, aad, aad, aad}, {true, true, true, true}); + + struct ModeCase { + std::string mode; + int num_args; + }; + for (const auto& mode_case : std::vector{{"AES-ECB", 3}, + {"aes-ecb-none", 3}, + {"AES-CBC", 4}, + {"AES-GCM", 4}, + {"AES-GCM", 5}}) { + SCOPED_TRACE(mode_case.mode + " args=" + std::to_string(mode_case.num_args)); + // No-padding ECB needs block-aligned input, so use only the aligned row + const bool aligned_only = mode_case.mode == "aes-ecb-none"; + auto in_plain = plain_array; + auto in_key = key_array; + auto in_iv = iv_array; + auto in_aad = aad_array; + int rows = num_records; + if (aligned_only) { + in_plain = MakeArrowArrayBinary({plain[2]}, {true}); + in_key = MakeArrowArrayBinary({key}, {true}); + in_iv = MakeArrowArrayBinary({iv}, {true}); + in_aad = MakeArrowArrayBinary({aad}, {true}); + rows = 1; + } + + std::shared_ptr encryptor; + ASSERT_OK(Projector::Make( + schema, + {MakeEncryptDecryptExpr("encrypt", mode_case.mode, mode_case.num_args, columns)}, + TestConfiguration(), &encryptor)); + auto in_batch = + arrow::RecordBatch::Make(schema, rows, {in_plain, in_key, in_iv, in_aad}); + arrow::ArrayVector encrypted; + ASSERT_OK(encryptor->Evaluate(*in_batch, pool_, &encrypted)); + + // The NULL input yields a NULL output + if (!aligned_only) { + EXPECT_TRUE(encrypted.at(0)->IsNull(3)); + EXPECT_FALSE(encrypted.at(0)->IsNull(0)); + } + + std::shared_ptr decryptor; + ASSERT_OK(Projector::Make( + schema, + {MakeEncryptDecryptExpr("decrypt", mode_case.mode, mode_case.num_args, columns)}, + TestConfiguration(), &decryptor)); + auto cipher_batch = + arrow::RecordBatch::Make(schema, rows, {encrypted.at(0), in_key, in_iv, in_aad}); + arrow::ArrayVector decrypted; + ASSERT_OK(decryptor->Evaluate(*cipher_batch, pool_, &decrypted)); + EXPECT_ARROW_ARRAY_EQUALS(in_plain, decrypted.at(0)); + } +} + +TEST_F(TestProjector, TestEncryptDecryptErrors) { + auto data_field = field("data", arrow::binary()); + auto key_field = field("key", arrow::binary()); + auto iv_field = field("iv", arrow::binary()); + auto aad_field = field("aad", arrow::binary()); + FieldVector columns = {data_field, key_field, iv_field, aad_field}; + auto schema = arrow::schema(columns); + + const std::string key = "12345678abcdefgh"; + const std::string iv = "123456789012"; + auto one = [](const std::string& value) { + return MakeArrowArrayBinary({value}, {true}); + }; + + auto evaluate = [&](const std::string& function, const std::string& mode, int num_args, + const std::string& data, const std::string& key_value, + const std::string& iv_value, const std::string& aad_value) { + std::shared_ptr projector; + EXPECT_OK(Projector::Make(schema, + {MakeEncryptDecryptExpr(function, mode, num_args, columns)}, + TestConfiguration(), &projector)); + auto batch = arrow::RecordBatch::Make( + schema, 1, {one(data), one(key_value), one(iv_value), one(aad_value)}); + arrow::ArrayVector out; + return projector->Evaluate(*batch, pool_, &out); + }; + + // Invalid key length + auto status = evaluate("encrypt", "AES-ECB", 3, "data", "short", iv, ""); + EXPECT_FALSE(status.ok()); + EXPECT_NE(status.message().find("key length"), std::string::npos) << status.message(); + + // Unsupported mode lists the supported ones + status = evaluate("encrypt", "AES-XYZ", 3, "data", key, iv, ""); + EXPECT_FALSE(status.ok()); + EXPECT_NE(status.message().find("Unsupported"), std::string::npos) << status.message(); + + // CBC requires a 16-byte IV + status = evaluate("encrypt", "AES-CBC", 4, "data", key, iv, ""); + EXPECT_FALSE(status.ok()); + EXPECT_NE(status.message().find("IV"), std::string::npos) << status.message(); + + // GCM decryption fails when the AAD differs from the one used for encryption + std::shared_ptr encryptor; + ASSERT_OK(Projector::Make(schema, + {MakeEncryptDecryptExpr("encrypt", "AES-GCM", 5, columns)}, + TestConfiguration(), &encryptor)); + auto batch = arrow::RecordBatch::Make(schema, 1, + {one("secret"), one(key), one(iv), one("aad-1")}); + arrow::ArrayVector encrypted; + ASSERT_OK(encryptor->Evaluate(*batch, pool_, &encrypted)); + + std::shared_ptr decryptor; + ASSERT_OK(Projector::Make(schema, + {MakeEncryptDecryptExpr("decrypt", "AES-GCM", 5, columns)}, + TestConfiguration(), &decryptor)); + auto bad_batch = arrow::RecordBatch::Make( + schema, 1, {encrypted.at(0), one(key), one(iv), one("aad-2")}); + arrow::ArrayVector decrypted; + status = decryptor->Evaluate(*bad_batch, pool_, &decrypted); + EXPECT_FALSE(status.ok()); + EXPECT_NE(status.message().find("tag verification failed"), std::string::npos) + << status.message(); +} + +// A NULL in any argument yields a NULL result without evaluating the function +TEST_F(TestProjector, TestEncryptDecryptNullArguments) { + auto data_field = field("data", arrow::binary()); + auto key_field = field("key", arrow::binary()); + auto mode_field = field("mode", arrow::utf8()); + auto iv_field = field("iv", arrow::binary()); + auto aad_field = field("aad", arrow::binary()); + auto schema = arrow::schema({data_field, key_field, mode_field, iv_field, aad_field}); + + NodeVector args; + for (const auto& f : schema->fields()) args.push_back(TreeExprBuilder::MakeField(f)); + auto make_projector = [&](const std::string& function) { + std::shared_ptr projector; + auto node = TreeExprBuilder::MakeFunction(function, args, arrow::binary()); + EXPECT_OK(Projector::Make( + schema, {TreeExprBuilder::MakeExpression(node, field("res", arrow::binary()))}, + TestConfiguration(), &projector)); + return projector; + }; + + const std::string key = "12345678abcdefgh"; + const std::string iv = "123456789012"; + const std::string aad = "aad"; + // Row 0 is fully valid; in rows 1-5 exactly one of data, key, mode, iv, aad is NULL + const int num_records = 6; + auto binary_column = [&](const std::string& value, int null_row) { + std::vector valid(num_records, true); + valid[null_row] = false; + return MakeArrowArrayBinary(std::vector(num_records, value), valid); + }; + auto utf8_column = [&](const std::string& value, int null_row) { + std::vector valid(num_records, true); + valid[null_row] = false; + return MakeArrowArrayUtf8(std::vector(num_records, value), valid); + }; + + auto in_batch = arrow::RecordBatch::Make( + schema, num_records, + {binary_column("some data", 1), binary_column(key, 2), utf8_column("AES-GCM", 3), + binary_column(iv, 4), binary_column(aad, 5)}); + + arrow::ArrayVector encrypted; + ASSERT_OK(make_projector("encrypt")->Evaluate(*in_batch, pool_, &encrypted)); + EXPECT_TRUE(encrypted.at(0)->IsValid(0)); + for (int row = 1; row < num_records; ++row) { + EXPECT_TRUE(encrypted.at(0)->IsNull(row)) << "row " << row; + } + + // Same for decrypt, using the ciphertext of the valid row as the data in every row + const auto& cipher_array = static_cast(*encrypted.at(0)); + const std::string cipher = cipher_array.GetString(0); + std::vector data_valid(num_records, true); + data_valid[1] = false; + auto cipher_column = + MakeArrowArrayBinary(std::vector(num_records, cipher), data_valid); + auto decrypt_batch = + arrow::RecordBatch::Make(schema, num_records, + {cipher_column, in_batch->column(1), in_batch->column(2), + in_batch->column(3), in_batch->column(4)}); + arrow::ArrayVector decrypted; + ASSERT_OK(make_projector("decrypt")->Evaluate(*decrypt_batch, pool_, &decrypted)); + EXPECT_TRUE(decrypted.at(0)->IsValid(0)); + for (int row = 1; row < num_records; ++row) { + EXPECT_TRUE(decrypted.at(0)->IsNull(row)) << "row " << row; + } +} + TEST_F(TestProjector, TestMaskFirstMaskLastN) { // schema for input fields auto field0 = field("f0", arrow::utf8());