Skip to content

Commit 86dffd6

Browse files
committed
fix: use handle.exportJwk() for ECDH deriveBits key export
ecDeriveBits was calling keyObject.export({ format: 'jwk' }) which throws at the TypeScript layer since JWK export isn't wired through the export() method. The C++ exportJwk() on the native handle works fine — use it directly, matching the pattern already used in subtle.ts exportKeyJWK. Also adds 'public' property to SubtleAlgorithm type to remove casting, applies the same cleanup in ed.ts xDeriveBits, and adds ECDH deriveBits and import/export test cases.
1 parent 397b111 commit 86dffd6

8 files changed

Lines changed: 296 additions & 26 deletions

File tree

.claude/plans/quick-wins.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ straightforward to implement.
3333
- Return the size of a symmetric key in bytes
3434
- Simple property accessor
3535

36+
## createDiffieHellmanGroup alias
37+
- Node.js exports `createDiffieHellmanGroup` as an alias for `getDiffieHellman`
38+
- `getDiffieHellman` already exists and works
39+
- Just add a re-export: `export { getDiffieHellman as createDiffieHellmanGroup }`
40+
3641
## diffieHellman.verifyError
3742
- DiffieHellman class is fully implemented except this property
3843
- Returns verification errors from DH parameter checking

docs/data/coverage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 'implemented' },
139+
{ name: 'createDiffieHellmanGroup', status: 'missing' },
140140
{ name: 'createECDH', status: 'implemented' },
141141
{ name: 'createHash', status: 'implemented' },
142142
{ name: 'createHmac', status: 'implemented' },
@@ -151,8 +151,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [
151151
{
152152
name: 'diffieHellman',
153153
subItems: [
154-
{ name: 'dh', status: 'implemented' },
155-
{ name: 'ec', status: 'implemented' },
154+
{ name: 'dh', status: 'missing' },
155+
{ name: 'ec', status: 'missing' },
156156
{ name: 'x448', status: 'implemented' },
157157
{ name: 'x25519', status: 'implemented' },
158158
],

example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2811,7 +2811,7 @@ SPEC CHECKSUMS:
28112811
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
28122812
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
28132813
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
2814-
QuickCrypto: 9e46baaa4fea5a22fdf23c3aae184b983c948b23
2814+
QuickCrypto: ac4d2eead1de738bcf06f565c4ab31db817f88c1
28152815
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
28162816
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
28172817
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a

example/src/tests/subtle/deriveBits.ts

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from 'chai';
2+
import type { WebCryptoKeyPair } from 'react-native-quick-crypto';
23
import crypto, {
34
Buffer,
45
subtle,
@@ -215,4 +216,193 @@ test(SUITE, 'x25519 - error handling', () => {
215216
}).to.throw();
216217
});
217218

218-
// ecdh deriveBits
219+
// --- ECDH subtle.deriveBits Tests ---
220+
221+
import type { NamedCurve } from 'react-native-quick-crypto';
222+
223+
const ecdhCurves: Array<{ curve: NamedCurve; bitLen: number }> = [
224+
{ curve: 'P-256', bitLen: 256 },
225+
{ curve: 'P-384', bitLen: 384 },
226+
{ curve: 'P-521', bitLen: 528 },
227+
];
228+
229+
for (const { curve, bitLen } of ecdhCurves) {
230+
test(SUITE, `ECDH deriveBits - ${curve}`, async () => {
231+
const alice = (await subtle.generateKey(
232+
{ name: 'ECDH', namedCurve: curve },
233+
true,
234+
['deriveBits'],
235+
)) as WebCryptoKeyPair;
236+
const bob = (await subtle.generateKey(
237+
{ name: 'ECDH', namedCurve: curve },
238+
true,
239+
['deriveBits'],
240+
)) as WebCryptoKeyPair;
241+
242+
const bits = await subtle.deriveBits(
243+
{ name: 'ECDH', public: bob.publicKey },
244+
alice.privateKey,
245+
bitLen,
246+
);
247+
expect(bits).to.be.an.instanceOf(ArrayBuffer);
248+
expect(bits.byteLength).to.equal(bitLen / 8);
249+
});
250+
251+
test(SUITE, `ECDH deriveBits symmetry - ${curve}`, async () => {
252+
const alice = (await subtle.generateKey(
253+
{ name: 'ECDH', namedCurve: curve },
254+
true,
255+
['deriveBits'],
256+
)) as WebCryptoKeyPair;
257+
const bob = (await subtle.generateKey(
258+
{ name: 'ECDH', namedCurve: curve },
259+
true,
260+
['deriveBits'],
261+
)) as WebCryptoKeyPair;
262+
263+
const aliceBits = await subtle.deriveBits(
264+
{ name: 'ECDH', public: bob.publicKey },
265+
alice.privateKey,
266+
bitLen,
267+
);
268+
const bobBits = await subtle.deriveBits(
269+
{ name: 'ECDH', public: alice.publicKey },
270+
bob.privateKey,
271+
bitLen,
272+
);
273+
274+
expect(Buffer.from(aliceBits).equals(Buffer.from(bobBits))).to.equal(true);
275+
});
276+
}
277+
278+
// --- X448 diffieHellman Tests ---
279+
280+
test(SUITE, 'x448 - shared secret', () => {
281+
const alice = crypto.generateKeyPairSync('x448', {});
282+
const bob = crypto.generateKeyPairSync('x448', {});
283+
284+
const privateKey = KeyObject.createKeyObject(
285+
'private',
286+
alice.privateKey as ArrayBuffer,
287+
);
288+
const publicKey = KeyObject.createKeyObject(
289+
'public',
290+
bob.publicKey as ArrayBuffer,
291+
);
292+
293+
const sharedSecret = crypto.diffieHellman({ privateKey, publicKey });
294+
expect(Buffer.isBuffer(sharedSecret)).to.equal(true);
295+
});
296+
297+
test(SUITE, 'x448 - shared secret symmetry', () => {
298+
const alice = crypto.generateKeyPairSync('x448', {});
299+
const bob = crypto.generateKeyPairSync('x448', {});
300+
301+
const alicePrivate = KeyObject.createKeyObject(
302+
'private',
303+
alice.privateKey as ArrayBuffer,
304+
);
305+
const alicePublic = KeyObject.createKeyObject(
306+
'public',
307+
alice.publicKey as ArrayBuffer,
308+
);
309+
const bobPrivate = KeyObject.createKeyObject(
310+
'private',
311+
bob.privateKey as ArrayBuffer,
312+
);
313+
const bobPublic = KeyObject.createKeyObject(
314+
'public',
315+
bob.publicKey as ArrayBuffer,
316+
);
317+
318+
const sharedSecretAlice = crypto.diffieHellman({
319+
privateKey: alicePrivate,
320+
publicKey: bobPublic,
321+
}) as Buffer;
322+
323+
const sharedSecretBob = crypto.diffieHellman({
324+
privateKey: bobPrivate,
325+
publicKey: alicePublic,
326+
}) as Buffer;
327+
328+
expect(Buffer.isBuffer(sharedSecretAlice)).to.equal(true);
329+
expect(Buffer.isBuffer(sharedSecretBob)).to.equal(true);
330+
expect(sharedSecretAlice.equals(sharedSecretBob)).to.equal(true);
331+
});
332+
333+
test(SUITE, 'x448 - shared secret properties', () => {
334+
const alice = crypto.generateKeyPairSync('x448', {});
335+
const bob = crypto.generateKeyPairSync('x448', {});
336+
337+
const alicePrivate = KeyObject.createKeyObject(
338+
'private',
339+
alice.privateKey as ArrayBuffer,
340+
);
341+
const bobPublic = KeyObject.createKeyObject(
342+
'public',
343+
bob.publicKey as ArrayBuffer,
344+
);
345+
346+
const sharedSecret = crypto.diffieHellman({
347+
privateKey: alicePrivate,
348+
publicKey: bobPublic,
349+
}) as Buffer;
350+
351+
expect(sharedSecret.length).to.equal(56);
352+
353+
const allZeros = Buffer.alloc(56, 0);
354+
expect(sharedSecret.equals(allZeros)).to.equal(false);
355+
356+
const sharedSecret2 = crypto.diffieHellman({
357+
privateKey: alicePrivate,
358+
publicKey: bobPublic,
359+
}) as Buffer;
360+
expect(sharedSecret.equals(sharedSecret2)).to.equal(true);
361+
});
362+
363+
test(SUITE, 'x448 - different key pairs produce different secrets', () => {
364+
const alice = crypto.generateKeyPairSync('x448', {});
365+
const bob = crypto.generateKeyPairSync('x448', {});
366+
const charlie = crypto.generateKeyPairSync('x448', {});
367+
368+
const alicePrivate = KeyObject.createKeyObject(
369+
'private',
370+
alice.privateKey as ArrayBuffer,
371+
);
372+
const bobPublic = KeyObject.createKeyObject(
373+
'public',
374+
bob.publicKey as ArrayBuffer,
375+
);
376+
const charliePublic = KeyObject.createKeyObject(
377+
'public',
378+
charlie.publicKey as ArrayBuffer,
379+
);
380+
381+
const secretAliceBob = crypto.diffieHellman({
382+
privateKey: alicePrivate,
383+
publicKey: bobPublic,
384+
}) as Buffer;
385+
386+
const secretAliceCharlie = crypto.diffieHellman({
387+
privateKey: alicePrivate,
388+
publicKey: charliePublic,
389+
}) as Buffer;
390+
391+
expect(secretAliceBob.equals(secretAliceCharlie)).to.equal(false);
392+
});
393+
394+
test(SUITE, 'x448 - error handling', () => {
395+
const alice = crypto.generateKeyPairSync('x448', {});
396+
397+
const alicePrivate = KeyObject.createKeyObject(
398+
'private',
399+
alice.privateKey as ArrayBuffer,
400+
);
401+
402+
expect(() => {
403+
crypto.diffieHellman({
404+
privateKey: alicePrivate,
405+
publicKey: {} as KeyObject,
406+
});
407+
}).to.throw();
408+
});

example/src/tests/subtle/import_export.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,3 +2193,91 @@ test(SUITE, 'ML-DSA-44 importKey rejects invalid format', async () => {
21932193
'NotSupportedError',
21942194
);
21952195
});
2196+
2197+
// --- Ed25519/Ed448 raw import/export Tests ---
2198+
2199+
const edCurves = [
2200+
{ name: 'Ed25519' as const, rawSize: 32 },
2201+
{ name: 'Ed448' as const, rawSize: 57 },
2202+
];
2203+
2204+
for (const { name: curveName, rawSize } of edCurves) {
2205+
test(SUITE, `${curveName} raw export public key`, async () => {
2206+
const keyPair = (await subtle.generateKey({ name: curveName }, true, [
2207+
'sign',
2208+
'verify',
2209+
])) as CryptoKeyPair;
2210+
2211+
const raw = (await subtle.exportKey(
2212+
'raw',
2213+
keyPair.publicKey as CryptoKey,
2214+
)) as ArrayBuffer;
2215+
2216+
expect(raw).to.be.instanceOf(ArrayBuffer);
2217+
expect(raw.byteLength).to.equal(rawSize);
2218+
});
2219+
2220+
test(SUITE, `${curveName} raw export/import round-trip`, async () => {
2221+
const keyPair = (await subtle.generateKey({ name: curveName }, true, [
2222+
'sign',
2223+
'verify',
2224+
])) as CryptoKeyPair;
2225+
2226+
const raw = (await subtle.exportKey(
2227+
'raw',
2228+
keyPair.publicKey as CryptoKey,
2229+
)) as ArrayBuffer;
2230+
2231+
const imported = await subtle.importKey(
2232+
'raw',
2233+
raw,
2234+
{ name: curveName },
2235+
true,
2236+
['verify'],
2237+
);
2238+
2239+
expect(imported.type).to.equal('public');
2240+
expect(imported.algorithm.name).to.equal(curveName);
2241+
expect(imported.extractable).to.equal(true);
2242+
expect(imported.usages).to.deep.equal(['verify']);
2243+
2244+
const reExported = (await subtle.exportKey('raw', imported)) as ArrayBuffer;
2245+
expect(Buffer.from(raw).equals(Buffer.from(reExported))).to.equal(true);
2246+
});
2247+
2248+
test(SUITE, `${curveName} raw import then verify signature`, async () => {
2249+
const testData = new TextEncoder().encode(`${curveName} raw import test`);
2250+
2251+
const keyPair = (await subtle.generateKey({ name: curveName }, true, [
2252+
'sign',
2253+
'verify',
2254+
])) as CryptoKeyPair;
2255+
2256+
const signature = await subtle.sign(
2257+
{ name: curveName },
2258+
keyPair.privateKey as CryptoKey,
2259+
testData,
2260+
);
2261+
2262+
const raw = (await subtle.exportKey(
2263+
'raw',
2264+
keyPair.publicKey as CryptoKey,
2265+
)) as ArrayBuffer;
2266+
2267+
const importedPublic = await subtle.importKey(
2268+
'raw',
2269+
raw,
2270+
{ name: curveName },
2271+
true,
2272+
['verify'],
2273+
);
2274+
2275+
const isValid = await subtle.verify(
2276+
{ name: curveName },
2277+
importedPublic,
2278+
signature,
2279+
testData,
2280+
);
2281+
expect(isValid).to.equal(true);
2282+
});
2283+
}

