From 91ca38cfbb275613376f29343e74487bc0f896c3 Mon Sep 17 00:00:00 2001 From: SmolinPavel Date: Fri, 29 Aug 2025 23:30:15 +0200 Subject: [PATCH 1/3] fix(biometric-ed25519): add "ed25519:" prefix to passkey-derived keys - Introduce `makeEd25519KeyString` helper to prepend the required prefix - Update all call sites to use the helper when creating or retrieving KeyPair from passkey data - Add regression test to ensure KeyPair.fromString parses correctly - Test verifies that bare keys fail and prefixed keys succeed --- .changeset/wise-mangos-push.md | 5 ++++ packages/biometric-ed25519/src/index.ts | 23 ++++++++++++++--- packages/biometric-ed25519/test/keys.test.ts | 27 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .changeset/wise-mangos-push.md create mode 100644 packages/biometric-ed25519/test/keys.test.ts diff --git a/.changeset/wise-mangos-push.md b/.changeset/wise-mangos-push.md new file mode 100644 index 0000000000..46ee03dbb8 --- /dev/null +++ b/.changeset/wise-mangos-push.md @@ -0,0 +1,5 @@ +--- +"@near-js/biometric-ed25519": patch +--- + +Ensure passkey-derived keys are prefixed with ed25519 diff --git a/packages/biometric-ed25519/src/index.ts b/packages/biometric-ed25519/src/index.ts index e81b5dac27..4ee361483c 100644 --- a/packages/biometric-ed25519/src/index.ts +++ b/packages/biometric-ed25519/src/index.ts @@ -87,7 +87,7 @@ export const createKey = async (username: string): Promise => { const publicKeyBytes = get64BytePublicKeyFromPEM(publicKey); const secretKey = sha256.create().update(Buffer.from(publicKeyBytes)).digest(); const pubKey = ed25519.getPublicKey(secretKey); - return KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))) as KeyPairString); + return KeyPair.fromString(makeEd25519KeyString(secretKey, pubKey)); }); }; @@ -130,8 +130,8 @@ export const getKeys = async (username: string): Promise<[KeyPair, KeyPair]> => const firstEDPublic = ed25519.getPublicKey(firstEDSecret); const secondEDSecret = sha256.create().update(Buffer.from(correctPKs[1])).digest(); const secondEDPublic = ed25519.getPublicKey(secondEDSecret); - const firstKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))) as KeyPairString); - const secondKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))) as KeyPairString); + const firstKeyPair = KeyPair.fromString(makeEd25519KeyString(firstEDSecret, firstEDPublic)); + const secondKeyPair = KeyPair.fromString(makeEd25519KeyString(secondEDSecret, secondEDPublic)); return [firstKeyPair, secondKeyPair]; }); }; @@ -149,4 +149,19 @@ export const isDeviceSupported = async (): Promise => { } catch (e) { return false; } -}; \ No newline at end of file +}; + +/** + * Combines a secret key and public key into a single string + * prefixed with "ed25519:", compatible with `KeyPair.fromString`. + * + * This is used for passkey-derived keys to ensure they can be + * correctly parsed and reconstructed as a `KeyPairEd25519`. + * + * @param secretKey - The 32-byte secret key. + * @param publicKey - The 32-byte public key. + * @returns A KeyPairString with the "ed25519:" prefix. + */ +export function makeEd25519KeyString(secretKey: Uint8Array, publicKey: Uint8Array): KeyPairString { + return ('ed25519:' + baseEncode(Buffer.concat([Buffer.from(secretKey), Buffer.from(publicKey)]))) as KeyPairString; +} \ No newline at end of file diff --git a/packages/biometric-ed25519/test/keys.test.ts b/packages/biometric-ed25519/test/keys.test.ts new file mode 100644 index 0000000000..c5aaf9be1f --- /dev/null +++ b/packages/biometric-ed25519/test/keys.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from '@jest/globals'; +import { KeyPair, KeyPairEd25519 } from '@near-js/crypto'; +import { baseEncode } from '@near-js/utils'; +import { makeEd25519KeyString } from '../src'; + +describe('makeEd25519KeyString regression', () => { + const secretKey = Buffer.from(new Array(32).fill(1)); + const pubKey = Buffer.from(new Array(32).fill(2)); + + it('should fail without the ed25519 prefix', () => { + const bareKey = Buffer.concat([secretKey, pubKey]); + const bareKeyString = baseEncode(bareKey); + console.log('bareKeyString', bareKeyString); + // @ts-expect-error testing invalid input + expect(() => KeyPair.fromString(bareKeyString)).toThrow( + 'Invalid encoded key format, must be :' + ); + }); + + it('should return a string parsable by KeyPair.fromString', () => { + const keyString = makeEd25519KeyString(secretKey, pubKey); + const keyPair = KeyPair.fromString(keyString) as KeyPairEd25519; + + expect(keyPair).toBeInstanceOf(KeyPairEd25519); + expect(keyPair.toString().startsWith('ed25519:')).toBe(true); + }); +}); From 1ce554260b994a8209d08f1bc3ae0028ff8eda7a Mon Sep 17 00:00:00 2001 From: denbite Date: Thu, 11 Sep 2025 20:37:17 +0200 Subject: [PATCH 2/3] refactor: switch to `new KeyPairEd25519` --- packages/biometric-ed25519/src/index.ts | 26 +++++-------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/biometric-ed25519/src/index.ts b/packages/biometric-ed25519/src/index.ts index 4ee361483c..a11d040ceb 100644 --- a/packages/biometric-ed25519/src/index.ts +++ b/packages/biometric-ed25519/src/index.ts @@ -3,7 +3,7 @@ import { ed25519 } from '@noble/curves/ed25519'; import { sha256 } from '@noble/hashes/sha256'; import { Buffer } from 'buffer'; import asn1 from 'asn1-parser'; -import { KeyPair } from '@near-js/crypto'; +import { KeyPair, KeyPairEd25519 } from '@near-js/crypto'; import { baseEncode } from '@near-js/utils'; import { validateUsername, @@ -18,7 +18,6 @@ import { } from './utils'; import { Fido2 } from './fido2'; import { AssertionResponse } from './index.d'; -import { KeyPairString } from '@near-js/crypto'; const CHALLENGE_TIMEOUT_MS = 90 * 1000; const RP_NAME = 'NEAR_API_JS_WEBAUTHN'; @@ -87,7 +86,7 @@ export const createKey = async (username: string): Promise => { const publicKeyBytes = get64BytePublicKeyFromPEM(publicKey); const secretKey = sha256.create().update(Buffer.from(publicKeyBytes)).digest(); const pubKey = ed25519.getPublicKey(secretKey); - return KeyPair.fromString(makeEd25519KeyString(secretKey, pubKey)); + return new KeyPairEd25519(baseEncode(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))); }); }; @@ -130,8 +129,8 @@ export const getKeys = async (username: string): Promise<[KeyPair, KeyPair]> => const firstEDPublic = ed25519.getPublicKey(firstEDSecret); const secondEDSecret = sha256.create().update(Buffer.from(correctPKs[1])).digest(); const secondEDPublic = ed25519.getPublicKey(secondEDSecret); - const firstKeyPair = KeyPair.fromString(makeEd25519KeyString(firstEDSecret, firstEDPublic)); - const secondKeyPair = KeyPair.fromString(makeEd25519KeyString(secondEDSecret, secondEDPublic)); + const firstKeyPair = new KeyPairEd25519(baseEncode(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))); + const secondKeyPair = new KeyPairEd25519(baseEncode(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))); return [firstKeyPair, secondKeyPair]; }); }; @@ -149,19 +148,4 @@ export const isDeviceSupported = async (): Promise => { } catch (e) { return false; } -}; - -/** - * Combines a secret key and public key into a single string - * prefixed with "ed25519:", compatible with `KeyPair.fromString`. - * - * This is used for passkey-derived keys to ensure they can be - * correctly parsed and reconstructed as a `KeyPairEd25519`. - * - * @param secretKey - The 32-byte secret key. - * @param publicKey - The 32-byte public key. - * @returns A KeyPairString with the "ed25519:" prefix. - */ -export function makeEd25519KeyString(secretKey: Uint8Array, publicKey: Uint8Array): KeyPairString { - return ('ed25519:' + baseEncode(Buffer.concat([Buffer.from(secretKey), Buffer.from(publicKey)]))) as KeyPairString; -} \ No newline at end of file +}; \ No newline at end of file From a9b56f475b49e183c639777a3456188fcd43f8f4 Mon Sep 17 00:00:00 2001 From: denbite Date: Thu, 11 Sep 2025 20:37:33 +0200 Subject: [PATCH 3/3] test: remove test for no longer existing function --- packages/biometric-ed25519/test/keys.test.ts | 27 -------------------- 1 file changed, 27 deletions(-) delete mode 100644 packages/biometric-ed25519/test/keys.test.ts diff --git a/packages/biometric-ed25519/test/keys.test.ts b/packages/biometric-ed25519/test/keys.test.ts deleted file mode 100644 index c5aaf9be1f..0000000000 --- a/packages/biometric-ed25519/test/keys.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { KeyPair, KeyPairEd25519 } from '@near-js/crypto'; -import { baseEncode } from '@near-js/utils'; -import { makeEd25519KeyString } from '../src'; - -describe('makeEd25519KeyString regression', () => { - const secretKey = Buffer.from(new Array(32).fill(1)); - const pubKey = Buffer.from(new Array(32).fill(2)); - - it('should fail without the ed25519 prefix', () => { - const bareKey = Buffer.concat([secretKey, pubKey]); - const bareKeyString = baseEncode(bareKey); - console.log('bareKeyString', bareKeyString); - // @ts-expect-error testing invalid input - expect(() => KeyPair.fromString(bareKeyString)).toThrow( - 'Invalid encoded key format, must be :' - ); - }); - - it('should return a string parsable by KeyPair.fromString', () => { - const keyString = makeEd25519KeyString(secretKey, pubKey); - const keyPair = KeyPair.fromString(keyString) as KeyPairEd25519; - - expect(keyPair).toBeInstanceOf(KeyPairEd25519); - expect(keyPair.toString().startsWith('ed25519:')).toBe(true); - }); -});