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
6 changes: 6 additions & 0 deletions .changeset/clean-breads-march.md
Original file line number Diff line number Diff line change
@@ -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()`.
10 changes: 10 additions & 0 deletions packages/keys/src/__tests__/key-pair-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions packages/keys/src/key-pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@ 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';
*
* const { privateKey, publicKey } = await generateKeyPair();
* ```
*/
export async function generateKeyPair(): Promise<CryptoKeyPair> {
export async function generateKeyPair(extractable: boolean = false): Promise<CryptoKeyPair> {
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;
Expand Down
23 changes: 21 additions & 2 deletions packages/signers/src/__tests__/keypair-signer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/signers/src/keypair-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export async function createSignerFromKeyPair(keyPair: CryptoKeyPair): Promise<K
* Generates a signer capable of signing messages and transactions by generating
* a {@link CryptoKeyPair} and creating a {@link KeyPairSigner} from it.
*
* @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 { generateKeyPairSigner } from '@solana/signers';
Expand All @@ -146,8 +150,8 @@ export async function createSignerFromKeyPair(keyPair: CryptoKeyPair): Promise<K
*
* @see {@link createSignerFromKeyPair}
*/
export async function generateKeyPairSigner(): Promise<KeyPairSigner> {
return await createSignerFromKeyPair(await generateKeyPair());
export async function generateKeyPairSigner(extractable: boolean = false): Promise<KeyPairSigner> {
return await createSignerFromKeyPair(await generateKeyPair(extractable));
}

/**
Expand Down
Loading