react-native-quick-crypto 1.0.0-beta.13 → 1.0.0-beta.15

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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/android/CMakeLists.txt +8 -4
  3. package/android/src/main/java/com/margelo/nitro/quickcrypto/QuickCryptoPackage.java +0 -2
  4. package/cpp/cipher/CCMCipher.cpp +199 -0
  5. package/cpp/cipher/CCMCipher.hpp +26 -0
  6. package/cpp/cipher/HybridCipher.cpp +324 -0
  7. package/cpp/cipher/HybridCipher.hpp +69 -0
  8. package/cpp/cipher/HybridCipherFactory.hpp +59 -0
  9. package/cpp/cipher/OCBCipher.cpp +55 -0
  10. package/cpp/cipher/OCBCipher.hpp +19 -0
  11. package/cpp/ed25519/HybridEdKeyPair.cpp +18 -3
  12. package/cpp/ed25519/HybridEdKeyPair.hpp +1 -0
  13. package/cpp/random/HybridRandom.cpp +1 -1
  14. package/lib/commonjs/cipher.js +157 -0
  15. package/lib/commonjs/cipher.js.map +1 -0
  16. package/lib/commonjs/index.js +26 -33
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/pbkdf2.js.map +1 -1
  19. package/lib/commonjs/specs/cipher.nitro.js +6 -0
  20. package/lib/commonjs/specs/cipher.nitro.js.map +1 -0
  21. package/lib/commonjs/utils/cipher.js +64 -0
  22. package/lib/commonjs/utils/cipher.js.map +1 -0
  23. package/lib/commonjs/utils/conversion.js +18 -2
  24. package/lib/commonjs/utils/conversion.js.map +1 -1
  25. package/lib/commonjs/utils/types.js +3 -1
  26. package/lib/commonjs/utils/types.js.map +1 -1
  27. package/lib/module/cipher.js +150 -0
  28. package/lib/module/cipher.js.map +1 -0
  29. package/lib/module/index.js +6 -23
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/pbkdf2.js.map +1 -1
  32. package/lib/module/specs/cipher.nitro.js +4 -0
  33. package/lib/module/specs/cipher.nitro.js.map +1 -0
  34. package/lib/module/utils/cipher.js +56 -0
  35. package/lib/module/utils/cipher.js.map +1 -0
  36. package/lib/module/utils/conversion.js +18 -2
  37. package/lib/module/utils/conversion.js.map +1 -1
  38. package/lib/module/utils/types.js +4 -0
  39. package/lib/module/utils/types.js.map +1 -1
  40. package/lib/tsconfig.tsbuildinfo +1 -1
  41. package/lib/typescript/cipher.d.ts +52 -0
  42. package/lib/typescript/cipher.d.ts.map +1 -0
  43. package/lib/typescript/hash.d.ts +0 -1
  44. package/lib/typescript/hash.d.ts.map +1 -1
  45. package/lib/typescript/hmac.d.ts +0 -1
  46. package/lib/typescript/hmac.d.ts.map +1 -1
  47. package/lib/typescript/index.d.ts +15 -12
  48. package/lib/typescript/index.d.ts.map +1 -1
  49. package/lib/typescript/keys/utils.d.ts.map +1 -1
  50. package/lib/typescript/pbkdf2.d.ts +1 -1
  51. package/lib/typescript/pbkdf2.d.ts.map +1 -1
  52. package/lib/typescript/specs/cipher.nitro.d.ts +29 -0
  53. package/lib/typescript/specs/cipher.nitro.d.ts.map +1 -0
  54. package/lib/typescript/utils/cipher.d.ts +7 -0
  55. package/lib/typescript/utils/cipher.d.ts.map +1 -0
  56. package/lib/typescript/utils/conversion.d.ts.map +1 -1
  57. package/lib/typescript/utils/types.d.ts +8 -3
  58. package/lib/typescript/utils/types.d.ts.map +1 -1
  59. package/nitrogen/generated/.gitattributes +1 -0
  60. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +18 -0
  61. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +30 -10
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/crypto/QuickCryptoOnLoad.kt +35 -0
  63. package/nitrogen/generated/ios/QuickCrypto+autolinking.rb +2 -0
  64. package/nitrogen/generated/ios/QuickCrypto-Swift-Cxx-Umbrella.hpp +0 -1
  65. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +30 -10
  66. package/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp +1 -1
  67. package/nitrogen/generated/shared/c++/CipherArgs.hpp +88 -0
  68. package/nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp +21 -0
  69. package/nitrogen/generated/shared/c++/HybridCipherFactorySpec.hpp +67 -0
  70. package/nitrogen/generated/shared/c++/HybridCipherSpec.cpp +28 -0
  71. package/nitrogen/generated/shared/c++/HybridCipherSpec.hpp +76 -0
  72. package/nitrogen/generated/shared/c++/HybridEdKeyPairSpec.hpp +1 -1
  73. package/nitrogen/generated/shared/c++/HybridHashSpec.hpp +1 -1
  74. package/nitrogen/generated/shared/c++/HybridHmacSpec.hpp +1 -1
  75. package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +1 -1
  76. package/nitrogen/generated/shared/c++/HybridPbkdf2Spec.hpp +1 -1
  77. package/nitrogen/generated/shared/c++/HybridRandomSpec.hpp +1 -1
  78. package/nitrogen/generated/shared/c++/JWK.hpp +2 -1
  79. package/nitrogen/generated/shared/c++/JWKkty.hpp +1 -1
  80. package/nitrogen/generated/shared/c++/JWKuse.hpp +1 -1
  81. package/nitrogen/generated/shared/c++/KFormatType.hpp +1 -1
  82. package/nitrogen/generated/shared/c++/KeyDetail.hpp +2 -1
  83. package/nitrogen/generated/shared/c++/KeyEncoding.hpp +1 -1
  84. package/nitrogen/generated/shared/c++/KeyType.hpp +1 -1
  85. package/nitrogen/generated/shared/c++/KeyUsage.hpp +1 -1
  86. package/nitrogen/generated/shared/c++/NamedCurve.hpp +1 -1
  87. package/package.json +5 -15
  88. package/src/cipher.ts +303 -0
  89. package/src/index.ts +6 -23
  90. package/src/pbkdf2.ts +1 -1
  91. package/src/specs/cipher.nitro.ts +25 -0
  92. package/src/utils/cipher.ts +60 -0
  93. package/src/utils/conversion.ts +23 -2
  94. package/src/utils/types.ts +29 -0
  95. package/lib/module/package.json +0 -1
