@hxa-rn/react-native-quick-crypto 1.1.0-beta.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,415 +1,415 @@
1
- #include <algorithm> // For std::sort
2
- #include <cstring> // For std::memcpy
3
- #include <memory>
4
- #include <stdexcept>
5
- #include <string>
6
- #include <vector>
7
-
8
- #include "HybridCipher.hpp"
9
- #include "QuickCryptoUtils.hpp"
10
-
11
- #include <ncrypto.h>
12
- #include <openssl/err.h>
13
- #include <openssl/evp.h>
14
-
15
- namespace margelo::nitro::crypto {
16
-
17
- HybridCipher::~HybridCipher() {
18
- if (ctx) {
19
- EVP_CIPHER_CTX_free(ctx);
20
- // No need to set ctx = nullptr here, object is being destroyed
21
- }
22
- }
23
-
24
- void HybridCipher::checkCtx() const {
25
- if (!ctx) {
26
- throw std::runtime_error("Cipher context is not initialized or has been disposed.");
27
- }
28
- }
29
-
30
- void HybridCipher::checkNotFinalized() const {
31
- if (is_finalized) {
32
- throw std::runtime_error("Unsupported state or unable to authenticate data");
33
- }
34
- }
35
-
36
- bool HybridCipher::maybePassAuthTagToOpenSSL() {
37
- if (auth_tag_state == kAuthTagKnown) {
38
- OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG, auth_tag, auth_tag_len),
39
- OSSL_PARAM_construct_end()};
40
- if (!EVP_CIPHER_CTX_set_params(ctx, params)) {
41
- unsigned long err = ERR_get_error();
42
- char err_buf[256];
43
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
44
- return false;
45
- }
46
- auth_tag_state = kAuthTagPassedToOpenSSL;
47
- }
48
- return true;
49
- }
50
-
51
- void HybridCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
52
- // Clean up any existing context
53
- if (ctx) {
54
- EVP_CIPHER_CTX_free(ctx);
55
- ctx = nullptr;
56
- }
57
- is_finalized = false;
58
-
59
- // 1. Get cipher implementation by name
60
- const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str());
61
- if (!cipher) {
62
- throw std::runtime_error("Unknown cipher " + cipher_type);
63
- }
64
-
65
- // 2. Create a new context
66
- ctx = EVP_CIPHER_CTX_new();
67
- if (!ctx) {
68
- throw std::runtime_error("Failed to create cipher context");
69
- }
70
-
71
- // Initialise the encryption/decryption operation with the cipher type.
72
- // Key and IV will be set later by the derived class if needed.
73
- if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
74
- unsigned long err = ERR_get_error();
75
- char err_buf[256];
76
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
77
- EVP_CIPHER_CTX_free(ctx);
78
- ctx = nullptr;
79
- throw std::runtime_error("HybridCipher: Failed initial CipherInit setup: " + std::string(err_buf));
80
- }
81
-
82
- // For base hybrid cipher, set key and IV immediately.
83
- // Derived classes like CCM might override init and handle this differently.
84
- auto native_key = ToNativeArrayBuffer(cipher_key);
85
- auto native_iv = ToNativeArrayBuffer(iv);
86
-
87
- // Match Node.js: reject incorrect key lengths before OpenSSL silently truncates/pads.
88
- const int expected_key_len = EVP_CIPHER_get_key_length(cipher);
89
- if (expected_key_len > 0 && static_cast<int>(native_key->size()) != expected_key_len) {
90
- EVP_CIPHER_CTX_free(ctx);
91
- ctx = nullptr;
92
- throw std::runtime_error("Invalid key length");
93
- }
94
-
95
- const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
96
- const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
97
-
98
- if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
99
- unsigned long err = ERR_get_error();
100
- char err_buf[256];
101
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
102
- EVP_CIPHER_CTX_free(ctx);
103
- ctx = nullptr;
104
- throw std::runtime_error("HybridCipher: Failed to set key/IV: " + std::string(err_buf));
105
- }
106
-
107
- // For AES-KW (wrap ciphers), set the WRAP_ALLOW flag and disable padding
108
- std::string cipher_name(cipher_type);
109
- if (cipher_name.find("-wrap") != std::string::npos) {
110
- // This flag is required for AES-KW in OpenSSL 3.x
111
- EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
112
- EVP_CIPHER_CTX_set_padding(ctx, 0);
113
- }
114
- }
115
-
116
- std::shared_ptr<ArrayBuffer> HybridCipher::update(const std::shared_ptr<ArrayBuffer>& data) {
117
- auto native_data = ToNativeArrayBuffer(data);
118
- checkCtx();
119
- checkNotFinalized();
120
- size_t in_len = native_data->size();
121
- if (in_len > INT_MAX) {
122
- throw std::runtime_error("Message too long");
123
- }
124
-
125
- int out_len = in_len + EVP_CIPHER_CTX_block_size(ctx);
126
- uint8_t* out = new uint8_t[out_len];
127
- // Perform the cipher update operation. The real size of the output is
128
- // returned in out_len
129
- int ret = EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len);
130
-
131
- if (!ret) {
132
- unsigned long err = ERR_get_error();
133
- char err_buf[256];
134
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
135
- delete[] out;
136
- throw std::runtime_error("Cipher update failed: " + std::string(err_buf));
137
- }
138
-
139
- // Create and return a new buffer of exact size needed
140
- return std::make_shared<NativeArrayBuffer>(out, out_len, [=]() { delete[] out; });
141
- }
142
-
143
- std::shared_ptr<ArrayBuffer> HybridCipher::final() {
144
- checkCtx();
145
- checkNotFinalized();
146
- // Block size is max output size for final, unless EVP_CIPH_NO_PADDING is set
147
- int block_size = EVP_CIPHER_CTX_block_size(ctx);
148
- if (block_size <= 0)
149
- block_size = 16; // Default if block size is weird (e.g., 0)
150
- auto out_buf = std::make_unique<uint8_t[]>(block_size);
151
- int out_len = 0;
152
-
153
- int ret = EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len);
154
- if (!ret) {
155
- unsigned long err = ERR_get_error();
156
- char err_buf[256];
157
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
158
- // Don't free context on error here either, rely on destructor
159
- throw std::runtime_error("Cipher final failed: " + std::string(err_buf));
160
- }
161
-
162
- // Get raw pointer before releasing unique_ptr
163
- uint8_t* raw_ptr = out_buf.get();
164
- // Create the specific NativeArrayBuffer first, using full namespace
165
- auto native_final_chunk = std::make_shared<margelo::nitro::NativeArrayBuffer>(out_buf.release(), static_cast<size_t>(out_len),
166
- [raw_ptr]() { delete[] raw_ptr; });
167
-
168
- // Context should NOT be freed here. It might be needed for getAuthTag() for GCM/OCB.
169
- // The context will be freed by the destructor (~HybridCipher) when the object goes out of scope.
170
- is_finalized = true;
171
-
172
- return native_final_chunk;
173
- }
174
-
175
- bool HybridCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) {
176
- checkCtx();
177
- auto native_data = ToNativeArrayBuffer(data);
178
-
179
- // Set the AAD
180
- int out_len;
181
- if (!EVP_CipherUpdate(ctx, nullptr, &out_len, native_data->data(), native_data->size())) {
182
- return false;
183
- }
184
-
185
- has_aad = true;
186
- return true;
187
- }
188
-
189
- bool HybridCipher::setAutoPadding(bool autoPad) {
190
- checkCtx();
191
- return EVP_CIPHER_CTX_set_padding(ctx, autoPad) == 1;
192
- }
193
-
194
- bool HybridCipher::setAuthTag(const std::shared_ptr<ArrayBuffer>& tag) {
195
- checkCtx();
196
-
197
- if (is_cipher) {
198
- throw std::runtime_error("setAuthTag can only be called during decryption.");
199
- }
200
-
201
- auto native_tag = ToNativeArrayBuffer(tag);
202
- size_t tag_len = native_tag->size();
203
- uint8_t* tag_ptr = native_tag->data();
204
-
205
- int mode = EVP_CIPHER_CTX_mode(ctx);
206
-
207
- if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
208
- // Use EVP_CTRL_AEAD_SET_TAG for GCM/OCB decryption
209
- if (tag_len < 1 || tag_len > 16) { // Check tag length bounds for GCM/OCB
210
- throw std::runtime_error("Invalid auth tag length for GCM/OCB. Must be between 1 and 16 bytes.");
211
- }
212
- // Add check for valid cipher in context before setting tag
213
- // Use the correct OpenSSL 3 function: EVP_CIPHER_CTX_cipher
214
- if (!EVP_CIPHER_CTX_cipher(ctx)) {
215
- throw std::runtime_error("Context has no cipher set before setting GCM/OCB tag");
216
- }
217
- if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag_ptr) <= 0) {
218
- unsigned long err = ERR_get_error();
219
- char err_buf[256];
220
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
221
- // Include the error code in the message
222
- throw std::runtime_error("Failed to set GCM/OCB auth tag: " + std::string(err_buf) + " (code: " + std::to_string(err) + ")");
223
- }
224
- auth_tag_state = kAuthTagPassedToOpenSSL; // Mark state
225
- return true;
226
-
227
- } else if (mode == EVP_CIPH_CCM_MODE) {
228
- // Store tag internally for CCM decryption (used in CCMCipher::final)
229
- if (tag_len < 4 || tag_len > 16) { // Check tag length bounds for CCM
230
- throw std::runtime_error("Invalid auth tag length for CCM. Must be between 4 and 16 bytes.");
231
- }
232
- auth_tag_state = kAuthTagKnown; // Correct state enum value
233
- auth_tag_len = tag_len;
234
- // Copy directly into the member buffer (assuming uint8_t auth_tag[16])
235
- std::memcpy(auth_tag, tag_ptr, tag_len);
236
- return true;
237
-
238
- } else {
239
- // Not an AEAD mode that supports setAuthTag for decryption
240
- throw std::runtime_error("setAuthTag is not supported for the current cipher mode.");
241
- }
242
- }
243
-
244
- std::shared_ptr<ArrayBuffer> HybridCipher::getAuthTag() {
245
- checkCtx();
246
-
247
- int mode = EVP_CIPHER_CTX_mode(ctx);
248
-
249
- if (!is_cipher) {
250
- throw std::runtime_error("getAuthTag can only be called during encryption.");
251
- }
252
-
253
- if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
254
- // Retrieve the tag using EVP_CIPHER_CTX_ctrl for GCM/OCB
255
- constexpr int max_tag_len = 16; // GCM/OCB tags are typically up to 16 bytes
256
- auto tag_buf = std::make_unique<uint8_t[]>(max_tag_len);
257
-
258
- int ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, max_tag_len, tag_buf.get());
259
-
260
- if (ret <= 0) {
261
- unsigned long err = ERR_get_error();
262
- char err_buf[256];
263
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
264
- throw std::runtime_error("Failed to get GCM/OCB auth tag: " + std::string(err_buf));
265
- }
266
-
267
- uint8_t* raw_ptr = tag_buf.get();
268
- auto final_tag_buffer =
269
- std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; });
270
- return final_tag_buffer;
271
-
272
- } else if (mode == EVP_CIPH_CCM_MODE) {
273
- // CCM: allow getAuthTag after encryption/finalization
274
- if (auth_tag_len > 0 && auth_tag_state == kAuthTagKnown) {
275
- // Return the stored tag buffer
276
- auto tag_buf = std::make_unique<uint8_t[]>(auth_tag_len);
277
- std::memcpy(tag_buf.get(), auth_tag, auth_tag_len);
278
- uint8_t* raw_ptr = tag_buf.get();
279
- auto final_tag_buffer =
280
- std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; });
281
- return final_tag_buffer;
282
- } else {
283
- throw std::runtime_error("CCM: Auth tag not available. Ensure encryption is finalized before calling getAuthTag.");
284
- }
285
- } else {
286
- // Not an AEAD mode that supports getAuthTag post-encryption
287
- throw std::runtime_error("getAuthTag is not supported for the current cipher mode.");
288
- }
289
- }
290
-
291
- int HybridCipher::getMode() {
292
- if (!ctx) {
293
- throw std::runtime_error("Cipher not initialized. Did you call setArgs()?");
294
- }
295
- return EVP_CIPHER_CTX_get_mode(ctx);
296
- }
297
-
298
- void HybridCipher::setArgs(const CipherArgs& args) {
299
- this->is_cipher = args.isCipher;
300
- this->cipher_type = args.cipherType;
301
-
302
- // Reset auth tag state
303
- auth_tag_state = kAuthTagUnknown;
304
- std::memset(auth_tag, 0, EVP_GCM_TLS_TAG_LEN);
305
-
306
- // Set auth tag length from args or use default
307
- if (args.authTagLen.has_value()) {
308
- if (!CheckIsUint32(args.authTagLen.value())) {
309
- throw std::runtime_error("authTagLen must be uint32");
310
- }
311
- uint32_t requested_len = static_cast<uint32_t>(args.authTagLen.value());
312
- if (requested_len > EVP_GCM_TLS_TAG_LEN) {
313
- throw std::runtime_error("Authentication tag length too large");
314
- }
315
- this->auth_tag_len = requested_len;
316
- } else {
317
- // Default to 16 bytes for all authenticated modes
318
- this->auth_tag_len = kDefaultAuthTagLength;
319
- }
320
- }
321
-
322
- // Corrected callback signature for EVP_CIPHER_do_all_provided
323
- void collect_ciphers(EVP_CIPHER* cipher, void* arg) {
324
- auto* names = static_cast<std::vector<std::string>*>(arg);
325
- if (cipher == nullptr)
326
- return;
327
- // Note: EVP_CIPHER_get0_name expects const EVP_CIPHER*, but the callback provides EVP_CIPHER*.
328
- // This implicit const cast should be safe here.
329
- const char* name = EVP_CIPHER_get0_name(cipher);
330
- if (name != nullptr) {
331
- std::string name_str(name);
332
- if (name_str == "NULL" || name_str.find("CTS") != std::string::npos ||
333
- name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV
334
- name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV
335
- name_str.find("SM4-") != std::string::npos ||
336
- name_str.find("-ETM") != std::string::npos) { // TLS-internal ciphers, not for general use
337
- return; // Skip adding this cipher
338
- }
339
-
340
- // If not filtered out, add it to the list
341
- names->push_back(name_str); // Use name_str here
342
- }
343
- }
344
-
345
- std::vector<std::string> HybridCipher::getSupportedCiphers() {
346
- std::vector<std::string> cipher_names;
347
-
348
- // Use the simpler approach with the separate callback
349
- EVP_CIPHER_do_all_provided(nullptr, // Default library context
350
- collect_ciphers, &cipher_names);
351
-
352
- // OpenSSL 3 doesn't guarantee sorted output with _do_all_provided, sort manually
353
- std::sort(cipher_names.begin(), cipher_names.end());
354
-
355
- return cipher_names;
356
- }
357
-
358
- std::optional<CipherInfo> HybridCipher::getCipherInfo(const std::string& name, std::optional<double> keyLength,
359
- std::optional<double> ivLength) {
360
- auto cipher = ncrypto::Cipher::FromName(name.c_str());
361
- if (!cipher)
362
- return std::nullopt;
363
-
364
- size_t iv_length = cipher.getIvLength();
365
- size_t key_length = cipher.getKeyLength();
366
-
367
- if (keyLength.has_value() || ivLength.has_value()) {
368
- auto ctx = ncrypto::CipherCtxPointer::New();
369
- if (!ctx.init(cipher, true))
370
- return std::nullopt;
371
-
372
- if (keyLength.has_value()) {
373
- size_t check_len = static_cast<size_t>(keyLength.value());
374
- if (!ctx.setKeyLength(check_len))
375
- return std::nullopt;
376
- key_length = check_len;
377
- }
378
-
379
- if (ivLength.has_value()) {
380
- size_t check_len = static_cast<size_t>(ivLength.value());
381
- if (cipher.isCcmMode()) {
382
- if (check_len < 7 || check_len > 13)
383
- return std::nullopt;
384
- } else if (cipher.isGcmMode()) {
385
- // GCM accepts flexible IV lengths
386
- } else if (cipher.isOcbMode()) {
387
- if (!ctx.setIvLength(check_len))
388
- return std::nullopt;
389
- } else {
390
- if (check_len != iv_length)
391
- return std::nullopt;
392
- }
393
- iv_length = check_len;
394
- }
395
- }
396
-
397
- std::string name_str(cipher.getName());
398
- std::transform(name_str.begin(), name_str.end(), name_str.begin(), ::tolower);
399
-
400
- std::string mode_str(cipher.getModeLabel());
401
-
402
- std::optional<double> block_size = std::nullopt;
403
- if (!cipher.isStreamMode()) {
404
- block_size = static_cast<double>(cipher.getBlockSize());
405
- }
406
-
407
- std::optional<double> iv_len = std::nullopt;
408
- if (iv_length != 0) {
409
- iv_len = static_cast<double>(iv_length);
410
- }
411
-
412
- return CipherInfo{name_str, static_cast<double>(cipher.getNid()), mode_str, static_cast<double>(key_length), block_size, iv_len};
413
- }
414
-
415
- } // namespace margelo::nitro::crypto
1
+ #include <algorithm> // For std::sort
2
+ #include <cstring> // For std::memcpy
3
+ #include <memory>
4
+ #include <stdexcept>
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ #include "HybridCipher.hpp"
9
+ #include "QuickCryptoUtils.hpp"
10
+
11
+ #include <ncrypto.h>
12
+ #include <openssl/err.h>
13
+ #include <openssl/evp.h>
14
+
15
+ namespace margelo::nitro::crypto {
16
+
17
+ HybridCipher::~HybridCipher() {
18
+ if (ctx) {
19
+ EVP_CIPHER_CTX_free(ctx);
20
+ // No need to set ctx = nullptr here, object is being destroyed
21
+ }
22
+ }
23
+
24
+ void HybridCipher::checkCtx() const {
25
+ if (!ctx) {
26
+ throw std::runtime_error("Cipher context is not initialized or has been disposed.");
27
+ }
28
+ }
29
+
30
+ void HybridCipher::checkNotFinalized() const {
31
+ if (is_finalized) {
32
+ throw std::runtime_error("Unsupported state or unable to authenticate data");
33
+ }
34
+ }
35
+
36
+ bool HybridCipher::maybePassAuthTagToOpenSSL() {
37
+ if (auth_tag_state == kAuthTagKnown) {
38
+ OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG, auth_tag, auth_tag_len),
39
+ OSSL_PARAM_construct_end()};
40
+ if (!EVP_CIPHER_CTX_set_params(ctx, params)) {
41
+ unsigned long err = ERR_get_error();
42
+ char err_buf[256];
43
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
44
+ return false;
45
+ }
46
+ auth_tag_state = kAuthTagPassedToOpenSSL;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ void HybridCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
52
+ // Clean up any existing context
53
+ if (ctx) {
54
+ EVP_CIPHER_CTX_free(ctx);
55
+ ctx = nullptr;
56
+ }
57
+ is_finalized = false;
58
+
59
+ // 1. Get cipher implementation by name
60
+ const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str());
61
+ if (!cipher) {
62
+ throw std::runtime_error("Unknown cipher " + cipher_type);
63
+ }
64
+
65
+ // 2. Create a new context
66
+ ctx = EVP_CIPHER_CTX_new();
67
+ if (!ctx) {
68
+ throw std::runtime_error("Failed to create cipher context");
69
+ }
70
+
71
+ // Initialise the encryption/decryption operation with the cipher type.
72
+ // Key and IV will be set later by the derived class if needed.
73
+ if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
74
+ unsigned long err = ERR_get_error();
75
+ char err_buf[256];
76
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
77
+ EVP_CIPHER_CTX_free(ctx);
78
+ ctx = nullptr;
79
+ throw std::runtime_error("HybridCipher: Failed initial CipherInit setup: " + std::string(err_buf));
80
+ }
81
+
82
+ // For base hybrid cipher, set key and IV immediately.
83
+ // Derived classes like CCM might override init and handle this differently.
84
+ auto native_key = ToNativeArrayBuffer(cipher_key);
85
+ auto native_iv = ToNativeArrayBuffer(iv);
86
+
87
+ // Match Node.js: reject incorrect key lengths before OpenSSL silently truncates/pads.
88
+ const int expected_key_len = EVP_CIPHER_get_key_length(cipher);
89
+ if (expected_key_len > 0 && static_cast<int>(native_key->size()) != expected_key_len) {
90
+ EVP_CIPHER_CTX_free(ctx);
91
+ ctx = nullptr;
92
+ throw std::runtime_error("Invalid key length");
93
+ }
94
+
95
+ const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
96
+ const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
97
+
98
+ if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
99
+ unsigned long err = ERR_get_error();
100
+ char err_buf[256];
101
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
102
+ EVP_CIPHER_CTX_free(ctx);
103
+ ctx = nullptr;
104
+ throw std::runtime_error("HybridCipher: Failed to set key/IV: " + std::string(err_buf));
105
+ }
106
+
107
+ // For AES-KW (wrap ciphers), set the WRAP_ALLOW flag and disable padding
108
+ std::string cipher_name(cipher_type);
109
+ if (cipher_name.find("-wrap") != std::string::npos) {
110
+ // This flag is required for AES-KW in OpenSSL 3.x
111
+ EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
112
+ EVP_CIPHER_CTX_set_padding(ctx, 0);
113
+ }
114
+ }
115
+
116
+ std::shared_ptr<ArrayBuffer> HybridCipher::update(const std::shared_ptr<ArrayBuffer>& data) {
117
+ auto native_data = ToNativeArrayBuffer(data);
118
+ checkCtx();
119
+ checkNotFinalized();
120
+ size_t in_len = native_data->size();
121
+ if (in_len > INT_MAX) {
122
+ throw std::runtime_error("Message too long");
123
+ }
124
+
125
+ int out_len = in_len + EVP_CIPHER_CTX_block_size(ctx);
126
+ uint8_t* out = new uint8_t[out_len];
127
+ // Perform the cipher update operation. The real size of the output is
128
+ // returned in out_len
129
+ int ret = EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len);
130
+
131
+ if (!ret) {
132
+ unsigned long err = ERR_get_error();
133
+ char err_buf[256];
134
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
135
+ delete[] out;
136
+ throw std::runtime_error("Cipher update failed: " + std::string(err_buf));
137
+ }
138
+
139
+ // Create and return a new buffer of exact size needed
140
+ return std::make_shared<NativeArrayBuffer>(out, out_len, [=]() { delete[] out; });
141
+ }
142
+
143
+ std::shared_ptr<ArrayBuffer> HybridCipher::final() {
144
+ checkCtx();
145
+ checkNotFinalized();
146
+ // Block size is max output size for final, unless EVP_CIPH_NO_PADDING is set
147
+ int block_size = EVP_CIPHER_CTX_block_size(ctx);
148
+ if (block_size <= 0)
149
+ block_size = 16; // Default if block size is weird (e.g., 0)
150
+ auto out_buf = std::make_unique<uint8_t[]>(block_size);
151
+ int out_len = 0;
152
+
153
+ int ret = EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len);
154
+ if (!ret) {
155
+ unsigned long err = ERR_get_error();
156
+ char err_buf[256];
157
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
158
+ // Don't free context on error here either, rely on destructor
159
+ throw std::runtime_error("Cipher final failed: " + std::string(err_buf));
160
+ }
161
+
162
+ // Get raw pointer before releasing unique_ptr
163
+ uint8_t* raw_ptr = out_buf.get();
164
+ // Create the specific NativeArrayBuffer first, using full namespace
165
+ auto native_final_chunk = std::make_shared<margelo::nitro::NativeArrayBuffer>(out_buf.release(), static_cast<size_t>(out_len),
166
+ [raw_ptr]() { delete[] raw_ptr; });
167
+
168
+ // Context should NOT be freed here. It might be needed for getAuthTag() for GCM/OCB.
169
+ // The context will be freed by the destructor (~HybridCipher) when the object goes out of scope.
170
+ is_finalized = true;
171
+
172
+ return native_final_chunk;
173
+ }
174
+
175
+ bool HybridCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) {
176
+ checkCtx();
177
+ auto native_data = ToNativeArrayBuffer(data);
178
+
179
+ // Set the AAD
180
+ int out_len;
181
+ if (!EVP_CipherUpdate(ctx, nullptr, &out_len, native_data->data(), native_data->size())) {
182
+ return false;
183
+ }
184
+
185
+ has_aad = true;
186
+ return true;
187
+ }
188
+
189
+ bool HybridCipher::setAutoPadding(bool autoPad) {
190
+ checkCtx();
191
+ return EVP_CIPHER_CTX_set_padding(ctx, autoPad) == 1;
192
+ }
193
+
194
+ bool HybridCipher::setAuthTag(const std::shared_ptr<ArrayBuffer>& tag) {
195
+ checkCtx();
196
+
197
+ if (is_cipher) {
198
+ throw std::runtime_error("setAuthTag can only be called during decryption.");
199
+ }
200
+
201
+ auto native_tag = ToNativeArrayBuffer(tag);
202
+ size_t tag_len = native_tag->size();
203
+ uint8_t* tag_ptr = native_tag->data();
204
+
205
+ int mode = EVP_CIPHER_CTX_mode(ctx);
206
+
207
+ if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
208
+ // Use EVP_CTRL_AEAD_SET_TAG for GCM/OCB decryption
209
+ if (tag_len < 1 || tag_len > 16) { // Check tag length bounds for GCM/OCB
210
+ throw std::runtime_error("Invalid auth tag length for GCM/OCB. Must be between 1 and 16 bytes.");
211
+ }
212
+ // Add check for valid cipher in context before setting tag
213
+ // Use the correct OpenSSL 3 function: EVP_CIPHER_CTX_cipher
214
+ if (!EVP_CIPHER_CTX_cipher(ctx)) {
215
+ throw std::runtime_error("Context has no cipher set before setting GCM/OCB tag");
216
+ }
217
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag_ptr) <= 0) {
218
+ unsigned long err = ERR_get_error();
219
+ char err_buf[256];
220
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
221
+ // Include the error code in the message
222
+ throw std::runtime_error("Failed to set GCM/OCB auth tag: " + std::string(err_buf) + " (code: " + std::to_string(err) + ")");
223
+ }
224
+ auth_tag_state = kAuthTagPassedToOpenSSL; // Mark state
225
+ return true;
226
+
227
+ } else if (mode == EVP_CIPH_CCM_MODE) {
228
+ // Store tag internally for CCM decryption (used in CCMCipher::final)
229
+ if (tag_len < 4 || tag_len > 16) { // Check tag length bounds for CCM
230
+ throw std::runtime_error("Invalid auth tag length for CCM. Must be between 4 and 16 bytes.");
231
+ }
232
+ auth_tag_state = kAuthTagKnown; // Correct state enum value
233
+ auth_tag_len = tag_len;
234
+ // Copy directly into the member buffer (assuming uint8_t auth_tag[16])
235
+ std::memcpy(auth_tag, tag_ptr, tag_len);
236
+ return true;
237
+
238
+ } else {
239
+ // Not an AEAD mode that supports setAuthTag for decryption
240
+ throw std::runtime_error("setAuthTag is not supported for the current cipher mode.");
241
+ }
242
+ }
243
+
244
+ std::shared_ptr<ArrayBuffer> HybridCipher::getAuthTag() {
245
+ checkCtx();
246
+
247
+ int mode = EVP_CIPHER_CTX_mode(ctx);
248
+
249
+ if (!is_cipher) {
250
+ throw std::runtime_error("getAuthTag can only be called during encryption.");
251
+ }
252
+
253
+ if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
254
+ // Retrieve the tag using EVP_CIPHER_CTX_ctrl for GCM/OCB
255
+ constexpr int max_tag_len = 16; // GCM/OCB tags are typically up to 16 bytes
256
+ auto tag_buf = std::make_unique<uint8_t[]>(max_tag_len);
257
+
258
+ int ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, max_tag_len, tag_buf.get());
259
+
260
+ if (ret <= 0) {
261
+ unsigned long err = ERR_get_error();
262
+ char err_buf[256];
263
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
264
+ throw std::runtime_error("Failed to get GCM/OCB auth tag: " + std::string(err_buf));
265
+ }
266
+
267
+ uint8_t* raw_ptr = tag_buf.get();
268
+ auto final_tag_buffer =
269
+ std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; });
270
+ return final_tag_buffer;
271
+
272
+ } else if (mode == EVP_CIPH_CCM_MODE) {
273
+ // CCM: allow getAuthTag after encryption/finalization
274
+ if (auth_tag_len > 0 && auth_tag_state == kAuthTagKnown) {
275
+ // Return the stored tag buffer
276
+ auto tag_buf = std::make_unique<uint8_t[]>(auth_tag_len);
277
+ std::memcpy(tag_buf.get(), auth_tag, auth_tag_len);
278
+ uint8_t* raw_ptr = tag_buf.get();
279
+ auto final_tag_buffer =
280
+ std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; });
281
+ return final_tag_buffer;
282
+ } else {
283
+ throw std::runtime_error("CCM: Auth tag not available. Ensure encryption is finalized before calling getAuthTag.");
284
+ }
285
+ } else {
286
+ // Not an AEAD mode that supports getAuthTag post-encryption
287
+ throw std::runtime_error("getAuthTag is not supported for the current cipher mode.");
288
+ }
289
+ }
290
+
291
+ int HybridCipher::getMode() {
292
+ if (!ctx) {
293
+ throw std::runtime_error("Cipher not initialized. Did you call setArgs()?");
294
+ }
295
+ return EVP_CIPHER_CTX_get_mode(ctx);
296
+ }
297
+
298
+ void HybridCipher::setArgs(const CipherArgs& args) {
299
+ this->is_cipher = args.isCipher;
300
+ this->cipher_type = args.cipherType;
301
+
302
+ // Reset auth tag state
303
+ auth_tag_state = kAuthTagUnknown;
304
+ std::memset(auth_tag, 0, EVP_GCM_TLS_TAG_LEN);
305
+
306
+ // Set auth tag length from args or use default
307
+ if (args.authTagLen.has_value()) {
308
+ if (!CheckIsUint32(args.authTagLen.value())) {
309
+ throw std::runtime_error("authTagLen must be uint32");
310
+ }
311
+ uint32_t requested_len = static_cast<uint32_t>(args.authTagLen.value());
312
+ if (requested_len > EVP_GCM_TLS_TAG_LEN) {
313
+ throw std::runtime_error("Authentication tag length too large");
314
+ }
315
+ this->auth_tag_len = requested_len;
316
+ } else {
317
+ // Default to 16 bytes for all authenticated modes
318
+ this->auth_tag_len = kDefaultAuthTagLength;
319
+ }
320
+ }
321
+
322
+ // Corrected callback signature for EVP_CIPHER_do_all_provided
323
+ void collect_ciphers(EVP_CIPHER* cipher, void* arg) {
324
+ auto* names = static_cast<std::vector<std::string>*>(arg);
325
+ if (cipher == nullptr)
326
+ return;
327
+ // Note: EVP_CIPHER_get0_name expects const EVP_CIPHER*, but the callback provides EVP_CIPHER*.
328
+ // This implicit const cast should be safe here.
329
+ const char* name = EVP_CIPHER_get0_name(cipher);
330
+ if (name != nullptr) {
331
+ std::string name_str(name);
332
+ if (name_str == "NULL" || name_str.find("CTS") != std::string::npos ||
333
+ name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV
334
+ name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV
335
+ name_str.find("SM4-") != std::string::npos ||
336
+ name_str.find("-ETM") != std::string::npos) { // TLS-internal ciphers, not for general use
337
+ return; // Skip adding this cipher
338
+ }
339
+
340
+ // If not filtered out, add it to the list
341
+ names->push_back(name_str); // Use name_str here
342
+ }
343
+ }
344
+
345
+ std::vector<std::string> HybridCipher::getSupportedCiphers() {
346
+ std::vector<std::string> cipher_names;
347
+
348
+ // Use the simpler approach with the separate callback
349
+ EVP_CIPHER_do_all_provided(nullptr, // Default library context
350
+ collect_ciphers, &cipher_names);
351
+
352
+ // OpenSSL 3 doesn't guarantee sorted output with _do_all_provided, sort manually
353
+ std::sort(cipher_names.begin(), cipher_names.end());
354
+
355
+ return cipher_names;
356
+ }
357
+
358
+ std::optional<CipherInfo> HybridCipher::getCipherInfo(const std::string& name, std::optional<double> keyLength,
359
+ std::optional<double> ivLength) {
360
+ auto cipher = ncrypto::Cipher::FromName(name.c_str());
361
+ if (!cipher)
362
+ return std::nullopt;
363
+
364
+ size_t iv_length = cipher.getIvLength();
365
+ size_t key_length = cipher.getKeyLength();
366
+
367
+ if (keyLength.has_value() || ivLength.has_value()) {
368
+ auto ctx = ncrypto::CipherCtxPointer::New();
369
+ if (!ctx.init(cipher, true))
370
+ return std::nullopt;
371
+
372
+ if (keyLength.has_value()) {
373
+ size_t check_len = static_cast<size_t>(keyLength.value());
374
+ if (!ctx.setKeyLength(check_len))
375
+ return std::nullopt;
376
+ key_length = check_len;
377
+ }
378
+
379
+ if (ivLength.has_value()) {
380
+ size_t check_len = static_cast<size_t>(ivLength.value());
381
+ if (cipher.isCcmMode()) {
382
+ if (check_len < 7 || check_len > 13)
383
+ return std::nullopt;
384
+ } else if (cipher.isGcmMode()) {
385
+ // GCM accepts flexible IV lengths
386
+ } else if (cipher.isOcbMode()) {
387
+ if (!ctx.setIvLength(check_len))
388
+ return std::nullopt;
389
+ } else {
390
+ if (check_len != iv_length)
391
+ return std::nullopt;
392
+ }
393
+ iv_length = check_len;
394
+ }
395
+ }
396
+
397
+ std::string name_str(cipher.getName());
398
+ std::transform(name_str.begin(), name_str.end(), name_str.begin(), ::tolower);
399
+
400
+ std::string mode_str(cipher.getModeLabel());
401
+
402
+ std::optional<double> block_size = std::nullopt;
403
+ if (!cipher.isStreamMode()) {
404
+ block_size = static_cast<double>(cipher.getBlockSize());
405
+ }
406
+
407
+ std::optional<double> iv_len = std::nullopt;
408
+ if (iv_length != 0) {
409
+ iv_len = static_cast<double>(iv_length);
410
+ }
411
+
412
+ return CipherInfo{name_str, static_cast<double>(cipher.getNid()), mode_str, static_cast<double>(key_length), block_size, iv_len};
413
+ }
414
+
415
+ } // namespace margelo::nitro::crypto