From 49778cf74d7e33e3b212e01fc7eec569df1c2e5e Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 22 Mar 2026 19:07:28 -0400 Subject: [PATCH 1/3] fix: return KeyObjects from generateKeyPairSync for Ed/X curves (#969, #970) Rewrite ed_generateKeyPair to always request DER-encoded keys from native (SPKI for public, PKCS8 for private), then wrap them in KeyObjects via KeyObject.createKeyObject. When no encoding is specified (format = -1), return the KeyObject directly. When encoding is specified, export from the KeyObject to the requested format. This follows the same pattern used by RSA key generation (rsa_formatKeyPairOutput) and fixes both the sync and async paths. Also adds comprehensive tests for Ed25519/Ed448/X25519/X448 KeyObject support including round-trip export/recreate verification. --- example/src/hooks/useTestsList.ts | 1 + example/src/tests/keys/ed_keyobject.ts | 315 +++++++++++++++++++ packages/react-native-quick-crypto/src/ed.ts | 104 ++++-- 3 files changed, 388 insertions(+), 32 deletions(-) create mode 100644 example/src/tests/keys/ed_keyobject.ts diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 8135e9a7..d2de08a1 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -19,6 +19,7 @@ import '../tests/hkdf/hkdf_tests'; import '../tests/hmac/hmac_tests'; import '../tests/jose/jose'; import '../tests/keys/create_keys'; +import '../tests/keys/ed_keyobject'; import '../tests/keys/generate_key'; import '../tests/keys/generate_keypair'; import '../tests/keys/keyobject_from_tocryptokey_tests'; diff --git a/example/src/tests/keys/ed_keyobject.ts b/example/src/tests/keys/ed_keyobject.ts new file mode 100644 index 00000000..5df34503 --- /dev/null +++ b/example/src/tests/keys/ed_keyobject.ts @@ -0,0 +1,315 @@ +import { + generateKeyPairSync, + generateKeyPair, + createPrivateKey, + createPublicKey, + KeyObject, + AsymmetricKeyObject, + Buffer, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; + +const SUITE = 'keys.edKeyObject'; + +// --- Ed25519 KeyObject tests --- + +test(SUITE, 'generateKeyPairSync ed25519 returns KeyObjects', () => { + const result = generateKeyPairSync('ed25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + expect(pub).to.be.an.instanceOf(KeyObject); + expect(priv).to.be.an.instanceOf(KeyObject); + expect(pub.type).to.equal('public'); + expect(priv.type).to.equal('private'); + expect(pub.asymmetricKeyType).to.equal('ed25519'); + expect(priv.asymmetricKeyType).to.equal('ed25519'); +}); + +test(SUITE, 'ed25519 KeyObject.export() works for PEM', () => { + const result = generateKeyPairSync('ed25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const pubPem = pub.export({ type: 'spki', format: 'pem' }); + const privPem = priv.export({ type: 'pkcs8', format: 'pem' }); + + expect(typeof pubPem).to.equal('string'); + expect(typeof privPem).to.equal('string'); + expect(pubPem as string).to.match(/^-----BEGIN PUBLIC KEY-----/); + expect(privPem as string).to.match(/^-----BEGIN PRIVATE KEY-----/); +}); + +test(SUITE, 'ed25519 KeyObject.export() works for DER', () => { + const result = generateKeyPairSync('ed25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const pubDer = pub.export({ type: 'spki', format: 'der' }); + const privDer = priv.export({ type: 'pkcs8', format: 'der' }); + + expect(Buffer.isBuffer(pubDer)).to.equal(true); + expect(Buffer.isBuffer(privDer)).to.equal(true); +}); + +test(SUITE, 'ed25519 with PEM encoding returns strings', () => { + const { publicKey, privateKey } = generateKeyPairSync('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof publicKey).to.equal('string'); + expect(typeof privateKey).to.equal('string'); + expect(publicKey as string).to.match(/^-----BEGIN PUBLIC KEY-----/); + expect(privateKey as string).to.match(/^-----BEGIN PRIVATE KEY-----/); +}); + +test(SUITE, 'ed25519 with DER encoding returns ArrayBuffers', () => { + const { publicKey, privateKey } = generateKeyPairSync('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'der' }, + privateKeyEncoding: { type: 'pkcs8', format: 'der' }, + }); + + expect(publicKey instanceof ArrayBuffer).to.equal(true); + expect(privateKey instanceof ArrayBuffer).to.equal(true); + expect((publicKey as ArrayBuffer).byteLength).to.be.greaterThan(0); + expect((privateKey as ArrayBuffer).byteLength).to.be.greaterThan(0); +}); + +// --- Round-trip tests --- + +test( + SUITE, + 'ed25519 round-trip: KeyObject -> export DER -> createKey -> export', + () => { + const result = generateKeyPairSync('ed25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const privDer = priv.export({ type: 'pkcs8', format: 'der' }); + const pubDer = pub.export({ type: 'spki', format: 'der' }); + + const recreatedPriv = createPrivateKey({ + key: privDer, + format: 'der', + type: 'pkcs8', + }); + const recreatedPub = createPublicKey({ + key: pubDer, + format: 'der', + type: 'spki', + }); + + expect(recreatedPriv.type).to.equal('private'); + expect(recreatedPub.type).to.equal('public'); + expect(recreatedPriv.asymmetricKeyType).to.equal('ed25519'); + expect(recreatedPub.asymmetricKeyType).to.equal('ed25519'); + + const reExportedPriv = recreatedPriv.export({ + type: 'pkcs8', + format: 'der', + }); + const reExportedPub = recreatedPub.export({ type: 'spki', format: 'der' }); + + expect(Buffer.compare(privDer, reExportedPriv)).to.equal(0); + expect(Buffer.compare(pubDer, reExportedPub)).to.equal(0); + }, +); + +test( + SUITE, + 'ed25519 round-trip: KeyObject -> export PEM -> createKey -> export', + () => { + const result = generateKeyPairSync('ed25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const privPem = priv.export({ + type: 'pkcs8', + format: 'pem', + }) as string; + const pubPem = pub.export({ type: 'spki', format: 'pem' }) as string; + + const recreatedPriv = createPrivateKey(privPem); + const recreatedPub = createPublicKey(pubPem); + + expect(recreatedPriv.asymmetricKeyType).to.equal('ed25519'); + expect(recreatedPub.asymmetricKeyType).to.equal('ed25519'); + + const reExportedPriv = recreatedPriv.export({ + type: 'pkcs8', + format: 'pem', + }) as string; + const reExportedPub = recreatedPub.export({ + type: 'spki', + format: 'pem', + }) as string; + + expect(reExportedPriv).to.equal(privPem); + expect(reExportedPub).to.equal(pubPem); + }, +); + +// --- Ed448 KeyObject tests --- + +test(SUITE, 'generateKeyPairSync ed448 returns KeyObjects', () => { + const result = generateKeyPairSync('ed448'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + expect(pub).to.be.an.instanceOf(KeyObject); + expect(priv).to.be.an.instanceOf(KeyObject); + expect(pub.asymmetricKeyType).to.equal('ed448'); + expect(priv.asymmetricKeyType).to.equal('ed448'); +}); + +test(SUITE, 'ed448 round-trip: KeyObject -> export -> recreate', () => { + const result = generateKeyPairSync('ed448'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const privDer = priv.export({ type: 'pkcs8', format: 'der' }); + const pubDer = pub.export({ type: 'spki', format: 'der' }); + + const recreatedPriv = createPrivateKey({ + key: privDer, + format: 'der', + type: 'pkcs8', + }); + const recreatedPub = createPublicKey({ + key: pubDer, + format: 'der', + type: 'spki', + }); + + expect(recreatedPriv.asymmetricKeyType).to.equal('ed448'); + expect(recreatedPub.asymmetricKeyType).to.equal('ed448'); +}); + +// --- X25519 KeyObject tests --- + +test(SUITE, 'generateKeyPairSync x25519 returns KeyObjects', () => { + const result = generateKeyPairSync('x25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + expect(pub).to.be.an.instanceOf(KeyObject); + expect(priv).to.be.an.instanceOf(KeyObject); + expect(pub.asymmetricKeyType).to.equal('x25519'); + expect(priv.asymmetricKeyType).to.equal('x25519'); +}); + +test(SUITE, 'x25519 round-trip: KeyObject -> export -> recreate', () => { + const result = generateKeyPairSync('x25519'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const privDer = priv.export({ type: 'pkcs8', format: 'der' }); + const pubDer = pub.export({ type: 'spki', format: 'der' }); + + const recreatedPriv = createPrivateKey({ + key: privDer, + format: 'der', + type: 'pkcs8', + }); + const recreatedPub = createPublicKey({ + key: pubDer, + format: 'der', + type: 'spki', + }); + + expect(recreatedPriv.asymmetricKeyType).to.equal('x25519'); + expect(recreatedPub.asymmetricKeyType).to.equal('x25519'); +}); + +// --- X448 KeyObject tests --- + +test(SUITE, 'generateKeyPairSync x448 returns KeyObjects', () => { + const result = generateKeyPairSync('x448'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + expect(pub).to.be.an.instanceOf(KeyObject); + expect(priv).to.be.an.instanceOf(KeyObject); + expect(pub.asymmetricKeyType).to.equal('x448'); + expect(priv.asymmetricKeyType).to.equal('x448'); +}); + +test(SUITE, 'x448 round-trip: KeyObject -> export -> recreate', () => { + const result = generateKeyPairSync('x448'); + const pub = result.publicKey as InstanceType; + const priv = result.privateKey as InstanceType; + + const privDer = priv.export({ type: 'pkcs8', format: 'der' }); + const pubDer = pub.export({ type: 'spki', format: 'der' }); + + const recreatedPriv = createPrivateKey({ + key: privDer, + format: 'der', + type: 'pkcs8', + }); + const recreatedPub = createPublicKey({ + key: pubDer, + format: 'der', + type: 'spki', + }); + + expect(recreatedPriv.asymmetricKeyType).to.equal('x448'); + expect(recreatedPub.asymmetricKeyType).to.equal('x448'); +}); + +// --- Async path tests --- + +test(SUITE, 'generateKeyPair ed25519 async returns KeyObjects', async () => { + const result = await new Promise<{ + publicKey: InstanceType; + privateKey: InstanceType; + }>((resolve, reject) => { + generateKeyPair('ed25519', {}, (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + publicKey: pubKey as InstanceType, + privateKey: privKey as InstanceType, + }); + }); + }); + + expect(result.publicKey).to.be.an.instanceOf(KeyObject); + expect(result.privateKey).to.be.an.instanceOf(KeyObject); + expect(result.publicKey.type).to.equal('public'); + expect(result.privateKey.type).to.equal('private'); +}); + +test( + SUITE, + 'generateKeyPair ed25519 async with PEM encoding returns strings', + async () => { + const { publicKey, privateKey } = await new Promise<{ + publicKey: string; + privateKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + publicKey: pubKey as string, + privateKey: privKey as string, + }); + }, + ); + }); + + expect(typeof publicKey).to.equal('string'); + expect(typeof privateKey).to.equal('string'); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + }, +); diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index 60270647..de68cb0c 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -179,50 +179,91 @@ export function diffieHellman( } // Node API +function ed_formatKeyPairOutput( + ed: Ed, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | string | ArrayBuffer; + privateKey: PrivateKeyObjectClass | string | ArrayBuffer; +} { + const { publicFormat, privateFormat, cipher, passphrase } = encoding; + + const publicKeyData = ed.getPublicKey(); + const privateKeyData = ed.getPrivateKey(); + + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObjectClass; + + let publicKey: PublicKeyObject | string | ArrayBuffer; + let privateKey: PrivateKeyObjectClass | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKey = pub; + } else { + const format = + publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const exported = pub.handle.exportKey(format, KeyEncoding.SPKI); + if (format === KFormatType.PEM) { + publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKey = exported; + } + } + + if (privateFormat === -1) { + privateKey = priv; + } else { + const format = + privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const exported = priv.handle.exportKey( + format, + KeyEncoding.PKCS8, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + privateKey = exported; + } + } + + return { publicKey, privateKey }; +} + export function ed_generateKeyPair( isAsync: boolean, type: CFRGKeyPairType, encoding: KeyPairGenConfig, callback: GenerateKeyPairCallback | undefined, ): GenerateKeyPairReturn | void { - const ed = new Ed(type, encoding); - - // Helper to convert keys to proper output format - const formatKeys = (): { - publicKey: string | ArrayBuffer; - privateKey: string | ArrayBuffer; - } => { - const publicKeyRaw = ed.getPublicKey(); - const privateKeyRaw = ed.getPrivateKey(); - - // Check if PEM format was requested (KFormatType.PEM = 1) - const isPemPublic = encoding.publicFormat === KFormatType.PEM; - const isPemPrivate = encoding.privateFormat === KFormatType.PEM; - - // Convert ArrayBuffer to string for PEM format - const arrayBufferToString = (ab: ArrayBuffer): string => { - return Buffer.from(new Uint8Array(ab)).toString('utf-8'); - }; - - const publicKey = isPemPublic - ? arrayBufferToString(publicKeyRaw) - : publicKeyRaw; - const privateKey = isPemPrivate - ? arrayBufferToString(privateKeyRaw) - : privateKeyRaw; - - return { publicKey, privateKey }; + const derConfig: KeyPairGenConfig = { + ...encoding, + publicFormat: KFormatType.DER, + publicType: KeyEncoding.SPKI, + privateFormat: KFormatType.DER, + privateType: KeyEncoding.PKCS8, }; + const ed = new Ed(type, derConfig); - // Async path if (isAsync) { if (!callback) { - // This should not happen if called from public API throw new Error('A callback is required for async key generation.'); } ed.generateKeyPair() .then(() => { - const { publicKey, privateKey } = formatKeys(); + const { publicKey, privateKey } = ed_formatKeyPairOutput(ed, encoding); callback(undefined, publicKey, privateKey); }) .catch(err => { @@ -231,7 +272,6 @@ export function ed_generateKeyPair( return; } - // Sync path let err: Error | undefined; try { ed.generateKeyPairSync(); @@ -241,7 +281,7 @@ export function ed_generateKeyPair( const { publicKey, privateKey } = err ? { publicKey: undefined, privateKey: undefined } - : formatKeys(); + : ed_formatKeyPairOutput(ed, encoding); if (callback) { callback(err, publicKey, privateKey); From 134b2021bc4d91a516f6d095706a02e53e2919f0 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 22 Mar 2026 19:17:05 -0400 Subject: [PATCH 2/3] refactor: clean up Ed/X key generation and add .ruby-version Extract ed_createKeyObjects helper to deduplicate KeyObject creation across ed_formatKeyPairOutput and both WebCrypto generators. Guard undefined format values in ed_formatKeyPairOutput, simplify test type casts, and wrap Buffer.compare args with Buffer.from for type safety. --- example/.ruby-version | 1 + example/src/tests/keys/ed_keyobject.ts | 60 +++++++++--------- packages/react-native-quick-crypto/src/ed.ts | 65 ++++++-------------- 3 files changed, 53 insertions(+), 73 deletions(-) create mode 100644 example/.ruby-version diff --git a/example/.ruby-version b/example/.ruby-version new file mode 100644 index 00000000..a0891f56 --- /dev/null +++ b/example/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/example/src/tests/keys/ed_keyobject.ts b/example/src/tests/keys/ed_keyobject.ts index 5df34503..a7baa52e 100644 --- a/example/src/tests/keys/ed_keyobject.ts +++ b/example/src/tests/keys/ed_keyobject.ts @@ -16,8 +16,8 @@ const SUITE = 'keys.edKeyObject'; test(SUITE, 'generateKeyPairSync ed25519 returns KeyObjects', () => { const result = generateKeyPairSync('ed25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; expect(pub).to.be.an.instanceOf(KeyObject); expect(priv).to.be.an.instanceOf(KeyObject); @@ -29,8 +29,8 @@ test(SUITE, 'generateKeyPairSync ed25519 returns KeyObjects', () => { test(SUITE, 'ed25519 KeyObject.export() works for PEM', () => { const result = generateKeyPairSync('ed25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const pubPem = pub.export({ type: 'spki', format: 'pem' }); const privPem = priv.export({ type: 'pkcs8', format: 'pem' }); @@ -43,8 +43,8 @@ test(SUITE, 'ed25519 KeyObject.export() works for PEM', () => { test(SUITE, 'ed25519 KeyObject.export() works for DER', () => { const result = generateKeyPairSync('ed25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const pubDer = pub.export({ type: 'spki', format: 'der' }); const privDer = priv.export({ type: 'pkcs8', format: 'der' }); @@ -84,8 +84,8 @@ test( 'ed25519 round-trip: KeyObject -> export DER -> createKey -> export', () => { const result = generateKeyPairSync('ed25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const privDer = priv.export({ type: 'pkcs8', format: 'der' }); const pubDer = pub.export({ type: 'spki', format: 'der' }); @@ -112,8 +112,12 @@ test( }); const reExportedPub = recreatedPub.export({ type: 'spki', format: 'der' }); - expect(Buffer.compare(privDer, reExportedPriv)).to.equal(0); - expect(Buffer.compare(pubDer, reExportedPub)).to.equal(0); + expect( + Buffer.compare(Buffer.from(privDer), Buffer.from(reExportedPriv)), + ).to.equal(0); + expect( + Buffer.compare(Buffer.from(pubDer), Buffer.from(reExportedPub)), + ).to.equal(0); }, ); @@ -122,8 +126,8 @@ test( 'ed25519 round-trip: KeyObject -> export PEM -> createKey -> export', () => { const result = generateKeyPairSync('ed25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const privPem = priv.export({ type: 'pkcs8', @@ -155,8 +159,8 @@ test( test(SUITE, 'generateKeyPairSync ed448 returns KeyObjects', () => { const result = generateKeyPairSync('ed448'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; expect(pub).to.be.an.instanceOf(KeyObject); expect(priv).to.be.an.instanceOf(KeyObject); @@ -166,8 +170,8 @@ test(SUITE, 'generateKeyPairSync ed448 returns KeyObjects', () => { test(SUITE, 'ed448 round-trip: KeyObject -> export -> recreate', () => { const result = generateKeyPairSync('ed448'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const privDer = priv.export({ type: 'pkcs8', format: 'der' }); const pubDer = pub.export({ type: 'spki', format: 'der' }); @@ -191,8 +195,8 @@ test(SUITE, 'ed448 round-trip: KeyObject -> export -> recreate', () => { test(SUITE, 'generateKeyPairSync x25519 returns KeyObjects', () => { const result = generateKeyPairSync('x25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; expect(pub).to.be.an.instanceOf(KeyObject); expect(priv).to.be.an.instanceOf(KeyObject); @@ -202,8 +206,8 @@ test(SUITE, 'generateKeyPairSync x25519 returns KeyObjects', () => { test(SUITE, 'x25519 round-trip: KeyObject -> export -> recreate', () => { const result = generateKeyPairSync('x25519'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const privDer = priv.export({ type: 'pkcs8', format: 'der' }); const pubDer = pub.export({ type: 'spki', format: 'der' }); @@ -227,8 +231,8 @@ test(SUITE, 'x25519 round-trip: KeyObject -> export -> recreate', () => { test(SUITE, 'generateKeyPairSync x448 returns KeyObjects', () => { const result = generateKeyPairSync('x448'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; expect(pub).to.be.an.instanceOf(KeyObject); expect(priv).to.be.an.instanceOf(KeyObject); @@ -238,8 +242,8 @@ test(SUITE, 'generateKeyPairSync x448 returns KeyObjects', () => { test(SUITE, 'x448 round-trip: KeyObject -> export -> recreate', () => { const result = generateKeyPairSync('x448'); - const pub = result.publicKey as InstanceType; - const priv = result.privateKey as InstanceType; + const pub = result.publicKey as AsymmetricKeyObject; + const priv = result.privateKey as AsymmetricKeyObject; const privDer = priv.export({ type: 'pkcs8', format: 'der' }); const pubDer = pub.export({ type: 'spki', format: 'der' }); @@ -263,15 +267,15 @@ test(SUITE, 'x448 round-trip: KeyObject -> export -> recreate', () => { test(SUITE, 'generateKeyPair ed25519 async returns KeyObjects', async () => { const result = await new Promise<{ - publicKey: InstanceType; - privateKey: InstanceType; + publicKey: AsymmetricKeyObject; + privateKey: AsymmetricKeyObject; }>((resolve, reject) => { generateKeyPair('ed25519', {}, (err, pubKey, privKey) => { if (err) reject(err); else resolve({ - publicKey: pubKey as InstanceType, - privateKey: privKey as InstanceType, + publicKey: pubKey as AsymmetricKeyObject, + privateKey: privKey as AsymmetricKeyObject, }); }); }); diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index de68cb0c..34b045d0 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -178,37 +178,42 @@ export function diffieHellman( return ed.diffieHellman(options, callback); } -// Node API -function ed_formatKeyPairOutput( - ed: Ed, - encoding: KeyPairGenConfig, -): { - publicKey: PublicKeyObject | string | ArrayBuffer; - privateKey: PrivateKeyObjectClass | string | ArrayBuffer; +function ed_createKeyObjects(ed: Ed): { + pub: PublicKeyObject; + priv: PrivateKeyObjectClass; } { - const { publicFormat, privateFormat, cipher, passphrase } = encoding; - const publicKeyData = ed.getPublicKey(); const privateKeyData = ed.getPrivateKey(); - const pub = KeyObject.createKeyObject( 'public', publicKeyData, KFormatType.DER, KeyEncoding.SPKI, ) as PublicKeyObject; - const priv = KeyObject.createKeyObject( 'private', privateKeyData, KFormatType.DER, KeyEncoding.PKCS8, ) as PrivateKeyObjectClass; + return { pub, priv }; +} + +// Node API +function ed_formatKeyPairOutput( + ed: Ed, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | string | ArrayBuffer; + privateKey: PrivateKeyObjectClass | string | ArrayBuffer; +} { + const { publicFormat, privateFormat, cipher, passphrase } = encoding; + const { pub, priv } = ed_createKeyObjects(ed); let publicKey: PublicKeyObject | string | ArrayBuffer; let privateKey: PrivateKeyObjectClass | string | ArrayBuffer; - if (publicFormat === -1) { + if (publicFormat == null || publicFormat === -1) { publicKey = pub; } else { const format = @@ -221,7 +226,7 @@ function ed_formatKeyPairOutput( } } - if (privateFormat === -1) { + if (privateFormat == null || privateFormat === -1) { privateKey = priv; } else { const format = @@ -414,29 +419,14 @@ export async function ed_generateKeyPairWebCrypto( await ed.generateKeyPair(); const algorithmName = type === 'ed25519' ? 'Ed25519' : 'Ed448'; + const { pub, priv } = ed_createKeyObjects(ed); - const publicKeyData = ed.getPublicKey(); - const privateKeyData = ed.getPrivateKey(); - - const pub = KeyObject.createKeyObject( - 'public', - publicKeyData, - KFormatType.DER, - KeyEncoding.SPKI, - ) as PublicKeyObject; const publicKey = new CryptoKey( pub, { name: algorithmName } as SubtleAlgorithm, publicUsages, true, ); - - const priv = KeyObject.createKeyObject( - 'private', - privateKeyData, - KFormatType.DER, - KeyEncoding.PKCS8, - ) as PrivateKeyObjectClass; const privateKey = new CryptoKey( priv, { name: algorithmName } as SubtleAlgorithm, @@ -474,29 +464,14 @@ export async function x_generateKeyPairWebCrypto( await ed.generateKeyPair(); const algorithmName = type === 'x25519' ? 'X25519' : 'X448'; + const { pub, priv } = ed_createKeyObjects(ed); - const publicKeyData = ed.getPublicKey(); - const privateKeyData = ed.getPrivateKey(); - - const pub = KeyObject.createKeyObject( - 'public', - publicKeyData, - KFormatType.DER, - KeyEncoding.SPKI, - ) as PublicKeyObject; const publicKey = new CryptoKey( pub, { name: algorithmName } as SubtleAlgorithm, publicUsages, true, ); - - const priv = KeyObject.createKeyObject( - 'private', - privateKeyData, - KFormatType.DER, - KeyEncoding.PKCS8, - ) as PrivateKeyObjectClass; const privateKey = new CryptoKey( priv, { name: algorithmName } as SubtleAlgorithm, From 75fae10b6b004795f4ac34b6c610f705a68e54ec Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 22 Mar 2026 19:45:22 -0400 Subject: [PATCH 3/3] fix: extract raw key bytes from KeyObject in Ed diffieHellman Ed.diffieHellman() was passing KeyObject instances directly to toAB() which can't convert them. Now extracts raw bytes via handle.exportKey(). Also updates deriveBits tests to use KeyObject directly from generateKeyPairSync instead of manually wrapping ArrayBuffer. --- example/ios/Podfile.lock | 2 +- example/package.json | 2 +- example/src/tests/subtle/deriveBits.ts | 172 ++++--------------- packages/react-native-quick-crypto/src/ed.ts | 8 +- 4 files changed, 39 insertions(+), 145 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 00a00261..6f6895ae 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2811,7 +2811,7 @@ SPEC CHECKSUMS: MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 - QuickCrypto: 422a7b7f5d0de18bec3837cbc2e7a865999334c0 + QuickCrypto: 414c0c5f7353bc7bf87ef3ae02485cf41b4a66d1 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/example/package.json b/example/package.json index af234c48..581faf5e 100644 --- a/example/package.json +++ b/example/package.json @@ -40,7 +40,7 @@ "react-native-mmkv": "4.0.1", "react-native-nitro-modules": "0.33.2", "react-native-quick-base64": "2.2.2", - "react-native-quick-crypto": "1.0.17", + "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.18.0", "react-native-vector-icons": "10.3.0", diff --git a/example/src/tests/subtle/deriveBits.ts b/example/src/tests/subtle/deriveBits.ts index a99b3b79..4163c066 100644 --- a/example/src/tests/subtle/deriveBits.ts +++ b/example/src/tests/subtle/deriveBits.ts @@ -82,19 +82,9 @@ test(SUITE, 'x25519 - shared secret', () => { const alice = crypto.generateKeyPairSync('x25519', {}); const bob = crypto.generateKeyPairSync('x25519', {}); - if (!alice.privateKey || !(alice.privateKey instanceof ArrayBuffer)) { - throw new Error('Failed to generate private key for Alice'); - } - if (!bob.publicKey || !(bob.publicKey instanceof ArrayBuffer)) { - throw new Error('Failed to generate public key for Bob'); - } - - const privateKey = KeyObject.createKeyObject('private', alice.privateKey); - const publicKey = KeyObject.createKeyObject('public', bob.publicKey); - const sharedSecret = crypto.diffieHellman({ - privateKey, - publicKey, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }); expect(Buffer.isBuffer(sharedSecret)).to.equal(true); }); @@ -103,31 +93,14 @@ test(SUITE, 'x25519 - shared secret symmetry', () => { const alice = crypto.generateKeyPairSync('x25519', {}); const bob = crypto.generateKeyPairSync('x25519', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const alicePublic = KeyObject.createKeyObject( - 'public', - alice.publicKey as ArrayBuffer, - ); - const bobPrivate = KeyObject.createKeyObject( - 'private', - bob.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const sharedSecretAlice = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; const sharedSecretBob = crypto.diffieHellman({ - privateKey: bobPrivate, - publicKey: alicePublic, + privateKey: bob.privateKey as KeyObject, + publicKey: alice.publicKey as KeyObject, }) as Buffer; expect(Buffer.isBuffer(sharedSecretAlice)).to.equal(true); @@ -139,18 +112,9 @@ test(SUITE, 'x25519 - shared secret properties', () => { const alice = crypto.generateKeyPairSync('x25519', {}); const bob = crypto.generateKeyPairSync('x25519', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const sharedSecret = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; expect(sharedSecret.length).to.equal(32); @@ -159,8 +123,8 @@ test(SUITE, 'x25519 - shared secret properties', () => { expect(sharedSecret.equals(allZeros)).to.equal(false); const sharedSecret2 = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; expect(sharedSecret.equals(sharedSecret2)).to.equal(true); }); @@ -170,27 +134,14 @@ test(SUITE, 'x25519 - different key pairs produce different secrets', () => { const bob = crypto.generateKeyPairSync('x25519', {}); const charlie = crypto.generateKeyPairSync('x25519', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const charliePublic = KeyObject.createKeyObject( - 'public', - charlie.publicKey as ArrayBuffer, - ); - const secretAliceBob = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; const secretAliceCharlie = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: charliePublic, + privateKey: alice.privateKey as KeyObject, + publicKey: charlie.publicKey as KeyObject, }) as Buffer; expect(secretAliceBob.equals(secretAliceCharlie)).to.equal(false); @@ -199,18 +150,9 @@ test(SUITE, 'x25519 - different key pairs produce different secrets', () => { test(SUITE, 'x25519 - error handling', () => { const alice = crypto.generateKeyPairSync('x25519', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - - expect(() => { - KeyObject.createKeyObject('public', new ArrayBuffer(16)); - }).to.throw(); - expect(() => { crypto.diffieHellman({ - privateKey: alicePrivate, + privateKey: alice.privateKey as KeyObject, publicKey: {} as KeyObject, }); }).to.throw(); @@ -337,16 +279,10 @@ test(SUITE, 'x448 - shared secret', () => { const alice = crypto.generateKeyPairSync('x448', {}); const bob = crypto.generateKeyPairSync('x448', {}); - const privateKey = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const publicKey = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - - const sharedSecret = crypto.diffieHellman({ privateKey, publicKey }); + const sharedSecret = crypto.diffieHellman({ + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, + }); expect(Buffer.isBuffer(sharedSecret)).to.equal(true); }); @@ -354,31 +290,14 @@ test(SUITE, 'x448 - shared secret symmetry', () => { const alice = crypto.generateKeyPairSync('x448', {}); const bob = crypto.generateKeyPairSync('x448', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const alicePublic = KeyObject.createKeyObject( - 'public', - alice.publicKey as ArrayBuffer, - ); - const bobPrivate = KeyObject.createKeyObject( - 'private', - bob.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const sharedSecretAlice = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; const sharedSecretBob = crypto.diffieHellman({ - privateKey: bobPrivate, - publicKey: alicePublic, + privateKey: bob.privateKey as KeyObject, + publicKey: alice.publicKey as KeyObject, }) as Buffer; expect(Buffer.isBuffer(sharedSecretAlice)).to.equal(true); @@ -390,18 +309,9 @@ test(SUITE, 'x448 - shared secret properties', () => { const alice = crypto.generateKeyPairSync('x448', {}); const bob = crypto.generateKeyPairSync('x448', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const sharedSecret = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; expect(sharedSecret.length).to.equal(56); @@ -410,8 +320,8 @@ test(SUITE, 'x448 - shared secret properties', () => { expect(sharedSecret.equals(allZeros)).to.equal(false); const sharedSecret2 = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; expect(sharedSecret.equals(sharedSecret2)).to.equal(true); }); @@ -421,27 +331,14 @@ test(SUITE, 'x448 - different key pairs produce different secrets', () => { const bob = crypto.generateKeyPairSync('x448', {}); const charlie = crypto.generateKeyPairSync('x448', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - const bobPublic = KeyObject.createKeyObject( - 'public', - bob.publicKey as ArrayBuffer, - ); - const charliePublic = KeyObject.createKeyObject( - 'public', - charlie.publicKey as ArrayBuffer, - ); - const secretAliceBob = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: bobPublic, + privateKey: alice.privateKey as KeyObject, + publicKey: bob.publicKey as KeyObject, }) as Buffer; const secretAliceCharlie = crypto.diffieHellman({ - privateKey: alicePrivate, - publicKey: charliePublic, + privateKey: alice.privateKey as KeyObject, + publicKey: charlie.publicKey as KeyObject, }) as Buffer; expect(secretAliceBob.equals(secretAliceCharlie)).to.equal(false); @@ -450,14 +347,9 @@ test(SUITE, 'x448 - different key pairs produce different secrets', () => { test(SUITE, 'x448 - error handling', () => { const alice = crypto.generateKeyPairSync('x448', {}); - const alicePrivate = KeyObject.createKeyObject( - 'private', - alice.privateKey as ArrayBuffer, - ); - expect(() => { crypto.diffieHellman({ - privateKey: alicePrivate, + privateKey: alice.privateKey as KeyObject, publicKey: {} as KeyObject, }); }).to.throw(); diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index 34b045d0..791e17dc 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -58,9 +58,11 @@ export class Ed { options: DiffieHellmanOptions, callback?: DiffieHellmanCallback, ): Buffer | void { - // extract the private and public keys as ArrayBuffers - const privateKey = toAB(options.privateKey); - const publicKey = toAB(options.publicKey); + // extract raw key bytes from KeyObject instances + const privKeyObj = options.privateKey as AsymmetricKeyObject; + const pubKeyObj = options.publicKey as AsymmetricKeyObject; + const privateKey = privKeyObj.handle.exportKey(); + const publicKey = pubKeyObj.handle.exportKey(); try { const ret = this.native.diffieHellman(privateKey, publicKey);