Skip to content

Commit 933db02

Browse files
DX-108149: Add support for CBC encryption mode (#104)
* DX-108149: Add support for AES CBC encryption mode * Extract AES mode validation into ensure_mode() helper function and update macOS runner versions - Create ensure_mode() helper that throws std::runtime_error for invalid modes - Update all ECB and CBC AES functions (3 encrypt + 3 decrypt) to use ensure_mode() - Wrap all function bodies in try-catch to handle exceptions from ensure_mode() - Consistent error handling across ECB and CBC modes - Update CBC function signatures to include mode parameter: (data, key, mode, iv, padding) - Update function registry to reflect new CBC parameter order - Update LLVM mappings comments for clarity - Update test expectations to match new error messages - Update macOS runner versions from macos-13 to macos-15-intel in CI workflows - Excludes GCM mode changes from the original commits * Add AES dispatcher function signatures for new VARBINARY mode
1 parent 8817691 commit 933db02

20 files changed

Lines changed: 1166 additions & 291 deletions

.github/workflows/csharp.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ jobs:
9494
run: ci/scripts/csharp_test.sh $(pwd)
9595

9696
macos:
97-
name: AMD64 macOS 13 C# ${{ matrix.dotnet }}
98-
runs-on: macos-13
97+
name: AMD64 macOS 15 C# ${{ matrix.dotnet }}
98+
runs-on: macos-15-intel
9999
if: ${{ !contains(github.event.pull_request.title, 'WIP') }}
100100
timeout-minutes: 15
101101
strategy:

.github/workflows/java.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ jobs:
106106
run: archery docker push ${{ matrix.image }}
107107

108108
macos:
109-
name: AMD64 macOS 13 Java JDK ${{ matrix.jdk }}
110-
runs-on: macos-13
109+
name: AMD64 macOS 15 Java JDK ${{ matrix.jdk }}
110+
runs-on: macos-15-intel
111111
if: ${{ !contains(github.event.pull_request.title, 'WIP') }}
112112
timeout-minutes: 30
113113
strategy:

.github/workflows/js.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ jobs:
8181
run: archery docker push debian-js
8282

8383
macos:
84-
name: AMD64 macOS 13 NodeJS ${{ matrix.node }}
85-
runs-on: macos-13
84+
name: AMD64 macOS 15 NodeJS ${{ matrix.node }}
85+
runs-on: macos-15-intel
8686
if: ${{ !contains(github.event.pull_request.title, 'WIP') }}
8787
timeout-minutes: 30
8888
strategy:

cpp/src/gandiva/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ set(SRC_FILES
5656
decimal_xlarge.cc
5757
engine.cc
5858
date_utils.cc
59+
encrypt_utils_common.cc
5960
encrypt_utils_ecb.cc
61+
encrypt_utils_cbc.cc
62+
encrypt_mode_dispatcher.cc
6063
expr_decomposer.cc
6164
expr_validator.cc
6265
expression.cc
@@ -257,6 +260,8 @@ add_gandiva_test(internals-test
257260
annotator_test.cc
258261
tree_expr_test.cc
259262
encrypt_utils_ecb_test.cc
263+
encrypt_utils_cbc_test.cc
264+
encrypt_utils_common_test.cc
260265
expr_decomposer_test.cc
261266
exported_funcs_registry_test.cc
262267
expression_registry_test.cc
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License") you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "gandiva/encrypt_mode_dispatcher.h"
19+
#include "gandiva/encrypt_utils_ecb.h"
20+
#include "gandiva/encrypt_utils_cbc.h"
21+
#include "arrow/util/string.h"
22+
#include <string>
23+
#include <sstream>
24+
#include <stdexcept>
25+
26+
namespace gandiva {
27+
28+
int32_t EncryptModeDispatcher::encrypt(
29+
const char* plaintext, int32_t plaintext_len, const char* key,
30+
int32_t key_len, const char* mode, int32_t mode_len, const char* iv,
31+
int32_t iv_len, const char* fifth_argument, int32_t fifth_argument_len,
32+
unsigned char* cipher) {
33+
std::string mode_str =
34+
arrow::internal::AsciiToUpper(std::string_view(mode, mode_len));
35+
36+
if (mode_str == "AES-ECB") {
37+
return aes_encrypt_ecb(plaintext, plaintext_len, key, key_len, cipher);
38+
} else if (mode_str == "AES-CBC-PKCS7") {
39+
return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len,
40+
iv, iv_len, true, cipher);
41+
} else if (mode_str == "AES-CBC-NONE") {
42+
return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len,
43+
iv, iv_len, false, cipher);
44+
} else if (mode_str == "AES-GCM") {
45+
throw std::runtime_error("AES-GCM encryption mode is not yet implemented");
46+
} else {
47+
std::ostringstream oss;
48+
oss << "Unsupported encryption mode: " << mode_str
49+
<< ". Supported modes: AES-ECB, AES-CBC-PKCS7, AES-CBC-NONE";
50+
throw std::runtime_error(oss.str());
51+
}
52+
}
53+
54+
int32_t EncryptModeDispatcher::decrypt(
55+
const char* ciphertext, int32_t ciphertext_len, const char* key,
56+
int32_t key_len, const char* mode, int32_t mode_len, const char* iv,
57+
int32_t iv_len, const char* fifth_argument, int32_t fifth_argument_len,
58+
unsigned char* plaintext) {
59+
std::string mode_str =
60+
arrow::internal::AsciiToUpper(std::string_view(mode, mode_len));
61+
62+
if (mode_str == "AES-ECB") {
63+
return aes_decrypt_ecb(ciphertext, ciphertext_len, key, key_len, plaintext);
64+
} else if (mode_str == "AES-CBC-PKCS7") {
65+
return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len,
66+
iv, iv_len, true, plaintext);
67+
} else if (mode_str == "AES-CBC-NONE") {
68+
return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len,
69+
iv, iv_len, false, plaintext);
70+
} else if (mode_str == "AES-GCM") {
71+
throw std::runtime_error("AES-GCM decryption mode is not yet implemented");
72+
} else {
73+
std::ostringstream oss;
74+
oss << "Unsupported decryption mode: " << mode_str
75+
<< ". Supported modes: AES-ECB, AES-CBC-PKCS7, AES-CBC-NONE";
76+
throw std::runtime_error(oss.str());
77+
}
78+
}
79+
80+
} // namespace gandiva
81+
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License") you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#ifndef GANDIVA_ENCRYPT_MODE_DISPATCHER_H
19+
#define GANDIVA_ENCRYPT_MODE_DISPATCHER_H
20+
21+
#include <cstdint>
22+
23+
namespace gandiva {
24+
25+
/**
26+
* Dispatcher for AES encryption/decryption based on mode string.
27+
* Routes calls to appropriate implementation.
28+
*/
29+
class EncryptModeDispatcher {
30+
public:
31+
/**
32+
* Encrypt data using the specified mode
33+
*
34+
* @param plaintext The data to encrypt
35+
* @param plaintext_len Length of plaintext in bytes
36+
* @param key The encryption key
37+
* @param key_len Length of key in bytes
38+
* @param mode Mode string
39+
* @param mode_len Length of mode string in bytes
40+
* @param iv The initialization vector (optional, only for modes that support it)
41+
* @param iv_len Length of the IV in bytes
42+
* @param fifth_argument Additional parameter (optional, only for modes that support it)
43+
* @param fifth_argument_len Length of fifth_argument in bytes
44+
* @param cipher Output buffer for encrypted data
45+
* @return Length of encrypted data in bytes
46+
* @throws std::runtime_error on encryption failure or unsupported mode
47+
*/
48+
static int32_t encrypt(const char* plaintext, int32_t plaintext_len,
49+
const char* key, int32_t key_len,
50+
const char* mode, int32_t mode_len,
51+
const char* iv, int32_t iv_len,
52+
const char* fifth_argument, int32_t fifth_argument_len,
53+
unsigned char* cipher);
54+
55+
/**
56+
* Decrypt data using the specified mode
57+
*
58+
* @param ciphertext The data to decrypt
59+
* @param ciphertext_len Length of ciphertext in bytes
60+
* @param key The decryption key
61+
* @param key_len Length of key in bytes
62+
* @param mode Mode string
63+
* @param mode_len Length of mode string in bytes
64+
* @param iv The initialization vector (optional, only for modes that support it)
65+
* @param iv_len Length of the IV in bytes
66+
* @param fifth_argument Additional parameter (optional, only for modes that support it)
67+
* @param fifth_argument_len Length of fifth_argument in bytes
68+
* @param plaintext Output buffer for decrypted data
69+
* @return Length of decrypted data in bytes
70+
* @throws std::runtime_error on decryption failure or unsupported mode
71+
*/
72+
static int32_t decrypt(const char* ciphertext, int32_t ciphertext_len,
73+
const char* key, int32_t key_len,
74+
const char* mode, int32_t mode_len,
75+
const char* iv, int32_t iv_len,
76+
const char* fifth_argument, int32_t fifth_argument_len,
77+
unsigned char* plaintext);
78+
};
79+
80+
} // namespace gandiva
81+
82+
#endif // GANDIVA_ENCRYPT_MODE_DISPATCHER_H
83+
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "gandiva/encrypt_utils_cbc.h"
19+
#include "gandiva/encrypt_utils_common.h"
20+
#include <openssl/aes.h>
21+
#include <openssl/err.h>
22+
#include <stdexcept>
23+
#include <cstring>
24+
#include <sstream>
25+
#include <cctype>
26+
27+
namespace gandiva {
28+
29+
namespace {
30+
31+
const EVP_CIPHER* get_cbc_cipher_algo(int32_t key_length) {
32+
switch (key_length) {
33+
case 16:
34+
return EVP_aes_128_cbc();
35+
case 24:
36+
return EVP_aes_192_cbc();
37+
case 32:
38+
return EVP_aes_256_cbc();
39+
default: {
40+
std::ostringstream oss;
41+
oss << "Unsupported key length for AES-CBC: " << key_length
42+
<< " bytes. Supported lengths: 16, 24, 32 bytes";
43+
throw std::runtime_error(oss.str());
44+
}
45+
}
46+
}
47+
48+
} // namespace
49+
50+
GANDIVA_EXPORT
51+
int32_t aes_encrypt_cbc(const char* plaintext, int32_t plaintext_len, const char* key,
52+
int32_t key_len, const char* iv, int32_t iv_len,
53+
bool use_padding, unsigned char* cipher) {
54+
// Validate IV length
55+
if (iv_len != 16) {
56+
std::ostringstream oss;
57+
oss << "Invalid IV length for AES-CBC: " << iv_len
58+
<< " bytes. IV must be exactly 16 bytes";
59+
throw std::runtime_error(oss.str());
60+
}
61+
62+
int32_t cipher_len = 0;
63+
int32_t len = 0;
64+
EVP_CIPHER_CTX* en_ctx = EVP_CIPHER_CTX_new();
65+
const EVP_CIPHER* cipher_algo = get_cbc_cipher_algo(key_len);
66+
67+
if (!en_ctx) {
68+
throw std::runtime_error("Could not create EVP cipher context for encryption: " +
69+
get_openssl_error_string());
70+
}
71+
72+
if (!EVP_EncryptInit_ex(en_ctx, cipher_algo, nullptr,
73+
reinterpret_cast<const unsigned char*>(key),
74+
reinterpret_cast<const unsigned char*>(iv))) {
75+
EVP_CIPHER_CTX_free(en_ctx);
76+
throw std::runtime_error("Could not initialize EVP cipher context for encryption: " +
77+
get_openssl_error_string());
78+
}
79+
80+
int padding_flag = use_padding ? 1 : 0;
81+
if (!EVP_CIPHER_CTX_set_padding(en_ctx, padding_flag)) {
82+
EVP_CIPHER_CTX_free(en_ctx);
83+
throw std::runtime_error("Could not set padding mode for encryption: " +
84+
get_openssl_error_string());
85+
}
86+
87+
if (!EVP_EncryptUpdate(en_ctx, cipher, &len,
88+
reinterpret_cast<const unsigned char*>(plaintext),
89+
plaintext_len)) {
90+
EVP_CIPHER_CTX_free(en_ctx);
91+
throw std::runtime_error("Could not update EVP cipher context for encryption: " +
92+
get_openssl_error_string());
93+
}
94+
95+
cipher_len += len;
96+
97+
if (!EVP_EncryptFinal_ex(en_ctx, cipher + len, &len)) {
98+
EVP_CIPHER_CTX_free(en_ctx);
99+
throw std::runtime_error("Could not finalize EVP cipher context for encryption: " +
100+
get_openssl_error_string());
101+
}
102+
103+
cipher_len += len;
104+
105+
EVP_CIPHER_CTX_free(en_ctx);
106+
return cipher_len;
107+
}
108+
109+
GANDIVA_EXPORT
110+
int32_t aes_decrypt_cbc(const char* ciphertext, int32_t ciphertext_len, const char* key,
111+
int32_t key_len, const char* iv, int32_t iv_len,
112+
bool use_padding, unsigned char* plaintext) {
113+
// Validate IV length
114+
if (iv_len != 16) {
115+
std::ostringstream oss;
116+
oss << "Invalid IV length for AES-CBC: " << iv_len
117+
<< " bytes. IV must be exactly 16 bytes";
118+
throw std::runtime_error(oss.str());
119+
}
120+
121+
int32_t plaintext_len = 0;
122+
int32_t len = 0;
123+
EVP_CIPHER_CTX* de_ctx = EVP_CIPHER_CTX_new();
124+
const EVP_CIPHER* cipher_algo = get_cbc_cipher_algo(key_len);
125+
126+
if (!de_ctx) {
127+
throw std::runtime_error("Could not create EVP cipher context for decryption: " +
128+
get_openssl_error_string());
129+
}
130+
131+
if (!EVP_DecryptInit_ex(de_ctx, cipher_algo, nullptr,
132+
reinterpret_cast<const unsigned char*>(key),
133+
reinterpret_cast<const unsigned char*>(iv))) {
134+
EVP_CIPHER_CTX_free(de_ctx);
135+
throw std::runtime_error("Could not initialize EVP cipher context for decryption: " +
136+
get_openssl_error_string());
137+
}
138+
139+
int padding_flag = use_padding ? 1 : 0;
140+
if (!EVP_CIPHER_CTX_set_padding(de_ctx, padding_flag)) {
141+
EVP_CIPHER_CTX_free(de_ctx);
142+
throw std::runtime_error("Could not set padding mode for decryption: " +
143+
get_openssl_error_string());
144+
}
145+
146+
if (!EVP_DecryptUpdate(de_ctx, plaintext, &len,
147+
reinterpret_cast<const unsigned char*>(ciphertext),
148+
ciphertext_len)) {
149+
EVP_CIPHER_CTX_free(de_ctx);
150+
throw std::runtime_error("Could not update EVP cipher context for decryption: " +
151+
get_openssl_error_string());
152+
}
153+
154+
plaintext_len += len;
155+
156+
if (!EVP_DecryptFinal_ex(de_ctx, plaintext + len, &len)) {
157+
EVP_CIPHER_CTX_free(de_ctx);
158+
throw std::runtime_error("Could not finalize EVP cipher context for decryption: " +
159+
get_openssl_error_string());
160+
}
161+
162+
plaintext_len += len;
163+
164+
EVP_CIPHER_CTX_free(de_ctx);
165+
return plaintext_len;
166+
}
167+
168+
} // namespace gandiva
169+

0 commit comments

Comments
 (0)