diff --git a/.changeset/curvy-stingrays-attend.md b/.changeset/curvy-stingrays-attend.md new file mode 100644 index 000000000000..2151b354f0df --- /dev/null +++ b/.changeset/curvy-stingrays-attend.md @@ -0,0 +1,11 @@ +--- +'@solana/keys': patch +--- + +Add a `createKeyPairFromPrivateKeyBytes` helper that creates a keypair from the 32-byte private key bytes. + +```ts +import { createKeyPairFromPrivateKeyBytes } from '@solana/keys'; + +const { privateKey, publicKey } = await createKeyPairFromPrivateKeyBytes(new Uint8Array([...])); +``` diff --git a/packages/keys/README.md b/packages/keys/README.md index 0efece358e2e..4a2ae6bd230d 100644 --- a/packages/keys/README.md +++ b/packages/keys/README.md @@ -81,6 +81,28 @@ const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString())); const { privateKey, publicKey } = await createKeyPairFromBytes(keypairBytes); ``` +### `createKeyPairFromPrivateKeyBytes()` + +Given a private key represented as a 32-bytes `Uint8Array`, creates an Ed25519 public/private key pair for use with other methods in this package that accept `CryptoKey` objects. + +```ts +import { createKeyPairFromPrivateKeyBytes } from '@solana/keys'; + +const { privateKey, publicKey } = await createKeyPairFromPrivateKeyBytes(new Uint8Array([...])); +``` + +This can be useful when you have a private key but not the corresponding public key or when you need to derive key pairs from seeds. For instance, the following code snippet derives a key pair from the hash of a message. + +```ts +import { getUtf8Encoder } from '@solana/codecs-strings'; +import { createKeyPairFromPrivateKeyBytes } from '@solana/keys'; + +const message = getUtf8Encoder().encode('Hello, World!'); +const seed = new Uint8Array(await crypto.subtle.digest('SHA-256', message)); + +const derivedKeypair = await createKeyPairFromPrivateKeyBytes(seed); +``` + ### `createPrivateKeyFromBytes()` Given a private key represented as a 32-bytes `Uint8Array`, creates an Ed25519 private key for use with other methods in this package that accept `CryptoKey` objects. diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index df26734350aa..a1197cae5bd7 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -1,10 +1,11 @@ import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, + SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, SolanaError, } from '@solana/errors'; -import { createKeyPairFromBytes, generateKeyPair } from '../key-pair'; +import { createKeyPairFromBytes, createKeyPairFromPrivateKeyBytes, generateKeyPair } from '../key-pair'; const MOCK_KEY_BYTES = new Uint8Array([ 0xeb, 0xfa, 0x65, 0xeb, 0x93, 0xdc, 0x79, 0x15, 0x7a, 0xba, 0xde, 0xa2, 0xf7, 0x94, 0x37, 0x9d, 0xfc, 0x07, 0x1d, @@ -85,4 +86,36 @@ describe('key-pair', () => { ); }); }); + + describe('createKeyPairFromPrivateKeyBytes', () => { + it('creates a key pair from a 32-bytes private key', async () => { + expect.assertions(1); + const keyPair = await createKeyPairFromPrivateKeyBytes(MOCK_KEY_BYTES.slice(0, 32)); + expect(keyPair).toMatchObject({ + privateKey: expect.objectContaining({ + [Symbol.toStringTag]: 'CryptoKey', + algorithm: { name: 'Ed25519' }, + type: 'private', + }), + publicKey: expect.objectContaining({ + [Symbol.toStringTag]: 'CryptoKey', + algorithm: { name: 'Ed25519' }, + type: 'public', + }), + }); + }); + it('uses the public key associated with the provided private key bytes', async () => { + expect.assertions(1); + const keyPair = await createKeyPairFromPrivateKeyBytes(MOCK_KEY_BYTES.slice(0, 32)); + const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)); + const expectedPublicKeyBytes = MOCK_KEY_BYTES.slice(32); + expect(publicKeyBytes).toEqual(expectedPublicKeyBytes); + }); + it('errors when the byte array is not 32 bytes', async () => { + expect.assertions(1); + await expect(createKeyPairFromPrivateKeyBytes(MOCK_KEY_BYTES.slice(0, 31))).rejects.toThrow( + new SolanaError(SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, { actualLength: 31 }), + ); + }); + }); }); diff --git a/packages/keys/src/__typetests__/key-pair-typetests.ts b/packages/keys/src/__typetests__/key-pair-typetests.ts index 367b1cdc166d..c8fbd63b6e59 100644 --- a/packages/keys/src/__typetests__/key-pair-typetests.ts +++ b/packages/keys/src/__typetests__/key-pair-typetests.ts @@ -1,6 +1,11 @@ import { ReadonlyUint8Array } from '@solana/codecs-core'; -import { createKeyPairFromBytes } from '../key-pair'; +import { createKeyPairFromBytes, createKeyPairFromPrivateKeyBytes } from '../key-pair'; createKeyPairFromBytes(new Uint8Array()) satisfies Promise; createKeyPairFromBytes(new Uint8Array() as ReadonlyUint8Array) satisfies Promise; +createKeyPairFromBytes(new Uint8Array(), true) satisfies Promise; + +createKeyPairFromPrivateKeyBytes(new Uint8Array()) satisfies Promise; +createKeyPairFromPrivateKeyBytes(new Uint8Array() as ReadonlyUint8Array) satisfies Promise; +createKeyPairFromPrivateKeyBytes(new Uint8Array(), true) satisfies Promise; diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 3eb0736b5311..9cfc802a8089 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -7,6 +7,7 @@ import { } from '@solana/errors'; import { createPrivateKeyFromBytes } from './private-key'; +import { getPublicKeyFromPrivateKey } from './public-key'; import { signBytes, verifySignature } from './signatures'; export async function generateKeyPair(): Promise { @@ -41,3 +42,19 @@ export async function createKeyPairFromBytes(bytes: ReadonlyUint8Array, extracta return { privateKey, publicKey } as CryptoKeyPair; } + +export async function createKeyPairFromPrivateKeyBytes( + bytes: ReadonlyUint8Array, + extractable: boolean = false, +): Promise { + const promises = [createPrivateKeyFromBytes(bytes, true)]; + + if (!extractable) { + promises.push(createPrivateKeyFromBytes(bytes, false)); + } + + const [extractablePrivateKey, privateKey] = await Promise.all(promises); + const publicKey = await getPublicKeyFromPrivateKey(extractablePrivateKey, true); + + return extractable ? { privateKey: extractablePrivateKey, publicKey } : { privateKey, publicKey }; +}