Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions example/src/tests/subtle/deriveBits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ test(SUITE, 'x25519 - error handling', () => {
// --- ECDH subtle.deriveBits Tests ---

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

const ecdhCurves: Array<{ curve: NamedCurve; bitLen: number }> = [
{ curve: 'P-256', bitLen: 256 },
Expand Down Expand Up @@ -461,3 +462,109 @@ test(SUITE, 'x448 - error handling', () => {
});
}).to.throw();
});

// --- EC diffieHellman Tests (regression for #959) ---

const ecDhCurves: Array<{ curve: string; secretLen: number }> = [
{ curve: 'P-256', secretLen: 32 },
{ curve: 'P-384', secretLen: 48 },
{ curve: 'P-521', secretLen: 66 },
];

function generateEcKeyObjects(curve: string) {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: curve,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
return {
privateKey: createPrivateKey(privateKey as string),
publicKey: createPublicKey(publicKey as string),
};
}

for (const { curve, secretLen } of ecDhCurves) {
test(SUITE, `EC diffieHellman - ${curve} shared secret`, () => {
const alice = generateEcKeyObjects(curve);
const bob = generateEcKeyObjects(curve);

const secret = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}) as Buffer;

expect(Buffer.isBuffer(secret)).to.equal(true);
expect(secret.length).to.equal(secretLen);

const allZeros = Buffer.alloc(secretLen, 0);
expect(secret.equals(allZeros)).to.equal(false);
});

test(SUITE, `EC diffieHellman - ${curve} symmetry`, () => {
const alice = generateEcKeyObjects(curve);
const bob = generateEcKeyObjects(curve);

const secretAlice = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}) as Buffer;

const secretBob = crypto.diffieHellman({
privateKey: bob.privateKey,
publicKey: alice.publicKey,
}) as Buffer;

expect(secretAlice.equals(secretBob)).to.equal(true);
});

test(SUITE, `EC diffieHellman - ${curve} deterministic`, () => {
const alice = generateEcKeyObjects(curve);
const bob = generateEcKeyObjects(curve);

const secret1 = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}) as Buffer;

const secret2 = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}) as Buffer;

expect(secret1.equals(secret2)).to.equal(true);
});

test(
SUITE,
`EC diffieHellman - ${curve} different pairs produce different secrets`,
() => {
const alice = generateEcKeyObjects(curve);
const bob = generateEcKeyObjects(curve);
const charlie = generateEcKeyObjects(curve);

const secretBob = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}) as Buffer;

const secretCharlie = crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: charlie.publicKey,
}) as Buffer;

expect(secretBob.equals(secretCharlie)).to.equal(false);
},
);
}

test(SUITE, 'EC diffieHellman - curve mismatch throws', () => {
const alice = generateEcKeyObjects('P-256');
const bob = generateEcKeyObjects('P-384');

expect(() => {
crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
});
}).to.throw('Private and public key curves do not match');
});
6 changes: 3 additions & 3 deletions packages/react-native-quick-crypto/src/ec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,13 +527,13 @@ export function ecDeriveBits(

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

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

const secret = ecdh.computeSecret(publicBytes);
Expand Down
73 changes: 59 additions & 14 deletions packages/react-native-quick-crypto/src/ed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
KFormatType,
KeyEncoding,
} from './utils';
import { ECDH } from './ecdh';

export class Ed {
type: CFRGKeyPairType;
Expand Down Expand Up @@ -57,19 +58,6 @@ export class Ed {
options: DiffieHellmanOptions,
callback?: DiffieHellmanCallback,
): Buffer | void {
checkDiffieHellmanOptions(options);

// key types must be of certain type
const keyType = (options.privateKey as AsymmetricKeyObject)
.asymmetricKeyType;
switch (keyType) {
case 'x25519':
case 'x448':
break;
default:
throw new Error(`Unsupported or unimplemented curve type: ${keyType}`);
}

// extract the private and public keys as ArrayBuffers
const privateKey = toAB(options.privateKey);
const publicKey = toAB(options.publicKey);
Expand Down Expand Up @@ -176,8 +164,16 @@ export function diffieHellman(
options: DiffieHellmanOptions,
callback?: DiffieHellmanCallback,
): Buffer | void {
checkDiffieHellmanOptions(options);

const privateKey = options.privateKey as PrivateKeyObject;
const type = privateKey.asymmetricKeyType as CFRGKeyPairType;
const keyType = privateKey.asymmetricKeyType;

if (keyType === 'ec') {
return ecDiffieHellman(options, callback);
}

const type = keyType as CFRGKeyPairType;
const ed = new Ed(type, {});
return ed.diffieHellman(options, callback);
}
Expand Down Expand Up @@ -254,6 +250,47 @@ export function ed_generateKeyPair(
return [err, publicKey, privateKey];
}

function ecDiffieHellman(
options: DiffieHellmanOptions,
callback?: DiffieHellmanCallback,
): Buffer | void {
const privateKey = options.privateKey as PrivateKeyObject;
const publicKey = options.publicKey as AsymmetricKeyObject;

const curveName = privateKey.namedCurve;
if (!curveName) {
throw new Error('Unable to determine EC curve name from private key');
}

const ecdh = new ECDH(curveName);

const jwkPrivate = privateKey.handle.exportJwk({}, false);
if (!jwkPrivate.d) throw new Error('Invalid private key');
ecdh.setPrivateKey(Buffer.from(jwkPrivate.d, 'base64url'));

const jwkPublic = publicKey.handle.exportJwk({}, false);
if (!jwkPublic.x || !jwkPublic.y) throw new Error('Invalid public key');
const x = Buffer.from(jwkPublic.x, 'base64url');
const y = Buffer.from(jwkPublic.y, 'base64url');
const publicBytes = Buffer.concat([Buffer.from([0x04]), x, y]);

try {
const secret = ecdh.computeSecret(publicBytes);
if (callback) {
callback(null, secret);
} else {
return secret;
}
} catch (e: unknown) {
const err = e as Error;
if (callback) {
callback(err, undefined);
} else {
throw err;
}
}
}

function checkDiffieHellmanOptions(options: DiffieHellmanOptions): void {
const { privateKey, publicKey } = options;

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

switch (privateKeyAsym.asymmetricKeyType) {
// case 'dh': // TODO: uncomment when implemented
case 'ec': {
const privateCurve = privateKeyAsym.namedCurve;
const publicCurve = publicKeyAsym.namedCurve;
if (privateCurve && publicCurve && privateCurve !== publicCurve) {
throw new Error('Private and public key curves do not match');
}
break;
}
case 'x25519':
case 'x448':
break;
Expand Down
Loading