package/README.md CHANGED
@@ -26,7 +26,7 @@ QuickCrypto can be used as a drop-in replacement for your Web3/Crypto apps to sp
26
26
 
27
27
  | Version | RN Architecture | Modules |
28
28
  | ------- | ------ | ------- |
29
- | `1.x` | new [->](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-apps.md) | Nitro Modules [->](https://github.com/margelo/react-native-nitro) |
29
+ | `1.x` | new [->](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-apps.md) | Nitro Modules [->](https://github.com/mrousavy/nitro) |
30
30
  | `0.x` | old | Bridge & JSI |
31
31
 
32
32
  ## Benchmarks
@@ -9,9 +9,12 @@ set(CMAKE_CXX_STANDARD 20)
9
9
  add_library(
10
10
  ${PACKAGE_NAME} SHARED
11
11
  src/main/cpp/cpp-adapter.cpp
12
- ../cpp/hmac/HybridHmac.cpp
13
- ../cpp/hash/HybridHash.cpp
12
+ ../cpp/cipher/CCMCipher.cpp
13
+ ../cpp/cipher/HybridCipher.cpp
14
+ ../cpp/cipher/OCBCipher.cpp
14
15
  ../cpp/ed25519/HybridEdKeyPair.cpp
16
+ ../cpp/hash/HybridHash.cpp
17
+ ../cpp/hmac/HybridHmac.cpp
15
18
  ../cpp/pbkdf2/HybridPbkdf2.cpp
16
19
  ../cpp/random/HybridRandom.cpp
17
20
  ../deps/fastpbkdf2/fastpbkdf2.c
@@ -23,9 +26,10 @@ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/QuickCrypto+autolinkin
23
26
  # local includes
24
27
  include_directories(
25
28
  "src/main/cpp"
26
- "../cpp/hmac"
27
- "../cpp/hash"
29
+ "../cpp/cipher"
28
30
  "../cpp/ed25519"
31
+ "../cpp/hash"
32
+ "../cpp/hmac"
29
33
  "../cpp/pbkdf2"
30
34
  "../cpp/random"
31
35
  "../cpp/utils"
@@ -8,8 +8,6 @@ import com.facebook.react.bridge.NativeModule;
8
8
  import com.facebook.react.bridge.ReactApplicationContext;
9
9
  import com.facebook.react.module.model.ReactModuleInfoProvider;
10
10
  import com.facebook.react.TurboReactPackage;
11
- import com.margelo.nitro.core.HybridObject;
12
- import com.margelo.nitro.core.HybridObjectRegistry;
13
11
 
14
12
  import java.util.HashMap;
15
13
  import java.util.function.Supplier;
@@ -0,0 +1,199 @@
1
+ #include "CCMCipher.hpp"
2
+ #include "Utils.hpp"
3
+ #include <openssl/err.h>
4
+ #include <openssl/evp.h>
5
+ #include <stdexcept>
6
+
7
+ namespace margelo::nitro::crypto {
8
+
9
+ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
10
+ // 1. Call the base class initializer first
11
+ try {
12
+ HybridCipher::init(cipher_key, iv);
13
+ } catch (const std::exception& e) {
14
+ throw; // Re-throw after logging
15
+ }
16
+
17
+ // Ensure context is valid after base init
18
+ checkCtx();
19
+
20
+ // 2. Perform CCM-specific initialization
21
+ auto native_iv = ToNativeArrayBuffer(iv);
22
+ size_t iv_len = native_iv->size();
23
+
24
+ // Set the IV length using CCM-specific control
25
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
26
+ unsigned long err = ERR_get_error();
27
+ char err_buf[256];
28
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
29
+ throw std::runtime_error("CCMCipher: Failed to set IV length: " + std::string(err_buf));
30
+ }
31
+
32
+ // Set the expected/output tag length using CCM-specific control.
33
+ // auth_tag_len should have been defaulted or set via setArgs in the base init.
34
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) {
35
+ unsigned long err = ERR_get_error();
36
+ char err_buf[256];
37
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
38
+ throw std::runtime_error("CCMCipher: Failed to set tag length: " + std::string(err_buf));
39
+ }
40
+
41
+ // Finally, initialize the key and IV using the parameters passed to this function.
42
+ auto native_key = ToNativeArrayBuffer(cipher_key); // Use 'cipher_key' parameter
43
+ const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
44
+ const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
45
+
46
+ // The last argument (is_cipher) should be consistent with the initial setup call.
47
+ if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
48
+ unsigned long err = ERR_get_error();
49
+ char err_buf[256];
50
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
51
+ throw std::runtime_error("CCMCipher: Failed to set key/IV: " + std::string(err_buf));
52
+ }
53
+ }
54
+
55
+ std::shared_ptr<ArrayBuffer> CCMCipher::update(const std::shared_ptr<ArrayBuffer>& data) {
56
+ checkCtx();
57
+ auto native_data = ToNativeArrayBuffer(data);
58
+ size_t in_len = native_data->size();
59
+ if (in_len < 0 || in_len > INT_MAX) {
60
+ throw std::runtime_error("Invalid message length");
61
+ }
62
+ int out_len = 0;
63
+
64
+ if (!is_cipher) {
65
+ maybePassAuthTagToOpenSSL();
66
+ }
67
+
68
+ int block_size = EVP_CIPHER_CTX_block_size(ctx);
69
+ if (block_size <= 0) {
70
+ throw std::runtime_error("Invalid block size in update");
71
+ }
72
+ out_len = in_len + block_size - 1;
73
+ if (out_len < 0 || out_len < in_len) {
74
+ throw std::runtime_error("Calculated output buffer size invalid in update");
75
+ }
76
+
77
+ auto out_buf = std::make_unique<unsigned char[]>(out_len);
78
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(native_data->data());
79
+
80
+ int actual_out_len = 0;
81
+ int ret = EVP_CipherUpdate(ctx, out_buf.get(), &actual_out_len, in, in_len);
82
+
83
+ if (!is_cipher) {
84
+ // Decryption: Check for tag verification failure
85
+ if (ret <= 0) {
86
+ // Tag verification failed (or other decryption error)
87
+ throw std::runtime_error("CCM Decryption: Tag verification failed");
88
+ }
89
+ } else {
90
+ // Encryption: Check for standard errors
91
+ if (ret != 1) {
92
+ pending_auth_failed = true; // Should this be set for encryption failure?
93
+ unsigned long err = ERR_get_error();
94
+ char err_buf[256];
95
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
96
+ throw std::runtime_error("Error in update() performing encryption operation: " + std::string(err_buf));
97
+ }
98
+ }
99
+ // If we reached here, the operation (encryption or decryption) succeeded
100
+
101
+ unsigned char* final_output = out_buf.release();
102
+ return std::make_shared<NativeArrayBuffer>(final_output, actual_out_len, [=]() { delete[] final_output; });
103
+ }
104
+
105
+ std::shared_ptr<ArrayBuffer> CCMCipher::final() {
106
+ checkCtx();
107
+
108
+ // CCM decryption does not use final. Verification happens in the last update call.
109
+ if (!is_cipher) {
110
+ // Return an empty buffer, matching Node.js behavior
111
+ unsigned char* empty_output = new unsigned char[0];
112
+ return std::make_shared<NativeArrayBuffer>(empty_output, 0, [=]() { delete[] empty_output; });
113
+ }
114
+
115
+ // Proceed only for encryption
116
+ int block_size = EVP_CIPHER_CTX_block_size(ctx);
117
+ if (block_size <= 0) {
118
+ throw std::runtime_error("Invalid block size");
119
+ }
120
+ auto out_buf = std::make_unique<unsigned char[]>(block_size);
121
+ int out_len = 0;
122
+
123
+ if (!EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len)) {
124
+ unsigned long err = ERR_get_error();
125
+ char err_buf[256];
126
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
127
+ throw std::runtime_error("Encryption finalization failed: " + std::string(err_buf));
128
+ }
129
+
130
+ if (auth_tag_len == 0) {
131
+ auth_tag_len = sizeof(auth_tag);
132
+ }
133
+
134
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) {
135
+ unsigned long err = ERR_get_error();
136
+ char err_buf[256];
137
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
138
+ throw std::runtime_error("Failed to get auth tag after finalization: " + std::string(err_buf));
139
+ }
140
+ auth_tag_state = kAuthTagKnown;
141
+
142
+ unsigned char* final_output = out_buf.release();
143
+ return std::make_shared<NativeArrayBuffer>(final_output, out_len, [=]() { delete[] final_output; });
144
+ }
145
+
146
+ bool CCMCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) {
147
+ checkCtx();
148
+ if (!plaintextLength.has_value()) {
149
+ throw std::runtime_error("CCM mode requires plaintextLength to be set");
150
+ }
151
+
152
+ // IMPORTANT: For CCM decryption (!is_cipher), OpenSSL requires this initial update
153
+ // call to specify the TOTAL LENGTH OF THE CIPHERTEXT, not the plaintext.
154
+ // The caller (JS) must ensure `plaintextLength` holds the ciphertext length when decrypting.
155
+ int data_len = static_cast<int>(plaintextLength.value());
156
+ if (data_len > kMaxMessageSize) {
157
+ throw std::runtime_error("Provided data length exceeds maximum allowed size");
158
+ }
159
+
160
+ if (!is_cipher) {
161
+ if (!maybePassAuthTagToOpenSSL()) {
162
+ unsigned long err = ERR_get_error();
163
+ char err_buf[256];
164
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
165
+ throw std::runtime_error("setAAD: Failed to set auth tag parameters: " + std::string(err_buf));
166
+ }
167
+ }
168
+
169
+ int out_len = 0;
170
+
171
+ // Get AAD data and length *before* deciding whether to set total length
172
+ auto native_aad = ToNativeArrayBuffer(data);
173
+ size_t aad_len = native_aad->size();
174
+
175
+ // 1. Set the total *ciphertext* length. This seems necessary based on examples,
176
+ // BUT the wiki says "(only needed if AAD is passed)". Let's skip if decrypting and AAD length is 0.
177
+ bool should_set_total_length = is_cipher || aad_len > 0;
178
+ if (should_set_total_length) {
179
+ if (EVP_CipherUpdate(ctx, nullptr, &out_len, nullptr, data_len) != 1) {
180
+ unsigned long err = ERR_get_error();
181
+ char err_buf[256];
182
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
183
+ throw std::runtime_error("CCMCipher: Failed to set expected length: " + std::string(err_buf));
184
+ }
185
+ }
186
+
187
+ // 2. Process AAD Data
188
+ // Per OpenSSL CCM decryption examples, this MUST be called even if aad_len is 0.
189
+ // Pass nullptr as the output buffer, the AAD data pointer, and its length.
190
+ if (EVP_CipherUpdate(ctx, nullptr, &out_len, native_aad->data(), aad_len) != 1) {
191
+ unsigned long err = ERR_get_error();
192
+ char err_buf[256];
193
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
194
+ throw std::runtime_error("CCMCipher: Failed to update AAD: " + std::string(err_buf));
195
+ }
196
+ return true;
197
+ }
198
+
199
+ } // namespace margelo::nitro::crypto
@@ -0,0 +1,26 @@
1
+ #pragma once
2
+
3
+ #include "HybridCipher.hpp"
4
+
5
+ namespace margelo::nitro::crypto {
6
+
7
+ class CCMCipher : public HybridCipher {
8
+ public:
9
+ CCMCipher() : HybridObject(TAG) {}
10
+ ~CCMCipher() {
11
+ // Let parent destructor free the context
12
+ ctx = nullptr;
13
+ }
14
+
15
+ void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
16
+ std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
17
+ std::shared_ptr<ArrayBuffer> final() override;
18
+ bool setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) override;
19
+
20
+ private:
21
+ // CCM mode supports messages up to 2^(8L) - 1 bytes where L is the length of nonce
22
+ // With a 12-byte nonce (L=3), max size is 2^24 - 1 bytes
23
+ static constexpr int kMaxMessageSize = (1 << 24) - 1;
24
+ };
25
+
26
+ } // namespace margelo::nitro::crypto
@@ -0,0 +1,324 @@
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 "Utils.hpp"
10
+
11
+ #include <openssl/err.h>
12
+ #include <openssl/evp.h>
13
+
14
+ namespace margelo::nitro::crypto {
15
+
16
+ HybridCipher::~HybridCipher() {
17
+ if (ctx) {
18
+ EVP_CIPHER_CTX_free(ctx);
19
+ // No need to set ctx = nullptr here, object is being destroyed
20
+ }
21
+ }
22
+
23
+ void HybridCipher::checkCtx() const {
24
+ if (!ctx) {
25
+ throw std::runtime_error("Cipher context is not initialized or has been disposed.");
26
+ }
27
+ }
28
+
29
+ bool HybridCipher::maybePassAuthTagToOpenSSL() {
30
+ if (auth_tag_state == kAuthTagKnown) {
31
+ OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG, auth_tag, auth_tag_len),
32
+ OSSL_PARAM_construct_end()};
33
+ if (!EVP_CIPHER_CTX_set_params(ctx, params)) {
34
+ unsigned long err = ERR_get_error();
35
+ char err_buf[256];
36
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
37
+ return false;
38
+ }
39
+ auth_tag_state = kAuthTagPassedToOpenSSL;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ void HybridCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
45
+ // Clean up any existing context
46
+ if (ctx) {
47
+ EVP_CIPHER_CTX_free(ctx);
48
+ ctx = nullptr;
49
+ }
50
+
51
+ // 1. Get cipher implementation by name
52
+ const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str());
53
+ if (!cipher) {
54
+ throw std::runtime_error("Unknown cipher " + cipher_type);
55
+ }
56
+
57
+ // 2. Create a new context
58
+ ctx = EVP_CIPHER_CTX_new();
59
+ if (!ctx) {
60
+ throw std::runtime_error("Failed to create cipher context");
61
+ }
62
+
63
+ // Initialise the encryption/decryption operation with the cipher type.
64
+ // Key and IV will be set later by the derived class if needed.
65
+ if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
66
+ unsigned long err = ERR_get_error();
67
+ char err_buf[256];
68
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
69
+ EVP_CIPHER_CTX_free(ctx);
70
+ ctx = nullptr;
71
+ throw std::runtime_error("HybridCipher: Failed initial CipherInit setup: " + std::string(err_buf));
72
+ }
73
+
74
+ // For base hybrid cipher, set key and IV immediately.
75
+ // Derived classes like CCM might override init and handle this differently.
76
+ auto native_key = ToNativeArrayBuffer(cipher_key);
77
+ auto native_iv = ToNativeArrayBuffer(iv);
78
+ const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
79
+ const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
80
+
81
+ if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
82
+ unsigned long err = ERR_get_error();
83
+ char err_buf[256];
84
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
85
+ EVP_CIPHER_CTX_free(ctx);
86
+ ctx = nullptr;
87
+ throw std::runtime_error("HybridCipher: Failed to set key/IV: " + std::string(err_buf));
88
+ }
89
+ }
90
+
91
+ std::shared_ptr<ArrayBuffer> HybridCipher::update(const std::shared_ptr<ArrayBuffer>& data) {
92
+ auto native_data = ToNativeArrayBuffer(data);
93
+ checkCtx();
94
+ size_t in_len = native_data->size();
95
+ if (in_len > INT_MAX) {
96
+ throw std::runtime_error("Message too long");
97
+ }
98
+
99
+ int out_len = in_len + EVP_CIPHER_CTX_block_size(ctx);
100
+ uint8_t* out = new uint8_t[out_len];
101
+ // Perform the cipher update operation. The real size of the output is
102
+ // returned in out_len
103
+ EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len);
104
+
105
+ // Create and return a new buffer of exact size needed
106
+ return std::make_shared<NativeArrayBuffer>(out, out_len, [=]() { delete[] out; });
107
+ }
108
+
109
+ std::shared_ptr<ArrayBuffer> HybridCipher::final() {
110
+ checkCtx();
111
+ // Block size is max output size for final, unless EVP_CIPH_NO_PADDING is set
112
+ int block_size = EVP_CIPHER_CTX_block_size(ctx);
113
+ if (block_size <= 0)
114
+ block_size = 16; // Default if block size is weird (e.g., 0)
115
+ auto out_buf = std::make_unique<uint8_t[]>(block_size);
116
+ int out_len = 0;
117
+
118
+ int mode = EVP_CIPHER_CTX_mode(ctx);
119
+ int ret = EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len);
120
+ if (!ret) {
121
+ unsigned long err = ERR_get_error();
122
+ char err_buf[256];
123
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
124
+ // Don't free context on error here either, rely on destructor
125
+ throw std::runtime_error("Cipher final failed: " + std::string(err_buf));
126
+ }
127
+
128
+ // Get raw pointer before releasing unique_ptr
129
+ uint8_t* raw_ptr = out_buf.get();
130
+ // Create the specific NativeArrayBuffer first, using full namespace
131
+ auto native_final_chunk = std::make_shared<margelo::nitro::NativeArrayBuffer>(out_buf.release(), static_cast<size_t>(out_len),
132
+ [raw_ptr]() { delete[] raw_ptr; });
133
+
134
+ // Context should NOT be freed here. It might be needed for getAuthTag() for GCM/OCB.
135
+ // The context will be freed by the destructor (~HybridCipher) when the object goes out of scope.
136
+
137
+ // Return the shared_ptr<NativeArrayBuffer> (implicit upcast to shared_ptr<ArrayBuffer>)
138
+ return native_final_chunk;
139
+ }
140
+
141
+ bool HybridCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) {
142
+ checkCtx();
143
+ auto native_data = ToNativeArrayBuffer(data);
144
+
145
+ // Set the AAD
146
+ int out_len;
147
+ if (!EVP_CipherUpdate(ctx, nullptr, &out_len, native_data->data(), native_data->size())) {
148
+ return false;
149
+ }
150
+
151
+ has_aad = true;
152
+ return true;
153
+ }
154
+
155
+ bool HybridCipher::setAutoPadding(bool autoPad) {
156
+ checkCtx();
157
+ return EVP_CIPHER_CTX_set_padding(ctx, autoPad) == 1;
158
+ }
159
+
160
+ bool HybridCipher::setAuthTag(const std::shared_ptr<ArrayBuffer>& tag) {
161
+ checkCtx();
162
+
163
+ if (is_cipher) {
164
+ throw std::runtime_error("setAuthTag can only be called during decryption.");
165
+ }
166
+
167
+ auto native_tag = ToNativeArrayBuffer(tag);
168
+ size_t tag_len = native_tag->size();
169
+ uint8_t* tag_ptr = native_tag->data();
170
+
171
+ int mode = EVP_CIPHER_CTX_mode(ctx);
172
+
173
+ if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
174
+ // Use EVP_CTRL_AEAD_SET_TAG for GCM/OCB decryption
175
+ if (tag_len < 1 || tag_len > 16) { // Check tag length bounds for GCM/OCB
176
+ throw std::runtime_error("Invalid auth tag length for GCM/OCB. Must be between 1 and 16 bytes.");
177
+ }
178
+ // Add check for valid cipher in context before setting tag
179
+ // Use the correct OpenSSL 3 function: EVP_CIPHER_CTX_cipher
180
+ if (!EVP_CIPHER_CTX_cipher(ctx)) {
181
+ throw std::runtime_error("Context has no cipher set before setting GCM/OCB tag");
182
+ }
183
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag_ptr) <= 0) {
184
+ unsigned long err = ERR_get_error();
185
+ char err_buf[256];
186
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
187
+ // Include the error code in the message
188
+ throw std::runtime_error("Failed to set GCM/OCB auth tag: " + std::string(err_buf) + " (code: " + std::to_string(err) + ")");
189
+ }
190
+ auth_tag_state = kAuthTagPassedToOpenSSL; // Mark state
191
+ return true;
192
+
193
+ } else if (mode == EVP_CIPH_CCM_MODE) {
194
+ // Store tag internally for CCM decryption (used in CCMCipher::final)
195
+ if (tag_len < 4 || tag_len > 16) { // Check tag length bounds for CCM
196
+ throw std::runtime_error("Invalid auth tag length for CCM. Must be between 4 and 16 bytes.");
197
+ }
198
+ auth_tag_state = kAuthTagKnown; // Correct state enum value
199
+ auth_tag_len = tag_len;
200
+ // Copy directly into the member buffer (assuming uint8_t auth_tag[16])
201
+ std::memcpy(auth_tag, tag_ptr, tag_len);
202
+ return true;
203
+
204
+ } else {
205
+ // Not an AEAD mode that supports setAuthTag for decryption
206
+ throw std::runtime_error("setAuthTag is not supported for the current cipher mode.");
207
+ }
208
+ }
209
+
210
+ std::shared_ptr<ArrayBuffer> HybridCipher::getAuthTag() {
211
+ checkCtx();
212
+
213
+ int mode = EVP_CIPHER_CTX_mode(ctx);
214
+
215
+ if (!is_cipher) {
216
+ throw std::runtime_error("getAuthTag can only be called during encryption.");
217
+ }
218
+
219
+ if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) {
220
+ // Retrieve the tag using EVP_CIPHER_CTX_ctrl for GCM/OCB
221
+ constexpr int max_tag_len = 16; // GCM/OCB tags are typically up to 16 bytes
222
+ auto tag_buf = std::make_unique<uint8_t[]>(max_tag_len);
223
+
224
+ int ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, max_tag_len, tag_buf.get());
225
+
226
+ if (ret <= 0) {
227
+ unsigned long err = ERR_get_error();
228
+ char err_buf[256];
229
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
230
+ throw std::runtime_error("Failed to get GCM/OCB auth tag: " + std::string(err_buf));
231
+ }
232
+
233
+ size_t actual_tag_len = static_cast<size_t>(ret);
234
+ uint8_t* raw_ptr = tag_buf.get();
235
+ auto final_tag_buffer =
236
+ std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), actual_tag_len, [raw_ptr]() { delete[] raw_ptr; });
237
+ return final_tag_buffer;
238
+
239
+ } else if (mode == EVP_CIPH_CCM_MODE) {
240
+ // CCM: allow getAuthTag after encryption/finalization
241
+ if (auth_tag_len > 0 && auth_tag_state == kAuthTagKnown) {
242
+ // Return the stored tag buffer
243
+ auto tag_buf = std::make_unique<uint8_t[]>(auth_tag_len);
244
+ std::memcpy(tag_buf.get(), auth_tag, auth_tag_len);
245
+ uint8_t* raw_ptr = tag_buf.get();
246
+ auto final_tag_buffer =
247
+ std::make_shared<margelo::nitro::NativeArrayBuffer>(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; });
248
+ return final_tag_buffer;
249
+ } else {
250
+ throw std::runtime_error("CCM: Auth tag not available. Ensure encryption is finalized before calling getAuthTag.");
251
+ }
252
+ } else {
253
+ // Not an AEAD mode that supports getAuthTag post-encryption
254
+ throw std::runtime_error("getAuthTag is not supported for the current cipher mode.");
255
+ }
256
+ }
257
+
258
+ int HybridCipher::getMode() {
259
+ if (!ctx) {
260
+ throw std::runtime_error("Cipher not initialized. Did you call setArgs()?");
261
+ }
262
+ return EVP_CIPHER_CTX_get_mode(ctx);
263
+ }
264
+
265
+ void HybridCipher::setArgs(const CipherArgs& args) {
266
+ this->is_cipher = args.isCipher;
267
+ this->cipher_type = args.cipherType;
268
+
269
+ // Reset auth tag state
270
+ auth_tag_state = kAuthTagUnknown;
271
+ std::memset(auth_tag, 0, EVP_GCM_TLS_TAG_LEN);
272
+
273
+ // Set auth tag length from args or use default
274
+ if (args.authTagLen.has_value()) {
275
+ if (!CheckIsUint32(args.authTagLen.value())) {
276
+ throw std::runtime_error("authTagLen must be uint32");
277
+ }
278
+ uint32_t requested_len = static_cast<uint32_t>(args.authTagLen.value());
279
+ if (requested_len > EVP_GCM_TLS_TAG_LEN) {
280
+ throw std::runtime_error("Authentication tag length too large");
281
+ }
282
+ this->auth_tag_len = requested_len;
283
+ } else {
284
+ // Default to 16 bytes for all authenticated modes
285
+ this->auth_tag_len = kDefaultAuthTagLength;
286
+ }
287
+ }
288
+
289
+ // Corrected callback signature for EVP_CIPHER_do_all_provided
290
+ void collect_ciphers(EVP_CIPHER* cipher, void* arg) {
291
+ auto* names = static_cast<std::vector<std::string>*>(arg);
292
+ if (cipher == nullptr)
293
+ return;
294
+ // Note: EVP_CIPHER_get0_name expects const EVP_CIPHER*, but the callback provides EVP_CIPHER*.
295
+ // This implicit const cast should be safe here.
296
+ const char* name = EVP_CIPHER_get0_name(cipher);
297
+ if (name != nullptr) {
298
+ std::string name_str(name);
299
+ if (name_str == "NULL" || name_str.find("CTS") != std::string::npos ||
300
+ name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV
301
+ name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV
302
+ name_str.find("SM4-") != std::string::npos) {
303
+ return; // Skip adding this cipher
304
+ }
305
+
306
+ // If not filtered out, add it to the list
307
+ names->push_back(name_str); // Use name_str here
308
+ }
309
+ }
310
+
311
+ std::vector<std::string> HybridCipher::getSupportedCiphers() {
312
+ std::vector<std::string> cipher_names;
313
+
314
+ // Use the simpler approach with the separate callback
315
+ EVP_CIPHER_do_all_provided(nullptr, // Default library context
316
+ collect_ciphers, &cipher_names);
317
+
318
+ // OpenSSL 3 doesn't guarantee sorted output with _do_all_provided, sort manually
319
+ std::sort(cipher_names.begin(), cipher_names.end());
320
+
321
+ return cipher_names;
322
+ }
323
+
324
+ } // namespace margelo::nitro::crypto
@@ -0,0 +1,69 @@
1
+ #pragma once
2
+
3
+ #include <memory>
4
+ #include <openssl/core_names.h>
5
+ #include <openssl/err.h>
6
+ #include <openssl/evp.h>
7
+ #include <openssl/param_build.h>
8
+ #include <optional>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ #include "HybridCipherSpec.hpp"
13
+
14
+ namespace margelo::nitro::crypto {
15
+
16
+ // Default tag length for OCB, SIV, CCM, ChaCha20-Poly1305
17
+ constexpr unsigned kDefaultAuthTagLength = 16;
18
+
19
+ class HybridCipher : public HybridCipherSpec {
20
+ public:
21
+ HybridCipher() : HybridObject(TAG) {}
22
+ ~HybridCipher() override;
23
+
24
+ public:
25
+ // Methods
26
+ std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
27
+
28
+ std::shared_ptr<ArrayBuffer> final() override;
29
+
30
+ virtual void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv);
31
+
32
+ void setArgs(const CipherArgs& args) override;
33
+
34
+ bool setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<double> plaintextLength) override;
35
+
36
+ bool setAutoPadding(bool autoPad) override;
37
+
38
+ bool setAuthTag(const std::shared_ptr<ArrayBuffer>& tag) override;
39
+
40
+ std::shared_ptr<ArrayBuffer> getAuthTag() override;
41
+
42
+ std::vector<std::string> getSupportedCiphers() override;
43
+
44
+ protected:
45
+ // Protected enums for state management
46
+ enum CipherKind { kCipher, kDecipher };
47
+ enum UpdateResult { kSuccess, kErrorMessageSize, kErrorState };
48
+ enum AuthTagState { kAuthTagUnknown, kAuthTagKnown, kAuthTagPassedToOpenSSL };
49
+
50
+ protected:
51
+ // Properties
52
+ bool is_cipher = true;
53
+ std::string cipher_type;
54
+ EVP_CIPHER_CTX* ctx = nullptr;
55
+ bool pending_auth_failed = false;
56
+ bool has_aad = false;
57
+ uint8_t auth_tag[EVP_GCM_TLS_TAG_LEN];
58
+ AuthTagState auth_tag_state;
59
+ unsigned int auth_tag_len = 0;
60
+ int max_message_size;
61
+
62
+ protected:
63
+ // Methods
64
+ int getMode();
65
+ void checkCtx() const;
66
+ bool maybePassAuthTagToOpenSSL();
67
+ };
68
+
69
+ } // namespace margelo::nitro::crypto