diff --git a/.cursorrules b/.cursorrules new file mode 120000 index 00000000..8a63b64b --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +.rules \ No newline at end of file diff --git a/.gitignore b/.gitignore index 058e4764..1766fcc4 100644 --- a/.gitignore +++ b/.gitignore @@ -186,4 +186,3 @@ tsconfig.tsbuildinfo # development stuffs *scratch* - diff --git a/.rules b/.rules new file mode 100644 index 00000000..33f88ff1 --- /dev/null +++ b/.rules @@ -0,0 +1,55 @@ +# React Native Quick Crypto + +Every time you choose to apply a rule(s), explicitly state the rule(s) in the output. You can abbreviate the rule description to a single word or phrase. + +## Project Context + +- This is a React Native project that offers cryptographic operations in native code. +- It uses Nitro Modules to bridge JS & C++. +- Use the documentation of Nitro Modules if you have access locally to its `llms.txt` file. +- Part of the API strives to be a polyfill of the Node.js `{crypto}` module. +- The goal is to migrate 0.x of this library that uses OpenSSL 1.1.1 to now use OpenSSL 3.3 and modern C++ with Nitro Modules. + +## Tech Stack + +- React Native +- TypeScript +- Nitro Modules +- C++ 20 and higher, modern +- OpenSSL 3.3 and higher +- TypeScript package manager is `bun` 1.2 or higher +- Don't ask to run tests. They have to be run in an example React Native app. + +## Rules + +- For C++ includes, do not try to add absolute or relative paths. They have to be resolved by the build system. +- Use smart pointers in C++. +- Use modern C++ features. +- Attempt to reduce the amount of code rather than add more. +- Prefer iteration and modularization over code duplication. + +## TypeScript Best Practices + +- Use TypeScript for all code; prefer interfaces over types. +- Use lowercase with dashes for directories (e.g., `components/auth-wizard`). +- Favor named exports for components. +- Avoid `any` and enums; use explicit types and maps instead. +- Use functional components with TypeScript interfaces. +- Enable strict mode in TypeScript for better type safety. +- Suggest the optimal implementation considering: + - Performance impact + - Maintenance overhead + - Testing strategy +- Code examples should follow TypeScript best practices. + +## React Best Practices + +- Minimize the use of `useEffect`. They should be a last resort. +- Use named functions for `useEffect`s with a meaningful function name. Avoid adding unnecessary comments on effect behavior. + +## Syntax & Formatting + +- Use the `function` keyword for pure functions. +- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. +- Use declarative JSX. +- Use Prettier for consistent code formatting. diff --git a/.windsurfrules b/.windsurfrules new file mode 120000 index 00000000..8a63b64b --- /dev/null +++ b/.windsurfrules @@ -0,0 +1 @@ +.rules \ No newline at end of file diff --git a/CPPLINT.cfg b/CPPLINT.cfg index 436151f0..e59254ab 100644 --- a/CPPLINT.cfg +++ b/CPPLINT.cfg @@ -1 +1 @@ -filter=-build/namespaces,-legal/copyright,-build/header_guard,-readability/casting,-runtime/references,-whitespace/newline,-build/c++11,-build/include_subdir,-whitespace/comments,-runtime/int,-runtime/printf \ No newline at end of file +filter=-build/namespaces,-legal/copyright,-build/header_guard,-readability/casting,-runtime/references,-whitespace/newline,-build/c++11,-build/include_subdir,-whitespace/comments,-runtime/int,-runtime/printf,-whitespace/blank_line diff --git a/README.md b/README.md index 41612eb8..e7223f98 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ QuickCrypto can be used as a drop-in replacement for your Web3/Crypto apps to sp | Version | RN Architecture | Modules | | ------- | ------ | ------- | -| `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) | +| `1.x` | new [->](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-apps.md) | Nitro Modules [->](https://github.com/mrousavy/nitro) | | `0.x` | old | Bridge & JSI | ## Benchmarks diff --git a/bun.lock b/bun.lock index 88d1f6a0..6607dcef 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ }, "example": { "name": "react-native-quick-crypto-example", - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "dependencies": { "@craftzdog/react-native-buffer": "6.0.5", "@noble/curves": "^1.7.0", @@ -25,10 +25,10 @@ "events": "3.3.0", "react": "18.3.1", "react-native": "0.76.1", - "react-native-bouncy-checkbox": "4.0.1", - "react-native-nitro-modules": "0.21.0", + "react-native-bouncy-checkbox": "4.1.2", + "react-native-nitro-modules": "0.25.2", "react-native-quick-base64": "2.1.2", - "react-native-quick-crypto": "1.0.0-beta.12", + "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "5.1.0", "react-native-screens": "3.35.0", "react-native-vector-icons": "^10.1.0", @@ -69,13 +69,12 @@ }, "packages/react-native-quick-crypto": { "name": "react-native-quick-crypto", - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "dependencies": { "@craftzdog/react-native-buffer": "6.0.5", "events": "3.3.0", "react-native-quick-base64": "2.1.2", "readable-stream": "4.5.2", - "string_decoder": "1.3.0", "util": "0.12.5", }, "devDependencies": { @@ -90,10 +89,10 @@ "eslint": "9.9.0", "eslint-plugin-react-native": "5.0.0", "jest": "29.7.0", - "nitro-codegen": "0.21.0", + "nitro-codegen": "0.25.2", "prettier": "3.3.3", "react-native-builder-bob": "0.35.2", - "react-native-nitro-modules": "0.21.0", + "react-native-nitro-modules": "0.25.2", "release-it": "18.1.1", "typescript": "5.1.6", "typescript-eslint": "^8.1.0", @@ -1683,7 +1682,7 @@ "new-github-release-url": ["new-github-release-url@2.0.0", "", { "dependencies": { "type-fest": "^2.5.1" } }, "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ=="], - "nitro-codegen": ["nitro-codegen@0.21.0", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.21.0", "ts-morph": "^25.0.0", "yargs": "^17.7.2", "zod": "^3.23.8" }, "bin": { "nitro-codegen": "lib/index.js" } }, "sha512-tp0hUKHN9a0IDCro9CKejQbcPDLKzYqF3U2lMPO/EVbcVlSqWLoO5dKdJ8+dzvIrbx32jIP/gK4fm4VXKIgbmg=="], + "nitro-codegen": ["nitro-codegen@0.25.2", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.25.2", "ts-morph": "^25.0.0", "yargs": "^17.7.2", "zod": "^3.23.8" }, "bin": { "nitro-codegen": "lib/index.js" } }, "sha512-i0pGujdtmUaSmsawU6bmyFfW6MQbq+PZCWDT10QQg1EQbdPRvYAB5773R9GZtYoGNMGJ5qZVZUWnPBJRPOe61A=="], "nocache": ["nocache@3.0.4", "", {}, "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw=="], @@ -1871,11 +1870,11 @@ "react-native": ["react-native@0.76.1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native/assets-registry": "0.76.1", "@react-native/codegen": "0.76.1", "@react-native/community-cli-plugin": "0.76.1", "@react-native/gradle-plugin": "0.76.1", "@react-native/js-polyfills": "0.76.1", "@react-native/normalize-colors": "0.76.1", "@react-native/virtualized-lists": "0.76.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "^0.23.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.81.0", "metro-source-map": "^0.81.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^5.3.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "^18.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-z4KnbrnnAvloRs9NGnah3u6/LK3IbtNMrvByxa3ifigbMlsMY4WPRYV9lvt/hH4Mzt8bfuI+utnOxFyJTTq3lg=="], - "react-native-bouncy-checkbox": ["react-native-bouncy-checkbox@4.0.1", "", { "dependencies": { "@freakycoder/react-native-bounceable": "^1.0.3" } }, "sha512-dlywsd3PWF47tkZKWFtnArtGM66Hkk1iUvlQhxSbnI56eo8BaQ4VnGFsrGxA3Jc/B7KDuzS9RCtaEflJJT5gYA=="], + "react-native-bouncy-checkbox": ["react-native-bouncy-checkbox@4.1.2", "", { "dependencies": { "@freakycoder/react-native-bounceable": "^1.0.3" } }, "sha512-hB7YwCGTNoMpTPOPiP+RWyQH35S6vxUbc7IGEW/Rqyp7GonEyhtqtthmxiphneRXnywMh8CZwND7OnvppJZscg=="], "react-native-builder-bob": ["react-native-builder-bob@0.35.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-transform-strict-mode": "^7.24.7", "@babel/preset-env": "^7.25.2", "@babel/preset-flow": "^7.24.7", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "babel-plugin-module-resolver": "^5.0.2", "browserslist": "^4.20.4", "cosmiconfig": "^9.0.0", "cross-spawn": "^7.0.3", "dedent": "^0.7.0", "del": "^6.1.1", "escape-string-regexp": "^4.0.0", "fs-extra": "^10.1.0", "glob": "^8.0.3", "is-git-dirty": "^2.0.1", "json5": "^2.2.1", "kleur": "^4.1.4", "metro-config": "^0.80.9", "prompts": "^2.4.2", "which": "^2.0.2", "yargs": "^17.5.1" }, "bin": { "bob": "bin/bob" } }, "sha512-/ehbjzO2GhDd8/noZiZVEGAVDkyZuWJ+zOrKcrNpqpoLOWhCO4y10FGIRkl5bfLvy7/2kXTwI6YnwiGIOODSGQ=="], - "react-native-nitro-modules": ["react-native-nitro-modules@0.21.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-7OQsFiApHnBmTmZj5Ge6A1FZb63v3BjI4p2Y0s6zff35Tq9LJemjB2tHdv5F/0tbjHBV4AYcZf5DMop3NdzOog=="], + "react-native-nitro-modules": ["react-native-nitro-modules@0.25.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rL+X0LzB8BXvpdrUE/+oZ5v4qS/1nZIq0M8Uctbvqq2q53sVCHX4995ffT8+lGIJe/f0QcBvvrEeXtBPl86iwQ=="], "react-native-quick-base64": ["react-native-quick-base64@2.1.2", "", { "dependencies": { "base64-js": "^1.5.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA=="], diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md index 1070126f..0aabdbe8 100644 --- a/docs/implementation-coverage.md +++ b/docs/implementation-coverage.md @@ -12,18 +12,18 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ Static method: `Certificate.exportChallenge(spkac[, encoding])` * ❌ Static method: `Certificate.exportPublicKey(spkac[, encoding])` * ❌ Static method: `Certificate.verifySpkac(spkac[, encoding])` -* ❌ Class: `Cipher` - * ❌ `cipher.final([outputEncoding])` - * ❌ `cipher.getAuthTag()` - * ❌ `cipher.setAAD(buffer[, options])` - * ❌ `cipher.setAutoPadding([autoPadding])` - * ❌ `cipher.update(data[, inputEncoding][, outputEncoding])` -* ❌ Class: `Decipher` - * ❌ `decipher.final([outputEncoding])` - * ❌ `decipher.setAAD(buffer[, options])` - * ❌ `decipher.setAuthTag(buffer[, encoding])` - * ❌ `decipher.setAutoPadding([autoPadding])` - * ❌ `decipher.update(data[, inputEncoding][, outputEncoding])` +* ✅ Class: `Cipher` + * ✅ `cipher.final([outputEncoding])` + * ✅ `cipher.getAuthTag()` + * ✅ `cipher.setAAD(buffer[, options])` + * ✅ `cipher.setAutoPadding([autoPadding])` + * ✅ `cipher.update(data[, inputEncoding][, outputEncoding])` +* ✅ Class: `Decipher` + * ✅ `decipher.final([outputEncoding])` + * ✅ `decipher.setAAD(buffer[, options])` + * ✅ `decipher.setAuthTag(buffer[, encoding])` + * ✅ `decipher.setAutoPadding([autoPadding])` + * ✅ `decipher.update(data[, inputEncoding][, outputEncoding])` * ❌ Class: `DiffieHellman` * ❌ `diffieHellman.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` * ❌ `diffieHellman.generateKeys([encoding])` @@ -95,8 +95,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ `crypto.fips` * ❌ `crypto.checkPrime(candidate[, options], callback)` * ❌ `crypto.checkPrimeSync(candidate[, options])` - * ❌ `crypto.createCipheriv(algorithm, key, iv[, options])` - * ❌ `crypto.createDecipheriv(algorithm, key, iv[, options])` + * ✅ `crypto.createCipheriv(algorithm, key, iv[, options])` + * ✅ `crypto.createDecipheriv(algorithm, key, iv[, options])` * ❌ `crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])` * ❌ `crypto.createDiffieHellman(primeLength[, generator])` * ❌ `crypto.createDiffieHellmanGroup(name)` @@ -117,7 +117,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ `crypto.generatePrime(size[, options[, callback]])` * ❌ `crypto.generatePrimeSync(size[, options])` * ❌ `crypto.getCipherInfo(nameOrNid[, options])` - * ❌ `crypto.getCiphers()` + * ✅ `crypto.getCiphers()` * ❌ `crypto.getCurves()` * ❌ `crypto.getDiffieHellman(groupName)` * ❌ `crypto.getFips()` diff --git a/docs/test_suite_results.png b/docs/test_suite_results.png deleted file mode 100644 index 6cf8b0e0..00000000 Binary files a/docs/test_suite_results.png and /dev/null differ diff --git a/docs/test_suite_results_android.png b/docs/test_suite_results_android.png new file mode 100644 index 00000000..48f265de Binary files /dev/null and b/docs/test_suite_results_android.png differ diff --git a/docs/test_suite_results_ios.png b/docs/test_suite_results_ios.png new file mode 100644 index 00000000..dbd184ce Binary files /dev/null and b/docs/test_suite_results_ios.png differ diff --git a/example/bun.lockb b/example/bun.lockb deleted file mode 100755 index a04de1ed..00000000 Binary files a/example/bun.lockb and /dev/null differ diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3f69babf..e37d1515 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -7,7 +7,7 @@ PODS: - hermes-engine (0.76.1): - hermes-engine/Pre-built (= 0.76.1) - hermes-engine/Pre-built (0.76.1) - - NitroModules (0.21.0): + - NitroModules (0.25.2): - DoubleConversion - glog - hermes-engine @@ -1940,9 +1940,9 @@ SPEC CHECKSUMS: fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd - NitroModules: 3a58d9bc70815a0d5de4476ed6a36eff05a6a0ae + NitroModules: 3a9c88afc1ca3dba01759ed410e8c2902a5d3dbb OpenSSL-Universal: b60a3702c9fea8b3145549d421fdb018e53ab7b4 - QuickCrypto: 8b714710db7acd4299e22db704ba85ea6e38c072 + QuickCrypto: d56c13c2a864ef4400d7aca6631d57989d05a0a3 RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 RCTDeprecation: fde92935b3caa6cb65cbff9fbb7d3a9867ffb259 RCTRequired: 75c6cee42d21c1530a6f204ba32ff57335d19007 diff --git a/example/package.json b/example/package.json index 50b4bc57..82fc2f83 100644 --- a/example/package.json +++ b/example/package.json @@ -33,10 +33,10 @@ "events": "3.3.0", "react": "18.3.1", "react-native": "0.76.1", - "react-native-bouncy-checkbox": "4.0.1", - "react-native-nitro-modules": "0.21.0", + "react-native-bouncy-checkbox": "4.1.2", + "react-native-nitro-modules": "0.25.2", "react-native-quick-base64": "2.1.2", - "react-native-quick-crypto": "1.0.0-beta.13", + "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "5.1.0", "react-native-screens": "3.35.0", "react-native-vector-icons": "^10.1.0", diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 02af821c..a1766313 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -2,28 +2,12 @@ import { useState, useCallback } from 'react'; import type { TestSuites } from '../types/tests'; import { TestsContext } from '../tests/util'; -import '../tests/hmac/hmac_tests'; -import '../tests/hash/hash_tests'; +import '../tests/cipher/cipher_tests'; import '../tests/ed25519/ed25519_tests'; +import '../tests/hash/hash_tests'; +import '../tests/hmac/hmac_tests'; import '../tests/pbkdf2/pbkdf2_tests'; import '../tests/random/random_tests'; -// import '../tests/HmacTests/HmacTests'; -// import '../tests/HashTests/HashTests'; -// import '../tests/CipherTests/CipherTestFirst'; -// import '../tests/CipherTests/CipherTestSecond'; -// import '../tests/CipherTests/PublicCipherTests'; -// import '../tests/CipherTests/test398'; -// import '../tests/CipherTests/generateKey'; -// import '../tests/CipherTests/GenerateKeyPairTests'; -// import '../tests/ConstantsTests/ConstantsTests'; -// import '../tests/SignTests/SignTests'; -// import '../tests/SmokeTests/bundlerTests'; -// import '../tests/webcryptoTests/deriveBits'; -// import '../tests/webcryptoTests/digest'; -// import '../tests/webcryptoTests/generateKey'; -// import '../tests/webcryptoTests/encrypt_decrypt'; -// import '../tests/webcryptoTests/import_export'; -// import '../tests/webcryptoTests/sign_verify'; export const useTestsList = (): [ TestSuites, diff --git a/example/src/hooks/useTestsRun.ts b/example/src/hooks/useTestsRun.ts index 71e4c0a0..8ec52260 100644 --- a/example/src/hooks/useTestsRun.ts +++ b/example/src/hooks/useTestsRun.ts @@ -84,96 +84,3 @@ const run = ( stats.duration = stats.end.valueOf() - stats.start.valueOf(); return stats; }; - -// const run = ( -// addTestResult: (testResult: TestResult) => void, -// tests: Suites = {}, -// ) => { -// const { -// EVENT_RUN_BEGIN, -// EVENT_RUN_END, -// EVENT_TEST_FAIL, -// EVENT_TEST_PASS, -// EVENT_TEST_PENDING, -// EVENT_TEST_END, -// EVENT_SUITE_BEGIN, -// EVENT_SUITE_END, -// } = Mocha.Runner.constants; - -// const stats: Stats = { ...defaultStats }; - -// const runner = new Mocha.Runner(rootSuite); -// runner.stats = stats; - -// // enable/disable tests based on checkbox value -// runner.suite.suites.map(s => { -// const suiteName = s.title; -// if (!tests[suiteName]?.value) { -// // console.log(`skipping '${suiteName}' suite`); -// s.tests.map(t => { -// t.skip(); -// }); -// } else { -// // console.log(`will run '${suiteName}' suite`); -// s.tests.map(t => { -// // @ts-expect-error - not sure why this is erroring -// t.reset(); -// }); -// } -// }); - -// let indents = -1; -// const indent = () => Array(indents).join(' '); -// runner -// .once(EVENT_RUN_BEGIN, () => { -// stats.start = new Date(); -// }) -// .on(EVENT_SUITE_BEGIN, (suite: MochaTypes.Suite) => { -// if (!suite.root) stats.suites++; -// indents++; -// }) -// .on(EVENT_SUITE_END, () => { -// indents--; -// }) -// .on(EVENT_TEST_PASS, (test: MochaTypes.Runnable) => { -// const name = test.parent?.title || ''; -// stats.passes++; -// addTestResult({ -// indentation: indents, -// description: test.title, -// suiteName: name, -// type: 'correct', -// }); -// console.log(`${indent()}pass: ${test.title}`); -// }) -// .on(EVENT_TEST_FAIL, (test: MochaTypes.Runnable, err: Error) => { -// const name = test.parent?.title || ''; -// stats.failures++; -// addTestResult({ -// indentation: indents, -// description: test.title, -// suiteName: name, -// type: 'incorrect', -// errorMsg: err.message, -// }); -// console.log(`${indent()}fail: ${test.title} - error: ${err.message}`); -// }) -// .on(EVENT_TEST_PENDING, function () { -// stats.pending++; -// }) -// .on(EVENT_TEST_END, function () { -// stats.tests++; -// }) -// .once(EVENT_RUN_END, () => { -// stats.end = new Date(); -// stats.duration = stats.end.valueOf() - stats.start.valueOf(); -// console.log(JSON.stringify(runner.stats, null, 2)); -// }); - -// runner.run(); - -// return () => { -// console.log('aborting'); -// runner.abort(); -// }; -// }; diff --git a/example/src/tests/cipher/cipher_tests.ts b/example/src/tests/cipher/cipher_tests.ts new file mode 100644 index 00000000..aa1ddc02 --- /dev/null +++ b/example/src/tests/cipher/cipher_tests.ts @@ -0,0 +1,200 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { + getCiphers, + createCipheriv, + createDecipheriv, + randomFillSync, + type Cipher, + type Decipher, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; + +const SUITE = 'cipher'; + +// --- Constants and Test Data --- +const key = Buffer.from('a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89', 'hex'); +const iv16 = randomFillSync(new Uint8Array(16)); +const iv12 = randomFillSync(new Uint8Array(12)); // Common IV size for GCM/CCM/OCB +const iv = Buffer.from(iv16); +const aad = Buffer.from('Additional Authenticated Data'); +const plaintext = 'abcdefghijklmnopqrstuvwxyz'; +const plaintextBuffer = Buffer.from(plaintext); + +// --- Helper Functions --- +// Helper for testing authenticated modes (GCM, CCM, OCB, Poly1305, SIV) +function roundTripAuth( + cipherName: string, + key: Buffer, + iv: Buffer, + plaintext: Buffer, + aad?: Buffer, + tagLength?: number, // Usually 16 for these modes +) { + let tag: Buffer | null = null; + const isChaChaPoly = cipherName.toLowerCase() === 'chacha20-poly1305'; // Exact match + const isCCM = cipherName.includes('CCM'); + + // Encrypt + const cipher: Cipher | null = createCipheriv(cipherName, key, iv, { + authTagLength: tagLength, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + if (aad) { + const options = isCCM ? { plaintextLength: plaintext.length } : undefined; + cipher.setAAD(aad, options); // Pass plaintextLength for CCM + } + const encryptedPart1: Buffer = cipher.update(plaintext) as Buffer; + const encryptedPart2: Buffer = cipher.final() as Buffer; + let encrypted = Buffer.concat([encryptedPart1, encryptedPart2]); + + if (!isChaChaPoly) { + // ChaChaPoly implicitly includes tag in final output + tag = cipher.getAuthTag() as Buffer; + } else { + // For ChaChaPoly, extract tag from the end of ciphertext + const expectedTagLength = tagLength ?? 16; + tag = encrypted.subarray(encrypted.length - expectedTagLength); + encrypted = encrypted.subarray(0, encrypted.length - expectedTagLength); + } + + // Keep original encrypted buffer for ChaChaPoly decryption + const originalEncryptedForChaCha = isChaChaPoly + ? Buffer.concat([encryptedPart1, encryptedPart2]) + : null; + + // Decrypt + const decipher: Decipher | null = createDecipheriv(cipherName, key, iv, { + authTagLength: tagLength, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + if (aad) { + const options = isCCM ? { plaintextLength: plaintext.length } : undefined; + decipher.setAAD(aad, options); // Pass plaintextLength for CCM + } + // Do not set AuthTag explicitly for ChaChaPoly + if (!isChaChaPoly) { + decipher.setAuthTag(tag); + } + + // For ChaChaPoly, pass the original buffer with tag appended + const bufferToDecrypt = isChaChaPoly + ? originalEncryptedForChaCha! + : encrypted; + const decryptedPart1: Buffer = decipher.update(bufferToDecrypt) as Buffer; + const decryptedPart2: Buffer = decipher.final() as Buffer; // Final verifies tag for ChaChaPoly + const decrypted = Buffer.concat([decryptedPart1, decryptedPart2]); + + // Verify + expect(decrypted).eql(plaintext); +} + +// Helper for non-authenticated modes +function roundTrip( + cipherName: string, + key: Buffer | string, + iv: Buffer | string, + plaintext: Buffer, +) { + // Encrypt + const cipher: Cipher | null = createCipheriv(cipherName, key, iv); + const encryptedPart1: Buffer = cipher.update(plaintext) as Buffer; + const encryptedPart2: Buffer = cipher.final() as Buffer; + const encrypted = Buffer.concat([encryptedPart1, encryptedPart2]); + + // Decrypt + const decipher: Decipher | null = createDecipheriv(cipherName, key, iv); + const decryptedPart1: Buffer = decipher.update(encrypted) as Buffer; + const decryptedPart2: Buffer = decipher.final() as Buffer; + const decrypted = Buffer.concat([decryptedPart1, decryptedPart2]); + + // Verify + expect(decrypted).eql(plaintext); // Use Chai's eql for deep equality +} + +// --- Tests --- +test(SUITE, 'valid algorithm', () => { + expect(() => { + createCipheriv('aes-128-cbc', Buffer.alloc(16), Buffer.alloc(16), {}); // Use alloc + }).to.not.throw(); +}); + +test(SUITE, 'invalid algorithm', () => { + expect(() => { + createCipheriv('aes-128-boorad', Buffer.alloc(16), Buffer.alloc(16), {}); // Use alloc + }).to.throw('Invalid cipher type: aes-128-boorad'); // Match exact error string +}); + +test(SUITE, 'strings', () => { + // roundtrip expects Buffers, convert strings first + roundTrip( + 'aes-128-cbc', + key.toString('hex'), + iv.toString('hex'), + plaintextBuffer, + ); +}); + +test(SUITE, 'buffers', () => { + roundTrip('aes-128-cbc', key, iv, plaintextBuffer); +}); + +// loop through each cipher and test roundtrip +const allCiphers = getCiphers(); +allCiphers.forEach(cipherName => { + test(SUITE, cipherName, () => { + try { + // Determine correct key length + let keyLen = 32; // Default to 256-bit + if (cipherName.includes('128')) { + keyLen = 16; + } else if (cipherName.includes('192')) { + keyLen = 24; + } + let testKey: Uint8Array; + if (cipherName.includes('XTS')) { + keyLen *= 2; // XTS requires double length key + testKey = randomFillSync(new Uint8Array(keyLen)); + const keyBuffer = Buffer.from(testKey); // Create Buffer once + // Ensure key halves are not identical for XTS + const half = keyLen / 2; + while ( + keyBuffer.subarray(0, half).equals(keyBuffer.subarray(half, keyLen)) + ) { + testKey = randomFillSync(new Uint8Array(keyLen)); + Object.assign(keyBuffer, Buffer.from(testKey)); + } + } else { + testKey = randomFillSync(new Uint8Array(keyLen)); + } + + // Select IV size based on mode + const testIv: Uint8Array = + cipherName.includes('GCM') || + cipherName.includes('OCB') || + cipherName.includes('CCM') + ? iv12 + : iv16; + + // Create key and iv as Buffers for the roundtrip functions + const key = Buffer.from(testKey); + const iv = Buffer.from(testIv); + + // Determine if authenticated mode and call appropriate roundtrip helper + if ( + cipherName.includes('GCM') || + cipherName.includes('CCM') || + cipherName.includes('OCB') || + cipherName.includes('Poly1305') || + cipherName.includes('SIV') // SIV modes also use auth + ) { + roundTripAuth(cipherName, key, iv, plaintextBuffer, aad); + } else { + roundTrip(cipherName, key, iv, plaintextBuffer); + } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + expect.fail(`Cipher ${cipherName} threw an error: ${message}`); + } + }); +}); diff --git a/package.json b/package.json index 233460c2..606109a8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "specs": "bun --filter='react-native-quick-crypto' specs", "bundle-install": "bun --filter='react-native-quick-crypto-example' bundle-install", "pods": "bun --filter='react-native-quick-crypto-example' pods", - "start": "bun --filter='react-native-quick-crypto-example' start", + "start": "bun --cwd example start", "bootstrap": "bun install && bun pods", "tsc": "bun --filter='*' typescript", "lint": "bun --filter='*' lint", diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index dd764141..49edb73b 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -9,9 +9,12 @@ set(CMAKE_CXX_STANDARD 20) add_library( ${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp - ../cpp/hmac/HybridHmac.cpp - ../cpp/hash/HybridHash.cpp + ../cpp/cipher/CCMCipher.cpp + ../cpp/cipher/HybridCipher.cpp + ../cpp/cipher/OCBCipher.cpp ../cpp/ed25519/HybridEdKeyPair.cpp + ../cpp/hash/HybridHash.cpp + ../cpp/hmac/HybridHmac.cpp ../cpp/pbkdf2/HybridPbkdf2.cpp ../cpp/random/HybridRandom.cpp ../deps/fastpbkdf2/fastpbkdf2.c @@ -23,9 +26,10 @@ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/QuickCrypto+autolinkin # local includes include_directories( "src/main/cpp" - "../cpp/hmac" - "../cpp/hash" + "../cpp/cipher" "../cpp/ed25519" + "../cpp/hash" + "../cpp/hmac" "../cpp/pbkdf2" "../cpp/random" "../cpp/utils" diff --git a/packages/react-native-quick-crypto/android/src/main/java/com/margelo/nitro/quickcrypto/QuickCryptoPackage.java b/packages/react-native-quick-crypto/android/src/main/java/com/margelo/nitro/quickcrypto/QuickCryptoPackage.java index ffd2eec2..678a7b47 100644 --- a/packages/react-native-quick-crypto/android/src/main/java/com/margelo/nitro/quickcrypto/QuickCryptoPackage.java +++ b/packages/react-native-quick-crypto/android/src/main/java/com/margelo/nitro/quickcrypto/QuickCryptoPackage.java @@ -8,8 +8,6 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.TurboReactPackage; -import com.margelo.nitro.core.HybridObject; -import com.margelo.nitro.core.HybridObjectRegistry; import java.util.HashMap; import java.util.function.Supplier; diff --git a/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp new file mode 100644 index 00000000..0482797e --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp @@ -0,0 +1,199 @@ +#include "CCMCipher.hpp" +#include "Utils.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + +void CCMCipher::init(const std::shared_ptr cipher_key, const std::shared_ptr iv) { + // 1. Call the base class initializer first + try { + HybridCipher::init(cipher_key, iv); + } catch (const std::exception& e) { + throw; // Re-throw after logging + } + + // Ensure context is valid after base init + checkCtx(); + + // 2. Perform CCM-specific initialization + auto native_iv = ToNativeArrayBuffer(iv); + size_t iv_len = native_iv->size(); + + // Set the IV length using CCM-specific control + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("CCMCipher: Failed to set IV length: " + std::string(err_buf)); + } + + // Set the expected/output tag length using CCM-specific control. + // auth_tag_len should have been defaulted or set via setArgs in the base init. + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("CCMCipher: Failed to set tag length: " + std::string(err_buf)); + } + + // Finally, initialize the key and IV using the parameters passed to this function. + auto native_key = ToNativeArrayBuffer(cipher_key); // Use 'cipher_key' parameter + const unsigned char* key_ptr = reinterpret_cast(native_key->data()); + const unsigned char* iv_ptr = reinterpret_cast(native_iv->data()); + + // The last argument (is_cipher) should be consistent with the initial setup call. + 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)); + throw std::runtime_error("CCMCipher: Failed to set key/IV: " + std::string(err_buf)); + } +} + +std::shared_ptr CCMCipher::update(const std::shared_ptr& data) { + checkCtx(); + auto native_data = ToNativeArrayBuffer(data); + size_t in_len = native_data->size(); + if (in_len < 0 || in_len > INT_MAX) { + throw std::runtime_error("Invalid message length"); + } + int out_len = 0; + + if (!is_cipher) { + maybePassAuthTagToOpenSSL(); + } + + int block_size = EVP_CIPHER_CTX_block_size(ctx); + if (block_size <= 0) { + throw std::runtime_error("Invalid block size in update"); + } + out_len = in_len + block_size - 1; + if (out_len < 0 || out_len < in_len) { + throw std::runtime_error("Calculated output buffer size invalid in update"); + } + + auto out_buf = std::make_unique(out_len); + const uint8_t* in = reinterpret_cast(native_data->data()); + + int actual_out_len = 0; + int ret = EVP_CipherUpdate(ctx, out_buf.get(), &actual_out_len, in, in_len); + + if (!is_cipher) { + // Decryption: Check for tag verification failure + if (ret <= 0) { + // Tag verification failed (or other decryption error) + throw std::runtime_error("CCM Decryption: Tag verification failed"); + } + } else { + // Encryption: Check for standard errors + if (ret != 1) { + pending_auth_failed = true; // Should this be set for encryption failure? + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("Error in update() performing encryption operation: " + std::string(err_buf)); + } + } + // If we reached here, the operation (encryption or decryption) succeeded + + unsigned char* final_output = out_buf.release(); + return std::make_shared(final_output, actual_out_len, [=]() { delete[] final_output; }); +} + +std::shared_ptr CCMCipher::final() { + checkCtx(); + + // CCM decryption does not use final. Verification happens in the last update call. + if (!is_cipher) { + // Return an empty buffer, matching Node.js behavior + unsigned char* empty_output = new unsigned char[0]; + return std::make_shared(empty_output, 0, [=]() { delete[] empty_output; }); + } + + // Proceed only for encryption + int block_size = EVP_CIPHER_CTX_block_size(ctx); + if (block_size <= 0) { + throw std::runtime_error("Invalid block size"); + } + auto out_buf = std::make_unique(block_size); + int out_len = 0; + + if (!EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len)) { + 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 finalization failed: " + std::string(err_buf)); + } + + if (auth_tag_len == 0) { + auth_tag_len = sizeof(auth_tag); + } + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) { + 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 get auth tag after finalization: " + std::string(err_buf)); + } + auth_tag_state = kAuthTagKnown; + + unsigned char* final_output = out_buf.release(); + return std::make_shared(final_output, out_len, [=]() { delete[] final_output; }); +} + +bool CCMCipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) { + checkCtx(); + if (!plaintextLength.has_value()) { + throw std::runtime_error("CCM mode requires plaintextLength to be set"); + } + + // IMPORTANT: For CCM decryption (!is_cipher), OpenSSL requires this initial update + // call to specify the TOTAL LENGTH OF THE CIPHERTEXT, not the plaintext. + // The caller (JS) must ensure `plaintextLength` holds the ciphertext length when decrypting. + int data_len = static_cast(plaintextLength.value()); + if (data_len > kMaxMessageSize) { + throw std::runtime_error("Provided data length exceeds maximum allowed size"); + } + + if (!is_cipher) { + if (!maybePassAuthTagToOpenSSL()) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("setAAD: Failed to set auth tag parameters: " + std::string(err_buf)); + } + } + + int out_len = 0; + + // Get AAD data and length *before* deciding whether to set total length + auto native_aad = ToNativeArrayBuffer(data); + size_t aad_len = native_aad->size(); + + // 1. Set the total *ciphertext* length. This seems necessary based on examples, + // BUT the wiki says "(only needed if AAD is passed)". Let's skip if decrypting and AAD length is 0. + bool should_set_total_length = is_cipher || aad_len > 0; + if (should_set_total_length) { + if (EVP_CipherUpdate(ctx, nullptr, &out_len, nullptr, data_len) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("CCMCipher: Failed to set expected length: " + std::string(err_buf)); + } + } + + // 2. Process AAD Data + // Per OpenSSL CCM decryption examples, this MUST be called even if aad_len is 0. + // Pass nullptr as the output buffer, the AAD data pointer, and its length. + if (EVP_CipherUpdate(ctx, nullptr, &out_len, native_aad->data(), aad_len) != 1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("CCMCipher: Failed to update AAD: " + std::string(err_buf)); + } + return true; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp new file mode 100644 index 00000000..e084544e --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "HybridCipher.hpp" + +namespace margelo::nitro::crypto { + +class CCMCipher : public HybridCipher { + public: + CCMCipher() : HybridObject(TAG) {} + ~CCMCipher() { + // Let parent destructor free the context + ctx = nullptr; + } + + void init(const std::shared_ptr cipher_key, const std::shared_ptr iv) override; + std::shared_ptr update(const std::shared_ptr& data) override; + std::shared_ptr final() override; + bool setAAD(const std::shared_ptr& data, std::optional plaintextLength) override; + + private: + // CCM mode supports messages up to 2^(8L) - 1 bytes where L is the length of nonce + // With a 12-byte nonce (L=3), max size is 2^24 - 1 bytes + static constexpr int kMaxMessageSize = (1 << 24) - 1; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp new file mode 100644 index 00000000..d684fe65 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -0,0 +1,324 @@ +#include // For std::sort +#include // For std::memcpy +#include +#include +#include +#include + +#include "HybridCipher.hpp" +#include "Utils.hpp" + +#include +#include + +namespace margelo::nitro::crypto { + +HybridCipher::~HybridCipher() { + if (ctx) { + EVP_CIPHER_CTX_free(ctx); + // No need to set ctx = nullptr here, object is being destroyed + } +} + +void HybridCipher::checkCtx() const { + if (!ctx) { + throw std::runtime_error("Cipher context is not initialized or has been disposed."); + } +} + +bool HybridCipher::maybePassAuthTagToOpenSSL() { + if (auth_tag_state == kAuthTagKnown) { + OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG, auth_tag, auth_tag_len), + OSSL_PARAM_construct_end()}; + if (!EVP_CIPHER_CTX_set_params(ctx, params)) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + return false; + } + auth_tag_state = kAuthTagPassedToOpenSSL; + } + return true; +} + +void HybridCipher::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"); + } + + // Initialise the encryption/decryption operation with the cipher type. + // Key and IV will be set later by the derived class if needed. + 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("HybridCipher: Failed initial CipherInit setup: " + std::string(err_buf)); + } + + // For base hybrid cipher, set key and IV immediately. + // Derived classes like CCM might override init and handle this differently. + auto native_key = ToNativeArrayBuffer(cipher_key); + auto native_iv = ToNativeArrayBuffer(iv); + 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("HybridCipher: Failed to set key/IV: " + std::string(err_buf)); + } +} + +std::shared_ptr HybridCipher::update(const std::shared_ptr& data) { + auto native_data = ToNativeArrayBuffer(data); + checkCtx(); + size_t in_len = native_data->size(); + if (in_len > INT_MAX) { + throw std::runtime_error("Message too long"); + } + + int out_len = in_len + EVP_CIPHER_CTX_block_size(ctx); + uint8_t* out = new uint8_t[out_len]; + // Perform the cipher update operation. The real size of the output is + // returned in out_len + EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len); + + // Create and return a new buffer of exact size needed + return std::make_shared(out, out_len, [=]() { delete[] out; }); +} + +std::shared_ptr HybridCipher::final() { + checkCtx(); + // Block size is max output size for final, unless EVP_CIPH_NO_PADDING is set + int block_size = EVP_CIPHER_CTX_block_size(ctx); + if (block_size <= 0) + block_size = 16; // Default if block size is weird (e.g., 0) + auto out_buf = std::make_unique(block_size); + int out_len = 0; + + int mode = EVP_CIPHER_CTX_mode(ctx); + int ret = EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len); + if (!ret) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + // Don't free context on error here either, rely on destructor + throw std::runtime_error("Cipher final failed: " + std::string(err_buf)); + } + + // Get raw pointer before releasing unique_ptr + uint8_t* raw_ptr = out_buf.get(); + // Create the specific NativeArrayBuffer first, using full namespace + auto native_final_chunk = std::make_shared(out_buf.release(), static_cast(out_len), + [raw_ptr]() { delete[] raw_ptr; }); + + // Context should NOT be freed here. It might be needed for getAuthTag() for GCM/OCB. + // The context will be freed by the destructor (~HybridCipher) when the object goes out of scope. + + // Return the shared_ptr (implicit upcast to shared_ptr) + return native_final_chunk; +} + +bool HybridCipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) { + checkCtx(); + auto native_data = ToNativeArrayBuffer(data); + + // Set the AAD + int out_len; + if (!EVP_CipherUpdate(ctx, nullptr, &out_len, native_data->data(), native_data->size())) { + return false; + } + + has_aad = true; + return true; +} + +bool HybridCipher::setAutoPadding(bool autoPad) { + checkCtx(); + return EVP_CIPHER_CTX_set_padding(ctx, autoPad) == 1; +} + +bool HybridCipher::setAuthTag(const std::shared_ptr& tag) { + checkCtx(); + + if (is_cipher) { + throw std::runtime_error("setAuthTag can only be called during decryption."); + } + + auto native_tag = ToNativeArrayBuffer(tag); + size_t tag_len = native_tag->size(); + uint8_t* tag_ptr = native_tag->data(); + + int mode = EVP_CIPHER_CTX_mode(ctx); + + if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) { + // Use EVP_CTRL_AEAD_SET_TAG for GCM/OCB decryption + if (tag_len < 1 || tag_len > 16) { // Check tag length bounds for GCM/OCB + throw std::runtime_error("Invalid auth tag length for GCM/OCB. Must be between 1 and 16 bytes."); + } + // Add check for valid cipher in context before setting tag + // Use the correct OpenSSL 3 function: EVP_CIPHER_CTX_cipher + if (!EVP_CIPHER_CTX_cipher(ctx)) { + throw std::runtime_error("Context has no cipher set before setting GCM/OCB tag"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag_ptr) <= 0) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + // Include the error code in the message + throw std::runtime_error("Failed to set GCM/OCB auth tag: " + std::string(err_buf) + " (code: " + std::to_string(err) + ")"); + } + auth_tag_state = kAuthTagPassedToOpenSSL; // Mark state + return true; + + } else if (mode == EVP_CIPH_CCM_MODE) { + // Store tag internally for CCM decryption (used in CCMCipher::final) + if (tag_len < 4 || tag_len > 16) { // Check tag length bounds for CCM + throw std::runtime_error("Invalid auth tag length for CCM. Must be between 4 and 16 bytes."); + } + auth_tag_state = kAuthTagKnown; // Correct state enum value + auth_tag_len = tag_len; + // Copy directly into the member buffer (assuming uint8_t auth_tag[16]) + std::memcpy(auth_tag, tag_ptr, tag_len); + return true; + + } else { + // Not an AEAD mode that supports setAuthTag for decryption + throw std::runtime_error("setAuthTag is not supported for the current cipher mode."); + } +} + +std::shared_ptr HybridCipher::getAuthTag() { + checkCtx(); + + int mode = EVP_CIPHER_CTX_mode(ctx); + + if (!is_cipher) { + throw std::runtime_error("getAuthTag can only be called during encryption."); + } + + if (mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_OCB_MODE) { + // Retrieve the tag using EVP_CIPHER_CTX_ctrl for GCM/OCB + constexpr int max_tag_len = 16; // GCM/OCB tags are typically up to 16 bytes + auto tag_buf = std::make_unique(max_tag_len); + + int ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, max_tag_len, tag_buf.get()); + + if (ret <= 0) { + 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 get GCM/OCB auth tag: " + std::string(err_buf)); + } + + size_t actual_tag_len = static_cast(ret); + uint8_t* raw_ptr = tag_buf.get(); + auto final_tag_buffer = + std::make_shared(tag_buf.release(), actual_tag_len, [raw_ptr]() { delete[] raw_ptr; }); + return final_tag_buffer; + + } else if (mode == EVP_CIPH_CCM_MODE) { + // CCM: allow getAuthTag after encryption/finalization + if (auth_tag_len > 0 && auth_tag_state == kAuthTagKnown) { + // Return the stored tag buffer + auto tag_buf = std::make_unique(auth_tag_len); + std::memcpy(tag_buf.get(), auth_tag, auth_tag_len); + uint8_t* raw_ptr = tag_buf.get(); + auto final_tag_buffer = + std::make_shared(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; }); + return final_tag_buffer; + } else { + throw std::runtime_error("CCM: Auth tag not available. Ensure encryption is finalized before calling getAuthTag."); + } + } else { + // Not an AEAD mode that supports getAuthTag post-encryption + throw std::runtime_error("getAuthTag is not supported for the current cipher mode."); + } +} + +int HybridCipher::getMode() { + if (!ctx) { + throw std::runtime_error("Cipher not initialized. Did you call setArgs()?"); + } + return EVP_CIPHER_CTX_get_mode(ctx); +} + +void HybridCipher::setArgs(const CipherArgs& args) { + this->is_cipher = args.isCipher; + this->cipher_type = args.cipherType; + + // Reset auth tag state + auth_tag_state = kAuthTagUnknown; + std::memset(auth_tag, 0, EVP_GCM_TLS_TAG_LEN); + + // Set auth tag length from args or use default + if (args.authTagLen.has_value()) { + if (!CheckIsUint32(args.authTagLen.value())) { + throw std::runtime_error("authTagLen must be uint32"); + } + uint32_t requested_len = static_cast(args.authTagLen.value()); + if (requested_len > EVP_GCM_TLS_TAG_LEN) { + throw std::runtime_error("Authentication tag length too large"); + } + this->auth_tag_len = requested_len; + } else { + // Default to 16 bytes for all authenticated modes + this->auth_tag_len = kDefaultAuthTagLength; + } +} + +// Corrected callback signature for EVP_CIPHER_do_all_provided +void collect_ciphers(EVP_CIPHER* cipher, void* arg) { + auto* names = static_cast*>(arg); + if (cipher == nullptr) + return; + // Note: EVP_CIPHER_get0_name expects const EVP_CIPHER*, but the callback provides EVP_CIPHER*. + // This implicit const cast should be safe here. + const char* name = EVP_CIPHER_get0_name(cipher); + if (name != nullptr) { + std::string name_str(name); + if (name_str == "NULL" || name_str.find("CTS") != std::string::npos || + name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV + name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV + name_str.find("SM4-") != std::string::npos) { + return; // Skip adding this cipher + } + + // If not filtered out, add it to the list + names->push_back(name_str); // Use name_str here + } +} + +std::vector HybridCipher::getSupportedCiphers() { + std::vector cipher_names; + + // Use the simpler approach with the separate callback + EVP_CIPHER_do_all_provided(nullptr, // Default library context + collect_ciphers, &cipher_names); + + // OpenSSL 3 doesn't guarantee sorted output with _do_all_provided, sort manually + std::sort(cipher_names.begin(), cipher_names.end()); + + return cipher_names; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp new file mode 100644 index 00000000..81f26f0e --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "HybridCipherSpec.hpp" + +namespace margelo::nitro::crypto { + +// Default tag length for OCB, SIV, CCM, ChaCha20-Poly1305 +constexpr unsigned kDefaultAuthTagLength = 16; + +class HybridCipher : public HybridCipherSpec { + public: + HybridCipher() : HybridObject(TAG) {} + ~HybridCipher() override; + + public: + // Methods + std::shared_ptr update(const std::shared_ptr& data) override; + + std::shared_ptr final() override; + + virtual void init(const std::shared_ptr cipher_key, const std::shared_ptr iv); + + void setArgs(const CipherArgs& args) override; + + bool setAAD(const std::shared_ptr& data, std::optional plaintextLength) override; + + bool setAutoPadding(bool autoPad) override; + + bool setAuthTag(const std::shared_ptr& tag) override; + + std::shared_ptr getAuthTag() override; + + std::vector getSupportedCiphers() override; + + protected: + // Protected enums for state management + enum CipherKind { kCipher, kDecipher }; + enum UpdateResult { kSuccess, kErrorMessageSize, kErrorState }; + enum AuthTagState { kAuthTagUnknown, kAuthTagKnown, kAuthTagPassedToOpenSSL }; + + protected: + // Properties + bool is_cipher = true; + std::string cipher_type; + EVP_CIPHER_CTX* ctx = nullptr; + bool pending_auth_failed = false; + bool has_aad = false; + uint8_t auth_tag[EVP_GCM_TLS_TAG_LEN]; + AuthTagState auth_tag_state; + unsigned int auth_tag_len = 0; + int max_message_size; + + protected: + // Methods + int getMode(); + void checkCtx() const; + bool maybePassAuthTagToOpenSSL(); +}; + +} // 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 new file mode 100644 index 00000000..f798e194 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include "CCMCipher.hpp" +#include "HybridCipherFactorySpec.hpp" +#include "OCBCipher.hpp" + +namespace margelo::nitro::crypto { + +using namespace facebook; + +class HybridCipherFactory : public HybridCipherFactorySpec { + public: + HybridCipherFactory() : HybridObject(TAG) {} + ~HybridCipherFactory() = default; + + public: + // Factory method exposed to JS + inline std::shared_ptr createCipher(const CipherArgs& args) { + // Create a temporary cipher context to determine the mode + EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr); + if (!cipher) { + throw std::runtime_error("Invalid cipher type: " + args.cipherType); + } + + int mode = EVP_CIPHER_get_mode(cipher); + EVP_CIPHER_free(cipher); + + // Create the appropriate cipher instance based on mode + std::shared_ptr cipherInstance; + switch (mode) { + case EVP_CIPH_OCB_MODE: { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + // Pass tag length (default 16 if not present) + size_t tag_len = args.authTagLen.has_value() ? static_cast(args.authTagLen.value()) : 16; + std::static_pointer_cast(cipherInstance)->init(args.cipherKey, args.iv, tag_len); + return cipherInstance; + } + case EVP_CIPH_CCM_MODE: { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + cipherInstance->init(args.cipherKey, args.iv); + return cipherInstance; + } + default: { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + cipherInstance->init(args.cipherKey, args.iv); + return cipherInstance; + } + } + } +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.cpp new file mode 100644 index 00000000..d97322bc --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.cpp @@ -0,0 +1,55 @@ +#include "OCBCipher.hpp" +#include +#include +#include + +#include "Utils.hpp" +#include +#include + +namespace margelo::nitro::crypto { + +void OCBCipher::init(const std::shared_ptr& key, const std::shared_ptr& iv, size_t tag_len) { + HybridCipher::init(key, iv); + auth_tag_len = tag_len; + + // Set tag length for OCB (must be 12-16 bytes) + if (auth_tag_len < 12 || auth_tag_len > 16) { + throw std::runtime_error("OCB tag length must be between 12 and 16 bytes"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, auth_tag_len, nullptr) != 1) { + throw std::runtime_error("Failed to set OCB tag length"); + } +} + +std::shared_ptr OCBCipher::getAuthTag() { + checkCtx(); + if (!is_cipher) { + throw std::runtime_error("getAuthTag can only be called during encryption."); + } + auto tag_buf = std::make_unique(auth_tag_len); + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, auth_tag_len, tag_buf.get()) != 1) { + throw std::runtime_error("Failed to get OCB auth tag"); + } + uint8_t* raw_ptr = tag_buf.get(); + return std::make_shared(tag_buf.release(), auth_tag_len, [raw_ptr]() { delete[] raw_ptr; }); +} + +bool OCBCipher::setAuthTag(const std::shared_ptr& tag) { + checkCtx(); + if (is_cipher) { + throw std::runtime_error("setAuthTag can only be called during decryption."); + } + auto native_tag = ToNativeArrayBuffer(tag); + size_t tag_len = native_tag->size(); + if (tag_len < 12 || tag_len > 16) { + throw std::runtime_error("Invalid OCB tag length"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, native_tag->data()) != 1) { + throw std::runtime_error("Failed to set OCB auth tag"); + } + auth_tag_len = tag_len; + return true; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.hpp new file mode 100644 index 00000000..2bef6041 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/OCBCipher.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "HybridCipher.hpp" + +namespace margelo::nitro::crypto { + +class OCBCipher : public HybridCipher { + public: + OCBCipher() : HybridObject(TAG) {} + void init(const std::shared_ptr& key, const std::shared_ptr& iv, size_t tag_len = 16); + + std::shared_ptr getAuthTag() override; + bool setAuthTag(const std::shared_ptr& tag) override; + + protected: + size_t auth_tag_len = 16; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp index 5caba089..7a04b12c 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp @@ -1,8 +1,8 @@ -#include "HybridEdKeyPair.hpp" - #include #include +#include "HybridEdKeyPair.hpp" + namespace margelo::nitro::crypto { std::shared_ptr> HybridEdKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat, diff --git a/packages/react-native-quick-crypto/cpp/random/HybridRandom.cpp b/packages/react-native-quick-crypto/cpp/random/HybridRandom.cpp index c2ccb48b..4d062b89 100644 --- a/packages/react-native-quick-crypto/cpp/random/HybridRandom.cpp +++ b/packages/react-native-quick-crypto/cpp/random/HybridRandom.cpp @@ -40,7 +40,7 @@ std::shared_ptr HybridRandom::randomFillSync(const std::shared_ptr< size_t offset = checkOffset(dSize, dOffset); uint8_t* data = buffer.get()->data(); if (RAND_bytes(data + offset, (int)size) != 1) { - throw std::runtime_error("error calling RAND_bytes" + std::to_string(ERR_get_error())); + throw std::runtime_error("error calling RAND_bytes: " + std::to_string(ERR_get_error())); } return buffer; }; diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 90aefe25..966286bd 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -8,9 +8,11 @@ "androidCxxLibName": "QuickCrypto" }, "autolinking": { - "Hmac": { "cpp": "HybridHmac" }, - "Hash": { "cpp": "HybridHash" }, + "Cipher": { "cpp": "HybridCipher" }, + "CipherFactory": { "cpp": "HybridCipherFactory" }, "EdKeyPair": { "cpp": "HybridEdKeyPair" }, + "Hash": { "cpp": "HybridHash" }, + "Hmac": { "cpp": "HybridHmac" }, "Pbkdf2": { "cpp": "HybridPbkdf2" }, "Random": { "cpp": "HybridRandom" } }, diff --git a/packages/react-native-quick-crypto/nitrogen/generated/.gitattributes b/packages/react-native-quick-crypto/nitrogen/generated/.gitattributes new file mode 100644 index 00000000..aae64e23 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/.gitattributes @@ -0,0 +1 @@ +* linguist-generated 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 1c3ffd01..da3e7bf3 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 @@ -27,6 +27,8 @@ target_sources( # Autolinking Setup ../nitrogen/generated/android/QuickCryptoOnLoad.cpp # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridCipherSpec.cpp + ../nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp ../nitrogen/generated/shared/c++/HybridEdKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridHashSpec.cpp ../nitrogen/generated/shared/c++/HybridHmacSpec.cpp @@ -40,6 +42,22 @@ target_sources( # Define a flag to check if we are building properly add_definitions(-DBUILDING_QUICKCRYPTO_WITH_GENERATED_CMAKE_PROJECT) +# From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake +# Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake + target_compile_definitions( + QuickCrypto PRIVATE + -DFOLLY_NO_CONFIG=1 + -DFOLLY_HAVE_CLOCK_GETTIME=1 + -DFOLLY_USE_LIBCPP=1 + -DFOLLY_CFG_NO_COROUTINES=1 + -DFOLLY_MOBILE=1 + -DFOLLY_HAVE_RECVMMSG=1 + -DFOLLY_HAVE_PTHREAD=1 + # Once we target android-23 above, we can comment + # the following line. NDK uses GNU style stderror_r() after API 23. + -DFOLLY_HAVE_XSI_STRERROR_R=1 +) + # Add all libraries required by the generated specs find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++ find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule) 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 6103dc90..a808f9c6 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -15,9 +15,11 @@ #include #include -#include "HybridHmac.hpp" -#include "HybridHash.hpp" +#include "HybridCipher.hpp" +#include "HybridCipherFactory.hpp" #include "HybridEdKeyPair.hpp" +#include "HybridHash.hpp" +#include "HybridHmac.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" @@ -34,21 +36,21 @@ int initialize(JavaVM* vm) { // Register Nitro Hybrid Objects HybridObjectRegistry::registerHybridObjectConstructor( - "Hmac", + "Cipher", []() -> std::shared_ptr { - static_assert(std::is_default_constructible_v, - "The HybridObject \"HybridHmac\" is not default-constructible! " + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCipher\" is not default-constructible! " "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); - return std::make_shared(); + return std::make_shared(); } ); HybridObjectRegistry::registerHybridObjectConstructor( - "Hash", + "CipherFactory", []() -> std::shared_ptr { - static_assert(std::is_default_constructible_v, - "The HybridObject \"HybridHash\" is not default-constructible! " + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCipherFactory\" is not default-constructible! " "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); - return std::make_shared(); + return std::make_shared(); } ); HybridObjectRegistry::registerHybridObjectConstructor( @@ -60,6 +62,24 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Hash", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridHash\" 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( + "Hmac", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridHmac\" 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( "Pbkdf2", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/kotlin/com/margelo/nitro/crypto/QuickCryptoOnLoad.kt b/packages/react-native-quick-crypto/nitrogen/generated/android/kotlin/com/margelo/nitro/crypto/QuickCryptoOnLoad.kt new file mode 100644 index 00000000..13258039 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/kotlin/com/margelo/nitro/crypto/QuickCryptoOnLoad.kt @@ -0,0 +1,35 @@ +/// +/// QuickCryptoOnLoad.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.crypto + +import android.util.Log + +internal class QuickCryptoOnLoad { + companion object { + private const val TAG = "QuickCryptoOnLoad" + private var didLoad = false + /** + * Initializes the native part of "QuickCrypto". + * This method is idempotent and can be called more than once. + */ + @JvmStatic + fun initializeNative() { + if (didLoad) return + try { + Log.i(TAG, "Loading QuickCrypto C++ library...") + System.loadLibrary("QuickCrypto") + Log.i(TAG, "Successfully loaded QuickCrypto C++ library!") + didLoad = true + } catch (e: Error) { + Log.e(TAG, "Failed to load QuickCrypto C++ library! Is it properly installed and linked? " + + "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) + throw e + } + } + } +} diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto+autolinking.rb b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto+autolinking.rb index e0ec8a7e..892447b5 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto+autolinking.rb +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto+autolinking.rb @@ -44,6 +44,8 @@ def add_nitrogen_files(spec) spec.private_header_files = current_private_header_files + [ # iOS specific specs "nitrogen/generated/ios/c++/**/*.{h,hpp}", + # Views are framework-specific and should be private + "nitrogen/generated/shared/**/views/**/*" ] current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto-Swift-Cxx-Umbrella.hpp b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto-Swift-Cxx-Umbrella.hpp index 1fcbabb4..20c81506 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto-Swift-Cxx-Umbrella.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCrypto-Swift-Cxx-Umbrella.hpp @@ -19,7 +19,6 @@ // Common C++ types used in Swift #include #include -#include #include // Forward declarations of Swift defined types 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 82355cc7..ccc5e921 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -10,9 +10,11 @@ #import -#include "HybridHmac.hpp" -#include "HybridHash.hpp" +#include "HybridCipher.hpp" +#include "HybridCipherFactory.hpp" #include "HybridEdKeyPair.hpp" +#include "HybridHash.hpp" +#include "HybridHmac.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" @@ -26,21 +28,21 @@ + (void) load { using namespace margelo::nitro::crypto; HybridObjectRegistry::registerHybridObjectConstructor( - "Hmac", + "Cipher", []() -> std::shared_ptr { - static_assert(std::is_default_constructible_v, - "The HybridObject \"HybridHmac\" is not default-constructible! " + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCipher\" is not default-constructible! " "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); - return std::make_shared(); + return std::make_shared(); } ); HybridObjectRegistry::registerHybridObjectConstructor( - "Hash", + "CipherFactory", []() -> std::shared_ptr { - static_assert(std::is_default_constructible_v, - "The HybridObject \"HybridHash\" is not default-constructible! " + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCipherFactory\" is not default-constructible! " "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); - return std::make_shared(); + return std::make_shared(); } ); HybridObjectRegistry::registerHybridObjectConstructor( @@ -52,6 +54,24 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Hash", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridHash\" 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( + "Hmac", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridHmac\" 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( "Pbkdf2", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp index 67874deb..8bb55d11 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp @@ -43,7 +43,7 @@ namespace margelo::nitro { // C++ CFRGKeyPairType <> JS CFRGKeyPairType (union) template <> - struct JSIConverter { + struct JSIConverter final { static inline CFRGKeyPairType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { std::string unionValue = JSIConverter::fromJSI(runtime, arg); switch (hashString(unionValue.c_str(), unionValue.size())) { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherArgs.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherArgs.hpp new file mode 100644 index 00000000..9119f0e7 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherArgs.hpp @@ -0,0 +1,88 @@ +/// +/// CipherArgs.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 +#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; } + +#include +#include +#include + +namespace margelo::nitro::crypto { + + /** + * A struct which can be represented as a JavaScript object (CipherArgs). + */ + struct CipherArgs { + public: + bool isCipher SWIFT_PRIVATE; + std::string cipherType SWIFT_PRIVATE; + std::shared_ptr cipherKey SWIFT_PRIVATE; + std::shared_ptr iv SWIFT_PRIVATE; + std::optional authTagLen SWIFT_PRIVATE; + + public: + CipherArgs() = default; + explicit CipherArgs(bool isCipher, std::string cipherType, std::shared_ptr cipherKey, std::shared_ptr iv, std::optional authTagLen): isCipher(isCipher), cipherType(cipherType), cipherKey(cipherKey), iv(iv), authTagLen(authTagLen) {} + }; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + using namespace margelo::nitro::crypto; + + // C++ CipherArgs <> JS CipherArgs (object) + template <> + struct JSIConverter final { + static inline CipherArgs fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return CipherArgs( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "isCipher")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "cipherType")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "cipherKey")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "iv")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "authTagLen")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const CipherArgs& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "isCipher", JSIConverter::toJSI(runtime, arg.isCipher)); + obj.setProperty(runtime, "cipherType", JSIConverter::toJSI(runtime, arg.cipherType)); + obj.setProperty(runtime, "cipherKey", JSIConverter>::toJSI(runtime, arg.cipherKey)); + obj.setProperty(runtime, "iv", JSIConverter>::toJSI(runtime, arg.iv)); + obj.setProperty(runtime, "authTagLen", JSIConverter>::toJSI(runtime, arg.authTagLen)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "isCipher"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "cipherType"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "cipherKey"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "iv"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "authTagLen"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp new file mode 100644 index 00000000..d5b5525a --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridCipherFactorySpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridCipherFactorySpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridCipherFactorySpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("createCipher", &HybridCipherFactorySpec::createCipher); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.hpp new file mode 100644 index 00000000..12e0bf90 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherFactorySpec.hpp @@ -0,0 +1,67 @@ +/// +/// HybridCipherFactorySpec.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 `HybridCipherSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridCipherSpec; } +// Forward declaration of `CipherArgs` to properly resolve imports. +namespace margelo::nitro::crypto { struct CipherArgs; } + +#include +#include "HybridCipherSpec.hpp" +#include "CipherArgs.hpp" + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `CipherFactory` + * Inherit this class to create instances of `HybridCipherFactorySpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridCipherFactory: public HybridCipherFactorySpec { + * public: + * HybridCipherFactory(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridCipherFactorySpec: public virtual HybridObject { + public: + // Constructor + explicit HybridCipherFactorySpec(): HybridObject(TAG) { } + + // Destructor + ~HybridCipherFactorySpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr createCipher(const CipherArgs& args) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "CipherFactory"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp new file mode 100644 index 00000000..a32443c9 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp @@ -0,0 +1,28 @@ +/// +/// HybridCipherSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridCipherSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridCipherSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("update", &HybridCipherSpec::update); + prototype.registerHybridMethod("final", &HybridCipherSpec::final); + prototype.registerHybridMethod("setArgs", &HybridCipherSpec::setArgs); + prototype.registerHybridMethod("setAAD", &HybridCipherSpec::setAAD); + prototype.registerHybridMethod("setAutoPadding", &HybridCipherSpec::setAutoPadding); + prototype.registerHybridMethod("setAuthTag", &HybridCipherSpec::setAuthTag); + prototype.registerHybridMethod("getAuthTag", &HybridCipherSpec::getAuthTag); + prototype.registerHybridMethod("getSupportedCiphers", &HybridCipherSpec::getSupportedCiphers); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp new file mode 100644 index 00000000..2afd74fd --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp @@ -0,0 +1,76 @@ +/// +/// HybridCipherSpec.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 `CipherArgs` to properly resolve imports. +namespace margelo::nitro::crypto { struct CipherArgs; } + +#include +#include "CipherArgs.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Cipher` + * Inherit this class to create instances of `HybridCipherSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridCipher: public HybridCipherSpec { + * public: + * HybridCipher(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridCipherSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridCipherSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridCipherSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr update(const std::shared_ptr& data) = 0; + virtual std::shared_ptr final() = 0; + virtual void setArgs(const CipherArgs& args) = 0; + virtual bool setAAD(const std::shared_ptr& data, std::optional plaintextLength) = 0; + virtual bool setAutoPadding(bool autoPad) = 0; + virtual bool setAuthTag(const std::shared_ptr& tag) = 0; + virtual std::shared_ptr getAuthTag() = 0; + virtual std::vector getSupportedCiphers() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Cipher"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEdKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEdKeyPairSpec.hpp index 0ee9e4d1..53be0b67 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEdKeyPairSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEdKeyPairSpec.hpp @@ -44,7 +44,7 @@ namespace margelo::nitro::crypto { explicit HybridEdKeyPairSpec(): HybridObject(TAG) { } // Destructor - virtual ~HybridEdKeyPairSpec() { } + ~HybridEdKeyPairSpec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.hpp index cc4559ca..75c50367 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.hpp @@ -48,7 +48,7 @@ namespace margelo::nitro::crypto { explicit HybridHashSpec(): HybridObject(TAG) { } // Destructor - virtual ~HybridHashSpec() { } + ~HybridHashSpec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHmacSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHmacSpec.hpp index f8be73eb..40af4171 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHmacSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHmacSpec.hpp @@ -42,7 +42,7 @@ namespace margelo::nitro::crypto { explicit HybridHmacSpec(): HybridObject(TAG) { } // Destructor - virtual ~HybridHmacSpec() { } + ~HybridHmacSpec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp index acc3e9c5..bf33b3f6 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp @@ -65,7 +65,7 @@ namespace margelo::nitro::crypto { explicit HybridKeyObjectHandleSpec(): HybridObject(TAG) { } // Destructor - virtual ~HybridKeyObjectHandleSpec() { } + ~HybridKeyObjectHandleSpec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPbkdf2Spec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPbkdf2Spec.hpp index 79b7e439..4b33d4f2 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPbkdf2Spec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPbkdf2Spec.hpp @@ -43,7 +43,7 @@ namespace margelo::nitro::crypto { explicit HybridPbkdf2Spec(): HybridObject(TAG) { } // Destructor - virtual ~HybridPbkdf2Spec() { } + ~HybridPbkdf2Spec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRandomSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRandomSpec.hpp index 621df6da..b3172cce 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRandomSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRandomSpec.hpp @@ -42,7 +42,7 @@ namespace margelo::nitro::crypto { explicit HybridRandomSpec(): HybridObject(TAG) { } // Destructor - virtual ~HybridRandomSpec() { } + ~HybridRandomSpec() override = default; public: // Properties diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWK.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWK.hpp index fbef4779..01f31823 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWK.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWK.hpp @@ -63,6 +63,7 @@ namespace margelo::nitro::crypto { std::optional ext SWIFT_PRIVATE; public: + JWK() = default; explicit JWK(std::optional kty, std::optional use, std::optional> key_ops, std::optional alg, std::optional crv, std::optional kid, std::optional x5u, std::optional> x5c, std::optional x5t, std::optional x5t_256, std::optional n, std::optional e, std::optional d, std::optional p, std::optional q, std::optional x, std::optional y, std::optional k, std::optional dp, std::optional dq, std::optional qi, std::optional ext): kty(kty), use(use), key_ops(key_ops), alg(alg), crv(crv), kid(kid), x5u(x5u), x5c(x5c), x5t(x5t), x5t_256(x5t_256), n(n), e(e), d(d), p(p), q(q), x(x), y(y), k(k), dp(dp), dq(dq), qi(qi), ext(ext) {} }; @@ -74,7 +75,7 @@ namespace margelo::nitro { // C++ JWK <> JS JWK (object) template <> - struct JSIConverter { + struct JSIConverter final { static inline JWK fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return JWK( diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKkty.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKkty.hpp index 8be347a3..3feaebf2 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKkty.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKkty.hpp @@ -43,7 +43,7 @@ namespace margelo::nitro { // C++ JWKkty <> JS JWKkty (union) template <> - struct JSIConverter { + struct JSIConverter final { static inline JWKkty fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { std::string unionValue = JSIConverter::fromJSI(runtime, arg); switch (hashString(unionValue.c_str(), unionValue.size())) { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKuse.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKuse.hpp index 237bbc80..9952e940 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKuse.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/JWKuse.hpp @@ -41,7 +41,7 @@ namespace margelo::nitro { // C++ JWKuse <> JS JWKuse (union) template <> - struct JSIConverter { + struct JSIConverter final { static inline JWKuse fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { std::string unionValue = JSIConverter::fromJSI(runtime, arg); switch (hashString(unionValue.c_str(), unionValue.size())) { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KFormatType.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KFormatType.hpp index 859e149d..32f2a419 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KFormatType.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KFormatType.hpp @@ -38,7 +38,7 @@ namespace margelo::nitro { // C++ KFormatType <> JS KFormatType (enum) template <> - struct JSIConverter { + struct JSIConverter final { static inline KFormatType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { int enumValue = JSIConverter::fromJSI(runtime, arg); return static_cast(enumValue); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyDetail.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyDetail.hpp index c684f48e..67b68267 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyDetail.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyDetail.hpp @@ -39,6 +39,7 @@ namespace margelo::nitro::crypto { std::optional namedCurve SWIFT_PRIVATE; public: + KeyDetail() = default; explicit KeyDetail(std::optional length, std::optional publicExponent, std::optional modulusLength, std::optional hashAlgorithm, std::optional mgf1HashAlgorithm, std::optional saltLength, std::optional namedCurve): length(length), publicExponent(publicExponent), modulusLength(modulusLength), hashAlgorithm(hashAlgorithm), mgf1HashAlgorithm(mgf1HashAlgorithm), saltLength(saltLength), namedCurve(namedCurve) {} }; @@ -50,7 +51,7 @@ namespace margelo::nitro { // C++ KeyDetail <> JS KeyDetail (object) template <> - struct JSIConverter { + struct JSIConverter final { static inline KeyDetail fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return KeyDetail( diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyEncoding.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyEncoding.hpp index 85546b49..728bd932 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyEncoding.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyEncoding.hpp @@ -39,7 +39,7 @@ namespace margelo::nitro { // C++ KeyEncoding <> JS KeyEncoding (enum) template <> - struct JSIConverter { + struct JSIConverter final { static inline KeyEncoding fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { int enumValue = JSIConverter::fromJSI(runtime, arg); return static_cast(enumValue); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyType.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyType.hpp index 002d27d4..ce3acf7e 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyType.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyType.hpp @@ -38,7 +38,7 @@ namespace margelo::nitro { // C++ KeyType <> JS KeyType (enum) template <> - struct JSIConverter { + struct JSIConverter final { static inline KeyType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { int enumValue = JSIConverter::fromJSI(runtime, arg); return static_cast(enumValue); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp index 92cfae66..78c6a40d 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp @@ -47,7 +47,7 @@ namespace margelo::nitro { // C++ KeyUsage <> JS KeyUsage (union) template <> - struct JSIConverter { + struct JSIConverter final { static inline KeyUsage fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { std::string unionValue = JSIConverter::fromJSI(runtime, arg); switch (hashString(unionValue.c_str(), unionValue.size())) { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/NamedCurve.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/NamedCurve.hpp index d48b860c..1cc83e62 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/NamedCurve.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/NamedCurve.hpp @@ -42,7 +42,7 @@ namespace margelo::nitro { // C++ NamedCurve <> JS NamedCurve (union) template <> - struct JSIConverter { + struct JSIConverter final { static inline NamedCurve fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { std::string unionValue = JSIConverter::fromJSI(runtime, arg); switch (hashString(unionValue.c_str(), unionValue.size())) { diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index 08e998f6..0e586797 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -72,7 +72,6 @@ "events": "3.3.0", "react-native-quick-base64": "2.1.2", "readable-stream": "4.5.2", - "string_decoder": "1.3.0", "util": "0.12.5" }, "devDependencies": { @@ -87,10 +86,10 @@ "eslint": "9.9.0", "eslint-plugin-react-native": "5.0.0", "jest": "29.7.0", - "nitro-codegen": "0.21.0", + "nitro-codegen": "0.25.2", "prettier": "3.3.3", "react-native-builder-bob": "0.35.2", - "react-native-nitro-modules": "0.21.0", + "react-native-nitro-modules": "0.25.2", "release-it": "18.1.1", "typescript": "5.1.6", "typescript-eslint": "^8.1.0" diff --git a/packages/react-native-quick-crypto/src/cipher.ts b/packages/react-native-quick-crypto/src/cipher.ts new file mode 100644 index 00000000..536a1915 --- /dev/null +++ b/packages/react-native-quick-crypto/src/cipher.ts @@ -0,0 +1,303 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import Stream, { type TransformOptions } from 'readable-stream'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import type { BinaryLike, BinaryLikeNode, Encoding } from './utils'; +import type { + CipherCCMOptions, + CipherCCMTypes, + CipherGCMTypes, + CipherGCMOptions, + CipherOCBOptions, + CipherOCBTypes, +} from 'crypto'; // @types/node +import type { + Cipher as NativeCipher, + CipherFactory, +} from './specs/cipher.nitro'; +import { ab2str, binaryLikeToArrayBuffer } from './utils'; +import { + getDefaultEncoding, + getUIntOption, + normalizeEncoding, + validateEncoding, +} from './utils/cipher'; + +export type CipherOptions = + | CipherCCMOptions + | CipherOCBOptions + | CipherGCMOptions + | TransformOptions; + +class CipherUtils { + private static native = + NitroModules.createHybridObject('Cipher'); + public static getSupportedCiphers(): string[] { + return this.native.getSupportedCiphers(); + } +} + +export function getCiphers(): string[] { + return CipherUtils.getSupportedCiphers(); +} + +interface CipherArgs { + isCipher: boolean; + cipherType: string; + cipherKey: BinaryLikeNode; + iv: BinaryLike; + options?: CipherOptions; +} + +class CipherCommon extends Stream.Transform { + private native: NativeCipher; + + constructor({ isCipher, cipherType, cipherKey, iv, options }: CipherArgs) { + // Explicitly create TransformOptions for super() + const streamOptions: TransformOptions = {}; + if (options) { + // List known TransformOptions keys (adjust if needed) + const transformKeys: Array = [ + 'readableHighWaterMark', + 'writableHighWaterMark', + 'decodeStrings', + 'defaultEncoding', + 'objectMode', + 'destroy', + 'read', + 'write', + 'writev', + 'final', + 'transform', + 'flush', + // Add any other relevant keys from readable-stream's TransformOptions + ]; + for (const key of transformKeys) { + if (key in options) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (streamOptions as any)[key] = (options as any)[key]; + } + } + } + super(streamOptions); // Pass filtered options + + const authTagLen: number = + getUIntOption(options ?? {}, 'authTagLength') !== -1 + ? getUIntOption(options ?? {}, 'authTagLength') + : 16; // defaults to 16 bytes + + const factory = + NitroModules.createHybridObject('CipherFactory'); + this.native = factory.createCipher({ + isCipher, + cipherType, + cipherKey: binaryLikeToArrayBuffer(cipherKey), + iv: binaryLikeToArrayBuffer(iv), + authTagLen, + }); + } + + update( + data: BinaryLike, + inputEncoding?: Encoding, + outputEncoding?: Encoding, + ): Buffer | string { + const defaultEncoding = getDefaultEncoding(); + inputEncoding = inputEncoding ?? defaultEncoding; + outputEncoding = outputEncoding ?? defaultEncoding; + + if (typeof data === 'string') { + validateEncoding(data, inputEncoding); + } else if (!ArrayBuffer.isView(data)) { + throw new Error('Invalid data argument'); + } + + const ret = this.native.update( + binaryLikeToArrayBuffer(data, inputEncoding), + ); + + if (outputEncoding && outputEncoding !== 'buffer') { + return ab2str(ret, outputEncoding); + } + + return Buffer.from(ret); + } + + final(): Buffer; + final(outputEncoding: BufferEncoding | 'buffer'): string; + final(outputEncoding?: BufferEncoding | 'buffer'): Buffer | string { + const ret = this.native.final(); + + if (outputEncoding && outputEncoding !== 'buffer') { + return ab2str(ret, outputEncoding); + } + + return Buffer.from(ret); + } + + _transform( + chunk: BinaryLike, + encoding: BufferEncoding, + callback: () => void, + ) { + this.push(this.update(chunk, normalizeEncoding(encoding))); + callback(); + } + + _flush(callback: () => void) { + this.push(this.final()); + callback(); + } + + public setAutoPadding(autoPadding?: boolean): this { + const res = this.native.setAutoPadding(!!autoPadding); + if (!res) { + throw new Error('setAutoPadding failed'); + } + return this; + } + + public setAAD( + buffer: Buffer, + options?: { + plaintextLength: number; + }, + ): this { + // Check if native parts are initialized + if (!this.native || typeof this.native.setAAD !== 'function') { + throw new Error('Cipher native object or setAAD method not initialized.'); + } + const res = this.native.setAAD(buffer.buffer, options?.plaintextLength); + if (!res) { + throw new Error('setAAD failed (native call returned false)'); + } + return this; + } + + public getAuthTag(): Buffer { + return Buffer.from(this.native.getAuthTag()); + } + + public setAuthTag(tag: Buffer): this { + const res = this.native.setAuthTag(binaryLikeToArrayBuffer(tag)); + if (!res) { + throw new Error('setAuthTag failed'); + } + return this; + } + + public getSupportedCiphers(): string[] { + return this.native.getSupportedCiphers(); + } +} + +class Cipheriv extends CipherCommon { + constructor( + cipherType: string, + cipherKey: BinaryLikeNode, + iv: BinaryLike, + options?: CipherOptions, + ) { + super({ + isCipher: true, + cipherType, + cipherKey: binaryLikeToArrayBuffer(cipherKey), + iv: binaryLikeToArrayBuffer(iv), + options, + }); + } +} + +type Cipher = Cipheriv; + +class Decipheriv extends CipherCommon { + constructor( + cipherType: string, + cipherKey: BinaryLikeNode, + iv: BinaryLike, + options?: CipherOptions, + ) { + super({ + isCipher: false, + cipherType, + cipherKey: binaryLikeToArrayBuffer(cipherKey), + iv: binaryLikeToArrayBuffer(iv), + options, + }); + } +} + +type Decipher = Decipheriv; + +export function createDecipheriv( + algorithm: CipherCCMTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options: CipherCCMOptions, +): Decipher; +export function createDecipheriv( + algorithm: CipherOCBTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options: CipherOCBOptions, +): Decipher; +export function createDecipheriv( + algorithm: CipherGCMTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options?: CipherGCMOptions, +): Decipher; +export function createDecipheriv( + algorithm: string, + key: BinaryLikeNode, + iv: BinaryLike, + options?: TransformOptions, +): Decipher; +export function createDecipheriv( + algorithm: string, + key: BinaryLikeNode, + iv: BinaryLike, + options?: CipherOptions, +): Decipher { + return new Decipheriv(algorithm, key, iv, options); +} + +export function createCipheriv( + algorithm: CipherCCMTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options: CipherCCMOptions, +): Cipher; +export function createCipheriv( + algorithm: CipherOCBTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options: CipherOCBOptions, +): Cipher; +export function createCipheriv( + algorithm: CipherGCMTypes, + key: BinaryLikeNode, + iv: BinaryLike, + options?: CipherGCMOptions, +): Cipher; +export function createCipheriv( + algorithm: string, + key: BinaryLikeNode, + iv: BinaryLike, + options?: TransformOptions, +): Cipher; +export function createCipheriv( + algorithm: string, + key: BinaryLikeNode, + iv: BinaryLike, + options?: CipherOptions, +): Cipher { + return new Cipheriv(algorithm, key, iv, options); +} + +export const cipherExports = { + createCipheriv, + createDecipheriv, + getCiphers, +}; + +export type { Cipher, Decipher }; diff --git a/packages/react-native-quick-crypto/src/index.ts b/packages/react-native-quick-crypto/src/index.ts index f6dae5c6..f0adcd17 100644 --- a/packages/react-native-quick-crypto/src/index.ts +++ b/packages/react-native-quick-crypto/src/index.ts @@ -3,9 +3,10 @@ import { Buffer } from '@craftzdog/react-native-buffer'; // API imports import * as keys from './keys'; +import { cipherExports as cipher } from './cipher'; +import * as ed from './ed'; import { hashExports as hash } from './hash'; import { hmacExports as hmac } from './hmac'; -import * as ed from './ed'; import * as pbkdf2 from './pbkdf2'; import * as random from './random'; @@ -17,32 +18,13 @@ import * as utils from './utils'; * See `docs/implementation-coverage.md` for status. */ const QuickCrypto = { - // createHmac, - // Hmac: createHmac, - // Hash: createHash, - // createHash, - // createCipher, - // createCipheriv, - // createDecipher, - // createDecipheriv, - // publicEncrypt, - // publicDecrypt, - // privateDecrypt, - // generateKey, - // generateKeySync, - // createSign, - // createVerify, - // subtle, - // constants, ...keys, + ...cipher, + ...ed, ...hash, ...hmac, - ...ed, ...pbkdf2, ...random, - // getCiphers, - // getHashes, - // webcrypto, ...utils, }; @@ -63,9 +45,10 @@ global.process.nextTick = setImmediate; // exports export default QuickCrypto; +export * from './cipher'; +export * from './ed'; export * from './hash'; export * from './hmac'; -export * from './ed'; export * from './pbkdf2'; export * from './random'; export * from './utils'; diff --git a/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts b/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts new file mode 100644 index 00000000..0d5d100a --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts @@ -0,0 +1,25 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +type CipherArgs = { + isCipher: boolean; + cipherType: string; + cipherKey: ArrayBuffer; + iv: ArrayBuffer; + authTagLen?: number; +}; + +export interface Cipher extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + update(data: ArrayBuffer): ArrayBuffer; + final(): ArrayBuffer; + setArgs(args: CipherArgs): void; + setAAD(data: ArrayBuffer, plaintextLength?: number): boolean; + setAutoPadding(autoPad: boolean): boolean; + setAuthTag(tag: ArrayBuffer): boolean; + getAuthTag(): ArrayBuffer; + getSupportedCiphers(): string[]; +} + +export interface CipherFactory + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + createCipher(args: CipherArgs): Cipher; +} diff --git a/packages/react-native-quick-crypto/src/utils/cipher.ts b/packages/react-native-quick-crypto/src/utils/cipher.ts new file mode 100644 index 00000000..df505beb --- /dev/null +++ b/packages/react-native-quick-crypto/src/utils/cipher.ts @@ -0,0 +1,60 @@ +import type { Encoding } from './types'; + +// Mimics node behavior for default global encoding +let defaultEncoding: Encoding = 'buffer'; + +export function setDefaultEncoding(encoding: Encoding) { + defaultEncoding = encoding; +} + +export function getDefaultEncoding(): Encoding { + return defaultEncoding; +} + +export function normalizeEncoding(enc: string) { + if (!enc) return 'utf8'; + let retried; + while (true) { + switch (enc) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 'utf16le'; + case 'latin1': + case 'binary': + return 'latin1'; + case 'base64': + case 'ascii': + case 'hex': + return enc; + default: + if (retried) return; // undefined + enc = ('' + enc).toLowerCase(); + retried = true; + } + } +} + +export function validateEncoding(data: string, encoding: string) { + const normalizedEncoding = normalizeEncoding(encoding); + const length = data.length; + + if (normalizedEncoding === 'hex' && length % 2 !== 0) { + throw new Error(`Encoding ${encoding} not valid for data length ${length}`); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getUIntOption(options: Record, key: string) { + let value; + if (options && (value = options[key]) != null) { + // >>> Turns any type into a positive integer (also sets the sign bit to 0) + if (value >>> 0 !== value) throw new Error(`options.${key}: ${value}`); + return value; + } + return -1; +} diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 7b4208b4..cfe6da8e 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -282,3 +282,30 @@ export type Encoding = | CharacterEncoding | LegacyCharacterEncoding | 'buffer'; + +// These are for shortcomings in @types/node +// Here we use "*Type" instead of "*Types" like node does. +// export type CipherCBCType = 'aes-128-cbc' | 'aes-192-cbc' | 'aes-256-cbc'; +export type CipherCFBType = + | 'aes-128-cfb' + | 'aes-192-cfb' + | 'aes-256-cfb' + | 'aes-128-cfb1' + | 'aes-192-cfb1' + | 'aes-256-cfb1' + | 'aes-128-cfb8' + | 'aes-192-cfb8' + | 'aes-256-cfb8'; +export type CipherCTRType = 'aes-128-ctr' | 'aes-192-ctr' | 'aes-256-ctr'; +export type CipherDESType = + | 'des' + | 'des3' + | 'des-cbc' + | 'des-ecb' + | 'des-ede' + | 'des-ede-cbc' + | 'des-ede3' + | 'des-ede3-cbc'; +export type CipherECBType = 'aes-128-ecb' | 'aes-192-ecb' | 'aes-256-ecb'; +export type CipherGCMType = 'aes-128-gcm' | 'aes-192-gcm' | 'aes-256-gcm'; +export type CipherOFBType = 'aes-128-ofb' | 'aes-192-ofb' | 'aes-256-ofb'; diff --git a/packages/react-native-quick-crypto/tsconfig.json b/packages/react-native-quick-crypto/tsconfig.json index 875ec696..21be80e6 100644 --- a/packages/react-native-quick-crypto/tsconfig.json +++ b/packages/react-native-quick-crypto/tsconfig.json @@ -25,5 +25,4 @@ "verbatimModuleSyntax": true, }, "include": ["src"], - "exclude": ["zzz"] }