react-native-quick-crypto 1.0.0 → 1.0.2

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 (92) hide show
  1. package/QuickCrypto.podspec +19 -52
  2. package/android/CMakeLists.txt +4 -2
  3. package/android/build.gradle +1 -1
  4. package/cpp/cipher/HybridCipher.cpp +20 -3
  5. package/cpp/cipher/HybridRsaCipher.cpp +20 -1
  6. package/cpp/ed25519/HybridEdKeyPair.cpp +8 -2
  7. package/cpp/keys/HybridKeyObjectHandle.cpp +8 -0
  8. package/cpp/keys/KeyObjectData.hpp +1 -1
  9. package/cpp/mldsa/HybridMlDsaKeyPair.cpp +264 -0
  10. package/cpp/mldsa/HybridMlDsaKeyPair.hpp +47 -0
  11. package/cpp/sign/HybridSignHandle.cpp +97 -22
  12. package/cpp/sign/HybridVerifyHandle.cpp +90 -21
  13. package/deps/ncrypto/.bazelignore +4 -0
  14. package/deps/ncrypto/.bazelrc +2 -0
  15. package/deps/ncrypto/.bazelversion +1 -0
  16. package/deps/ncrypto/.clang-format +111 -0
  17. package/deps/ncrypto/.github/workflows/bazel.yml +58 -0
  18. package/deps/ncrypto/.github/workflows/linter.yml +38 -0
  19. package/deps/ncrypto/.github/workflows/macos.yml +43 -0
  20. package/deps/ncrypto/.github/workflows/ubuntu.yml +46 -0
  21. package/deps/ncrypto/.github/workflows/visual-studio.yml +49 -0
  22. package/deps/ncrypto/.python-version +1 -0
  23. package/deps/ncrypto/BUILD.bazel +36 -0
  24. package/deps/ncrypto/CMakeLists.txt +55 -0
  25. package/deps/ncrypto/LICENSE +21 -0
  26. package/deps/ncrypto/MODULE.bazel +1 -0
  27. package/deps/ncrypto/MODULE.bazel.lock +280 -0
  28. package/deps/ncrypto/README.md +18 -0
  29. package/deps/ncrypto/WORKSPACE +15 -0
  30. package/deps/ncrypto/cmake/CPM.cmake +1225 -0
  31. package/deps/ncrypto/cmake/ncrypto-flags.cmake +16 -0
  32. package/deps/ncrypto/include/dh-primes.h +67 -0
  33. package/deps/ncrypto/{ncrypto.h → include/ncrypto.h} +361 -89
  34. package/deps/ncrypto/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch +28 -0
  35. package/deps/ncrypto/pyproject.toml +38 -0
  36. package/deps/ncrypto/src/CMakeLists.txt +15 -0
  37. package/deps/ncrypto/src/engine.cpp +93 -0
  38. package/deps/ncrypto/{ncrypto.cc → src/ncrypto.cpp} +1168 -234
  39. package/deps/ncrypto/tests/BUILD.bazel +9 -0
  40. package/deps/ncrypto/tests/CMakeLists.txt +7 -0
  41. package/deps/ncrypto/tests/basic.cpp +86 -0
  42. package/deps/ncrypto/tools/run-clang-format.sh +42 -0
  43. package/lib/commonjs/ed.js +68 -0
  44. package/lib/commonjs/ed.js.map +1 -1
  45. package/lib/commonjs/keys/classes.js +6 -0
  46. package/lib/commonjs/keys/classes.js.map +1 -1
  47. package/lib/commonjs/mldsa.js +69 -0
  48. package/lib/commonjs/mldsa.js.map +1 -0
  49. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js +6 -0
  50. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js.map +1 -0
  51. package/lib/commonjs/subtle.js +483 -13
  52. package/lib/commonjs/subtle.js.map +1 -1
  53. package/lib/commonjs/utils/types.js.map +1 -1
  54. package/lib/module/ed.js +66 -0
  55. package/lib/module/ed.js.map +1 -1
  56. package/lib/module/keys/classes.js +6 -0
  57. package/lib/module/keys/classes.js.map +1 -1
  58. package/lib/module/mldsa.js +63 -0
  59. package/lib/module/mldsa.js.map +1 -0
  60. package/lib/module/specs/mlDsaKeyPair.nitro.js +4 -0
  61. package/lib/module/specs/mlDsaKeyPair.nitro.js.map +1 -0
  62. package/lib/module/subtle.js +484 -14
  63. package/lib/module/subtle.js.map +1 -1
  64. package/lib/module/utils/types.js.map +1 -1
  65. package/lib/tsconfig.tsbuildinfo +1 -1
  66. package/lib/typescript/ed.d.ts +4 -1
  67. package/lib/typescript/ed.d.ts.map +1 -1
  68. package/lib/typescript/index.d.ts +2 -0
  69. package/lib/typescript/index.d.ts.map +1 -1
  70. package/lib/typescript/keys/classes.d.ts +2 -0
  71. package/lib/typescript/keys/classes.d.ts.map +1 -1
  72. package/lib/typescript/mldsa.d.ts +18 -0
  73. package/lib/typescript/mldsa.d.ts.map +1 -0
  74. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts +16 -0
  75. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts.map +1 -0
  76. package/lib/typescript/subtle.d.ts +4 -1
  77. package/lib/typescript/subtle.d.ts.map +1 -1
  78. package/lib/typescript/utils/types.d.ts +14 -6
  79. package/lib/typescript/utils/types.d.ts.map +1 -1
  80. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +1 -0
  81. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +10 -0
  82. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +10 -0
  83. package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +12 -0
  84. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.cpp +29 -0
  85. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.hpp +73 -0
  86. package/package.json +7 -3
  87. package/src/ed.ts +102 -0
  88. package/src/keys/classes.ts +9 -0
  89. package/src/mldsa.ts +125 -0
  90. package/src/specs/mlDsaKeyPair.nitro.ts +29 -0
  91. package/src/subtle.ts +667 -17
  92. package/src/utils/types.ts +27 -6
