Skip to content

Commit 8446e86

Browse files
authored
fix: support EC keys in diffieHellman() and validate curve match (#960)
1 parent 44913a5 commit 8446e86

3 files changed

Lines changed: 169 additions & 17 deletions

File tree

example/src/tests/subtle/deriveBits.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ test(SUITE, 'x25519 - error handling', () => {
219219
// --- ECDH subtle.deriveBits Tests ---
220220

221221
import type { NamedCurve } from 'react-native-quick-crypto';
222+
import { createPrivateKey, createPublicKey } from 'react-native-quick-crypto';
222223

223224
const ecdhCurves: Array<{ curve: NamedCurve; bitLen: number }> = [
224225
{ curve: 'P-256', bitLen: 256 },
@@ -461,3 +462,109 @@ test(SUITE, 'x448 - error handling', () => {
461462
});
462463
}).to.throw();
463464
});
465+
466+
// --- EC diffieHellman Tests (regression for #959) ---
467+
468+
const ecDhCurves: Array<{ curve: string; secretLen: number }> = [
469+
{ curve: 'P-256', secretLen: 32 },
470+
{ curve: 'P-384', secretLen: 48 },
471+
{ curve: 'P-521', secretLen: 66 },
472+
];
473+
474+
function generateEcKeyObjects(curve: string) {
475+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
476+
namedCurve: curve,
477+
publicKeyEncoding: { type: 'spki', format: 'pem' },
478+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
479+
});
480+
return {
481+
privateKey: createPrivateKey(privateKey as string),
482+
publicKey: createPublicKey(publicKey as string),
483+
};
484+
}
485+
486+
for (const { curve, secretLen } of ecDhCurves) {
487+
test(SUITE, `EC diffieHellman - ${curve} shared secret`, () => {
488+
const alice = generateEcKeyObjects(curve);
489+
const bob = generateEcKeyObjects(curve);
490+
491+
const secret = crypto.diffieHellman({
492+
privateKey: alice.privateKey,
493+
publicKey: bob.publicKey,
494+
}) as Buffer;
495+
496+
expect(Buffer.isBuffer(secret)).to.equal(true);
497+
expect(secret.length).to.equal(secretLen);
498+
499+
const allZeros = Buffer.alloc(secretLen, 0);
500+
expect(secret.equals(allZeros)).to.equal(false);
501+
});
502+
503+
test(SUITE, `EC diffieHellman - ${curve} symmetry`, () => {
504+
const alice = generateEcKeyObjects(curve);
505+
const bob = generateEcKeyObjects(curve);
506+
507+
const secretAlice = crypto.diffieHellman({
508+
privateKey: alice.privateKey,
509+
publicKey: bob.publicKey,
510+
}) as Buffer;
511+
512+
const secretBob = crypto.diffieHellman({
513+
privateKey: bob.privateKey,
514+
publicKey: alice.publicKey,
515+
}) as Buffer;
516+
517+
expect(secretAlice.equals(secretBob)).to.equal(true);
518+
});
519+
520+
test(SUITE, `EC diffieHellman - ${curve} deterministic`, () => {
521+
const alice = generateEcKeyObjects(curve);
522+
const bob = generateEcKeyObjects(curve);
523+
524+
const secret1 = crypto.diffieHellman({
525+
privateKey: alice.privateKey,
526+
publicKey: bob.publicKey,
527+
}) as Buffer;
528+
529+
const secret2 = crypto.diffieHellman({
530+
privateKey: alice.privateKey,
531+
publicKey: bob.publicKey,
532+
}) as Buffer;
533+
534+
expect(secret1.equals(secret2)).to.equal(true);
535+
});
536+
537+
test(
538+
SUITE,
539+
`EC diffieHellman - ${curve} different pairs produce different secrets`,
540+
() => {
541+
const alice = generateEcKeyObjects(curve);
542+
const bob = generateEcKeyObjects(curve);
543+
const charlie = generateEcKeyObjects(curve);
544+
545+
const secretBob = crypto.diffieHellman({
546+
privateKey: alice.privateKey,
547+
publicKey: bob.publicKey,
548+
}) as Buffer;
549+
550+
const secretCharlie = crypto.diffieHellman({
551+
privateKey: alice.privateKey,
552+
publicKey: charlie.publicKey,
553+
}) as Buffer;
554+
555+
expect(secretBob.equals(secretCharlie)).to.equal(false);
556+
},
557+
);
558+
}
559+
560+
test(SUITE, 'EC diffieHellman - curve mismatch throws', () => {
561+
const alice = generateEcKeyObjects('P-256');
562+
const bob = generateEcKeyObjects('P-384');
563+
564+
expect(() => {
565+
crypto.diffieHellman({
566+
privateKey: alice.privateKey,
567+
publicKey: bob.publicKey,
568+
});
569+
}).to.throw('Private and public key curves do not match');
570+
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,13 +527,13 @@ export function ecDeriveBits(
527527

528528
const jwkPrivate = baseKey.keyObject.handle.exportJwk({}, false);
529529
if (!jwkPrivate.d) throw new Error('Invalid private key');
530-
const privateBytes = Buffer.from(jwkPrivate.d, 'base64');
530+
const privateBytes = Buffer.from(jwkPrivate.d, 'base64url');
531531
ecdh.setPrivateKey(privateBytes);
532532

533533
const jwkPublic = publicKey.keyObject.handle.exportJwk({}, false);
534534
if (!jwkPublic.x || !jwkPublic.y) throw new Error('Invalid public key');
535-
const x = Buffer.from(jwkPublic.x, 'base64');
536-
const y = Buffer.from(jwkPublic.y, 'base64');
535+
const x = Buffer.from(jwkPublic.x, 'base64url');
536+
const y = Buffer.from(jwkPublic.y, 'base64url');
537537
const publicBytes = Buffer.concat([Buffer.from([0x04]), x, y]);
538538

539539
const secret = ecdh.computeSecret(publicBytes);

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

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
KFormatType,
3030
KeyEncoding,
3131
} from './utils';
32+
import { ECDH } from './ecdh';
3233

3334
export class Ed {
3435
type: CFRGKeyPairType;
@@ -57,19 +58,6 @@ export class Ed {
5758
options: DiffieHellmanOptions,
5859
callback?: DiffieHellmanCallback,
5960
): Buffer | void {
60-
checkDiffieHellmanOptions(options);
61-
62-
// key types must be of certain type
63-
const keyType = (options.privateKey as AsymmetricKeyObject)
64-
.asymmetricKeyType;
65-
switch (keyType) {
66-
case 'x25519':
67-
case 'x448':
68-
break;
69-
default:
70-
throw new Error(`Unsupported or unimplemented curve type: ${keyType}`);
71-
}
72-
7361
// extract the private and public keys as ArrayBuffers
7462
const privateKey = toAB(options.privateKey);
7563
const publicKey = toAB(options.publicKey);
@@ -176,8 +164,16 @@ export function diffieHellman(
176164
options: DiffieHellmanOptions,
177165
callback?: DiffieHellmanCallback,
178166
): Buffer | void {
167+
checkDiffieHellmanOptions(options);
168+
179169
const privateKey = options.privateKey as PrivateKeyObject;
180-
const type = privateKey.asymmetricKeyType as CFRGKeyPairType;
170+
const keyType = privateKey.asymmetricKeyType;
171+
172+
if (keyType === 'ec') {
173+
return ecDiffieHellman(options, callback);
174+
}
175+
176+
const type = keyType as CFRGKeyPairType;
181177
const ed = new Ed(type, {});
182178
return ed.diffieHellman(options, callback);
183179
}
@@ -254,6 +250,47 @@ export function ed_generateKeyPair(
254250
return [err, publicKey, privateKey];
255251
}
256252

253+
function ecDiffieHellman(
254+
options: DiffieHellmanOptions,
255+
callback?: DiffieHellmanCallback,
256+
): Buffer | void {
257+
const privateKey = options.privateKey as PrivateKeyObject;
258+
const publicKey = options.publicKey as AsymmetricKeyObject;
259+
260+
const curveName = privateKey.namedCurve;
261+
if (!curveName) {
262+
throw new Error('Unable to determine EC curve name from private key');
263+
}
264+
265+
const ecdh = new ECDH(curveName);
266+
267+
const jwkPrivate = privateKey.handle.exportJwk({}, false);
268+
if (!jwkPrivate.d) throw new Error('Invalid private key');
269+
ecdh.setPrivateKey(Buffer.from(jwkPrivate.d, 'base64url'));
270+
271+
const jwkPublic = publicKey.handle.exportJwk({}, false);
272+
if (!jwkPublic.x || !jwkPublic.y) throw new Error('Invalid public key');
273+
const x = Buffer.from(jwkPublic.x, 'base64url');
274+
const y = Buffer.from(jwkPublic.y, 'base64url');
275+
const publicBytes = Buffer.concat([Buffer.from([0x04]), x, y]);
276+
277+
try {
278+
const secret = ecdh.computeSecret(publicBytes);
279+
if (callback) {
280+
callback(null, secret);
281+
} else {
282+
return secret;
283+
}
284+
} catch (e: unknown) {
285+
const err = e as Error;
286+
if (callback) {
287+
callback(err, undefined);
288+
} else {
289+
throw err;
290+
}
291+
}
292+
}
293+
257294
function checkDiffieHellmanOptions(options: DiffieHellmanOptions): void {
258295
const { privateKey, publicKey } = options;
259296

@@ -292,6 +329,14 @@ function checkDiffieHellmanOptions(options: DiffieHellmanOptions): void {
292329

293330
switch (privateKeyAsym.asymmetricKeyType) {
294331
// case 'dh': // TODO: uncomment when implemented
332+
case 'ec': {
333+
const privateCurve = privateKeyAsym.namedCurve;
334+
const publicCurve = publicKeyAsym.namedCurve;
335+
if (privateCurve && publicCurve && privateCurve !== publicCurve) {
336+
throw new Error('Private and public key curves do not match');
337+
}
338+
break;
339+
}
295340
case 'x25519':
296341
case 'x448':
297342
break;

0 commit comments

Comments
 (0)