diff --git a/.husky/pre-commit b/.husky/pre-commit index b22bdeb2..7b562e82 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,8 @@ # Run linting and formatting on staged files bun lint-staged +# Check C++ formatting +find packages/react-native-quick-crypto/cpp -name "*.cpp" -o -name "*.hpp" | xargs clang-format --dry-run --Werror --style=file + # Run prepare script bun --filter="react-native-quick-crypto" prepare diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md index 0be031a7..4315367c 100644 --- a/docs/implementation-coverage.md +++ b/docs/implementation-coverage.md @@ -244,7 +244,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ static `supports(operation, algorithm[, lengthOrAdditionalAlgorithm])` * ❌ `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)` * ❌ `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` - * 🚧 `subtle.decrypt(algorithm, key, data)` + * ✅ `subtle.decrypt(algorithm, key, data)` * 🚧 `subtle.deriveBits(algorithm, baseKey, length)` * ❌ `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)` * 🚧 `subtle.digest(algorithm, data)` @@ -263,10 +263,10 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ## `subtle.decrypt` | Algorithm | Status | | --------- | :----: | -| `RSA-OAEP` | ❌ | -| `AES-CTR` | ❌ | -| `AES-CBC` | ❌ | -| `AES-GCM` | ❌ | +| `RSA-OAEP` | ✅ | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | ## `subtle.deriveBits` | Algorithm | Status | @@ -302,12 +302,12 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ## `subtle.encrypt` | Algorithm | Status | | ------------------- | :----: | -| `AES-CTR` | ❌ | -| `AES-CBC` | ❌ | -| `AES-GCM` | ❌ | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | | `AES-OCB` | ❌ | | `ChaCha20-Poly1305` | ❌ | -| `RSA-OAEP` | ❌ | +| `RSA-OAEP` | ✅ | ## `subtle.exportKey` | Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | @@ -361,9 +361,9 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ### `CryptoKey` algorithms | Algorithm | Status | | --------- | :----: | -| `AES-CTR` | ❌ | -| `AES-CBC` | ❌ | -| `AES-GCM` | ❌ | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | | `AES-KW` | ❌ | | `AES-OCB` | ❌ | | `ChaCha20-Poly1305` | ❌ | diff --git a/example/package.json b/example/package.json index c74aa580..bd5d4942 100644 --- a/example/package.json +++ b/example/package.json @@ -15,7 +15,7 @@ "lint:fix": "eslint \"src/**/*.{js,ts,tsx}\" --fix", "format": "prettier --check \"**/*.{js,ts,tsx}\"", "format:fix": "prettier --write \"**/*.{js,ts,tsx}\"", - "start": "react-native start", + "start": "sh -c 'react-native start --client-logs \"$@\" 2>&1 | tee /tmp/rnqc-metro.log' --", "pods": "RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1 bundle install && bundle exec pod install --project-directory=ios", "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", "build:ios": "cd ios && xcodebuild -workspace QuickCryptoExample.xcworkspace -scheme QuickCryptoExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 61ea2043..faf17dd8 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -14,7 +14,7 @@ import '../tests/pbkdf2/pbkdf2_tests'; import '../tests/random/random_tests'; import '../tests/subtle/deriveBits'; import '../tests/subtle/digest'; -// import '../tests/subtle/encrypt_decrypt'; +import '../tests/subtle/encrypt_decrypt'; import '../tests/subtle/generateKey'; import '../tests/subtle/import_export'; import '../tests/subtle/jwk_rfc7517_tests'; diff --git a/example/src/tests/subtle/encrypt_decrypt.ts b/example/src/tests/subtle/encrypt_decrypt.ts index 850f4e75..901e1739 100644 --- a/example/src/tests/subtle/encrypt_decrypt.ts +++ b/example/src/tests/subtle/encrypt_decrypt.ts @@ -108,12 +108,11 @@ test(SUITE, 'RSA-OAEP', async () => { // from https://github.com/nodejs/node/blob/main/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js async function importRSAVectorKey( publicKeyBuffer: ArrayBuffer, - _privateKeyBuffer: ArrayBuffer | null, + privateKeyBuffer: ArrayBuffer | null, name: AnyAlgorithm, hash: DigestAlgorithm, publicUsages: KeyUsage[], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _privateUsages: KeyUsage[], + privateUsages: KeyUsage[], ): Promise { const publicKey = await subtle.importKey( 'spki', @@ -122,15 +121,19 @@ async function importRSAVectorKey( false, publicUsages, ); - // const privateKey = await subtle.importKey( - // 'pkcs8', - // privateKeyBuffer, - // { name, hash }, - // false, - // privateUsages - // ), - return { publicKey, privateKey: publicKey }; // Using publicKey as placeholder since privateKey import is commented out + let privateKey: CryptoKey | undefined; + if (privateKeyBuffer !== null) { + privateKey = await subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name, hash }, + false, + privateUsages, + ); + } + + return { publicKey, privateKey: privateKey || publicKey }; } async function testRSADecryption({ @@ -216,7 +219,7 @@ async function testRSAEncryption( // TODO: remove condition when importKey() rsa pkcs8 is implemented if (privateKey !== undefined) { - const encodedPlaintext = Buffer.from(plaintext).toString('hex'); + const encodedPlaintext = Buffer.from(plaintextCopy).toString('hex'); expect(result.byteLength * 8).to.equal( (privateKey as CryptoKey).algorithm.modulusLength, @@ -253,7 +256,7 @@ async function testRSAEncryptionLongPlaintext({ return assertThrowsAsync( async () => await subtle.encrypt(algorithm, publicKey as CryptoKey, newplaintext), - 'error in DoCipher, status: 2', + 'data too large for key size', ); } @@ -275,7 +278,7 @@ async function testRSAEncryptionWrongKey({ return assertThrowsAsync( async () => await subtle.encrypt(algorithm, privateKey as CryptoKey, plaintext), - "Cannot read property 'algorithm' of undefined", + 'The requested operation is not valid for the provided key', ); } @@ -715,7 +718,7 @@ async function testAESDecrypt({ async () => { await assertThrowsAsync( async () => await testAESDecrypt(vector), - 'error in DoCipher, status: 2', + 'bad decrypt', ); }, ); @@ -783,7 +786,7 @@ async function testAESDecrypt({ async () => { await assertThrowsAsync( async () => await testAESDecrypt(vector), - 'error in DoCipher, status: 2', + 'bad decrypt', ); }, ); @@ -861,7 +864,7 @@ async function testAESDecrypt({ async () => { await assertThrowsAsync( async () => await testAESDecrypt(vector), - 'error in DoCipher, status: 2', + 'bad decrypt', ); }, ); diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 57153eb7..38fd0ebe 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -27,7 +27,9 @@ add_library( src/main/cpp/cpp-adapter.cpp ../cpp/blake3/HybridBlake3.cpp ../cpp/cipher/CCMCipher.cpp + ../cpp/cipher/GCMCipher.cpp ../cpp/cipher/HybridCipher.cpp + ../cpp/cipher/HybridRsaCipher.cpp ../cpp/cipher/OCBCipher.cpp ../cpp/cipher/XSalsa20Cipher.cpp ../cpp/cipher/ChaCha20Cipher.cpp diff --git a/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.cpp new file mode 100644 index 00000000..676ba980 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.cpp @@ -0,0 +1,68 @@ +#include "GCMCipher.hpp" +#include "Utils.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + +void GCMCipher::init(const std::shared_ptr cipher_key, const std::shared_ptr iv) { + // Clean up any existing context + if (ctx) { + EVP_CIPHER_CTX_free(ctx); + ctx = nullptr; + } + + // 1. Get cipher implementation by name + const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str()); + if (!cipher) { + throw std::runtime_error("Unknown cipher " + cipher_type); + } + + // 2. Create a new context + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + throw std::runtime_error("Failed to create cipher context"); + } + + // 3. Initialize with cipher type only (no key/IV yet) + if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + EVP_CIPHER_CTX_free(ctx); + ctx = nullptr; + throw std::runtime_error("GCMCipher: Failed initial CipherInit setup: " + std::string(err_buf)); + } + + // 4. Set IV length for non-standard IV sizes (GCM default is 96 bits/12 bytes) + auto native_iv = ToNativeArrayBuffer(iv); + size_t iv_len = native_iv->size(); + + if (iv_len != 12) { // Only set if not the default length + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, static_cast(iv_len), nullptr) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + EVP_CIPHER_CTX_free(ctx); + ctx = nullptr; + throw std::runtime_error("GCMCipher: Failed to set IV length: " + std::string(err_buf)); + } + } + + // 5. Now set the key and IV + auto native_key = ToNativeArrayBuffer(cipher_key); + const unsigned char* key_ptr = reinterpret_cast(native_key->data()); + const unsigned char* iv_ptr = reinterpret_cast(native_iv->data()); + + if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + EVP_CIPHER_CTX_free(ctx); + ctx = nullptr; + throw std::runtime_error("GCMCipher: Failed to set key/IV: " + std::string(err_buf)); + } +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.hpp new file mode 100644 index 00000000..65ce8e63 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/GCMCipher.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "HybridCipher.hpp" + +namespace margelo::nitro::crypto { + +class GCMCipher : public HybridCipher { + public: + GCMCipher() : HybridObject(TAG) {} + + void init(const std::shared_ptr cipher_key, const std::shared_ptr iv) override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp index ae842bbd..d094adc7 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp @@ -7,6 +7,7 @@ #include "CCMCipher.hpp" #include "ChaCha20Cipher.hpp" #include "ChaCha20Poly1305Cipher.hpp" +#include "GCMCipher.hpp" #include "HybridCipherFactorySpec.hpp" #include "OCBCipher.hpp" #include "Utils.hpp" @@ -50,6 +51,13 @@ class HybridCipherFactory : public HybridCipherFactorySpec { EVP_CIPHER_free(cipher); return cipherInstance; } + case EVP_CIPH_GCM_MODE: { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + cipherInstance->init(args.cipherKey, args.iv); + EVP_CIPHER_free(cipher); + return cipherInstance; + } case EVP_CIPH_STREAM_CIPHER: { // Check for ChaCha20 variants specifically std::string cipherName = toLower(args.cipherType); diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp new file mode 100644 index 00000000..109e81a9 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp @@ -0,0 +1,229 @@ +#include "HybridRsaCipher.hpp" +#include "../keys/HybridKeyObjectHandle.hpp" +#include "Utils.hpp" + +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +using margelo::nitro::NativeArrayBuffer; + +// Helper to get OpenSSL digest from hash algorithm name +const EVP_MD* getDigestByName(const std::string& hashAlgorithm) { + if (hashAlgorithm == "SHA-1" || hashAlgorithm == "SHA1" || hashAlgorithm == "sha1" || hashAlgorithm == "sha-1") { + return EVP_sha1(); + } else if (hashAlgorithm == "SHA-256" || hashAlgorithm == "SHA256" || hashAlgorithm == "sha256" || hashAlgorithm == "sha-256") { + return EVP_sha256(); + } else if (hashAlgorithm == "SHA-384" || hashAlgorithm == "SHA384" || hashAlgorithm == "sha384" || hashAlgorithm == "sha-384") { + return EVP_sha384(); + } else if (hashAlgorithm == "SHA-512" || hashAlgorithm == "SHA512" || hashAlgorithm == "sha512" || hashAlgorithm == "sha-512") { + return EVP_sha512(); + } + throw std::runtime_error("Unsupported hash algorithm: " + hashAlgorithm); +} + +std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr& keyHandle, + const std::shared_ptr& data, const std::string& hashAlgorithm, + const std::optional>& label) { + // Get the EVP_PKEY from the key handle + auto keyHandleImpl = std::static_pointer_cast(keyHandle); + EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get(); + + if (!pkey) { + throw std::runtime_error("Invalid key for RSA encryption"); + } + + // Create context for encryption + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (!ctx) { + throw std::runtime_error("Failed to create EVP_PKEY_CTX"); + } + + // Initialize encryption + if (EVP_PKEY_encrypt_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Failed to initialize encryption: " + std::string(err_buf)); + } + + // Set padding to OAEP + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set RSA OAEP padding"); + } + + // Set OAEP hash algorithm + const EVP_MD* md = getDigestByName(hashAlgorithm); + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set OAEP hash algorithm"); + } + + // Set MGF1 hash (same as OAEP hash per WebCrypto spec) + if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set MGF1 hash algorithm"); + } + + // Set OAEP label if provided + if (label.has_value() && label.value()->size() > 0) { + auto native_label = ToNativeArrayBuffer(label.value()); + // OpenSSL takes ownership of the label, so we need to allocate a copy + unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size()); + if (!label_copy) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to allocate memory for label"); + } + std::memcpy(label_copy, native_label->data(), native_label->size()); + + if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) { + OPENSSL_free(label_copy); + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set OAEP label"); + } + } + + // Get input data + auto native_data = ToNativeArrayBuffer(data); + const unsigned char* in = native_data->data(); + size_t inlen = native_data->size(); + + // Determine output length + size_t outlen; + if (EVP_PKEY_encrypt(ctx, nullptr, &outlen, in, inlen) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Failed to determine output length: " + std::string(err_buf)); + } + + // Allocate output buffer + auto out_buf = std::make_unique(outlen); + + // Perform encryption + if (EVP_PKEY_encrypt(ctx, out_buf.get(), &outlen, in, inlen) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Encryption failed: " + std::string(err_buf)); + } + + EVP_PKEY_CTX_free(ctx); + + // Create ArrayBuffer from result + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; }); +} + +std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr& keyHandle, + const std::shared_ptr& data, const std::string& hashAlgorithm, + const std::optional>& label) { + // Get the EVP_PKEY from the key handle + auto keyHandleImpl = std::static_pointer_cast(keyHandle); + EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get(); + + if (!pkey) { + throw std::runtime_error("Invalid key for RSA decryption"); + } + + // Create context for decryption + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (!ctx) { + throw std::runtime_error("Failed to create EVP_PKEY_CTX"); + } + + // Initialize decryption + if (EVP_PKEY_decrypt_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Failed to initialize decryption: " + std::string(err_buf)); + } + + // Set padding to OAEP + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set RSA OAEP padding"); + } + + // Set OAEP hash algorithm + const EVP_MD* md = getDigestByName(hashAlgorithm); + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set OAEP hash algorithm"); + } + + // Set MGF1 hash (same as OAEP hash per WebCrypto spec) + if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set MGF1 hash algorithm"); + } + + // Set OAEP label if provided + if (label.has_value() && label.value()->size() > 0) { + auto native_label = ToNativeArrayBuffer(label.value()); + // OpenSSL takes ownership of the label, so we need to allocate a copy + unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size()); + if (!label_copy) { + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to allocate memory for label"); + } + std::memcpy(label_copy, native_label->data(), native_label->size()); + + if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) { + OPENSSL_free(label_copy); + EVP_PKEY_CTX_free(ctx); + throw std::runtime_error("Failed to set OAEP label"); + } + } + + // Get input data + auto native_data = ToNativeArrayBuffer(data); + const unsigned char* in = native_data->data(); + size_t inlen = native_data->size(); + + // Determine output length + size_t outlen; + if (EVP_PKEY_decrypt(ctx, nullptr, &outlen, in, inlen) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Failed to determine output length: " + std::string(err_buf)); + } + + // Allocate output buffer + auto out_buf = std::make_unique(outlen); + + // Perform decryption + if (EVP_PKEY_decrypt(ctx, out_buf.get(), &outlen, in, inlen) <= 0) { + EVP_PKEY_CTX_free(ctx); + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Decryption failed: " + std::string(err_buf)); + } + + EVP_PKEY_CTX_free(ctx); + + // Create ArrayBuffer from result + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; }); +} + +void HybridRsaCipher::loadHybridMethods() { + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("encrypt", &HybridRsaCipher::encrypt); + prototype.registerHybridMethod("decrypt", &HybridRsaCipher::decrypt); + }); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp new file mode 100644 index 00000000..d04ae984 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "HybridRsaCipherSpec.hpp" +#include + +namespace margelo::nitro::crypto { + +class HybridRsaCipher : public HybridRsaCipherSpec { + public: + HybridRsaCipher() : HybridObject(TAG) {} + + std::shared_ptr encrypt(const std::shared_ptr& keyHandle, + const std::shared_ptr& data, const std::string& hashAlgorithm, + const std::optional>& label) override; + + std::shared_ptr decrypt(const std::shared_ptr& keyHandle, + const std::shared_ptr& data, const std::string& hashAlgorithm, + const std::optional>& label) override; + + void loadHybridMethods() override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp index e7528894..925f3014 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp @@ -35,6 +35,13 @@ class HybridKeyObjectHandle : public HybridKeyObjectHandleSpec { KeyDetail keyDetail() override; + KeyObjectData& getKeyObjectData() { + return data_; + } + const KeyObjectData& getKeyObjectData() const { + return data_; + } + private: KeyObjectData data_; diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 392d891b..9dbcfc97 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -18,6 +18,7 @@ "KeyObjectHandle": { "cpp": "HybridKeyObjectHandle" }, "Pbkdf2": { "cpp": "HybridPbkdf2" }, "Random": { "cpp": "HybridRandom" }, + "RsaCipher": { "cpp": "HybridRsaCipher" }, "RsaKeyPair": { "cpp": "HybridRsaKeyPair" } }, "ignorePaths": ["node_modules", "lib"] diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake index 328409ba..ebf152eb 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake @@ -37,6 +37,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridPbkdf2Spec.cpp ../nitrogen/generated/shared/c++/HybridRandomSpec.cpp + ../nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.cpp # Android-specific Nitrogen C++ sources diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp index c1b0c419..7c8c28e4 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -25,6 +25,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" +#include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" namespace margelo::nitro::crypto { @@ -129,6 +130,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RsaCipher", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRsaCipher\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "RsaKeyPair", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm index 34acb954..63dda86c 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -20,6 +20,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" +#include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" @interface QuickCryptoAutolinking : NSObject @@ -121,6 +122,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RsaCipher", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRsaCipher\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "RsaKeyPair", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp new file mode 100644 index 00000000..1ec4d242 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp @@ -0,0 +1,22 @@ +/// +/// HybridRsaCipherSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridRsaCipherSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridRsaCipherSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("encrypt", &HybridRsaCipherSpec::encrypt); + prototype.registerHybridMethod("decrypt", &HybridRsaCipherSpec::decrypt); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp new file mode 100644 index 00000000..2212c668 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp @@ -0,0 +1,70 @@ +/// +/// HybridRsaCipherSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `HybridKeyObjectHandleSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridKeyObjectHandleSpec; } + +#include +#include +#include "HybridKeyObjectHandleSpec.hpp" +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `RsaCipher` + * Inherit this class to create instances of `HybridRsaCipherSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridRsaCipher: public HybridRsaCipherSpec { + * public: + * HybridRsaCipher(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridRsaCipherSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridRsaCipherSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridRsaCipherSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr encrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, const std::string& hashAlgorithm, const std::optional>& label) = 0; + virtual std::shared_ptr decrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, const std::string& hashAlgorithm, const std::optional>& label) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "RsaCipher"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts b/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts new file mode 100644 index 00000000..39f0ccde --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts @@ -0,0 +1,35 @@ +import type { HybridObject } from 'react-native-nitro-modules'; +import type { KeyObjectHandle } from './keyObjectHandle.nitro'; + +export interface RsaCipher + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + /** + * Encrypt data using RSA-OAEP + * @param keyHandle The public key handle + * @param data The data to encrypt + * @param hashAlgorithm The hash algorithm (e.g., 'SHA-256') + * @param label Optional label for OAEP + * @returns Encrypted data + */ + encrypt( + keyHandle: KeyObjectHandle, + data: ArrayBuffer, + hashAlgorithm: string, + label?: ArrayBuffer, + ): ArrayBuffer; + + /** + * Decrypt data using RSA-OAEP + * @param keyHandle The private key handle + * @param data The data to decrypt + * @param hashAlgorithm The hash algorithm (e.g., 'SHA-256') + * @param label Optional label for OAEP + * @returns Decrypted data + */ + decrypt( + keyHandle: KeyObjectHandle, + data: ArrayBuffer, + hashAlgorithm: string, + label?: ArrayBuffer, + ): ArrayBuffer; +} diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 9d3cc487..4026875f 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -11,6 +11,10 @@ import type { AesKeyGenParams, EncryptDecryptParams, Operation, + AesCtrParams, + AesCbcParams, + AesGcmParams, + RsaOaepParams, } from './utils'; import { CryptoKey, @@ -28,9 +32,12 @@ import { asyncDigest } from './hash'; import { createSecretKey } from './keys'; import { NitroModules } from 'react-native-nitro-modules'; import type { KeyObjectHandle } from './specs/keyObjectHandle.nitro'; +import type { RsaCipher } from './specs/rsaCipher.nitro'; +import type { CipherFactory } from './specs/cipher.nitro'; import { pbkdf2DeriveBits } from './pbkdf2'; import { ecImportKey, ecdsaSignVerify, ec_generateKeyPair } from './ec'; import { rsa_generateKeyPair } from './rsa'; +import { getRandomValues } from './random'; // import { pbkdf2DeriveBits } from './pbkdf2'; // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes'; // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa'; @@ -105,30 +112,295 @@ function rsaExportKey( } } -function rsaCipher( - _mode: CipherOrWrapMode, - _key: CryptoKey, - _data: ArrayBuffer, - _algorithm: EncryptDecryptParams, +async function rsaCipher( + mode: CipherOrWrapMode, + key: CryptoKey, + data: ArrayBuffer, + algorithm: EncryptDecryptParams, +): Promise { + const rsaParams = algorithm as RsaOaepParams; + + // Validate key type matches operation + const expectedType = + mode === CipherOrWrapMode.kWebCryptoCipherEncrypt ? 'public' : 'private'; + if (key.type !== expectedType) { + throw lazyDOMException( + 'The requested operation is not valid for the provided key', + 'InvalidAccessError', + ); + } + + // Get hash algorithm from key + const hashAlgorithm = normalizeHashName(key.algorithm.hash); + + // Prepare label (optional) + const label = rsaParams.label + ? bufferLikeToArrayBuffer(rsaParams.label) + : undefined; + + // Create RSA cipher instance + const rsaCipherModule = + NitroModules.createHybridObject('RsaCipher'); + + if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) { + // Encrypt with public key + return rsaCipherModule.encrypt( + key.keyObject.handle, + data, + hashAlgorithm, + label, + ); + } else { + // Decrypt with private key + return rsaCipherModule.decrypt( + key.keyObject.handle, + data, + hashAlgorithm, + label, + ); + } +} + +async function aesCipher( + mode: CipherOrWrapMode, + key: CryptoKey, + data: ArrayBuffer, + algorithm: EncryptDecryptParams, +): Promise { + const { name } = algorithm; + + switch (name) { + case 'AES-CTR': + return aesCtrCipher(mode, key, data, algorithm as AesCtrParams); + case 'AES-CBC': + return aesCbcCipher(mode, key, data, algorithm as AesCbcParams); + case 'AES-GCM': + return aesGcmCipher(mode, key, data, algorithm as AesGcmParams); + default: + throw lazyDOMException( + `Unsupported AES algorithm: ${name}`, + 'NotSupportedError', + ); + } +} + +async function aesCtrCipher( + mode: CipherOrWrapMode, + key: CryptoKey, + data: ArrayBuffer, + algorithm: AesCtrParams, +): Promise { + // Validate counter and length + if (!algorithm.counter || algorithm.counter.byteLength !== 16) { + throw lazyDOMException( + 'AES-CTR algorithm.counter must be 16 bytes', + 'OperationError', + ); + } + + if (algorithm.length < 1 || algorithm.length > 128) { + throw lazyDOMException( + 'AES-CTR algorithm.length must be between 1 and 128', + 'OperationError', + ); + } + + // Get cipher type based on key length + const keyLength = (key.algorithm as { length: number }).length; + const cipherType = `aes-${keyLength}-ctr`; + + // Create cipher + const factory = + NitroModules.createHybridObject('CipherFactory'); + const cipher = factory.createCipher({ + isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, + cipherType, + cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), + iv: bufferLikeToArrayBuffer(algorithm.counter), + }); + + // Process data + const updated = cipher.update(data); + const final = cipher.final(); + + // Concatenate results + const result = new Uint8Array(updated.byteLength + final.byteLength); + result.set(new Uint8Array(updated), 0); + result.set(new Uint8Array(final), updated.byteLength); + + return result.buffer; +} + +async function aesCbcCipher( + mode: CipherOrWrapMode, + key: CryptoKey, + data: ArrayBuffer, + algorithm: AesCbcParams, ): Promise { - throw new Error('rsaCipher not implemented'); + // Validate IV + const iv = bufferLikeToArrayBuffer(algorithm.iv); + if (iv.byteLength !== 16) { + throw lazyDOMException( + 'algorithm.iv must contain exactly 16 bytes', + 'OperationError', + ); + } + + // Get cipher type based on key length + const keyLength = (key.algorithm as { length: number }).length; + const cipherType = `aes-${keyLength}-cbc`; + + // Create cipher + const factory = + NitroModules.createHybridObject('CipherFactory'); + const cipher = factory.createCipher({ + isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, + cipherType, + cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), + iv, + }); + + // Process data + const updated = cipher.update(data); + const final = cipher.final(); + + // Concatenate results + const result = new Uint8Array(updated.byteLength + final.byteLength); + result.set(new Uint8Array(updated), 0); + result.set(new Uint8Array(final), updated.byteLength); + + return result.buffer; } -function aesCipher( - _mode: CipherOrWrapMode, - _key: CryptoKey, - _data: ArrayBuffer, - _algorithm: EncryptDecryptParams, +async function aesGcmCipher( + mode: CipherOrWrapMode, + key: CryptoKey, + data: ArrayBuffer, + algorithm: AesGcmParams, ): Promise { - throw new Error('aesCipher not implemented'); + const { tagLength = 128 } = algorithm; + + // Validate tag length + const validTagLengths = [32, 64, 96, 104, 112, 120, 128]; + if (!validTagLengths.includes(tagLength)) { + throw lazyDOMException( + `${tagLength} is not a valid AES-GCM tag length`, + 'OperationError', + ); + } + + const tagByteLength = tagLength / 8; + + // Get cipher type based on key length + const keyLength = (key.algorithm as { length: number }).length; + const cipherType = `aes-${keyLength}-gcm`; + + // Create cipher + const factory = + NitroModules.createHybridObject('CipherFactory'); + const cipher = factory.createCipher({ + isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, + cipherType, + cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), + iv: bufferLikeToArrayBuffer(algorithm.iv), + authTagLen: tagByteLength, + }); + + let processData: ArrayBuffer; + let authTag: ArrayBuffer | undefined; + + if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) { + // For decryption, extract auth tag from end of data + const dataView = new Uint8Array(data); + + if (dataView.byteLength < tagByteLength) { + throw lazyDOMException( + 'The provided data is too small.', + 'OperationError', + ); + } + + // Split data and tag + const ciphertextLength = dataView.byteLength - tagByteLength; + processData = dataView.slice(0, ciphertextLength).buffer; + authTag = dataView.slice(ciphertextLength).buffer; + + // Set auth tag for verification + cipher.setAuthTag(authTag); + } else { + processData = data; + } + + // Set additional authenticated data if provided + if (algorithm.additionalData) { + cipher.setAAD(bufferLikeToArrayBuffer(algorithm.additionalData)); + } + + // Process data + const updated = cipher.update(processData); + const final = cipher.final(); + + if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) { + // For encryption, append auth tag to result + const tag = cipher.getAuthTag(); + const result = new Uint8Array( + updated.byteLength + final.byteLength + tag.byteLength, + ); + result.set(new Uint8Array(updated), 0); + result.set(new Uint8Array(final), updated.byteLength); + result.set(new Uint8Array(tag), updated.byteLength + final.byteLength); + return result.buffer; + } else { + // For decryption, just concatenate plaintext + const result = new Uint8Array(updated.byteLength + final.byteLength); + result.set(new Uint8Array(updated), 0); + result.set(new Uint8Array(final), updated.byteLength); + return result.buffer; + } } async function aesGenerateKey( - _algorithm: AesKeyGenParams, - _extractable: boolean, - _keyUsages: KeyUsage[], + algorithm: AesKeyGenParams, + extractable: boolean, + keyUsages: KeyUsage[], ): Promise { - throw new Error('aesGenerateKey not implemented'); + const { length } = algorithm; + const name = algorithm.name; + + if (!name) { + throw lazyDOMException('Algorithm name is required', 'OperationError'); + } + + // Validate key length + if (![128, 192, 256].includes(length)) { + throw lazyDOMException( + `Invalid AES key length: ${length}. Must be 128, 192, or 256.`, + 'OperationError', + ); + } + + // Validate usages + const validUsages: KeyUsage[] = [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ]; + if (hasAnyNotIn(keyUsages, validUsages)) { + throw lazyDOMException(`Unsupported key usage for ${name}`, 'SyntaxError'); + } + + // Generate random key bytes + const keyBytes = new Uint8Array(length / 8); + getRandomValues(keyBytes); + + // Create secret key + const keyObject = createSecretKey(keyBytes); + + // Construct algorithm object with guaranteed name + const keyAlgorithm: SubtleAlgorithm = { name, length }; + + return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } function rsaImportKey(