@@ -23,63 +23,21 @@ Pod::Spec.new do |s|
23
23
  Pod::UI.puts("[QuickCrypto] 🧂 has libsodium #{sodium_enabled ? "enabled" : "disabled"}!")
24
24
 
25
25
  if sodium_enabled
26
- # cocoapod for Sodium has not been updated for a while, so we need to build it ourselves
27
- # https://github.com/jedisct1/swift-sodium/issues/264#issuecomment-2864963850
26
+ # Build libsodium from source for XSalsa20 cipher support
27
+ # CocoaPods packages are outdated (1.0.12) and SPM causes module conflicts
28
28
  s.prepare_command = <<-CMD
29
- set -e # Exit on any error
30
- set -x # Print commands as they execute
31
-
32
- # Create ios directory if it doesn't exist
29
+ set -e
33
30
  mkdir -p ios
34
-
35
- # Download libsodium with verbose output
36
- echo "Downloading libsodium..."
37
- curl -L -v -o ios/libsodium.tar.gz https://download.libsodium.org/libsodium/releases/libsodium-1.0.20-stable.tar.gz
38
-
39
- # Verify download
40
- if [ ! -f ios/libsodium.tar.gz ]; then
41
- echo "ERROR: Failed to download libsodium.tar.gz"
42
- exit 1
43
- fi
44
-
45
- echo "Download size: $(wc -c < ios/libsodium.tar.gz) bytes"
46
-
47
- # Clean previous extraction
48
- rm -rf ios/libsodium-stable
49
-
50
- # Extract the full tarball
51
- echo "Extracting libsodium..."
31
+ curl -L -o ios/libsodium.tar.gz https://download.libsodium.org/libsodium/releases/libsodium-1.0.20-stable.tar.gz
52
32
  tar -xzf ios/libsodium.tar.gz -C ios
53
-
54
- # Verify extraction
55
- if [ ! -d ios/libsodium-stable ]; then
56
- echo "ERROR: Failed to extract libsodium"
57
- exit 1
58
- fi
59
-
60
- # Run configure and make to generate all headers including private ones
61
- echo "Configuring libsodium..."
62
33
  cd ios/libsodium-stable
63
34
  ./configure --disable-shared --enable-static
64
-
65
- echo "Building libsodium..."
66
35
  make -j$(sysctl -n hw.ncpu)
67
-
68
- # Verify build success
69
- if [ ! -f src/libsodium/.libs/libsodium.a ]; then
70
- echo "ERROR: libsodium build failed - static library not found"
71
- exit 1
72
- fi
73
-
74
- echo "libsodium build completed successfully"
75
-
76
- # Cleanup
77
36
  cd ../../
