diff --git a/bun.lock b/bun.lock index 903fe945..535c2ed2 100644 --- a/bun.lock +++ b/bun.lock @@ -21,9 +21,9 @@ }, "example": { "name": "react-native-quick-crypto-example", - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "dependencies": { - "@craftzdog/react-native-buffer": "6.0.5", + "@craftzdog/react-native-buffer": "6.1.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.7.0", "@noble/hashes": "^1.5.0", @@ -39,8 +39,8 @@ "react-native": "0.76.9", "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.15", + "react-native-quick-base64": "2.2.0", + "react-native-quick-crypto": "1.0.0-beta.16", "react-native-safe-area-context": "5.1.0", "react-native-screens": "3.35.0", "react-native-vector-icons": "^10.1.0", @@ -74,11 +74,11 @@ }, "packages/react-native-quick-crypto": { "name": "react-native-quick-crypto", - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "dependencies": { - "@craftzdog/react-native-buffer": "6.0.5", + "@craftzdog/react-native-buffer": "6.1.0", "events": "3.3.0", - "react-native-quick-base64": "2.1.2", + "react-native-quick-base64": "2.2.0", "readable-stream": "4.5.2", "util": "0.12.5", }, @@ -372,7 +372,7 @@ "@conventional-changelog/git-client": ["@conventional-changelog/git-client@1.0.1", "", { "dependencies": { "@types/semver": "^7.5.5", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.0.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw=="], - "@craftzdog/react-native-buffer": ["@craftzdog/react-native-buffer@6.0.5", "", { "dependencies": { "ieee754": "^1.2.1", "react-native-quick-base64": "^2.0.5" } }, "sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw=="], + "@craftzdog/react-native-buffer": ["@craftzdog/react-native-buffer@6.1.0", "", { "dependencies": { "ieee754": "^1.2.1", "react-native-quick-base64": "^2.0.5" } }, "sha512-lJXdjZ7fTllLbzDrwg/FrJLjQ5sBcAgwcqgAB6OPpXTHdCenEhHZblQpfmBLLe7/S7m0yKXL3kN3jpwOEkpjGg=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.0", "", { "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA=="], @@ -1890,7 +1890,7 @@ "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=="], + "react-native-quick-base64": ["react-native-quick-base64@2.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r7/BRsRl8QKEhS0JsHW6QX9+8LrC6NNWlwNnBnZ69h2kbcfABYsUILT71obrs9fqElEIMzuYSI5aHID955akyQ=="], "react-native-quick-crypto": ["react-native-quick-crypto@workspace:packages/react-native-quick-crypto"], diff --git a/docs/test_suite_results_android.png b/docs/test_suite_results_android.png index 7e4ff76b..909a69af 100644 Binary files a/docs/test_suite_results_android.png 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 index 7ccffac5..59289fb6 100644 Binary files a/docs/test_suite_results_ios.png and b/docs/test_suite_results_ios.png differ diff --git a/example/package.json b/example/package.json index abba3a5c..ea3ce771 100644 --- a/example/package.json +++ b/example/package.json @@ -20,7 +20,7 @@ "build:ios": "cd ios && xcodebuild -workspace QuickCrytpExample.xcworkspace -scheme QuickCrytpExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { - "@craftzdog/react-native-buffer": "6.0.5", + "@craftzdog/react-native-buffer": "6.1.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.7.0", "@noble/hashes": "^1.5.0", @@ -36,7 +36,7 @@ "react-native": "0.76.9", "react-native-bouncy-checkbox": "4.1.2", "react-native-nitro-modules": "0.25.2", - "react-native-quick-base64": "2.1.2", + "react-native-quick-base64": "2.2.0", "react-native-quick-crypto": "1.0.0-beta.16", "react-native-safe-area-context": "5.1.0", "react-native-screens": "3.35.0", diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index a1766313..6c9e14a9 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -3,6 +3,8 @@ import type { TestSuites } from '../types/tests'; import { TestsContext } from '../tests/util'; import '../tests/cipher/cipher_tests'; +import '../tests/cipher/chacha_tests'; +import '../tests/cipher/xsalsa20_tests'; import '../tests/ed25519/ed25519_tests'; import '../tests/hash/hash_tests'; import '../tests/hmac/hmac_tests'; diff --git a/example/src/tests/cipher/chacha_tests.ts b/example/src/tests/cipher/chacha_tests.ts new file mode 100644 index 00000000..3b00c4f9 --- /dev/null +++ b/example/src/tests/cipher/chacha_tests.ts @@ -0,0 +1,315 @@ +/** + * ChaCha20 and ChaCha20-Poly1305 tests + * + * Test vectors from IETF RFC 7539 and draft-irtf-cfrg-chacha20-poly1305-03 + * @see https://github.com/calvinmetcalf/chacha20poly1305/blob/master/test/chacha20.js + * @see https://datatracker.ietf.org/doc/html/rfc7539 + */ + +import { Buffer } from '@craftzdog/react-native-buffer'; +import { createCipheriv } from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; +import { roundTrip, roundTripAuth } from './roundTrip'; + +const SUITE = 'cipher'; + +function fromHex(h: string | Buffer): Buffer { + if (typeof h === 'string') { + h = h.replace(/([^0-9a-f])/g, ''); + return Buffer.from(h, 'hex'); + } + return h; +} + +interface ChaCha20TestVector { + key: string; + nonce: string; + counter?: number; + plaintext?: string; + expected: string; +} + +interface ChaCha20Poly1305TestVector { + key: string; + nonce: string; + plaintext: string; + aad: string | Buffer; + tag: string; + expected: string; +} + +// Test vectors from RFC 7539 and other sources +const testVectors = { + rfc7539_vector1: { + key: '00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f', + nonce: '00:00:00:00:00:00:00:4a:00:00:00:00', + counter: 1, + plaintext: + // Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it. + '4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c' + + '65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73' + + '73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63' + + '6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f' + + '6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20' + + '74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73' + + '63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69' + + '74 2e', + expected: + '6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81' + + 'e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b' + + 'f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57' + + '16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8' + + '07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e' + + '52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36' + + '5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42' + + '87 4d', + } as ChaCha20TestVector, + rfc7539_vector2: { + key: + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00', + nonce: '00 00 00 00 00 00 00 00 00 00 00 00', + counter: 0, + plaintext: + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00', + expected: + '76 b8 e0 ad a0 f1 3d 90 40 5d 6a e5 53 86 bd 28' + + 'bd d2 19 b8 a0 8d ed 1a a8 36 ef cc 8b 77 0d c7' + + 'da 41 59 7c 51 57 48 8d 77 24 e0 3f b8 d8 4a 37' + + '6a 43 b8 f4 15 18 a1 1c c3 87 b6 69 b2 ee 65 86', + } as ChaCha20TestVector, + rfc7539_vector3: { + key: + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01', + nonce: '00 00 00 00 00 00 00 00 00 00 00 00', + plaintext: + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' + + '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00', + expected: + '45 40 f0 5a 9f 1f b2 96 d7 73 6e 7b 20 8e 3c 96 ' + + 'eb 4f e1 83 46 88 d2 60 4f 45 09 52 ed 43 2d 41 ' + + 'bb e2 a0 b6 ea 75 66 d2 a5 d1 e7 e2 0d 42 af 2c ' + + '53 d7 92 b1 c4 3f ea 81 7e 9a d2 75 ae 54 69 63', + } as ChaCha20TestVector, + poly1305_vector1: { + key: + '80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f ' + + '90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f', + nonce: '07 00 00 00 40 41 42 43 44 45 46 47', + plaintext: + '4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c' + + '65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73' + + '73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63' + + '6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f' + + '6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20' + + '74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73' + + '63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69' + + '74 2e', + aad: '50 51 52 53 c0 c1 c2 c3 c4 c5 c6 c7', + expected: + 'd3 1a 8d 34 64 8e 60 db 7b 86 af bc 53 ef 7e c2 ' + + 'a4 ad ed 51 29 6e 08 fe a9 e2 b5 a7 36 ee 62 d6 ' + + '3d be a4 5e 8c a9 67 12 82 fa fb 69 da 92 72 8b ' + + '1a 71 de 0a 9e 06 0b 29 05 d6 a5 b6 7e cd 3b 36 ' + + '92 dd bd 7f 2d 77 8b 8c 98 03 ae e3 28 09 1b 58 ' + + 'fa b3 24 e4 fa d6 75 94 55 85 80 8b 48 31 d7 bc ' + + '3f f4 de f0 8e 4b 7a 9d e5 76 d2 65 86 ce c6 4b' + + '61 16', + tag: '1a e1 0b 59 4f 09 e2 6a 7e 90 2e cb d0 60 06 91', + } as ChaCha20Poly1305TestVector, + poly1305_vector2: { + key: + 'bb 63 42 cb 4f bb 91 69 84 4e b9 bc d1 d1 ab c3 ' + + '9b ea 97 d4 d6 e5 ff 43 95 0c 81 d3 1d 50 bd 52', + nonce: '85 b7 2e 32 dc 35 79 3a b9 f1 bb d4', + plaintext: + '7b 22 6d 6e 65 6d 6f 6e 69 63 22 3a 22 61 73 6b ' + + '20 66 72 6f 77 6e 20 62 75 74 74 65 72 20 61 73 ' + + '74 68 6d 61 20 73 6f 63 69 61 6c 20 61 74 74 69 ' + + '74 75 64 65 20 6c 6f 6e 67 20 64 79 6e 61 6d 69 ' + + '63 20 61 77 66 75 6c 20 6d 61 67 69 63 20 61 74 ' + + '74 65 6e 64 20 70 6f 6e 64 22 7d', + aad: Buffer.alloc(0), + expected: + 'f1 25 c4 92 02 4c 5f dd 31 5d 5a e3 f4 88 23 4f ad ' + + 'e3 66 40 17 55 6b 90 90 0d 4f e0 66 48 d5 4e 4f 28 ' + + '1a 6b 3f 4b 0e 53 f9 bc 12 d2 6f d3 49 62 a2 cf 39 ' + + 'f1 d9 2c 46 c3 7f 34 ac 0d ba ae c6 72 eb 57 05 89 ' + + '86 ca 35 fc d9 f6 ce f7 5a 3b 1d a9 5f a0 f8 7a 4e ' + + '0b aa ce f9 77 68', + tag: 'ca 39 c0 e6 b2 e5 65 2a e0 7f 42 6e b2 dd f3 86', + } as ChaCha20Poly1305TestVector, +}; + +// Helper function to create ChaCha20 IV from nonce and counter +function createChaCha20IV(originalNonce: Buffer, counter = 0): Buffer { + const iv = Buffer.alloc(16); // 128 bits + iv.writeUInt32LE(counter, 0); + iv.writeUInt32LE(0, 4); // High 32 bits of counter + originalNonce.copy(iv, 8, 4, 12); + return iv; +} + +function testChaCha20Vector(vector: ChaCha20TestVector, description: string) { + test(SUITE, `chacha20 ${description}`, () => { + const key = fromHex(vector.key); + const originalNonce = fromHex(vector.nonce); + const plaintext = fromHex(vector.plaintext || '00'); + const expected = fromHex(vector.expected); + const iv = createChaCha20IV(originalNonce, vector.counter); + + roundTrip('chacha20', key, iv, plaintext); + + const cipher = createCipheriv('chacha20', key, iv); + const actual = Buffer.concat([cipher.update(plaintext), cipher.final()]); + + expect(actual).to.deep.equal(expected); + }); +} + +testChaCha20Vector(testVectors.rfc7539_vector1, 'rfc7539 test vector 1'); +testChaCha20Vector(testVectors.rfc7539_vector2, 'rfc7539 test vector 2'); +testChaCha20Vector(testVectors.rfc7539_vector3, 'rfc7539 test vector 3'); + +function testChaCha20Poly1305Vector( + vector: ChaCha20Poly1305TestVector, + description: string, +) { + test(SUITE, `chacha20-poly1305 ${description}`, () => { + const key = fromHex(vector.key); + const nonce = fromHex(vector.nonce); + const plaintext = fromHex(vector.plaintext); + const aad = fromHex(vector.aad); + const expectedCiphertext = fromHex(vector.expected); + const expectedTag = fromHex(vector.tag); + + // First test round trip + roundTripAuth('chacha20-poly1305', key, nonce, plaintext, aad); + + // Then test against expected values + const cipher = createCipheriv('chacha20-poly1305', key, nonce); + cipher.setAAD(aad); + const actualCipherText = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + + expect(actualCipherText).to.deep.equal(expectedCiphertext); + + const actualTag = cipher.getAuthTag(); + expect(actualTag).to.deep.equal(expectedTag); + }); +} + +testChaCha20Poly1305Vector( + testVectors.poly1305_vector1, + 'rfc7539 test vector 1', +); +testChaCha20Poly1305Vector( + testVectors.poly1305_vector2, + 'rfc7539 test vector 2', +); + +// Helper function for common test setup +function createTestSetup( + keyHex = '0000000000000000000000000000000000000000000000000000000000000000', + nonceHex = '000000000000000000000000', +) { + return { + key: Buffer.from(keyHex, 'hex'), + nonce: Buffer.from(nonceHex, 'hex'), + }; +} + +// Additional ChaCha20-Poly1305 test vectors with different scenarios +test(SUITE, 'chacha20-poly1305 empty plaintext', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.alloc(0); + const aad = Buffer.from('00000000000000000000000000000000', 'hex'); + + roundTripAuth('chacha20-poly1305', key, nonce, plaintext, aad); +}); + +test(SUITE, 'chacha20-poly1305 no aad', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.from('00000000000000000000000000000000', 'hex'); + + roundTripAuth('chacha20-poly1305', key, nonce, plaintext); +}); + +test(SUITE, 'chacha20-poly1305 large plaintext', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.alloc(1024, 0x42); // 1KB of 0x42 + const aad = Buffer.from('additional authenticated data', 'utf8'); + + roundTripAuth('chacha20-poly1305', key, nonce, plaintext, aad); +}); + +// Test different tag lengths for ChaCha20-Poly1305 +test(SUITE, 'chacha20-poly1305 custom tag length', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.from('Hello, ChaCha20-Poly1305!', 'utf8'); + const aad = Buffer.from('test aad', 'utf8'); + + // Test with 12-byte tag + roundTripAuth('chacha20-poly1305', key, nonce, plaintext, aad, 12); + + // Test with 8-byte tag + roundTripAuth('chacha20-poly1305', key, nonce, plaintext, aad, 8); +}); + +// ChaCha20 edge cases +test(SUITE, 'chacha20 empty plaintext', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.alloc(0); + const iv = createChaCha20IV(nonce); + + roundTrip('chacha20', key, iv, plaintext); +}); + +test(SUITE, 'chacha20 single byte', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.from([0x42]); + const iv = createChaCha20IV(nonce); + + roundTrip('chacha20', key, iv, plaintext); +}); + +test(SUITE, 'chacha20 large plaintext', () => { + const { key, nonce } = createTestSetup(); + const plaintext = Buffer.alloc(4096, 0x55); // 4KB of 0x55 + const iv = createChaCha20IV(nonce); + + roundTrip('chacha20', key, iv, plaintext); +}); + +// Test with different nonce formats (96-bit vs 64-bit + counter) +test(SUITE, 'chacha20 different nonce sizes', () => { + const { key } = createTestSetup(); + const plaintext = Buffer.from('test message', 'utf8'); + + // 96-bit nonce (IETF ChaCha20) + const nonce96 = Buffer.from('000000000000000000000000', 'hex'); + const iv96 = createChaCha20IV(nonce96); + roundTrip('chacha20', key, iv96, plaintext); + + // 64-bit nonce (original ChaCha20) - if supported + try { + const nonce64 = Buffer.from('0000000000000000', 'hex'); + const iv64 = Buffer.alloc(16); + iv64.writeUInt32LE(0, 0); + iv64.writeUInt32LE(0, 4); + nonce64.copy(iv64, 8, 0, 8); + roundTrip('chacha20', key, iv64, plaintext); + } catch { + // Some implementations only support 96-bit nonces + console.log('64-bit nonce not supported, skipping'); + } +}); diff --git a/example/src/tests/cipher/cipher_tests.ts b/example/src/tests/cipher/cipher_tests.ts index 47838a4a..49022bac 100644 --- a/example/src/tests/cipher/cipher_tests.ts +++ b/example/src/tests/cipher/cipher_tests.ts @@ -2,23 +2,16 @@ import { Buffer } from '@craftzdog/react-native-buffer'; import { getCiphers, createCipheriv, - createDecipheriv, randomFillSync, - xsalsa20, - type Cipher, - type Decipher, } from 'react-native-quick-crypto'; import { expect } from 'chai'; import { test } from '../util'; +import { roundTrip, roundTripAuth } from './roundTrip'; const SUITE = 'cipher'; // --- Constants and Test Data --- const key16 = Buffer.from('a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89', 'hex'); -const key32 = Buffer.from( - 'a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89', - '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); @@ -26,97 +19,6 @@ 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(() => { @@ -177,7 +79,8 @@ allCiphers.forEach(cipherName => { const testIv: Uint8Array = cipherName.includes('GCM') || cipherName.includes('OCB') || - cipherName.includes('CCM') + cipherName.includes('CCM') || + cipherName.includes('Poly1305') ? iv12 : iv16; @@ -203,16 +106,3 @@ allCiphers.forEach(cipherName => { } }); }); - -// libsodium cipher tests -test(SUITE, 'xsalsa20', () => { - const key = new Uint8Array(key32); - const nonce = randomFillSync(new Uint8Array(24)); - const data = new Uint8Array(plaintextBuffer); - // encrypt - const ciphertext = xsalsa20(key, nonce, data); - // decrypt - must use the same nonce as encryption - const decrypted = xsalsa20(key, nonce, ciphertext); - // test decrypted == data - expect(decrypted).eql(data); -}); diff --git a/example/src/tests/cipher/roundTrip.ts b/example/src/tests/cipher/roundTrip.ts new file mode 100644 index 00000000..02c1885b --- /dev/null +++ b/example/src/tests/cipher/roundTrip.ts @@ -0,0 +1,75 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { + createCipheriv, + createDecipheriv, + type Cipher, + type Decipher, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; + +// --- Helper Functions --- +// Helper for testing authenticated modes (GCM, CCM, OCB, Poly1305, SIV) +export function roundTripAuth( + cipherName: string, + key: Buffer, + iv: Buffer, + plaintext: Buffer, + aad?: Buffer, + tagLength?: number, // Usually 16 for these modes +) { + 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; + const encrypted = Buffer.concat([encryptedPart1, encryptedPart2]); + const tag = cipher.getAuthTag(); + + // 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 + } + decipher.setAuthTag(tag); + 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); +} + +// Helper for non-authenticated modes +export 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 +} diff --git a/example/src/tests/cipher/xsalsa20_tests.ts b/example/src/tests/cipher/xsalsa20_tests.ts new file mode 100644 index 00000000..bdbe796c --- /dev/null +++ b/example/src/tests/cipher/xsalsa20_tests.ts @@ -0,0 +1,27 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { randomFillSync, xsalsa20 } from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; + +const SUITE = 'cipher'; + +// --- Constants and Test Data --- +const key32 = Buffer.from( + 'a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89a8a7d6a5d4a3d2a1a09f9e9d9c8b8a89', + 'hex', +); +const plaintext = 'abcdefghijklmnopqrstuvwxyz'; +const plaintextBuffer = Buffer.from(plaintext); + +// libsodium cipher tests +test(SUITE, 'xsalsa20', () => { + const key = new Uint8Array(key32); + const nonce = randomFillSync(new Uint8Array(24)); + const data = new Uint8Array(plaintextBuffer); + // encrypt + const ciphertext = xsalsa20(key, nonce, data); + // decrypt - must use the same nonce as encryption + const decrypted = xsalsa20(key, nonce, ciphertext); + // test decrypted == data + expect(decrypted).eql(data); +}); diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 13b718c9..46014cb2 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -13,6 +13,8 @@ add_library( ../cpp/cipher/HybridCipher.cpp ../cpp/cipher/OCBCipher.cpp ../cpp/cipher/XSalsa20Cipher.cpp + ../cpp/cipher/ChaCha20Cipher.cpp + ../cpp/cipher/ChaCha20Poly1305Cipher.cpp ../cpp/ed25519/HybridEdKeyPair.cpp ../cpp/hash/HybridHash.cpp ../cpp/hmac/HybridHmac.cpp diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp new file mode 100644 index 00000000..c8177a93 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp @@ -0,0 +1,69 @@ +#include "ChaCha20Cipher.hpp" +#include "Utils.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + +using namespace margelo::nitro; + +// Implement virtual methods from HybridCipher +const EVP_CIPHER* ChaCha20Cipher::getCipherImpl() { + return EVP_chacha20(); +} + +void ChaCha20Cipher::validateKeySize(size_t key_size) const { + if (key_size != kKeySize) { + throw std::runtime_error("ChaCha20 key must be 32 bytes"); + } +} + +void ChaCha20Cipher::validateIVSize(size_t iv_size) const { + if (iv_size != kIVSize) { + throw std::runtime_error("ChaCha20 IV must be 16 bytes"); + } +} + +std::string ChaCha20Cipher::getCipherName() const { + return "ChaCha20"; +} + +// Use the base class implementation which now uses our virtual methods +void ChaCha20Cipher::init(const std::shared_ptr cipher_key, const std::shared_ptr iv) { + HybridCipher::init(cipher_key, iv); +} + +std::shared_ptr ChaCha20Cipher::update(const std::shared_ptr& data) { + checkCtx(); + auto native_data = ToNativeArrayBuffer(data); + size_t in_len = native_data->size(); + if (in_len > INT_MAX) { + throw std::runtime_error("Message too long"); + } + + // For ChaCha20, output size equals input size since it's a stream cipher + int out_len = in_len; + uint8_t* out = new uint8_t[out_len]; + + // Perform the cipher update operation + if (EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len) != 1) { + delete[] out; + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("ChaCha20Cipher: Failed to update: " + std::string(err_buf)); + } + + // Create and return a new buffer of exact size needed + return std::make_shared(out, out_len, [=]() { delete[] out; }); +} + +std::shared_ptr ChaCha20Cipher::final() { + checkCtx(); + // For ChaCha20, final() should return an empty buffer since it's a stream cipher + unsigned char* empty_output = new unsigned char[0]; + return std::make_shared(empty_output, 0, [=]() { delete[] empty_output; }); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.hpp new file mode 100644 index 00000000..cc5991b1 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "HybridCipher.hpp" +#include +#include + +namespace margelo::nitro::crypto { + +using namespace margelo::nitro; + +class ChaCha20Cipher : public HybridCipher { + public: + ChaCha20Cipher() : HybridCipher() {} + ~ChaCha20Cipher() override { + // 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; + + protected: + // Implement virtual methods from HybridCipher + const EVP_CIPHER* getCipherImpl() override; + void validateKeySize(size_t key_size) const override; + void validateIVSize(size_t iv_size) const override; + std::string getCipherName() const override; + + private: + // ChaCha20 uses a 256-bit key (32 bytes) and a 128-bit IV (16 bytes) + static constexpr int kKeySize = 32; + static constexpr int kIVSize = 16; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp new file mode 100644 index 00000000..d1db2a39 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp @@ -0,0 +1,170 @@ +#include "ChaCha20Poly1305Cipher.hpp" +#include "Utils.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + +void ChaCha20Poly1305Cipher::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; + } + + // Get ChaCha20-Poly1305 cipher implementation + const EVP_CIPHER* cipher = EVP_chacha20_poly1305(); + if (!cipher) { + throw std::runtime_error("Failed to get ChaCha20-Poly1305 cipher implementation"); + } + + // Create a new context + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + throw std::runtime_error("Failed to create cipher context"); + } + + // Initialize the encryption/decryption operation + 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("ChaCha20Poly1305Cipher: Failed initial CipherInit setup: " + std::string(err_buf)); + } + + // Set key and IV + auto native_key = ToNativeArrayBuffer(cipher_key); + auto native_iv = ToNativeArrayBuffer(iv); + + // Validate key size + if (native_key->size() != kKeySize) { + throw std::runtime_error("ChaCha20-Poly1305 key must be 32 bytes"); + } + + // Validate nonce size + if (native_iv->size() != kNonceSize) { + throw std::runtime_error("ChaCha20-Poly1305 nonce must be 12 bytes"); + } + + 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("ChaCha20Poly1305Cipher: Failed to set key/IV: " + std::string(err_buf)); + } + + // Reset final_called flag + final_called = false; +} + +std::shared_ptr ChaCha20Poly1305Cipher::update(const std::shared_ptr& data) { + checkCtx(); + auto native_data = ToNativeArrayBuffer(data); + size_t in_len = native_data->size(); + if (in_len > INT_MAX) { + throw std::runtime_error("Message too long"); + } + + // For ChaCha20-Poly1305, output size equals input size since it's a stream cipher + int out_len = in_len; + uint8_t* out = new uint8_t[out_len]; + + // Perform the cipher update operation + if (EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len) != 1) { + delete[] out; + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("ChaCha20Poly1305Cipher: Failed to update: " + std::string(err_buf)); + } + + // Create and return a new buffer of exact size needed + return std::make_shared(out, out_len, [=]() { delete[] out; }); +} + +std::shared_ptr ChaCha20Poly1305Cipher::final() { + checkCtx(); + + // For ChaCha20-Poly1305, we need to call final to generate the tag + int out_len = 0; + unsigned char* out = new unsigned char[0]; + + if (EVP_CipherFinal_ex(ctx, out, &out_len) != 1) { + delete[] out; + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + throw std::runtime_error("ChaCha20Poly1305Cipher: Failed to finalize: " + std::string(err_buf)); + } + + final_called = true; + return std::make_shared(out, out_len, [=]() { delete[] out; }); +} + +bool ChaCha20Poly1305Cipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) { + checkCtx(); + auto native_aad = ToNativeArrayBuffer(data); + size_t aad_len = native_aad->size(); + + // Set AAD data + int out_len = 0; + 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("ChaCha20Poly1305Cipher: Failed to set AAD: " + std::string(err_buf)); + } + return true; +} + +std::shared_ptr ChaCha20Poly1305Cipher::getAuthTag() { + checkCtx(); + if (!is_cipher) { + throw std::runtime_error("getAuthTag can only be called during encryption"); + } + if (!final_called) { + throw std::runtime_error("getAuthTag must be called after final()"); + } + + // Get the authentication tag + auto tag_buf = std::make_unique(kTagSize); + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, kTagSize, tag_buf.get()) != 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("ChaCha20Poly1305Cipher: Failed to get auth tag: " + std::string(err_buf)); + } + + uint8_t* raw_ptr = tag_buf.get(); + return std::make_shared(tag_buf.release(), kTagSize, [raw_ptr]() { delete[] raw_ptr; }); +} + +bool ChaCha20Poly1305Cipher::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); + if (native_tag->size() != kTagSize) { + throw std::runtime_error("ChaCha20-Poly1305 tag must be 16 bytes"); + } + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, kTagSize, native_tag->data()) != 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("ChaCha20Poly1305Cipher: Failed to set auth tag: " + std::string(err_buf)); + } + return true; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.hpp new file mode 100644 index 00000000..a01d68bb --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "HybridCipher.hpp" + +namespace margelo::nitro::crypto { + +class ChaCha20Poly1305Cipher : public HybridCipher { + public: + ChaCha20Poly1305Cipher() : HybridObject(TAG), final_called(false) {} + ~ChaCha20Poly1305Cipher() { + // 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; + std::shared_ptr getAuthTag() override; + bool setAuthTag(const std::shared_ptr& tag) override; + + private: + // ChaCha20-Poly1305 uses a 256-bit key (32 bytes) and a 96-bit nonce (12 bytes) + static constexpr int kKeySize = 32; + static constexpr int kNonceSize = 12; + static constexpr int kTagSize = 16; // Poly1305 tag is always 16 bytes + bool final_called; +}; + +} // 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 index 969c20ce..b602990a 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -48,33 +48,36 @@ void HybridCipher::init(const std::shared_ptr cipher_key, const std ctx = nullptr; } - // 1. Get cipher implementation by name - const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str()); + // Get cipher implementation from derived class + const EVP_CIPHER* cipher = getCipherImpl(); if (!cipher) { - throw std::runtime_error("Unknown cipher " + cipher_type); + throw std::runtime_error("Failed to get " + getCipherName() + " cipher implementation"); } - // 2. Create a new context + // 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. + // Initialize the encryption/decryption operation with the cipher type. 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)); + throw std::runtime_error(getCipherName() + ": 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. + // Set key and IV auto native_key = ToNativeArrayBuffer(cipher_key); auto native_iv = ToNativeArrayBuffer(iv); + + // Validate key and IV sizes using derived class methods + validateKeySize(native_key->size()); + validateIVSize(native_iv->size()); + const unsigned char* key_ptr = reinterpret_cast(native_key->data()); const unsigned char* iv_ptr = reinterpret_cast(native_iv->data()); @@ -84,7 +87,7 @@ void HybridCipher::init(const std::shared_ptr cipher_key, const std 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)); + throw std::runtime_error(getCipherName() + ": Failed to set key/IV: " + std::string(err_buf)); } } diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp index 7f418ba3..2d3be51f 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp @@ -46,6 +46,12 @@ class HybridCipher : public HybridCipherSpec { enum UpdateResult { kSuccess, kErrorMessageSize, kErrorState }; enum AuthTagState { kAuthTagUnknown, kAuthTagKnown, kAuthTagPassedToOpenSSL }; + // Virtual methods for cipher-specific implementations + virtual const EVP_CIPHER* getCipherImpl() = 0; + virtual void validateKeySize(size_t key_size) const = 0; + virtual void validateIVSize(size_t iv_size) const = 0; + virtual std::string getCipherName() const = 0; + protected: // Properties bool is_cipher = true; diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp index 3c68f375..30ed4412 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp @@ -5,6 +5,8 @@ #include #include "CCMCipher.hpp" +#include "ChaCha20Cipher.hpp" +#include "ChaCha20Poly1305Cipher.hpp" #include "HybridCipherFactorySpec.hpp" #include "OCBCipher.hpp" #include "Utils.hpp" @@ -22,7 +24,6 @@ class HybridCipherFactory : public HybridCipherFactorySpec { public: // Factory method exposed to JS inline std::shared_ptr createCipher(const CipherArgs& args) { - // Create the appropriate cipher instance based on mode std::shared_ptr cipherInstance; @@ -47,7 +48,24 @@ class HybridCipherFactory : public HybridCipherFactorySpec { cipherInstance->init(args.cipherKey, args.iv); return cipherInstance; } + case EVP_CIPH_STREAM_CIPHER: { + // Check for ChaCha20 variants specifically + std::string cipherName = toLower(args.cipherType); + if (cipherName == "chacha20") { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + cipherInstance->init(args.cipherKey, args.iv); + return cipherInstance; + } + if (cipherName == "chacha20-poly1305") { + cipherInstance = std::make_shared(); + cipherInstance->setArgs(args); + cipherInstance->init(args.cipherKey, args.iv); + return cipherInstance; + } + } default: { + // Default case for other ciphers cipherInstance = std::make_shared(); cipherInstance->setArgs(args); cipherInstance->init(args.cipherKey, args.iv); diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index 5fcc73f8..eab9c4a1 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -68,9 +68,9 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { - "@craftzdog/react-native-buffer": "6.0.5", + "@craftzdog/react-native-buffer": "6.1.0", "events": "3.3.0", - "react-native-quick-base64": "2.1.2", + "react-native-quick-base64": "2.2.0", "readable-stream": "4.5.2", "util": "0.12.5" }, diff --git a/packages/react-native-quick-crypto/src/cipher.ts b/packages/react-native-quick-crypto/src/cipher.ts index c1e056e1..969cdbae 100644 --- a/packages/react-native-quick-crypto/src/cipher.ts +++ b/packages/react-native-quick-crypto/src/cipher.ts @@ -96,6 +96,13 @@ class CipherCommon extends Stream.Transform { }); } + update(data: Buffer): Buffer; + update(data: BinaryLike, inputEncoding?: Encoding): Buffer; + update( + data: BinaryLike, + inputEncoding: Encoding, + outputEncoding: Encoding, + ): string; update( data: BinaryLike, inputEncoding?: Encoding,