Skip to content

Commit bca1210

Browse files
DX-108149: Add support for GCM encryption mode (#107)
* DX-108149: Add support for GCM encryption mode * Move AES mode names into constants
1 parent 933db02 commit bca1210

8 files changed

Lines changed: 542 additions & 17 deletions

cpp/src/gandiva/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ set(SRC_FILES
5959
encrypt_utils_common.cc
6060
encrypt_utils_ecb.cc
6161
encrypt_utils_cbc.cc
62+
encrypt_utils_gcm.cc
6263
encrypt_mode_dispatcher.cc
6364
expr_decomposer.cc
6465
expr_validator.cc
@@ -261,6 +262,7 @@ add_gandiva_test(internals-test
261262
tree_expr_test.cc
262263
encrypt_utils_ecb_test.cc
263264
encrypt_utils_cbc_test.cc
265+
encrypt_utils_gcm_test.cc
264266
encrypt_utils_common_test.cc
265267
expr_decomposer_test.cc
266268
exported_funcs_registry_test.cc

cpp/src/gandiva/encrypt_mode_dispatcher.cc

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "gandiva/encrypt_mode_dispatcher.h"
1919
#include "gandiva/encrypt_utils_ecb.h"
2020
#include "gandiva/encrypt_utils_cbc.h"
21+
#include "gandiva/encrypt_utils_gcm.h"
2122
#include "arrow/util/string.h"
2223
#include <string>
2324
#include <sstream>
@@ -33,20 +34,22 @@ int32_t EncryptModeDispatcher::encrypt(
3334
std::string mode_str =
3435
arrow::internal::AsciiToUpper(std::string_view(mode, mode_len));
3536

36-
if (mode_str == "AES-ECB") {
37+
if (mode_str == AES_ECB_MODE) {
3738
return aes_encrypt_ecb(plaintext, plaintext_len, key, key_len, cipher);
38-
} else if (mode_str == "AES-CBC-PKCS7") {
39+
} else if (mode_str == AES_CBC_PKCS7_MODE) {
3940
return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len,
4041
iv, iv_len, true, cipher);
41-
} else if (mode_str == "AES-CBC-NONE") {
42+
} else if (mode_str == AES_CBC_NONE_MODE) {
4243
return aes_encrypt_cbc(plaintext, plaintext_len, key, key_len,
4344
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");
45+
} else if (mode_str == AES_GCM_MODE) {
46+
return aes_encrypt_gcm(plaintext, plaintext_len, key, key_len,
47+
iv, iv_len, fifth_argument, fifth_argument_len, cipher);
4648
} else {
4749
std::ostringstream oss;
4850
oss << "Unsupported encryption mode: " << mode_str
49-
<< ". Supported modes: AES-ECB, AES-CBC-PKCS7, AES-CBC-NONE";
51+
<< ". Supported modes: " << AES_ECB_MODE << ", " << AES_CBC_PKCS7_MODE
52+
<< ", " << AES_CBC_NONE_MODE << ", " << AES_GCM_MODE;
5053
throw std::runtime_error(oss.str());
5154
}
5255
}
@@ -59,20 +62,22 @@ int32_t EncryptModeDispatcher::decrypt(
5962
std::string mode_str =
6063
arrow::internal::AsciiToUpper(std::string_view(mode, mode_len));
6164

62-
if (mode_str == "AES-ECB") {
65+
if (mode_str == AES_ECB_MODE) {
6366
return aes_decrypt_ecb(ciphertext, ciphertext_len, key, key_len, plaintext);
64-
} else if (mode_str == "AES-CBC-PKCS7") {
67+
} else if (mode_str == AES_CBC_PKCS7_MODE) {
6568
return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len,
6669
iv, iv_len, true, plaintext);
67-
} else if (mode_str == "AES-CBC-NONE") {
70+
} else if (mode_str == AES_CBC_NONE_MODE) {
6871
return aes_decrypt_cbc(ciphertext, ciphertext_len, key, key_len,
6972
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");
73+
} else if (mode_str == AES_GCM_MODE) {
74+
return aes_decrypt_gcm(ciphertext, ciphertext_len, key, key_len,
75+
iv, iv_len, fifth_argument, fifth_argument_len, plaintext);
7276
} else {
7377
std::ostringstream oss;
7478
oss << "Unsupported decryption mode: " << mode_str
75-
<< ". Supported modes: AES-ECB, AES-CBC-PKCS7, AES-CBC-NONE";
79+
<< ". Supported modes: " << AES_ECB_MODE << ", " << AES_CBC_PKCS7_MODE
80+
<< ", " << AES_CBC_NONE_MODE << ", " << AES_GCM_MODE;
7681
throw std::runtime_error(oss.str());
7782
}
7883
}

cpp/src/gandiva/encrypt_utils_cbc.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323

2424
namespace gandiva {
2525

26+
// CBC mode identifiers
27+
constexpr const char* AES_CBC_PKCS7_MODE = "AES-CBC-PKCS7";
28+
constexpr const char* AES_CBC_NONE_MODE = "AES-CBC-NONE";
29+
2630
/**
2731
* Encrypt data using AES-CBC algorithm with explicit padding mode
2832
*

cpp/src/gandiva/encrypt_utils_ecb.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
namespace gandiva {
2525

26+
// ECB mode identifier
27+
constexpr const char* AES_ECB_MODE = "AES-ECB";
28+
2629
/**
2730
* Encrypt data using AES-ECB algorithm (legacy, insecure)
2831
*
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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_gcm.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+
26+
namespace gandiva {
27+
28+
namespace {
29+
30+
const EVP_CIPHER* get_gcm_cipher_algo(int32_t key_length) {
31+
switch (key_length) {
32+
case 16:
33+
return EVP_aes_128_gcm();
34+
case 24:
35+
return EVP_aes_192_gcm();
36+
case 32:
37+
return EVP_aes_256_gcm();
38+
default: {
39+
std::ostringstream oss;
40+
oss << "Unsupported key length for AES-GCM: " << key_length
41+
<< " bytes. Supported lengths: 16, 24, 32 bytes";
42+
throw std::runtime_error(oss.str());
43+
}
44+
}
45+
}
46+
47+
} // namespace
48+
49+
GANDIVA_EXPORT
50+
int32_t aes_encrypt_gcm(const char* plaintext, int32_t plaintext_len,
51+
const char* key, int32_t key_len, const char* iv,
52+
int32_t iv_len, const char* aad, int32_t aad_len,
53+
unsigned char* cipher) {
54+
if (iv_len <= 0) {
55+
throw std::runtime_error(
56+
"Invalid IV length for AES-GCM: IV length must be greater than 0");
57+
}
58+
59+
int32_t cipher_len = 0;
60+
int32_t len = 0;
61+
EVP_CIPHER_CTX* en_ctx = EVP_CIPHER_CTX_new();
62+
const EVP_CIPHER* cipher_algo = get_gcm_cipher_algo(key_len);
63+
64+
if (!en_ctx) {
65+
throw std::runtime_error("Could not create EVP cipher context for encryption: " +
66+
get_openssl_error_string());
67+
}
68+
69+
try {
70+
if (!EVP_EncryptInit_ex(en_ctx, cipher_algo, nullptr,
71+
reinterpret_cast<const unsigned char*>(key),
72+
reinterpret_cast<const unsigned char*>(iv))) {
73+
throw std::runtime_error(
74+
"Could not initialize EVP cipher context for encryption: " +
75+
get_openssl_error_string());
76+
}
77+
78+
// Set IV length for GCM mode
79+
if (!EVP_CIPHER_CTX_ctrl(en_ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) {
80+
throw std::runtime_error("Could not set GCM IV length: " +
81+
get_openssl_error_string());
82+
}
83+
84+
// Process AAD if provided
85+
if (aad != nullptr && aad_len > 0) {
86+
if (!EVP_EncryptUpdate(en_ctx, nullptr, &len,
87+
reinterpret_cast<const unsigned char*>(aad), aad_len)) {
88+
throw std::runtime_error("Could not process AAD for encryption: " +
89+
get_openssl_error_string());
90+
}
91+
}
92+
93+
// Encrypt plaintext
94+
if (!EVP_EncryptUpdate(en_ctx, cipher, &len,
95+
reinterpret_cast<const unsigned char*>(plaintext),
96+
plaintext_len)) {
97+
throw std::runtime_error("Could not update EVP cipher context for encryption: " +
98+
get_openssl_error_string());
99+
}
100+
101+
cipher_len += len;
102+
103+
// Finalize encryption
104+
if (!EVP_EncryptFinal_ex(en_ctx, cipher + len, &len)) {
105+
throw std::runtime_error("Could not finalize EVP cipher context for encryption: " +
106+
get_openssl_error_string());
107+
}
108+
109+
cipher_len += len;
110+
111+
// Get the authentication tag and append it to ciphertext
112+
if (!EVP_CIPHER_CTX_ctrl(en_ctx, EVP_CTRL_GCM_GET_TAG, GCM_TAG_LENGTH,
113+
cipher + cipher_len)) {
114+
throw std::runtime_error("Could not get GCM authentication tag: " +
115+
get_openssl_error_string());
116+
}
117+
cipher_len += GCM_TAG_LENGTH;
118+
} catch (...) {
119+
EVP_CIPHER_CTX_free(en_ctx);
120+
throw;
121+
}
122+
123+
EVP_CIPHER_CTX_free(en_ctx);
124+
return cipher_len;
125+
}
126+
127+
GANDIVA_EXPORT
128+
int32_t aes_decrypt_gcm(const char* ciphertext, int32_t ciphertext_len,
129+
const char* key, int32_t key_len, const char* iv,
130+
int32_t iv_len, const char* aad, int32_t aad_len,
131+
unsigned char* plaintext) {
132+
if (iv_len <= 0) {
133+
throw std::runtime_error(
134+
"Invalid IV length for AES-GCM: IV length must be greater than 0");
135+
}
136+
137+
if (ciphertext_len < GCM_TAG_LENGTH) {
138+
throw std::runtime_error(
139+
"Ciphertext too short for AES-GCM: must be at least 16 bytes for tag");
140+
}
141+
142+
int32_t plaintext_len = 0;
143+
int32_t len = 0;
144+
EVP_CIPHER_CTX* de_ctx = EVP_CIPHER_CTX_new();
145+
const EVP_CIPHER* cipher_algo = get_gcm_cipher_algo(key_len);
146+
147+
if (!de_ctx) {
148+
throw std::runtime_error("Could not create EVP cipher context for decryption: " +
149+
get_openssl_error_string());
150+
}
151+
152+
try {
153+
if (!EVP_DecryptInit_ex(de_ctx, cipher_algo, nullptr,
154+
reinterpret_cast<const unsigned char*>(key),
155+
reinterpret_cast<const unsigned char*>(iv))) {
156+
throw std::runtime_error(
157+
"Could not initialize EVP cipher context for decryption: " +
158+
get_openssl_error_string());
159+
}
160+
161+
// Set IV length for GCM mode
162+
if (!EVP_CIPHER_CTX_ctrl(de_ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) {
163+
throw std::runtime_error("Could not set GCM IV length: " +
164+
get_openssl_error_string());
165+
}
166+
167+
// Process AAD if provided
168+
if (aad != nullptr && aad_len > 0) {
169+
if (!EVP_DecryptUpdate(de_ctx, nullptr, &len,
170+
reinterpret_cast<const unsigned char*>(aad), aad_len)) {
171+
throw std::runtime_error("Could not process AAD for decryption: " +
172+
get_openssl_error_string());
173+
}
174+
}
175+
176+
// Extract tag from end of ciphertext
177+
int32_t actual_ciphertext_len = ciphertext_len - GCM_TAG_LENGTH;
178+
const unsigned char* tag =
179+
reinterpret_cast<const unsigned char*>(ciphertext + actual_ciphertext_len);
180+
181+
// Set the authentication tag
182+
if (!EVP_CIPHER_CTX_ctrl(de_ctx, EVP_CTRL_GCM_SET_TAG, GCM_TAG_LENGTH,
183+
const_cast<unsigned char*>(tag))) {
184+
throw std::runtime_error("Could not set GCM authentication tag: " +
185+
get_openssl_error_string());
186+
}
187+
188+
// Decrypt ciphertext
189+
if (!EVP_DecryptUpdate(de_ctx, plaintext, &len,
190+
reinterpret_cast<const unsigned char*>(ciphertext),
191+
actual_ciphertext_len)) {
192+
throw std::runtime_error("Could not update EVP cipher context for decryption: " +
193+
get_openssl_error_string());
194+
}
195+
196+
plaintext_len += len;
197+
198+
// Finalize decryption (this verifies the tag)
199+
if (!EVP_DecryptFinal_ex(de_ctx, plaintext + len, &len)) {
200+
throw std::runtime_error("GCM tag verification failed or decryption error: " +
201+
get_openssl_error_string());
202+
}
203+
plaintext_len += len;
204+
} catch (...) {
205+
EVP_CIPHER_CTX_free(de_ctx);
206+
throw;
207+
}
208+
209+
EVP_CIPHER_CTX_free(de_ctx);
210+
return plaintext_len;
211+
}
212+
213+
} // namespace gandiva
214+
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
#pragma once
19+
20+
#include <cstdint>
21+
#include <openssl/evp.h>
22+
#include "gandiva/visibility.h"
23+
24+
namespace gandiva {
25+
26+
// GCM mode identifier
27+
constexpr const char* AES_GCM_MODE = "AES-GCM";
28+
29+
// GCM authentication tag length in bytes
30+
constexpr int32_t GCM_TAG_LENGTH = 16;
31+
32+
/**
33+
* Encrypt data using AES-GCM algorithm
34+
*
35+
* @param plaintext The data to encrypt
36+
* @param plaintext_len Length of plaintext in bytes
37+
* @param key The encryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys)
38+
* @param key_len Length of key in bytes
39+
* @param iv The initialization vector (variable length, typically 12 bytes)
40+
* @param iv_len Length of IV in bytes
41+
* @param aad Optional additional authenticated data (can be null)
42+
* @param aad_len Length of AAD in bytes (0 if aad is null)
43+
* @param cipher Output buffer for encrypted data (must be at least plaintext_len + 16 bytes)
44+
* @return Length of encrypted data in bytes (plaintext_len + 16 for the tag)
45+
* @throws std::runtime_error on encryption failure or invalid parameters
46+
*/
47+
GANDIVA_EXPORT
48+
int32_t aes_encrypt_gcm(const char* plaintext, int32_t plaintext_len, const char* key,
49+
int32_t key_len, const char* iv, int32_t iv_len,
50+
const char* aad, int32_t aad_len, unsigned char* cipher);
51+
52+
/**
53+
* Decrypt data using AES-GCM algorithm
54+
*
55+
* @param ciphertext The data to decrypt (includes 16-byte authentication tag at the end)
56+
* @param ciphertext_len Length of ciphertext in bytes (includes tag)
57+
* @param key The decryption key (16, 24, or 32 bytes for 128, 192, 256-bit keys)
58+
* @param key_len Length of key in bytes
59+
* @param iv The initialization vector (variable length, typically 12 bytes)
60+
* @param iv_len Length of IV in bytes
61+
* @param aad Optional additional authenticated data (can be null)
62+
* @param aad_len Length of AAD in bytes (0 if aad is null)
63+
* @param plaintext Output buffer for decrypted data
64+
* @return Length of decrypted data in bytes (ciphertext_len - 16)
65+
* @throws std::runtime_error on decryption failure, invalid parameters, or tag verification failure
66+
*/
67+
GANDIVA_EXPORT
68+
int32_t aes_decrypt_gcm(const char* ciphertext, int32_t ciphertext_len, const char* key,
69+
int32_t key_len, const char* iv, int32_t iv_len,
70+
const char* aad, int32_t aad_len, unsigned char* plaintext);
71+
72+
} // namespace gandiva
73+

0 commit comments

Comments
 (0)