diff --git a/example/src/components/TestItem.tsx b/example/src/components/TestItem.tsx index 184e1d51..5c92e7ee 100644 --- a/example/src/components/TestItem.tsx +++ b/example/src/components/TestItem.tsx @@ -123,8 +123,7 @@ const styles = StyleSheet.create({ color: colors.red, }, count: { - fontSize: 12, - fontWeight: 'bold', + fontSize: 11, flex: 1, textAlign: 'right', }, diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 95ac199e..f2c63fd0 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -6,9 +6,6 @@ import '../tests/blake3/blake3_tests'; import '../tests/cipher/cipher_tests'; import '../tests/cipher/chacha_tests'; import '../tests/cipher/xsalsa20_tests'; -import '../tests/cfrg/ed25519_tests'; -import '../tests/cfrg/x25519_tests'; -import '../tests/constants/constants_tests'; import '../tests/hash/hash_tests'; import '../tests/hmac/hmac_tests'; import '../tests/hkdf/hkdf_tests'; @@ -19,10 +16,8 @@ import '../tests/keys/generate_keypair'; import '../tests/keys/public_cipher'; import '../tests/keys/sign_verify_streaming'; import '../tests/pbkdf2/pbkdf2_tests'; -import '../tests/scrypt/scrypt_tests'; import '../tests/random/random_tests'; -import '../tests/utils/timingSafeEqual_tests'; -import '../tests/subtle/x25519_x448'; +import '../tests/scrypt/scrypt_tests'; import '../tests/subtle/deriveBits'; import '../tests/subtle/derive_key'; import '../tests/subtle/digest'; @@ -32,6 +27,7 @@ import '../tests/subtle/import_export'; import '../tests/subtle/jwk_rfc7517_tests'; import '../tests/subtle/sign_verify'; import '../tests/subtle/wrap_unwrap'; +import '../tests/utils/utils_tests'; export const useTestsList = (): [ TestSuites, diff --git a/example/src/navigators/children/TestSuitesScreen.tsx b/example/src/navigators/children/TestSuitesScreen.tsx index 96747440..0d99a93c 100644 --- a/example/src/navigators/children/TestSuitesScreen.tsx +++ b/example/src/navigators/children/TestSuitesScreen.tsx @@ -1,5 +1,11 @@ -import React from 'react'; -import { Text, View, ScrollView, StyleSheet } from 'react-native'; +import React, { useMemo } from 'react'; +import { + Text, + View, + FlatList, + StyleSheet, + TouchableOpacity, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Button } from '../../components/Button'; import { TestItem } from '../../components/TestItem'; @@ -7,56 +13,80 @@ import { useTestsList } from '../../hooks/useTestsList'; import { useTestsRun } from '../../hooks/useTestsRun'; import { colors } from '../../styles/colors'; +type SuiteEntry = { + name: string; + suite: { value: boolean; tests: Record void | Promise> }; + count: number; +}; + export const TestSuitesScreen = () => { const [suites, toggle, clearAll, checkAll] = useTestsList(); const [results, runTests, stats] = useTestsRun(); - let totalCount = 0; + + const suiteEntries = useMemo(() => { + return Object.entries(suites).map(([name, suite]) => ({ + name, + suite, + count: Object.keys(suite.tests).length, + })); + }, [suites]); + + const totalCount = useMemo( + () => suiteEntries.reduce((sum, entry) => sum + entry.count, 0), + [suiteEntries], + ); + + const renderItem = ({ item, index }: { item: SuiteEntry; index: number }) => ( + + ); return ( - - {Object.entries(suites).map(([suiteName, suite], index) => { - const suiteTestCount = Object.keys(suite.tests).length; - totalCount += suiteTestCount; - return ( - - ); - })} - + index.toString()} + testID="test-suites-list" + /> {results && Object.keys(results).length > 0 && stats && ( - - ⏱️ {stats.duration}ms - - {Object.values(results).reduce( - (sum, suite) => - sum + suite.results.filter(r => r.type === 'correct').length, - 0, - )} - - - {Object.values(results).reduce( - (sum, suite) => - sum + suite.results.filter(r => r.type === 'incorrect').length, - 0, - )} - - {totalCount} + + + + ⏱️ {stats.duration}ms + + {Object.values(results).reduce( + (sum, suite) => + sum + suite.results.filter(r => r.type === 'correct').length, + 0, + )} + + + {Object.values(results).reduce( + (sum, suite) => + sum + + suite.results.filter(r => r.type === 'incorrect').length, + 0, + )} + + + {totalCount} + + )} @@ -97,24 +127,31 @@ const styles = StyleSheet.create({ alignContent: 'space-around', justifyContent: 'space-around', }, - scrollView: {}, - statsContainer: { - paddingHorizontal: 10, - paddingVertical: 5, + footerItem: { + width: '100%', flexDirection: 'row', - alignItems: 'center', alignContent: 'center', - justifyContent: 'space-evenly', + alignItems: 'center', gap: 10, + borderTopWidth: 1, + borderTopColor: colors.gray, + paddingHorizontal: 10, + paddingVertical: 5, + }, + footerCheckbox: { + width: 24, }, - timeLabel: { - fontSize: 12, + footerContent: { + flex: 1, + flexDirection: 'row', + }, + footerLabel: { + fontSize: 11, fontWeight: 'bold', flex: 8, }, - statNumber: { - fontSize: 12, - fontWeight: 'bold', + footerCount: { + fontSize: 11, flex: 1, textAlign: 'right', }, diff --git a/example/src/tests/cfrg/ed25519_tests.ts b/example/src/tests/cfrg/ed25519_tests.ts deleted file mode 100644 index 679bc19e..00000000 --- a/example/src/tests/cfrg/ed25519_tests.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -import { Buffer } from '@craftzdog/react-native-buffer'; -import { expect } from 'chai'; -import { ab2str, Ed, randomBytes } from 'react-native-quick-crypto'; -import { test } from '../util'; - -const SUITE = 'cfrg'; - -const encoder = new TextEncoder(); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const encode = (data: any): Uint8Array => encoder.encode(JSON.stringify(data)); - -/* -const jwkOptions: GenerateKeyPairOptions = { - publicKeyEncoding: { - format: 'jwk', - }, - privateKeyEncoding: { - format: 'jwk', - }, -}; - -const types: CFRGKeyPairType[] = ['ed25519', 'ed448', 'x25519', 'x448']; -types.map((type) => { - test(SUITE, `generateKeyPair - ${type}`, () => { - const callback: GenerateKeyPairCallback = ( - err: Error | undefined, - publicKey: KeyPairKey, - privateKey: KeyPairKey, - ) => { - expect(err).to.be.undefined; - expect(publicKey).not.to.be.null; - expect(privateKey).not.to.be.null; - // console.log('publ', ab2str(publicKey as ArrayBuffer)); - // console.log('priv', ab2str(privateKey as ArrayBuffer)); - }; - - crypto.generateKeyPair( - type, - jwkOptions, - callback - ); - }); -}); -*/ - -const data1 = Buffer.from('hello world'); - -test(SUITE, 'ed25519 - sign/verify - round trip happy', async () => { - const ed = new Ed('ed25519', {}); - await ed.generateKeyPair(); - const signature = await ed.sign(data1.buffer); - const verified = await ed.verify(signature, data1.buffer); - expect(verified).to.be.true; -}); - -test(SUITE, 'ed25519 - sign/verify - round trip sad', async () => { - const data2 = Buffer.from('goodbye cruel world'); - const ed = new Ed('ed25519', {}); - await ed.generateKeyPair(); - const signature = await ed.sign(data1.buffer); - const verified = await ed.verify(signature, data2.buffer); - expect(verified).to.be.false; -}); - -test( - SUITE, - 'ed25519 - sign/verify - bad signature does not verify', - async () => { - const ed = new Ed('ed25519', {}); - await ed.generateKeyPair(); - const signature = await ed.sign(data1.buffer); - const signature2 = randomBytes(64).buffer; - expect(ab2str(signature2)).not.to.equal(ab2str(signature)); - const verified = await ed.verify(signature2, data1.buffer); - expect(verified).to.be.false; - }, -); - -test( - SUITE, - 'ed25519 - sign/verify - switched args does not verify', - async () => { - const ed = new Ed('ed25519', {}); - await ed.generateKeyPair(); - const signature = await ed.sign(data1.buffer); - // verify(message, signature) is switched - const verified = await ed.verify(data1.buffer, signature); - expect(verified).to.be.false; - }, -); - -test( - SUITE, - 'ed25519 - sign/verify - non-internally generated private key', - async () => { - const pub = Buffer.from( - 'e106bf015ad54a64022295c7af2c35f9511eb37264a7722a9642eaac6c59a494', - 'hex', - ); - const priv = Buffer.from( - '5f27e170afc5091c4933d980c5fe86af997b91375115c6ee2c0fe4ea12400ed0', - 'hex', - ); - - const ed2 = new Ed('ed25519', {}); - const signature = await ed2.sign(data1.buffer, priv); - const verified = await ed2.verify(signature, data1.buffer, pub); - expect(verified).to.be.true; - }, -); - -test(SUITE, 'ed25519 - sign/verify - bad signature', async () => { - let ed1: Ed | null = new Ed('ed25519', {}); - await ed1.generateKeyPair(); - const pub = ed1.getPublicKey(); - const priv = ed1.getPrivateKey(); - ed1 = null; - - const ed2 = new Ed('ed25519', {}); - const signature = await ed2.sign(data1.buffer, priv); - const signature2 = randomBytes(64).buffer; - expect(ab2str(signature2)).not.to.equal(ab2str(signature)); - const verified = await ed2.verify(signature2, data1.buffer, pub); - expect(verified).to.be.false; -}); - -test( - SUITE, - 'ed25519 - sign/verify - bad verify with private key, not public', - async () => { - let ed1: Ed | null = new Ed('ed25519', {}); - await ed1.generateKeyPair(); - const priv = ed1.getPrivateKey(); - ed1 = null; - - const ed2 = new Ed('ed25519', {}); - const signature = await ed2.sign(data1.buffer, priv); - const verified = await ed2.verify(signature, data1.buffer, priv); - expect(verified).to.be.false; - }, -); - -test(SUITE, 'ed25519 - sign/verify - Uint8Arrays', () => { - const data = { b: 'world', a: 'hello' }; - - const ed1 = new Ed('ed25519', {}); - ed1.generateKeyPairSync(); - const pub = new Uint8Array(ed1.getPublicKey()); - const priv = new Uint8Array(ed1.getPrivateKey()); - - const ed2 = new Ed('ed25519', {}); - const signature = new Uint8Array(ed2.signSync(encode(data), priv)); - const verified = ed2.verifySync(signature, encode(data), pub); - expect(verified).to.be.true; -}); diff --git a/example/src/tests/cfrg/x25519_tests.ts b/example/src/tests/cfrg/x25519_tests.ts deleted file mode 100644 index e3da3bfa..00000000 --- a/example/src/tests/cfrg/x25519_tests.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { expect } from 'chai'; -import { Buffer } from '@craftzdog/react-native-buffer'; -import crypto, { KeyObject } from 'react-native-quick-crypto'; -import { test } from '../util'; - -const SUITE = 'cfrg'; - -test(SUITE, 'x25519 - shared secret', () => { - // Generate key pairs - const alice = crypto.generateKeyPairSync('x25519', {}); - const bob = crypto.generateKeyPairSync('x25519', {}); - - // Check that keys were generated - if (!alice.privateKey || !(alice.privateKey instanceof ArrayBuffer)) { - throw new Error('Failed to generate private key for Alice'); - } - if (!bob.publicKey || !(bob.publicKey instanceof ArrayBuffer)) { - throw new Error('Failed to generate public key for Bob'); - } - - // Create KeyObject instances from the raw keys using the factory method - const privateKey = KeyObject.createKeyObject('private', alice.privateKey); - const publicKey = KeyObject.createKeyObject('public', bob.publicKey); - - // Use the keys for Diffie-Hellman - const sharedSecret = crypto.diffieHellman({ - privateKey, - publicKey, - }); - void expect(Buffer.isBuffer(sharedSecret)).to.be.true; -}); - -test(SUITE, 'x25519 - shared secret symmetry', () => { - // Generate key pairs - const alice = crypto.generateKeyPairSync('x25519', {}); - const bob = crypto.generateKeyPairSync('x25519', {}); - - // Create KeyObject instances - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const alicePublic = KeyObject.createKeyObject( - 'public', - alice.publicKey as ArrayBuffer, - ); - const bobPrivate = KeyObject.createKeyObject( - 'private', - bob.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - - // Compute shared secrets from both sides - const sharedSecretAlice = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, - }) as Buffer; - - const sharedSecretBob = crypto.diffieHellman({ - privateKey: bobPrivate, - publicKey: alicePublic, - }) as Buffer; - - // Verify both sides compute the same shared secret - void expect(Buffer.isBuffer(sharedSecretAlice)).to.be.true; - void expect(Buffer.isBuffer(sharedSecretBob)).to.be.true; - void expect(sharedSecretAlice.equals(sharedSecretBob)).to.be.true; -}); - -test(SUITE, 'x25519 - shared secret properties', () => { - const alice = crypto.generateKeyPairSync('x25519', {}); - const bob = crypto.generateKeyPairSync('x25519', {}); - - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - - const sharedSecret = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, - }) as Buffer; - - // X25519 shared secrets should be exactly 32 bytes - void expect(sharedSecret.length).to.equal(32); - - // Should not be all zeros (extremely unlikely with proper implementation) - const allZeros = Buffer.alloc(32, 0); - void expect(sharedSecret.equals(allZeros)).to.be.false; - - // Should be deterministic - same keys produce same secret - const sharedSecret2 = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, - }) as Buffer; - void expect(sharedSecret.equals(sharedSecret2)).to.be.true; -}); - -test(SUITE, 'x25519 - different key pairs produce different secrets', () => { - const alice = crypto.generateKeyPairSync('x25519', {}); - const bob = crypto.generateKeyPairSync('x25519', {}); - const charlie = crypto.generateKeyPairSync('x25519', {}); - - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const charliePublic = KeyObject.createKeyObject( - 'public', - charlie.publicKey as ArrayBuffer, - ); - - const secretAliceBob = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, - }) as Buffer; - - const secretAliceCharlie = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: charliePublic, - }) as Buffer; - - // Different public keys should produce different shared secrets - void expect(secretAliceBob.equals(secretAliceCharlie)).to.be.false; -}); - -test(SUITE, 'x25519 - error handling', () => { - const alice = crypto.generateKeyPairSync('x25519', {}); - - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - - // Should throw when creating KeyObject with invalid key data - void expect(() => { - KeyObject.createKeyObject('public', new ArrayBuffer(16)); // Invalid size - }).to.throw(); - - // Should throw when using invalid key types - void expect(() => { - crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: {} as KeyObject, - }); - }).to.throw(); -}); diff --git a/example/src/tests/subtle/deriveBits.ts b/example/src/tests/subtle/deriveBits.ts index 4866c41d..950d20f3 100644 --- a/example/src/tests/subtle/deriveBits.ts +++ b/example/src/tests/subtle/deriveBits.ts @@ -4,8 +4,11 @@ import { ab2str, type HashAlgorithm, normalizeHashName, + KeyObject, } from 'react-native-quick-crypto'; import { test } from '../util'; +import crypto from 'react-native-quick-crypto'; +import { Buffer } from '@craftzdog/react-native-buffer'; type TestFixture = [ string, @@ -73,4 +76,144 @@ kTests.forEach(async ([pass, salt, iterations, hash, length, expected]) => { ); }); +// --- X25519 deriveBits Tests (from cfrg suite) --- + +test(SUITE, 'x25519 - shared secret', () => { + const alice = crypto.generateKeyPairSync('x25519', {}); + const bob = crypto.generateKeyPairSync('x25519', {}); + + if (!alice.privateKey || !(alice.privateKey instanceof ArrayBuffer)) { + throw new Error('Failed to generate private key for Alice'); + } + if (!bob.publicKey || !(bob.publicKey instanceof ArrayBuffer)) { + throw new Error('Failed to generate public key for Bob'); + } + + const privateKey = KeyObject.createKeyObject('private', alice.privateKey); + const publicKey = KeyObject.createKeyObject('public', bob.publicKey); + + const sharedSecret = crypto.diffieHellman({ + privateKey, + publicKey, + }); + expect(Buffer.isBuffer(sharedSecret)).to.equal(true); +}); + +test(SUITE, 'x25519 - shared secret symmetry', () => { + const alice = crypto.generateKeyPairSync('x25519', {}); + const bob = crypto.generateKeyPairSync('x25519', {}); + + const alicePrivate = KeyObject.createKeyObject( + 'private', + alice.privateKey as ArrayBuffer, + ); + const alicePublic = KeyObject.createKeyObject( + 'public', + alice.publicKey as ArrayBuffer, + ); + const bobPrivate = KeyObject.createKeyObject( + 'private', + bob.privateKey as ArrayBuffer, + ); + const bobPublic = KeyObject.createKeyObject( + 'public', + bob.publicKey as ArrayBuffer, + ); + + const sharedSecretAlice = crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: bobPublic, + }) as Buffer; + + const sharedSecretBob = crypto.diffieHellman({ + privateKey: bobPrivate, + publicKey: alicePublic, + }) as Buffer; + + expect(Buffer.isBuffer(sharedSecretAlice)).to.equal(true); + expect(Buffer.isBuffer(sharedSecretBob)).to.equal(true); + expect(sharedSecretAlice.equals(sharedSecretBob)).to.equal(true); +}); + +test(SUITE, 'x25519 - shared secret properties', () => { + const alice = crypto.generateKeyPairSync('x25519', {}); + const bob = crypto.generateKeyPairSync('x25519', {}); + + const alicePrivate = KeyObject.createKeyObject( + 'private', + alice.privateKey as ArrayBuffer, + ); + const bobPublic = KeyObject.createKeyObject( + 'public', + bob.publicKey as ArrayBuffer, + ); + + const sharedSecret = crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: bobPublic, + }) as Buffer; + + expect(sharedSecret.length).to.equal(32); + + const allZeros = Buffer.alloc(32, 0); + expect(sharedSecret.equals(allZeros)).to.equal(false); + + const sharedSecret2 = crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: bobPublic, + }) as Buffer; + expect(sharedSecret.equals(sharedSecret2)).to.equal(true); +}); + +test(SUITE, 'x25519 - different key pairs produce different secrets', () => { + const alice = crypto.generateKeyPairSync('x25519', {}); + const bob = crypto.generateKeyPairSync('x25519', {}); + const charlie = crypto.generateKeyPairSync('x25519', {}); + + const alicePrivate = KeyObject.createKeyObject( + 'private', + alice.privateKey as ArrayBuffer, + ); + const bobPublic = KeyObject.createKeyObject( + 'public', + bob.publicKey as ArrayBuffer, + ); + const charliePublic = KeyObject.createKeyObject( + 'public', + charlie.publicKey as ArrayBuffer, + ); + + const secretAliceBob = crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: bobPublic, + }) as Buffer; + + const secretAliceCharlie = crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: charliePublic, + }) as Buffer; + + expect(secretAliceBob.equals(secretAliceCharlie)).to.equal(false); +}); + +test(SUITE, 'x25519 - error handling', () => { + const alice = crypto.generateKeyPairSync('x25519', {}); + + const alicePrivate = KeyObject.createKeyObject( + 'private', + alice.privateKey as ArrayBuffer, + ); + + expect(() => { + KeyObject.createKeyObject('public', new ArrayBuffer(16)); + }).to.throw(); + + expect(() => { + crypto.diffieHellman({ + privateKey: alicePrivate, + publicKey: {} as KeyObject, + }); + }).to.throw(); +}); + // ecdh deriveBits diff --git a/example/src/tests/subtle/generateKey.ts b/example/src/tests/subtle/generateKey.ts index cf9b23e2..524d05f7 100644 --- a/example/src/tests/subtle/generateKey.ts +++ b/example/src/tests/subtle/generateKey.ts @@ -413,6 +413,128 @@ testRSAKeyGen( ['encrypt', 'wrapKey'], ); +// --- X25519/X448 Key Generation Tests (from subtle.cfrg suite) --- + +test( + SUITE, + 'X25519 - generateKey, exportKey, importKey, deriveBits', + async () => { + const format = 'raw'; + const algorithm = { name: 'X25519' } as const; + + const aliceKeys = (await subtle.generateKey(algorithm, true, [ + 'deriveKey', + 'deriveBits', + ])) as TestCryptoKeyPair; + + const bobKeys = (await subtle.generateKey(algorithm, true, [ + 'deriveKey', + 'deriveBits', + ])) as TestCryptoKeyPair; + + expect(aliceKeys.publicKey.algorithm.name).to.equal('X25519'); + expect(aliceKeys.privateKey.algorithm.name).to.equal('X25519'); + + const alicePubRaw = await subtle.exportKey(format, aliceKeys.publicKey); + const bobPubRaw = await subtle.exportKey(format, bobKeys.publicKey); + + const alicePubImported = await subtle.importKey( + format, + alicePubRaw, + algorithm, + true, + [], + ); + + const bobPubImported = await subtle.importKey( + format, + bobPubRaw, + algorithm, + true, + [], + ); + + const bitsLength = 256; + const aliceShared = await subtle.deriveBits( + { name: 'X25519', public: bobPubImported } as any, + aliceKeys.privateKey, + bitsLength, + ); + + const bobShared = await subtle.deriveBits( + { name: 'X25519', public: alicePubImported } as any, + bobKeys.privateKey, + bitsLength, + ); + + const aliceSharedView = new Uint8Array(aliceShared); + const bobSharedView = new Uint8Array(bobShared); + + expect(aliceSharedView.length).to.equal(bitsLength / 8); + expect(aliceSharedView).to.deep.equal(bobSharedView); + }, +); + +test( + SUITE, + 'X448 - generateKey, exportKey, importKey, deriveBits', + async () => { + const format = 'spki'; + const algorithm = { name: 'X448' } as const; + + const aliceKeys = (await subtle.generateKey(algorithm, true, [ + 'deriveKey', + 'deriveBits', + ])) as TestCryptoKeyPair; + + const bobKeys = (await subtle.generateKey(algorithm, true, [ + 'deriveKey', + 'deriveBits', + ])) as TestCryptoKeyPair; + + expect(aliceKeys.publicKey.algorithm.name).to.equal('X448'); + expect(aliceKeys.privateKey.algorithm.name).to.equal('X448'); + + const alicePubSpki = await subtle.exportKey(format, aliceKeys.publicKey); + const bobPubSpki = await subtle.exportKey(format, bobKeys.publicKey); + + const alicePubImported = await subtle.importKey( + format, + alicePubSpki, + algorithm, + true, + [], + ); + + const bobPubImported = await subtle.importKey( + format, + bobPubSpki, + algorithm, + true, + [], + ); + + const bitsLength = 448; + const aliceShared = await subtle.deriveBits( + { name: 'X448', public: bobPubImported } as any, + aliceKeys.privateKey, + bitsLength, + ); + + const bobShared = await subtle.deriveBits( + { name: 'X448', public: alicePubImported } as any, + bobKeys.privateKey, + bitsLength, + ); + + const aliceSharedView = new Uint8Array(aliceShared); + const bobSharedView = new Uint8Array(bobShared); + + expect(aliceSharedView.length).to.equal(56); + expect(aliceSharedView).to.deep.equal(bobSharedView); + }, +); + // --- ML-DSA Key Generation Tests --- type MlDsaVariant = 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87'; diff --git a/example/src/tests/subtle/sign_verify.ts b/example/src/tests/subtle/sign_verify.ts index a2cfafd6..2c121626 100644 --- a/example/src/tests/subtle/sign_verify.ts +++ b/example/src/tests/subtle/sign_verify.ts @@ -3,6 +3,9 @@ import { subtle, isCryptoKeyPair, CryptoKey, + ab2str, + Ed, + randomBytes, } from 'react-native-quick-crypto'; import type { CryptoKeyPair, @@ -10,6 +13,7 @@ import type { } from 'react-native-quick-crypto'; import { test } from '../util'; import { expect } from 'chai'; +import { Buffer } from '@craftzdog/react-native-buffer'; const encoder = new TextEncoder(); @@ -843,3 +847,118 @@ test(SUITE, 'Sign with imported Ed25519 key', async () => { expect(isValid).to.equal(true); }); + +// --- Ed25519 Legacy API Tests (from cfrg suite) --- + +const data1 = Buffer.from('hello world'); + +test(SUITE, 'ed25519 - sign/verify - round trip happy', async () => { + const ed = new Ed('ed25519', {}); + await ed.generateKeyPair(); + const signature = await ed.sign(data1.buffer); + const verified = await ed.verify(signature, data1.buffer); + expect(verified).to.equal(true); +}); + +test(SUITE, 'ed25519 - sign/verify - round trip sad', async () => { + const data2 = Buffer.from('goodbye cruel world'); + const ed = new Ed('ed25519', {}); + await ed.generateKeyPair(); + const signature = await ed.sign(data1.buffer); + const verified = await ed.verify(signature, data2.buffer); + expect(verified).to.equal(false); +}); + +test( + SUITE, + 'ed25519 - sign/verify - bad signature does not verify', + async () => { + const ed = new Ed('ed25519', {}); + await ed.generateKeyPair(); + const signature = await ed.sign(data1.buffer); + const signature2 = randomBytes(64).buffer; + expect(ab2str(signature2)).not.to.equal(ab2str(signature)); + const verified = await ed.verify(signature2, data1.buffer); + expect(verified).to.equal(false); + }, +); + +test( + SUITE, + 'ed25519 - sign/verify - switched args does not verify', + async () => { + const ed = new Ed('ed25519', {}); + await ed.generateKeyPair(); + const signature = await ed.sign(data1.buffer); + const verified = await ed.verify(data1.buffer, signature); + expect(verified).to.equal(false); + }, +); + +test( + SUITE, + 'ed25519 - sign/verify - non-internally generated private key', + async () => { + const pub = Buffer.from( + 'e106bf015ad54a64022295c7af2c35f9511eb37264a7722a9642eaac6c59a494', + 'hex', + ); + const priv = Buffer.from( + '5f27e170afc5091c4933d980c5fe86af997b91375115c6ee2c0fe4ea12400ed0', + 'hex', + ); + + const ed2 = new Ed('ed25519', {}); + const signature = await ed2.sign(data1.buffer, priv); + const verified = await ed2.verify(signature, data1.buffer, pub); + expect(verified).to.equal(true); + }, +); + +test(SUITE, 'ed25519 - sign/verify - bad signature', async () => { + let ed1: Ed | null = new Ed('ed25519', {}); + await ed1.generateKeyPair(); + const pub = ed1.getPublicKey(); + const priv = ed1.getPrivateKey(); + ed1 = null; + + const ed2 = new Ed('ed25519', {}); + const signature = await ed2.sign(data1.buffer, priv); + const signature2 = randomBytes(64).buffer; + expect(ab2str(signature2)).not.to.equal(ab2str(signature)); + const verified = await ed2.verify(signature2, data1.buffer, pub); + expect(verified).to.equal(false); +}); + +test( + SUITE, + 'ed25519 - sign/verify - bad verify with private key, not public', + async () => { + let ed1: Ed | null = new Ed('ed25519', {}); + await ed1.generateKeyPair(); + const priv = ed1.getPrivateKey(); + ed1 = null; + + const ed2 = new Ed('ed25519', {}); + const signature = await ed2.sign(data1.buffer, priv); + const verified = await ed2.verify(signature, data1.buffer, priv); + expect(verified).to.equal(false); + }, +); + +test(SUITE, 'ed25519 - sign/verify - Uint8Arrays', () => { + const data = { b: 'world', a: 'hello' }; + const encoder2 = new TextEncoder(); + const encode = (data: unknown): Uint8Array => + encoder2.encode(JSON.stringify(data)); + + const ed1 = new Ed('ed25519', {}); + ed1.generateKeyPairSync(); + const pub = new Uint8Array(ed1.getPublicKey()); + const priv = new Uint8Array(ed1.getPrivateKey()); + + const ed2 = new Ed('ed25519', {}); + const signature = new Uint8Array(ed2.signSync(encode(data), priv)); + const verified = ed2.verifySync(signature, encode(data), pub); + expect(verified).to.equal(true); +}); diff --git a/example/src/tests/subtle/x25519_x448.ts b/example/src/tests/subtle/x25519_x448.ts deleted file mode 100644 index dca7fde2..00000000 --- a/example/src/tests/subtle/x25519_x448.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { expect } from 'chai'; -import { - subtle, - type CryptoKeyPair, - type CryptoKey, -} from 'react-native-quick-crypto'; -import { test } from '../util'; - -const SUITE = 'subtle.cfrg'; - -test( - SUITE, - 'X25519 - generateKey, exportKey, importKey, deriveBits', - async () => { - const format = 'raw'; - const algorithm = { name: 'X25519' } as const; - - // 1. Generate Keys - const aliceKeys = (await subtle.generateKey(algorithm, true, [ - 'deriveKey', - 'deriveBits', - ])) as CryptoKeyPair; - - const bobKeys = (await subtle.generateKey(algorithm, true, [ - 'deriveKey', - 'deriveBits', - ])) as CryptoKeyPair; - - expect((aliceKeys.publicKey as CryptoKey).algorithm.name).to.equal( - 'X25519', - ); - expect((aliceKeys.privateKey as CryptoKey).algorithm.name).to.equal( - 'X25519', - ); - - // 2. Export Keys - const alicePubRaw = await subtle.exportKey( - format, - aliceKeys.publicKey as CryptoKey, - ); - const bobPubRaw = await subtle.exportKey( - format, - bobKeys.publicKey as CryptoKey, - ); - - // 3. Import Keys - const alicePubImported = await subtle.importKey( - format, - alicePubRaw, - algorithm, - true, - [], - ); - - const bobPubImported = await subtle.importKey( - format, - bobPubRaw, - algorithm, - true, - [], - ); - - // 4. Derive Bits - const bitsLength = 256; - const aliceShared = await subtle.deriveBits( - { name: 'X25519', public: bobPubImported } as any, // eslint-disable-line @typescript-eslint/no-explicit-any - aliceKeys.privateKey as CryptoKey, - bitsLength, - ); - - const bobShared = await subtle.deriveBits( - { name: 'X25519', public: alicePubImported } as any, // eslint-disable-line @typescript-eslint/no-explicit-any - bobKeys.privateKey as CryptoKey, - bitsLength, - ); - - // Verify shared secrets match - const aliceSharedView = new Uint8Array(aliceShared); - const bobSharedView = new Uint8Array(bobShared); - - expect(aliceSharedView.length).to.equal(bitsLength / 8); - expect(aliceSharedView).to.deep.equal(bobSharedView); - }, -); - -test( - SUITE, - 'X448 - generateKey, exportKey, importKey, deriveBits', - async () => { - const format = 'spki'; // Use SPKI for X448 public key export test - const algorithm = { name: 'X448' } as const; - - // 1. Generate Keys - const aliceKeys = (await subtle.generateKey(algorithm, true, [ - 'deriveKey', - 'deriveBits', - ])) as CryptoKeyPair; - - const bobKeys = (await subtle.generateKey(algorithm, true, [ - 'deriveKey', - 'deriveBits', - ])) as CryptoKeyPair; - - expect((aliceKeys.publicKey as CryptoKey).algorithm.name).to.equal('X448'); - expect((aliceKeys.privateKey as CryptoKey).algorithm.name).to.equal('X448'); - - // 2. Export Keys - const alicePubSpki = await subtle.exportKey( - format, - aliceKeys.publicKey as CryptoKey, - ); - const bobPubSpki = await subtle.exportKey( - format, - bobKeys.publicKey as CryptoKey, - ); - - // 3. Import Keys - const alicePubImported = await subtle.importKey( - format, - alicePubSpki, - algorithm, - true, - [], - ); - - const bobPubImported = await subtle.importKey( - format, - bobPubSpki, - algorithm, - true, - [], - ); - - // 4. Derive Bits - const bitsLength = 448; // X448 produces 56 bytes - const aliceShared = await subtle.deriveBits( - { name: 'X448', public: bobPubImported } as any, // eslint-disable-line @typescript-eslint/no-explicit-any - aliceKeys.privateKey as CryptoKey, - bitsLength, - ); - - const bobShared = await subtle.deriveBits( - { name: 'X448', public: alicePubImported } as any, // eslint-disable-line @typescript-eslint/no-explicit-any - bobKeys.privateKey as CryptoKey, - bitsLength, - ); - - // Verify shared secrets match - const aliceSharedView = new Uint8Array(aliceShared); - const bobSharedView = new Uint8Array(bobShared); - - expect(aliceSharedView.length).to.equal(56); - expect(aliceSharedView).to.deep.equal(bobSharedView); - }, -); diff --git a/example/src/tests/utils/timingSafeEqual_tests.ts b/example/src/tests/utils/timingSafeEqual_tests.ts deleted file mode 100644 index eb783e9a..00000000 --- a/example/src/tests/utils/timingSafeEqual_tests.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Buffer } from '@craftzdog/react-native-buffer'; -import { expect } from 'chai'; -import { test } from '../util'; - -import crypto from 'react-native-quick-crypto'; - -const SUITE = 'timingSafeEqual'; - -test(SUITE, 'should return true for equal buffers', () => { - const a = Buffer.from('hello world'); - const b = Buffer.from('hello world'); - expect(crypto.timingSafeEqual(a, b)).to.equal(true); -}); - -test(SUITE, 'should return false for different buffers', () => { - const a = Buffer.from('hello world'); - const b = Buffer.from('hello worlD'); - expect(crypto.timingSafeEqual(a, b)).to.equal(false); -}); - -test(SUITE, 'should work with Uint8Array', () => { - const a = new Uint8Array([1, 2, 3, 4, 5]); - const b = new Uint8Array([1, 2, 3, 4, 5]); - expect(crypto.timingSafeEqual(a, b)).to.equal(true); -}); - -test(SUITE, 'should return false for different Uint8Array', () => { - const a = new Uint8Array([1, 2, 3, 4, 5]); - const b = new Uint8Array([1, 2, 3, 4, 6]); - expect(crypto.timingSafeEqual(a, b)).to.equal(false); -}); - -test(SUITE, 'should work with ArrayBuffer', () => { - const a = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer; - const b = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer; - expect(crypto.timingSafeEqual(a, b)).to.equal(true); -}); - -test(SUITE, 'should throw for different length buffers', () => { - const a = Buffer.from('hello'); - const b = Buffer.from('hello world'); - expect(() => crypto.timingSafeEqual(a, b)).to.throw(RangeError); -}); - -test(SUITE, 'should work with empty buffers', () => { - const a = Buffer.alloc(0); - const b = Buffer.alloc(0); - expect(crypto.timingSafeEqual(a, b)).to.equal(true); -}); - -test(SUITE, 'should work with single byte buffers', () => { - const a = Buffer.from([0xff]); - const b = Buffer.from([0xff]); - expect(crypto.timingSafeEqual(a, b)).to.equal(true); - - const c = Buffer.from([0x00]); - expect(crypto.timingSafeEqual(a, c)).to.equal(false); -}); - -test(SUITE, 'should work for HMAC comparison use case', () => { - const hmac1 = crypto - .createHmac('sha256', 'secret') - .update('message') - .digest(); - const hmac2 = crypto - .createHmac('sha256', 'secret') - .update('message') - .digest(); - const hmac3 = crypto - .createHmac('sha256', 'secret') - .update('different') - .digest(); - - expect(crypto.timingSafeEqual(hmac1, hmac2)).to.equal(true); - expect(crypto.timingSafeEqual(hmac1, hmac3)).to.equal(false); -}); diff --git a/example/src/tests/constants/constants_tests.ts b/example/src/tests/utils/utils_tests.ts similarity index 60% rename from example/src/tests/constants/constants_tests.ts rename to example/src/tests/utils/utils_tests.ts index 20b73c8b..c6237a4d 100644 --- a/example/src/tests/constants/constants_tests.ts +++ b/example/src/tests/utils/utils_tests.ts @@ -1,10 +1,12 @@ import { constants } from 'react-native-quick-crypto'; import { expect } from 'chai'; import { test } from '../util'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import crypto from 'react-native-quick-crypto'; -const SUITE = 'constants'; +const SUITE = 'utils'; -// --- RSA Padding Constants --- +// --- Constants Tests --- test(SUITE, 'RSA_PKCS1_PADDING exists and is a number', () => { expect(typeof constants.RSA_PKCS1_PADDING).to.equal('number'); @@ -26,8 +28,6 @@ test(SUITE, 'RSA_PKCS1_PSS_PADDING exists and is a number', () => { expect(constants.RSA_PKCS1_PSS_PADDING).to.equal(6); }); -// --- RSA PSS Salt Length Constants --- - test(SUITE, 'RSA_PSS_SALTLEN_DIGEST exists and is a number', () => { expect(typeof constants.RSA_PSS_SALTLEN_DIGEST).to.equal('number'); expect(constants.RSA_PSS_SALTLEN_DIGEST).to.equal(-1); @@ -43,8 +43,6 @@ test(SUITE, 'RSA_PSS_SALTLEN_AUTO exists and is a number', () => { expect(constants.RSA_PSS_SALTLEN_AUTO).to.equal(-2); }); -// --- Point Conversion Form Constants --- - test(SUITE, 'POINT_CONVERSION_COMPRESSED exists', () => { expect(typeof constants.POINT_CONVERSION_COMPRESSED).to.equal('number'); expect(constants.POINT_CONVERSION_COMPRESSED).to.equal(2); @@ -60,8 +58,6 @@ test(SUITE, 'POINT_CONVERSION_HYBRID exists', () => { expect(constants.POINT_CONVERSION_HYBRID).to.equal(6); }); -// --- DH Constants --- - test(SUITE, 'DH_CHECK_P_NOT_PRIME exists', () => { expect(typeof constants.DH_CHECK_P_NOT_PRIME).to.equal('number'); }); @@ -78,15 +74,11 @@ test(SUITE, 'DH_UNABLE_TO_CHECK_GENERATOR exists', () => { expect(typeof constants.DH_UNABLE_TO_CHECK_GENERATOR).to.equal('number'); }); -// --- Cipher Constants --- - test(SUITE, 'OPENSSL_VERSION_NUMBER exists', () => { expect(typeof constants.OPENSSL_VERSION_NUMBER).to.equal('number'); expect(constants.OPENSSL_VERSION_NUMBER).to.be.greaterThan(0); }); -// --- All Constants Are Numbers --- - test(SUITE, 'All exported constants are numbers', () => { const allKeys = Object.keys(constants); expect(allKeys.length).to.be.greaterThan(0); @@ -97,10 +89,7 @@ test(SUITE, 'All exported constants are numbers', () => { } }); -// --- Node.js Compatibility --- - test(SUITE, 'RSA padding constants match Node.js values', () => { - // These values are defined by OpenSSL and should match Node.js expect(constants.RSA_PKCS1_PADDING).to.equal(1); expect(constants.RSA_NO_PADDING).to.equal(3); expect(constants.RSA_PKCS1_OAEP_PADDING).to.equal(4); @@ -108,15 +97,88 @@ test(SUITE, 'RSA padding constants match Node.js values', () => { }); test(SUITE, 'RSA PSS salt length constants match Node.js values', () => { - // These values are defined by OpenSSL and should match Node.js expect(constants.RSA_PSS_SALTLEN_DIGEST).to.equal(-1); expect(constants.RSA_PSS_SALTLEN_MAX_SIGN).to.equal(-2); expect(constants.RSA_PSS_SALTLEN_AUTO).to.equal(-2); }); test(SUITE, 'Point conversion constants match Node.js values', () => { - // These values are defined by OpenSSL EC_POINT conversion forms expect(constants.POINT_CONVERSION_COMPRESSED).to.equal(2); expect(constants.POINT_CONVERSION_UNCOMPRESSED).to.equal(4); expect(constants.POINT_CONVERSION_HYBRID).to.equal(6); }); + +// --- timingSafeEqual Tests --- + +test(SUITE, 'timingSafeEqual should return true for equal buffers', () => { + const a = Buffer.from('hello world'); + const b = Buffer.from('hello world'); + expect(crypto.timingSafeEqual(a, b)).to.equal(true); +}); + +test(SUITE, 'timingSafeEqual should return false for different buffers', () => { + const a = Buffer.from('hello world'); + const b = Buffer.from('hello worlD'); + expect(crypto.timingSafeEqual(a, b)).to.equal(false); +}); + +test(SUITE, 'timingSafeEqual should work with Uint8Array', () => { + const a = new Uint8Array([1, 2, 3, 4, 5]); + const b = new Uint8Array([1, 2, 3, 4, 5]); + expect(crypto.timingSafeEqual(a, b)).to.equal(true); +}); + +test( + SUITE, + 'timingSafeEqual should return false for different Uint8Array', + () => { + const a = new Uint8Array([1, 2, 3, 4, 5]); + const b = new Uint8Array([1, 2, 3, 4, 6]); + expect(crypto.timingSafeEqual(a, b)).to.equal(false); + }, +); + +test(SUITE, 'timingSafeEqual should work with ArrayBuffer', () => { + const a = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer; + const b = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer; + expect(crypto.timingSafeEqual(a, b)).to.equal(true); +}); + +test(SUITE, 'timingSafeEqual should throw for different length buffers', () => { + const a = Buffer.from('hello'); + const b = Buffer.from('hello world'); + expect(() => crypto.timingSafeEqual(a, b)).to.throw(RangeError); +}); + +test(SUITE, 'timingSafeEqual should work with empty buffers', () => { + const a = Buffer.alloc(0); + const b = Buffer.alloc(0); + expect(crypto.timingSafeEqual(a, b)).to.equal(true); +}); + +test(SUITE, 'timingSafeEqual should work with single byte buffers', () => { + const a = Buffer.from([0xff]); + const b = Buffer.from([0xff]); + expect(crypto.timingSafeEqual(a, b)).to.equal(true); + + const c = Buffer.from([0x00]); + expect(crypto.timingSafeEqual(a, c)).to.equal(false); +}); + +test(SUITE, 'timingSafeEqual should work for HMAC comparison use case', () => { + const hmac1 = crypto + .createHmac('sha256', 'secret') + .update('message') + .digest(); + const hmac2 = crypto + .createHmac('sha256', 'secret') + .update('message') + .digest(); + const hmac3 = crypto + .createHmac('sha256', 'secret') + .update('different') + .digest(); + + expect(crypto.timingSafeEqual(hmac1, hmac2)).to.equal(true); + expect(crypto.timingSafeEqual(hmac1, hmac3)).to.equal(false); +});