78
37
  rm -f ios/libsodium.tar.gz
79
38
  CMD
80
39
  else
81
40
  s.prepare_command = <<-CMD
82
- # Clean up libsodium files if they exist
83
41
  rm -rf ios/libsodium-stable
84
42
  rm -f ios/libsodium.tar.gz
85
43
  CMD
@@ -92,10 +50,12 @@ Pod::Spec.new do |s|
92
50
  "ios/**/*.{h,m,mm}",
93
51
  # implementation (C++)
94
52
  "cpp/**/*.{hpp,cpp}",
95
- # dependencies (C++)
96
- "deps/**/*.{h,cc}",
53
+ # dependencies (C++) - ncrypto
54
+ "deps/ncrypto/include/*.{h}",
55
+ "deps/ncrypto/src/*.{cpp}",
97
56
  # dependencies (C) - exclude BLAKE3 x86 SIMD files (only use portable + NEON for ARM)
98
- "deps/**/*.{h,c}",
57
+ "deps/blake3/c/*.{h,c}",
58
+ "deps/fastpbkdf2/*.{h,c}",
99
59
  ]
100
60
 
101
61
  # Exclude BLAKE3 x86-specific SIMD implementations (SSE2, SSE4.1, AVX2, AVX-512)
@@ -148,7 +108,7 @@ Pod::Spec.new do |s|
148
108
  # Add cpp subdirectories to header search paths
149
109
  cpp_headers = [
150
110
  "\"$(PODS_TARGET_SRCROOT)/cpp/utils\"",
151
- "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto\"",
111
+ "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto/include\"",
152
112
  "\"$(PODS_TARGET_SRCROOT)/deps/blake3/c\"",
153
113
  "\"$(PODS_TARGET_SRCROOT)/deps/fastpbkdf2\""
154
114
  ]
@@ -162,7 +122,7 @@ Pod::Spec.new do |s|
162
122
  "\"$(PODS_ROOT)/../../packages/react-native-quick-crypto/ios/libsodium-stable/src/libsodium/include/sodium\""
163
123
  ]
164
124
  xcconfig["HEADER_SEARCH_PATHS"] = (cpp_headers + sodium_headers).join(' ')
165
- xcconfig["GCC_PREPROCESSOR_DEFINITIONS"] = "$(inherited) BLSALLOC_SODIUM=1"
125
+ xcconfig["GCC_PREPROCESSOR_DEFINITIONS"] = "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES BLSALLOC_SODIUM=1"
166
126
  else
167
127
  xcconfig["HEADER_SEARCH_PATHS"] = cpp_headers.join(' ')
168
128
  end
@@ -175,6 +135,13 @@ Pod::Spec.new do |s|
175
135
 
176
136
  s.dependency "React-jsi"
177
137
  s.dependency "React-callinvoker"
178
- s.dependency "OpenSSL-Universal", "3.3.3001"
138
+
139
+ # OpenSSL 3.6+ via SPM for ML-DSA (post-quantum cryptography) support
140
+ spm_dependency(s,
141
+ url: 'https://github.com/krzyzanowskim/OpenSSL.git',
142
+ requirement: {kind: 'upToNextMajorVersion', minimumVersion: '3.6.0'},
143
+ products: ['OpenSSL']
144
+ )
145
+
179
146
  install_modules_dependencies(s)
180
147
  end
