From 9c43c16e2c00748676c62b3c1fcb557940b63a0b Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 8 Apr 2026 14:00:54 +0100 Subject: [PATCH] Add `extractable` option to `generateKeyPair` and `generateKeyPairSigner` This PR adds an optional `extractable` boolean argument to `generateKeyPair` in `@solana/keys` and threads it through `generateKeyPairSigner` in `@solana/signers`. The argument defaults to `false`, preserving the existing secure-by-default behavior that prevents the bytes of the private key from being visible to JS. When set to `true`, the generated private key can be exported via `crypto.subtle.exportKey()`, which is useful for scenarios like persisting a generated key pair. This brings `generateKeyPair` and `generateKeyPairSigner` in line with the sibling helpers `createKeyPairFromBytes`, `createKeyPairFromPrivateKeyBytes`, `createKeyPairSignerFromBytes`, and `createKeyPairSignerFromPrivateKeyBytes`, which already accept an `extractable` parameter. --- .changeset/clean-breads-march.md | 6 +++++ packages/keys/src/__tests__/key-pair-test.ts | 10 ++++++++ packages/keys/src/key-pair.ts | 8 +++++-- .../src/__tests__/keypair-signer-test.ts | 23 +++++++++++++++++-- packages/signers/src/keypair-signer.ts | 8 +++++-- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/clean-breads-march.md diff --git a/.changeset/clean-breads-march.md b/.changeset/clean-breads-march.md new file mode 100644 index 000000000..406b5e1ca --- /dev/null +++ b/.changeset/clean-breads-march.md @@ -0,0 +1,6 @@ +--- +'@solana/signers': minor +'@solana/keys': minor +--- + +Add an optional `extractable` argument to `generateKeyPair` and `generateKeyPairSigner`. It defaults to `false`, preserving the existing secure-by-default behavior, but can be set to `true` when you need to export the generated private key bytes via `crypto.subtle.exportKey()`. diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index a1197cae5..acbe822af 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -39,6 +39,16 @@ describe('key-pair', () => { const { privateKey } = await generateKeyPair(); expect(privateKey).toHaveProperty('extractable', false); }); + it('generates a non-extractable private key when `extractable` is explicitly `false`', async () => { + expect.assertions(1); + const { privateKey } = await generateKeyPair(false); + expect(privateKey).toHaveProperty('extractable', false); + }); + it('generates an extractable private key when `extractable` is `true`', async () => { + expect.assertions(1); + const { privateKey } = await generateKeyPair(true); + expect(privateKey).toHaveProperty('extractable', true); + }); it('generates a private key usable for signing operations', async () => { expect.assertions(1); const { privateKey } = await generateKeyPair(); diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 2d765d047..b594bf9fe 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -15,6 +15,10 @@ import { signBytes, verifySignature } from './signatures'; * Generates an Ed25519 public/private key pair for use with other methods in this package that * accept [`CryptoKey`](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey) objects. * + * @param extractable Setting this to `true` makes it possible to extract the bytes of the private + * key using the [`crypto.subtle.exportKey()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey) + * API. Defaults to `false`, which prevents the bytes of the private key from being visible to JS. + * * @example * ```ts * import { generateKeyPair } from '@solana/keys'; @@ -22,11 +26,11 @@ import { signBytes, verifySignature } from './signatures'; * const { privateKey, publicKey } = await generateKeyPair(); * ``` */ -export async function generateKeyPair(): Promise { +export async function generateKeyPair(extractable: boolean = false): Promise { await assertKeyGenerationIsAvailable(); const keyPair = await crypto.subtle.generateKey( /* algorithm */ ED25519_ALGORITHM_IDENTIFIER, // Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20 - /* extractable */ false, // Prevents the bytes of the private key from being visible to JS. + extractable, /* allowed uses */ ['sign', 'verify'], ); return keyPair; diff --git a/packages/signers/src/__tests__/keypair-signer-test.ts b/packages/signers/src/__tests__/keypair-signer-test.ts index 1e8405c88..566a21274 100644 --- a/packages/signers/src/__tests__/keypair-signer-test.ts +++ b/packages/signers/src/__tests__/keypair-signer-test.ts @@ -185,7 +185,7 @@ describe('createSignerFromKeyPair', () => { describe('generateKeyPairSigner', () => { it('generates a new KeyPairSigner using the generateKeyPair function', async () => { - expect.assertions(3); + expect.assertions(4); // Given we mock the return value of generateKeyPair. const mockKeypair = getMockCryptoKeyPair(); @@ -203,8 +203,27 @@ describe('generateKeyPairSigner', () => { expect(mySigner.keyPair).toBe(mockKeypair); expect(mySigner.address).toBe(mockAddress); - // And generateKeyPair was called once. + // And generateKeyPair was called once with the default `extractable` value. expect(jest.mocked(generateKeyPair)).toHaveBeenCalledTimes(1); + expect(jest.mocked(generateKeyPair)).toHaveBeenCalledWith(false); + }); + + it('forwards the `extractable` argument to `generateKeyPair`', async () => { + expect.assertions(2); + + // Given we mock the return value of generateKeyPair. + const mockKeypair = getMockCryptoKeyPair(); + jest.mocked(generateKeyPair).mockResolvedValueOnce(mockKeypair); + jest.mocked(getAddressFromPublicKey).mockResolvedValueOnce( + address('Gp7YgHcJciP4px5FdFnywUiMG4UcfMZV9UagSAZzDxdy'), + ); + + // When we generate a new KeyPairSigner requesting an extractable key pair. + await generateKeyPairSigner(true); + + // Then generateKeyPair was called with `extractable` set to `true`. + expect(jest.mocked(generateKeyPair)).toHaveBeenCalledTimes(1); + expect(jest.mocked(generateKeyPair)).toHaveBeenCalledWith(true); }); it('freezes the generated signer', async () => { diff --git a/packages/signers/src/keypair-signer.ts b/packages/signers/src/keypair-signer.ts index 8595271ae..9288b902b 100644 --- a/packages/signers/src/keypair-signer.ts +++ b/packages/signers/src/keypair-signer.ts @@ -137,6 +137,10 @@ export async function createSignerFromKeyPair(keyPair: CryptoKeyPair): Promise { - return await createSignerFromKeyPair(await generateKeyPair()); +export async function generateKeyPairSigner(extractable: boolean = false): Promise { + return await createSignerFromKeyPair(await generateKeyPair(extractable)); } /**