From 913533a1a819384ab5fff15b5cc1e6f16eee8576 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 2 Aug 2024 15:31:45 +0100 Subject: [PATCH] Add createKeyPairFromPrivateKeyBytes helper --- .changeset/curvy-stingrays-attend.md | 11 ++++++ packages/keys/README.md | 22 ++++++++++++ packages/keys/src/__tests__/key-pair-test.ts | 35 ++++++++++++++++++- .../src/__typetests__/key-pair-typetests.ts | 7 +++- packages/keys/src/key-pair.ts | 25 +++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/curvy-stingrays-attend.md diff --git a/.changeset/curvy-stingrays-attend.md b/.changeset/curvy-stingrays-attend.md new file mode 100644 index 00000000000..2151b354f0d --- /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 0efece358e2..4a2ae6bd230 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 df26734350a..a1197cae5bd 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 367b1cdc166..c8fbd63b6e5 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 3eb0736b531..bd144048e3e 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,27 @@ export async function createKeyPairFromBytes(bytes: ReadonlyUint8Array, extracta return { privateKey, publicKey } as CryptoKeyPair; } + +export async function createKeyPairFromPrivateKeyBytes( + bytes: ReadonlyUint8Array, + extractable: boolean = false, +): Promise { + const privateKeyPromise = createPrivateKeyFromBytes(bytes, extractable); + + // Here we need the private key to be extractable in order to export + // it as a public key. Therefore, if the `extractable` parameter + // is `false`, we need to create two private keys such that: + // - The extractable one is used to create the public key and + // - The non-extractable one is the one we will return. + const [publicKey, privateKey] = await Promise.all([ + // This nested promise makes things efficient by + // creating the public key in parallel with the + // second private key creation, if it is needed. + (extractable ? privateKeyPromise : createPrivateKeyFromBytes(bytes, true /* extractable */)).then( + async privateKey => await getPublicKeyFromPrivateKey(privateKey, true /* extractable */), + ), + privateKeyPromise, + ]); + + return { privateKey, publicKey }; +}