Skip to content

Commit 7bf6e53

Browse files
authored
feat: quick-win API additions, OKP JWK support, and native optimizations (#912)
1 parent 90d0f4d commit 7bf6e53

86 files changed

Lines changed: 885 additions & 405 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/plans/quick-wins.md

Lines changed: 0 additions & 43 deletions
This file was deleted.

.docs/implementation-coverage.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ These algorithms provide quantum-resistant cryptography.
4343
*`diffieHellman.getPublicKey([encoding])`
4444
*`diffieHellman.setPrivateKey(privateKey[, encoding])`
4545
*`diffieHellman.setPublicKey(publicKey[, encoding])`
46-
* `diffieHellman.verifyError`
46+
* `diffieHellman.verifyError`
4747
* ✅ Class: `DiffieHellmanGroup`
4848
* ✅ Class: `ECDH`
4949
* ❌ static `ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])`
@@ -65,8 +65,8 @@ These algorithms provide quantum-resistant cryptography.
6565
*`keyObject.asymmetricKeyDetails`
6666
*`keyObject.asymmetricKeyType`
6767
*`keyObject.export([options])`
68-
* `keyObject.equals(otherKeyObject)`
69-
* `keyObject.symmetricKeySize`
68+
* `keyObject.equals(otherKeyObject)`
69+
* `keyObject.symmetricKeySize`
7070
*`keyObject.toCryptoKey(algorithm, extractable, keyUsages)`
7171
*`keyObject.type`
7272
* ✅ Class: `Sign`
@@ -111,6 +111,7 @@ These algorithms provide quantum-resistant cryptography.
111111
*`crypto.createDecipheriv(algorithm, key, iv[, options])`
112112
*`crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])`
113113
*`crypto.createDiffieHellman(primeLength[, generator])`
114+
*`crypto.createDiffieHellmanGroup(groupName)`
114115
*`crypto.getDiffieHellman(groupName)`
115116
*`crypto.createECDH(curveName)`
116117
*`crypto.createHash(algorithm[, options])`
@@ -136,7 +137,7 @@ These algorithms provide quantum-resistant cryptography.
136137
*`crypto.getFips()`
137138
*`crypto.getHashes()`
138139
*`crypto.getRandomValues(typedArray)`
139-
* `crypto.hash(algorithm, data[, options])`
140+
* `crypto.hash(algorithm, data[, outputEncoding])`
140141
*`crypto.hkdf(digest, ikm, salt, info, keylen, callback)`
141142
*`crypto.hkdfSync(digest, ikm, salt, info, keylen)`
142143
*`crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`
@@ -263,8 +264,8 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs
263264
*`subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)`
264265
*`subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)`
265266
*`subtle.decrypt(algorithm, key, data)`
266-
* 🚧 `subtle.deriveBits(algorithm, baseKey, length)`
267-
* 🚧 `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)`
267+
* `subtle.deriveBits(algorithm, baseKey, length)`
268+
* `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)`
268269
* 🚧 `subtle.digest(algorithm, data)`
269270
*`subtle.encapsulateBits(encapsulationAlgorithm, encapsulationKey)`
270271
*`subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)`
@@ -299,7 +300,7 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs
299300
## `subtle.deriveKey`
300301
| Algorithm | Status |
301302
| --------- | :----: |
302-
| `ECDH` | |
303+
| `ECDH` | |
303304
| `HKDF` ||
304305
| `PBKDF2` ||
305306
| `X25519` ||
@@ -339,8 +340,8 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs
339340
| `ChaCha20-Poly1305` | | || || | |
340341
| `ECDH` ||||| || |
341342
| `ECDSA` ||||| || |
342-
| `Ed25519` ||| || || |
343-
| `Ed448` ||| || || |
343+
| `Ed25519` ||| || || |
344+
| `Ed448` ||| || || |
344345
| `HMAC` | | |||| | |
345346
| `ML-DSA-44` |||| | |||
346347
| `ML-DSA-65` |||| | |||
@@ -399,8 +400,8 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs
399400
| `ChaCha20-Poly1305` | | || || | |
400401
| `ECDH` ||||| || |
401402
| `ECDSA` ||||| || |
402-
| `Ed25519` ||| | | || |
403-
| `Ed448` ||| | | || |
403+
| `Ed25519` ||| | | || |
404+
| `Ed448` ||| | | || |
404405
| `HKDF` | | | ||| | |
405406
| `HMAC` | | |||| | |
406407
| `ML-DSA-44` |||| | |||

docs/data/coverage.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [
115115
{ name: 'export', status: 'implemented' },
116116
{ name: 'type', status: 'implemented' },
117117
{ name: 'asymmetricKeyDetails', status: 'missing' },
118-
{ name: 'equals', status: 'missing' },
119-
{ name: 'symmetricKeySize', status: 'missing' },
118+
{ name: 'equals', status: 'implemented' },
119+
{ name: 'symmetricKeySize', status: 'implemented' },
120120
{ name: 'toCryptoKey', status: 'missing' },
121121
{ name: 'from', status: 'missing', note: 'static' },
122122
],
@@ -136,7 +136,7 @@ export const COVERAGE_DATA: CoverageCategory[] = [
136136
{ name: 'createCipheriv', status: 'implemented' },
137137
{ name: 'createDecipheriv', status: 'implemented' },
138138
{ name: 'createDiffieHellman', status: 'implemented' },
139-
{ name: 'createDiffieHellmanGroup', status: 'missing' },
139+
{ name: 'createDiffieHellmanGroup', status: 'implemented' },
140140
{ name: 'createECDH', status: 'implemented' },
141141
{ name: 'createHash', status: 'implemented' },
142142
{ name: 'createHmac', status: 'implemented' },
@@ -209,7 +209,7 @@ export const COVERAGE_DATA: CoverageCategory[] = [
209209
{ name: 'getFips', status: 'missing' },
210210
{ name: 'getHashes', status: 'implemented' },
211211
{ name: 'getRandomValues', status: 'implemented' },
212-
{ name: 'hash', status: 'missing' },
212+
{ name: 'hash', status: 'implemented' },
213213
{ name: 'hkdf', status: 'implemented' },
214214
{ name: 'pbkdf2', status: 'implemented' },
215215
{ name: 'privateDecrypt / privateEncrypt', status: 'implemented' },
@@ -284,7 +284,7 @@ export const COVERAGE_DATA: CoverageCategory[] = [
284284
{
285285
name: 'crypto.subtle.deriveKey',
286286
subItems: [
287-
{ name: 'ECDH', status: 'missing' },
287+
{ name: 'ECDH', status: 'implemented' },
288288
{ name: 'HKDF', status: 'implemented' },
289289
{ name: 'PBKDF2', status: 'implemented' },
290290
{ name: 'X25519', status: 'implemented' },
@@ -339,8 +339,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [
339339
status: 'partial',
340340
note: 'spki, pkcs8, jwk, raw, raw-public',
341341
},
342-
{ name: 'Ed25519', status: 'partial', note: 'spki, pkcs8, raw' },
343-
{ name: 'Ed448', status: 'partial', note: 'spki, pkcs8, raw' },
342+
{ name: 'Ed25519', status: 'partial', note: 'spki, pkcs8, raw, jwk' },
343+
{ name: 'Ed448', status: 'partial', note: 'spki, pkcs8, raw, jwk' },
344344
{ name: 'HMAC', status: 'partial', note: 'jwk, raw, raw-secret' },
345345
{
346346
name: 'ML-DSA-44',
@@ -415,8 +415,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [
415415
status: 'partial',
416416
note: 'spki, pkcs8, jwk, raw, raw-public',
417417
},
418-
{ name: 'Ed25519', status: 'partial', note: 'spki, pkcs8' },
419-
{ name: 'Ed448', status: 'partial', note: 'spki, pkcs8' },
418+
{ name: 'Ed25519', status: 'partial', note: 'spki, pkcs8, raw, jwk' },
419+
{ name: 'Ed448', status: 'partial', note: 'spki, pkcs8, raw, jwk' },
420420
{ name: 'HKDF', status: 'partial', note: 'raw' },
421421
{ name: 'HMAC', status: 'partial', note: 'jwk, raw, raw-secret' },
422422
{

example/src/tests/dh/dh_tests.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,34 @@ test(SUITE, 'should reject prime length below 2048 bits', () => {
8888
crypto.createDiffieHellman(512);
8989
}, /prime length must be at least 2048 bits/);
9090
});
91+
92+
// createDiffieHellmanGroup alias
93+
test(
94+
SUITE,
95+
'createDiffieHellmanGroup should be an alias for getDiffieHellman',
96+
() => {
97+
const dh1 = crypto.getDiffieHellman('modp14');
98+
const dh2 = crypto.createDiffieHellmanGroup('modp14');
99+
100+
assert.strictEqual(dh1.getPrime('hex'), dh2.getPrime('hex'));
101+
assert.strictEqual(dh1.getGenerator('hex'), dh2.getGenerator('hex'));
102+
},
103+
);
104+
105+
test(SUITE, 'createDiffieHellmanGroup should throw for unknown group', () => {
106+
assert.throws(() => {
107+
crypto.createDiffieHellmanGroup('modp999');
108+
}, /Unknown group/);
109+
});
110+
111+
// verifyError property
112+
test(SUITE, 'verifyError should return 0 for valid DH params', () => {
113+
const dh = crypto.getDiffieHellman('modp14');
114+
assert.strictEqual(dh.verifyError, 0);
115+
});
116+
117+
test(SUITE, 'verifyError should return 0 for created DH', () => {
118+
const prime = Buffer.from(MODP14_PRIME, 'hex');
119+
const dh = crypto.createDiffieHellman(prime, 2);
120+
assert.strictEqual(dh.verifyError, 0);
121+
});

example/src/tests/hash/hash_tests.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {
77
Buffer,
88
createHash,
9+
hash,
910
getHashes,
1011
type Encoding,
1112
} from 'react-native-quick-crypto';
@@ -283,3 +284,40 @@ test(SUITE, 'createHash with null outputLength', () => {
283284
createHash('shake128', { outputLength: null });
284285
}).to.throw(/Output length must be a number/);
285286
});
287+
288+
// crypto.hash() oneshot function tests
289+
test(SUITE, 'hash() oneshot - sha256 hex', () => {
290+
const result = hash('sha256', 'Test123', 'hex');
291+
const expected = createHash('sha256').update('Test123').digest('hex');
292+
expect(result).to.equal(expected);
293+
});
294+
295+
test(SUITE, 'hash() oneshot - sha256 base64', () => {
296+
const result = hash('sha256', 'Test123', 'base64');
297+
const expected = createHash('sha256').update('Test123').digest('base64');
298+
expect(result).to.equal(expected);
299+
});
300+
301+
test(SUITE, 'hash() oneshot - returns Buffer without encoding', () => {
302+
const result = hash('sha256', 'Test123');
303+
expect(Buffer.isBuffer(result)).to.equal(true);
304+
expect(typeof result).to.not.equal('string');
305+
});
306+
307+
test(SUITE, 'hash() oneshot - sha512', () => {
308+
const result = hash('sha512', 'hello world', 'hex');
309+
const expected = createHash('sha512').update('hello world').digest('hex');
310+
expect(result).to.equal(expected);
311+
});
312+
313+
test(SUITE, 'hash() oneshot - md5', () => {
314+
const result = hash('md5', 'Test123', 'hex');
315+
expect(result).to.equal('68eacb97d86f0c4621fa2b0e17cabd8c');
316+
});
317+
318+
test(SUITE, 'hash() oneshot - Buffer input', () => {
319+
const data = Buffer.from('hello');
320+
const result = hash('sha256', data, 'hex');
321+
const expected = createHash('sha256').update(data).digest('hex');
322+
expect(result).to.equal(expected);
323+
});

example/src/tests/keys/create_keys.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,47 @@ test(
411411
}, '');
412412
},
413413
);
414+
415+
// --- KeyObject.equals() Tests ---
416+
417+
test(SUITE, 'equals - same secret keys are equal', () => {
418+
const keyData = randomBytes(32);
419+
const key1 = createSecretKey(keyData);
420+
const key2 = createSecretKey(keyData);
421+
expect(key1.equals(key2)).to.equal(true);
422+
});
423+
424+
test(SUITE, 'equals - different secret keys are not equal', () => {
425+
const key1 = createSecretKey(randomBytes(32));
426+
const key2 = createSecretKey(randomBytes(32));
427+
expect(key1.equals(key2)).to.equal(false);
428+
});
429+
430+
test(SUITE, 'equals - same RSA public keys are equal', () => {
431+
const key1 = createPublicKey(rsaPublicKeyPem);
432+
const key2 = createPublicKey(rsaPublicKeyPem);
433+
expect(key1.equals(key2)).to.equal(true);
434+
});
435+
436+
test(SUITE, 'equals - different key types are not equal', () => {
437+
const secretKey = createSecretKey(randomBytes(32));
438+
const publicKey = createPublicKey(rsaPublicKeyPem);
439+
expect(secretKey.equals(publicKey)).to.equal(false);
440+
});
441+
442+
// --- KeyObject.symmetricKeySize Tests ---
443+
444+
test(SUITE, 'symmetricKeySize - 16 byte key', () => {
445+
const key = createSecretKey(randomBytes(16));
446+
expect(key.symmetricKeySize).to.equal(16);
447+
});
448+
449+
test(SUITE, 'symmetricKeySize - 32 byte key', () => {
450+
const key = createSecretKey(randomBytes(32));
451+
expect(key.symmetricKeySize).to.equal(32);
452+
});
453+
454+
test(SUITE, 'symmetricKeySize - 64 byte key', () => {
455+
const key = createSecretKey(randomBytes(64));
456+
expect(key.symmetricKeySize).to.equal(64);
457+
});

example/src/tests/subtle/derive_key.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,67 @@ test(SUITE, 'X25519 deriveKey to AES-GCM', async () => {
9898
Buffer.from(bobRaw as ArrayBuffer).toString('hex'),
9999
);
100100
});
101+
102+
// Test 3: ECDH deriveKey
103+
test(SUITE, 'ECDH P-256 deriveKey to AES-GCM', async () => {
104+
const aliceKeyPair = await subtle.generateKey(
105+
{ name: 'ECDH', namedCurve: 'P-256' },
106+
false,
107+
['deriveKey', 'deriveBits'],
108+
);
109+
110+
const bobKeyPair = await subtle.generateKey(
111+
{ name: 'ECDH', namedCurve: 'P-256' },
112+
false,
113+
['deriveKey', 'deriveBits'],
114+
);
115+
116+
const aliceDerivedKey = await subtleAny.deriveKey(
117+
{
118+
name: 'ECDH',
119+
public: (aliceKeyPair as CryptoKeyPair).publicKey,
120+
},
121+
(bobKeyPair as CryptoKeyPair).privateKey,
122+
{ name: 'AES-GCM', length: 256 },
123+
true,
124+
['encrypt', 'decrypt'],
125+
);
126+
127+
const bobDerivedKey = await subtleAny.deriveKey(
128+
{
129+
name: 'ECDH',
130+
public: (bobKeyPair as CryptoKeyPair).publicKey,
131+
},
132+
(aliceKeyPair as CryptoKeyPair).privateKey,
133+
{ name: 'AES-GCM', length: 256 },
134+
true,
135+
['encrypt', 'decrypt'],
136+
);
137+
138+
const aliceRaw = await subtle.exportKey('raw', aliceDerivedKey as CryptoKey);
139+
const bobRaw = await subtle.exportKey('raw', bobDerivedKey as CryptoKey);
140+
141+
expect(Buffer.from(aliceRaw as ArrayBuffer).toString('hex')).to.equal(
142+
Buffer.from(bobRaw as ArrayBuffer).toString('hex'),
143+
);
144+
145+
// Verify key works for encrypt/decrypt
146+
const plaintext = new Uint8Array([1, 2, 3, 4]);
147+
const iv = getRandomValues(new Uint8Array(12));
148+
149+
const ciphertext = await subtle.encrypt(
150+
{ name: 'AES-GCM', iv },
151+
aliceDerivedKey as CryptoKey,
152+
plaintext,
153+
);
154+
155+
const decrypted = await subtle.decrypt(
156+
{ name: 'AES-GCM', iv },
157+
bobDerivedKey as CryptoKey,
158+
ciphertext,
159+
);
160+
161+
expect(Buffer.from(decrypted).toString('hex')).to.equal(
162+
Buffer.from(plaintext).toString('hex'),
163+
);
164+
});

0 commit comments

Comments
 (0)