packages/react-native-quick-crypto/src/ec.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,7 @@ export function ecDeriveBits(
488488
baseKey: CryptoKey,
489489
length: number | null,
490490
): ArrayBuffer {
491-
const publicParams = algorithm as SubtleAlgorithm & { public?: CryptoKey };
492-
const publicKey = publicParams.public;
491+
const publicKey = algorithm.public;
493492

494493
if (!publicKey) {
495494
throw new Error('Public key is required for ECDH derivation');
@@ -508,31 +507,19 @@ export function ecDeriveBits(
508507
throw new Error('Curve name is missing');
509508
}
510509

511-
// Create new ECDH instance (Node.js style wrapper)
512-
const ecdh = new ECDH(namedCurve);
510+
const opensslCurve =
511+
kNamedCurveAliases[namedCurve as keyof typeof kNamedCurveAliases];
512+
const ecdh = new ECDH(opensslCurve);
513513

514-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
515-
const jwkPrivate = baseKey.keyObject.export({ format: 'jwk' }) as any;
514+
const jwkPrivate = baseKey.keyObject.handle.exportJwk({}, false);
516515
if (!jwkPrivate.d) throw new Error('Invalid private key');
517516
const privateBytes = Buffer.from(jwkPrivate.d, 'base64');
518-
519517
ecdh.setPrivateKey(privateBytes);
520518

521-
// Public key
522-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
523-
const jwkPublic = publicKey.keyObject.export({ format: 'jwk' }) as any;
524-
525-
// HybridECDH `computeSecret` takes public key.
526-
// My implementation `HybridECDH.cpp` `computeSecret` expects what?
527-
// `derive_secret` -> `EVP_PKEY_derive_set_peer`
528-
// `computeSecret` calls `EC_POINT_oct2point`. So it expects an uncompressed/compressed point (04... or 02/03...).
529-
// JWK gives `x` and `y`. We can construct the uncompressed point 04 + x + y.
530-
519+
const jwkPublic = publicKey.keyObject.handle.exportJwk({}, false);
531520
if (!jwkPublic.x || !jwkPublic.y) throw new Error('Invalid public key');
532521
const x = Buffer.from(jwkPublic.x, 'base64');
533522
const y = Buffer.from(jwkPublic.y, 'base64');
534-
535-
// Uncompressed point: 0x04 || x || y
536523
const publicBytes = Buffer.concat([Buffer.from([0x04]), x, y]);
537524

538525
const secret = ecdh.computeSecret(publicBytes);

packages/react-native-quick-crypto/src/ed.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,7 @@ export function xDeriveBits(
427427
baseKey: CryptoKey,
428428
length: number | null,
429429
): ArrayBuffer {
430-
const publicParams = algorithm as SubtleAlgorithm & { public?: CryptoKey };
431-
const publicKey = publicParams.public;
430+
const publicKey = algorithm.public;
432431

433432
if (!publicKey) {
434433
throw new Error('Public key is required for X25519/X448 derivation');

packages/react-native-quick-crypto/src/utils/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export type SubtleAlgorithm = {
187187
modulusLength?: number;
188188
publicExponent?: number | Uint8Array;
189189
saltLength?: number;
190+
public?: CryptoKey;
190191
};
191192

192193
export type KeyPairType =

0 commit comments

Comments
 (0)