diff --git a/example/src/tests/subtle/deriveBits.ts b/example/src/tests/subtle/deriveBits.ts index 271e0dfa..a99b3b79 100644 --- a/example/src/tests/subtle/deriveBits.ts +++ b/example/src/tests/subtle/deriveBits.ts @@ -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 }, @@ -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'); +}); diff --git a/packages/react-native-quick-crypto/src/ec.ts b/packages/react-native-quick-crypto/src/ec.ts index 890cf1ef..76703557 100644 --- a/packages/react-native-quick-crypto/src/ec.ts +++ b/packages/react-native-quick-crypto/src/ec.ts @@ -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); diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index 7906579e..60270647 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -29,6 +29,7 @@ import { KFormatType, KeyEncoding, } from './utils'; +import { ECDH } from './ecdh'; export class Ed { type: CFRGKeyPairType; @@ -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); @@ -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); } @@ -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; @@ -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;