@@ -40,6 +40,7 @@ add_library(
40
40
  ../cpp/hmac/HybridHmac.cpp
41
41
  ../cpp/keys/HybridKeyObjectHandle.cpp
42
42
  ../cpp/keys/KeyObjectData.cpp
43
+ ../cpp/mldsa/HybridMlDsaKeyPair.cpp
43
44
  ../cpp/pbkdf2/HybridPbkdf2.cpp
44
45
  ../cpp/random/HybridRandom.cpp
45
46
  ../cpp/rsa/HybridRsaKeyPair.cpp
@@ -47,7 +48,7 @@ add_library(
47
48
  ../cpp/sign/HybridVerifyHandle.cpp
48
49
  ${BLAKE3_SOURCES}
49
50
  ../deps/fastpbkdf2/fastpbkdf2.c
50
- ../deps/ncrypto/ncrypto.cc
51
+ ../deps/ncrypto/src/ncrypto.cpp
51
52
  )
52
53
 
53
54
  # add Nitrogen specs
@@ -63,6 +64,7 @@ include_directories(
63
64
  "../cpp/hash"
64
65
  "../cpp/hmac"
65
66
  "../cpp/keys"
67
+ "../cpp/mldsa"
66
68
  "../cpp/pbkdf2"
67
69
  "../cpp/random"
68
70
  "../cpp/rsa"
@@ -70,7 +72,7 @@ include_directories(
70
72
  "../cpp/utils"
71
73
  "../deps/blake3/c"
72
74
  "../deps/fastpbkdf2"
73
- "../deps/ncrypto"
75
+ "../deps/ncrypto/include"
74
76
  )
75
77
 
76
78
  # Third party libraries (Prefabs)
@@ -143,7 +143,7 @@ dependencies {
143
143
  implementation project(":react-native-nitro-modules")
144
144
 
145
145
  // Add a dependency on OpenSSL
146
- implementation 'io.github.ronickg:openssl:3.3.2-1'
146
+ implementation 'io.github.ronickg:openssl:3.6.0-1'
147
147
 
148
148
  if (sodiumEnabled) {
149
149
  // Add a dependency on libsodium
@@ -86,6 +86,14 @@ void HybridCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std
86
86
  ctx = nullptr;
87
87
  throw std::runtime_error("HybridCipher: Failed to set key/IV: " + std::string(err_buf));
88
88
  }
89
+
90
+ // For AES-KW (wrap ciphers), set the WRAP_ALLOW flag and disable padding
91
+ std::string cipher_name(cipher_type);
92
+ if (cipher_name.find("-wrap") != std::string::npos) {
93
+ // This flag is required for AES-KW in OpenSSL 3.x
94
+ EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
95
+ EVP_CIPHER_CTX_set_padding(ctx, 0);
96
+ }
89
97
  }
90
98
 
91
99
  std::shared_ptr<ArrayBuffer> HybridCipher::update(const std::shared_ptr<ArrayBuffer>& data) {
@@ -100,7 +108,15 @@ std::shared_ptr<ArrayBuffer> HybridCipher::update(const std::shared_ptr<ArrayBuf
100
108
  uint8_t* out = new uint8_t[out_len];
101
109
  // Perform the cipher update operation. The real size of the output is
102
110
  // returned in out_len
103
- EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len);
111
+ int ret = EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len);
112
+
113
+ if (!ret) {
114
+ unsigned long err = ERR_get_error();
115
+ char err_buf[256];
116
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
117
+ delete[] out;
118
+ throw std::runtime_error("Cipher update failed: " + std::string(err_buf));
119
+ }
104
120
 
105
121
  // Create and return a new buffer of exact size needed
106
122
  return std::make_shared<NativeArrayBuffer>(out, out_len, [=]() { delete[] out; });
@@ -297,8 +313,9 @@ void collect_ciphers(EVP_CIPHER* cipher, void* arg) {
297
313
  if (name_str == "NULL" || name_str.find("CTS") != std::string::npos ||
298
314
  name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV
299
315
  name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV
300
- name_str.find("SM4-") != std::string::npos) {
301
- return; // Skip adding this cipher
316
+ name_str.find("SM4-") != std::string::npos ||
317
+ name_str.find("-ETM") != std::string::npos) { // TLS-internal ciphers, not for general use
318
+ return; // Skip adding this cipher
302
319
  }
303
320
 
304
321
  // If not filtered out, add it to the list
@@ -320,13 +320,32 @@ std::shared_ptr<ArrayBuffer> HybridRsaCipher::privateDecrypt(const std::shared_p
320
320
  throw std::runtime_error("Failed to determine output length: " + std::string(err_buf));
321
321
  }
322
322
 
323
+ if (outlen == 0) {
324
+ EVP_PKEY_CTX_free(ctx);
325
+ uint8_t* empty_buf = new uint8_t[1];
326
+ return std::make_shared<NativeArrayBuffer>(empty_buf, 0, [empty_buf]() { delete[] empty_buf; });
327
+ }
328
+
323
329
  auto out_buf = std::make_unique<uint8_t[]>(outlen);
324
330
 
325
331
  if (EVP_PKEY_verify_recover(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
326
- EVP_PKEY_CTX_free(ctx);
332
+ // OpenSSL 3.x may return failure when recovering empty plaintext
333
+ // In this case outlen is not updated from the initial buffer size
334
+ // Check the error and attempt to handle the empty data case
327
335
  unsigned long err = ERR_get_error();
328
336
  char err_buf[256];
329
337
  ERR_error_string_n(err, err_buf, sizeof(err_buf));
338
+
339
+ // Check if this is an RSA library error that might indicate empty recovered data
340
+ // Error code 0x1C880004 is "RSA lib" error from OpenSSL 3.x provider
341
+ if ((err & 0xFFFFFFF) == 0x1C880004 || (err & 0xFF) == 0x04) {
342
+ ERR_clear_error();
343
+ EVP_PKEY_CTX_free(ctx);
344
+ uint8_t* empty_buf = new uint8_t[1];
345
+ return std::make_shared<NativeArrayBuffer>(empty_buf, 0, [empty_buf]() { delete[] empty_buf; });
346
+ }
347
+
348
+ EVP_PKEY_CTX_free(ctx);
330
349
  throw std::runtime_error("Private decryption failed: " + std::string(err_buf));
331
350
  }
332
351
 
@@ -14,14 +14,20 @@ std::shared_ptr<ArrayBuffer> HybridEdKeyPair::diffieHellman(const std::shared_pt
14
14
  using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
15
15
  using EVP_PKEY_CTX_ptr = std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)>;
16
16
 
17
+ // Determine key type from curve name
18
+ int keyType = EVP_PKEY_X25519;
19
+ if (this->curve == "x448" || this->curve == "X448") {
20
+ keyType = EVP_PKEY_X448;
21
+ }
22
+
17
23
  // 1. Create EVP_PKEY for private key (our key)
18
- EVP_PKEY_ptr pkey_priv(EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, privateKey->data(), privateKey->size()), EVP_PKEY_free);
24
+ EVP_PKEY_ptr pkey_priv(EVP_PKEY_new_raw_private_key(keyType, NULL, privateKey->data(), privateKey->size()), EVP_PKEY_free);
19
25
  if (!pkey_priv) {
20
26
  throw std::runtime_error("Failed to create private key: " + getOpenSSLError());
21
27
  }
22
28
 
23
29
  // 2. Create EVP_PKEY for public key (peer's key)
24
- EVP_PKEY_ptr pkey_pub(EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, publicKey->data(), publicKey->size()), EVP_PKEY_free);
30
+ EVP_PKEY_ptr pkey_pub(EVP_PKEY_new_raw_public_key(keyType, NULL, publicKey->data(), publicKey->size()), EVP_PKEY_free);
25
31
  if (!pkey_pub) {
26
32
  throw std::runtime_error("Failed to create public key: " + getOpenSSLError());
27
33
  }
@@ -322,6 +322,14 @@ AsymmetricKeyType HybridKeyObjectHandle::getAsymmetricKeyType() {
322
322
  return AsymmetricKeyType::ED25519;
323
323
  case EVP_PKEY_ED448:
324
324
  return AsymmetricKeyType::ED448;
325
+ #if OPENSSL_VERSION_NUMBER >= 0x30500000L
326
+ case EVP_PKEY_ML_DSA_44:
327
+ return AsymmetricKeyType::ML_DSA_44;
328
+ case EVP_PKEY_ML_DSA_65:
329
+ return AsymmetricKeyType::ML_DSA_65;
330
+ case EVP_PKEY_ML_DSA_87:
331
+ return AsymmetricKeyType::ML_DSA_87;
332
+ #endif
325
333
  default:
326
334
  throw std::runtime_error("Unsupported asymmetric key type");
327
335
  }
@@ -2,11 +2,11 @@
2
2
 
3
3
  #include <NitroModules/ArrayBuffer.hpp>
4
4
 
5
- #include "../../deps/ncrypto/ncrypto.h"
6
5
  #include "KFormatType.hpp"
7
6
  #include "KeyEncoding.hpp"
8
7
  #include "KeyType.hpp"
9
8
  #include "Utils.hpp"
9
+ #include <ncrypto.h>
10
10
 
11
11
  namespace margelo::nitro::crypto {
12
12
 
@@ -0,0 +1,264 @@
1
+ #include "HybridMlDsaKeyPair.hpp"
2
+
3
+ #include <NitroModules/ArrayBuffer.hpp>
4
+ #include <openssl/bio.h>
5
+ #include <openssl/err.h>
6
+ #include <openssl/pem.h>
7
+
8
+ #include "Utils.hpp"
9
+
10
+ #if OPENSSL_VERSION_NUMBER >= 0x30500000L
11
+ #define RNQC_HAS_ML_DSA 1
12
+ #else
13
+ #define RNQC_HAS_ML_DSA 0
14
+ #endif
15
+
16
+ namespace margelo::nitro::crypto {
17
+
18
+ HybridMlDsaKeyPair::~HybridMlDsaKeyPair() {
19
+ if (pkey_ != nullptr) {
20
+ EVP_PKEY_free(pkey_);
21
+ pkey_ = nullptr;
22
+ }
23
+ }
24
+
25
+ int HybridMlDsaKeyPair::getEvpPkeyType() const {
26
+ #if RNQC_HAS_ML_DSA
27
+ if (variant_ == "ML-DSA-44")
28
+ return EVP_PKEY_ML_DSA_44;
29
+ if (variant_ == "ML-DSA-65")
30
+ return EVP_PKEY_ML_DSA_65;
31
+ if (variant_ == "ML-DSA-87")
32
+ return EVP_PKEY_ML_DSA_87;
33
+ #endif
34
+ return 0;
35
+ }
36
+
37
+ void HybridMlDsaKeyPair::setVariant(const std::string& variant) {
38
+ #if !RNQC_HAS_ML_DSA
39
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
40
+ #endif
41
+ if (variant != "ML-DSA-44" && variant != "ML-DSA-65" && variant != "ML-DSA-87") {
42
+ throw std::runtime_error("Invalid ML-DSA variant: " + variant + ". Must be ML-DSA-44, ML-DSA-65, or ML-DSA-87");
43
+ }
44
+ variant_ = variant;
45
+ }
46
+
47
+ std::shared_ptr<Promise<void>> HybridMlDsaKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat,
48
+ double privateType) {
49
+ return Promise<void>::async([this, publicFormat, publicType, privateFormat, privateType]() {
50
+ this->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType);
51
+ });
52
+ }
53
+
54
+ void HybridMlDsaKeyPair::generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) {
55
+ #if !RNQC_HAS_ML_DSA
56
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
57
+ #else
58
+ clearOpenSSLErrors();
59
+
60
+ if (variant_.empty()) {
61
+ throw std::runtime_error("ML-DSA variant not set. Call setVariant() first.");
62
+ }
63
+
64
+ publicFormat_ = static_cast<int>(publicFormat);
65
+ publicType_ = static_cast<int>(publicType);
66
+ privateFormat_ = static_cast<int>(privateFormat);
67
+ privateType_ = static_cast<int>(privateType);
68
+
69
+ if (pkey_ != nullptr) {
70
+ EVP_PKEY_free(pkey_);
71
+ pkey_ = nullptr;
72
+ }
73
+
74
+ EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
75
+ if (pctx == nullptr) {
76
+ throw std::runtime_error("Failed to create key context for " + variant_ + ": " + getOpenSSLError());
77
+ }
78
+
79
+ if (EVP_PKEY_keygen_init(pctx) <= 0) {
80
+ EVP_PKEY_CTX_free(pctx);
81
+ throw std::runtime_error("Failed to initialize keygen: " + getOpenSSLError());
82
+ }
83
+
84
+ if (EVP_PKEY_keygen(pctx, &pkey_) <= 0) {
85
+ EVP_PKEY_CTX_free(pctx);
86
+ throw std::runtime_error("Failed to generate ML-DSA key pair: " + getOpenSSLError());
87
+ }
88
+
89
+ EVP_PKEY_CTX_free(pctx);
90
+ #endif
91
+ }
92
+
93
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::getPublicKey() {
94
+ #if !RNQC_HAS_ML_DSA
95
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
96
+ #else
97
+ checkKeyPair();
98
+
99
+ BIO* bio = BIO_new(BIO_s_mem());
100
+ if (!bio) {
101
+ throw std::runtime_error("Failed to create BIO for public key export");
102
+ }
103
+
104
+ int result;
105
+ if (publicFormat_ == 1) {
106
+ result = PEM_write_bio_PUBKEY(bio, pkey_);
107
+ } else {
108
+ result = i2d_PUBKEY_bio(bio, pkey_);
109
+ }
110
+
111
+ if (result != 1) {
112
+ BIO_free(bio);
113
+ throw std::runtime_error("Failed to export public key: " + getOpenSSLError());
114
+ }
115
+
116
+ BUF_MEM* bptr;
117
+ BIO_get_mem_ptr(bio, &bptr);
118
+
119
+ uint8_t* data = new uint8_t[bptr->length];
120
+ memcpy(data, bptr->data, bptr->length);
121
+ size_t len = bptr->length;
122
+
123
+ BIO_free(bio);
124
+
125
+ return std::make_shared<NativeArrayBuffer>(data, len, [=]() { delete[] data; });
126
+ #endif
127
+ }
128
+
129
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::getPrivateKey() {
130
+ #if !RNQC_HAS_ML_DSA
131
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
132
+ #else
133
+ checkKeyPair();
134
+
135
+ BIO* bio = BIO_new(BIO_s_mem());
136
+ if (!bio) {
137
+ throw std::runtime_error("Failed to create BIO for private key export");
138
+ }
139
+
140
+ int result;
141
+ if (privateFormat_ == 1) {
142
+ result = PEM_write_bio_PrivateKey(bio, pkey_, nullptr, nullptr, 0, nullptr, nullptr);
143
+ } else {
144
+ // Use PKCS8 format for DER export (not raw private key format)
145
+ result = i2d_PKCS8PrivateKey_bio(bio, pkey_, nullptr, nullptr, 0, nullptr, nullptr);
146
+ }
147
+
148
+ if (result != 1) {
149
+ BIO_free(bio);
150
+ throw std::runtime_error("Failed to export private key: " + getOpenSSLError());
151
+ }
152
+
153
+ BUF_MEM* bptr;
154
+ BIO_get_mem_ptr(bio, &bptr);
155
+
156
+ uint8_t* data = new uint8_t[bptr->length];
157
+ memcpy(data, bptr->data, bptr->length);
158
+ size_t len = bptr->length;
159
+
160
+ BIO_free(bio);
161
+
162
+ return std::make_shared<NativeArrayBuffer>(data, len, [=]() { delete[] data; });
163
+ #endif
164
+ }
165
+
166
+ std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridMlDsaKeyPair::sign(const std::shared_ptr<ArrayBuffer>& message) {
167
+ auto nativeMessage = ToNativeArrayBuffer(message);
168
+ return Promise<std::shared_ptr<ArrayBuffer>>::async([this, nativeMessage]() { return this->signSync(nativeMessage); });
169
+ }
170
+
171
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::signSync(const std::shared_ptr<ArrayBuffer>& message) {
172
+ #if !RNQC_HAS_ML_DSA
173
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
174
+ #else
175
+ clearOpenSSLErrors();
176
+ checkKeyPair();
177
+
178
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
179
+ if (md_ctx == nullptr) {
180
+ throw std::runtime_error("Failed to create signing context");
181
+ }
182
+
183
+ EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
184
+ if (pkey_ctx == nullptr) {
185
+ EVP_MD_CTX_free(md_ctx);
186
+ throw std::runtime_error("Failed to create signing context for " + variant_);
187
+ }
188
+
189
+ if (EVP_DigestSignInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_) <= 0) {
190
+ EVP_MD_CTX_free(md_ctx);
191
+ EVP_PKEY_CTX_free(pkey_ctx);
192
+ throw std::runtime_error("Failed to initialize signing: " + getOpenSSLError());
193
+ }
194
+
195
+ size_t sig_len = 0;
196
+ if (EVP_DigestSign(md_ctx, nullptr, &sig_len, message->data(), message->size()) <= 0) {
197
+ EVP_MD_CTX_free(md_ctx);
198
+ throw std::runtime_error("Failed to calculate signature size: " + getOpenSSLError());
199
+ }
200
+
201
+ uint8_t* sig = new uint8_t[sig_len];
202
+
203
+ if (EVP_DigestSign(md_ctx, sig, &sig_len, message->data(), message->size()) <= 0) {
204
+ EVP_MD_CTX_free(md_ctx);
205
+ delete[] sig;
206
+ throw std::runtime_error("Failed to sign message: " + getOpenSSLError());
207
+ }
208
+
209
+ EVP_MD_CTX_free(md_ctx);
210
+
211
+ return std::make_shared<NativeArrayBuffer>(sig, sig_len, [=]() { delete[] sig; });
212
+ #endif
213
+ }
214
+
215
+ std::shared_ptr<Promise<bool>> HybridMlDsaKeyPair::verify(const std::shared_ptr<ArrayBuffer>& signature,
216
+ const std::shared_ptr<ArrayBuffer>& message) {
217
+ auto nativeSignature = ToNativeArrayBuffer(signature);
218
+ auto nativeMessage = ToNativeArrayBuffer(message);
219
+ return Promise<bool>::async([this, nativeSignature, nativeMessage]() { return this->verifySync(nativeSignature, nativeMessage); });
220
+ }
221
+
222
+ bool HybridMlDsaKeyPair::verifySync(const std::shared_ptr<ArrayBuffer>& signature, const std::shared_ptr<ArrayBuffer>& message) {
223
+ #if !RNQC_HAS_ML_DSA
224
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
225
+ #else
226
+ clearOpenSSLErrors();
227
+ checkKeyPair();
228
+
229
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
230
+ if (md_ctx == nullptr) {
231
+ throw std::runtime_error("Failed to create verify context");
232
+ }
233
+
234
+ EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
235
+ if (pkey_ctx == nullptr) {
236
+ EVP_MD_CTX_free(md_ctx);
237
+ throw std::runtime_error("Failed to create verify context for " + variant_);
238
+ }
239
+
240
+ if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_) <= 0) {
241
+ EVP_MD_CTX_free(md_ctx);
242
+ EVP_PKEY_CTX_free(pkey_ctx);
243
+ throw std::runtime_error("Failed to initialize verification: " + getOpenSSLError());
244
+ }
245
+
246
+ int result = EVP_DigestVerify(md_ctx, signature->data(), signature->size(), message->data(), message->size());
247
+
248
+ EVP_MD_CTX_free(md_ctx);
249
+
250
+ if (result < 0) {
251
+ throw std::runtime_error("Verification error: " + getOpenSSLError());
252
+ }
253
+
254
+ return result == 1;
255
+ #endif
256
+ }
257
+
258
+ void HybridMlDsaKeyPair::checkKeyPair() {
259
+ if (pkey_ == nullptr) {
260
+ throw std::runtime_error("Key pair not initialized");
261
+ }
262
+ }
263
+
264
+ } // namespace margelo::nitro::crypto
@@ -0,0 +1,47 @@
1
+ #pragma once
2
+
3
+ #include <memory>
4
+ #include <openssl/evp.h>
5
+ #include <string>
6
+
7
+ #include "HybridMlDsaKeyPairSpec.hpp"
8
+
9
+ namespace margelo::nitro::crypto {
10
+
11
+ class HybridMlDsaKeyPair : public HybridMlDsaKeyPairSpec {
12
+ public:
13
+ HybridMlDsaKeyPair() : HybridObject(TAG) {}
14
+ ~HybridMlDsaKeyPair();
15
+
16
+ std::shared_ptr<Promise<void>> generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) override;
17
+
18
+ void generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) override;
19
+
20
+ std::shared_ptr<ArrayBuffer> getPublicKey() override;
21
+ std::shared_ptr<ArrayBuffer> getPrivateKey() override;
22
+
23
+ std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> sign(const std::shared_ptr<ArrayBuffer>& message) override;
24
+
25
+ std::shared_ptr<ArrayBuffer> signSync(const std::shared_ptr<ArrayBuffer>& message) override;
26
+
27
+ std::shared_ptr<Promise<bool>> verify(const std::shared_ptr<ArrayBuffer>& signature,
28
+ const std::shared_ptr<ArrayBuffer>& message) override;
29
+
30
+ bool verifySync(const std::shared_ptr<ArrayBuffer>& signature, const std::shared_ptr<ArrayBuffer>& message) override;
31
+
32
+ void setVariant(const std::string& variant) override;
33
+
34
+ private:
35
+ std::string variant_;
36
+ EVP_PKEY* pkey_ = nullptr;
37
+
38
+ int publicFormat_ = -1;
39
+ int publicType_ = -1;
40
+ int privateFormat_ = -1;
41
+ int privateType_ = -1;
42
+
43
+ void checkKeyPair();
44
+ int getEvpPkeyType() const;
45
+ };
46
+
47
+ } // namespace margelo::nitro::crypto