Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Add getPublicKeyFromPrivateKey helper #3049

Merged
merged 1 commit into from
Aug 7, 2024
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
14 changes: 14 additions & 0 deletions .changeset/wicked-toys-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@solana/keys': patch
---

Add a `getPublicKeyFromPrivateKey` helper that, given an extractable `CryptoKey` private key, gets the corresponding public key as a `CryptoKey`.

```ts
import { createPrivateKeyFromBytes, getPublicKeyFromPrivateKey } from '@solana/keys';

const privateKey = await createPrivateKeyFromBytes(new Uint8Array([...]), true);

const publicKey = await getPublicKeyFromPrivateKey(privateKey);
const extractablePublicKey = await getPublicKeyFromPrivateKey(privateKey, true);
```
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const SOLANA_ERROR__SUBTLE_CRYPTO__EXPORT_FUNCTION_UNIMPLEMENTED = 361000
export const SOLANA_ERROR__SUBTLE_CRYPTO__GENERATE_FUNCTION_UNIMPLEMENTED = 3610004 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO__SIGN_FUNCTION_UNIMPLEMENTED = 3610005 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO__VERIFY_FUNCTION_UNIMPLEMENTED = 3610006 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY = 3610007 as const;

// Crypto-related errors.
// Reserve error codes in the range [3611000-3611050].
Expand Down Expand Up @@ -470,6 +471,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__SIGNER__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS
| typeof SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING
| typeof SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__ED25519_ALGORITHM_UNIMPLEMENTED
Expand Down
4 changes: 4 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import {
SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_PARTIAL_SIGNER,
SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SENDING_SIGNER,
SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SIGNER,
SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY,
SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING,
SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION,
Expand Down Expand Up @@ -527,6 +528,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SIGNER]: {
address: string;
};
[SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY]: {
key: CryptoKey;
};
[SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE]: {
value: number;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import {
SOLANA_ERROR__SIGNER__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS,
SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING,
SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED,
SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY,
SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED,
SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT,
SOLANA_ERROR__SUBTLE_CRYPTO__ED25519_ALGORITHM_UNIMPLEMENTED,
Expand Down Expand Up @@ -484,6 +485,7 @@ export const SolanaErrorMessages: Readonly<{
'`ITransactionWithSingleSendingSigner` transaction.',
[SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED]:
'Wallet account signers do not support signing multiple messages/transactions in a single operation',
[SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY]: 'Cannot export a non-extractable key.',
[SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED]: 'No digest implementation could be found.',
[SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT]:
'Cryptographic operations are only allowed in secure browser contexts. Read more ' +
Expand Down
24 changes: 24 additions & 0 deletions packages/keys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString()));
const { privateKey, publicKey } = await createKeyPairFromBytes(keypairBytes);
```

### `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.

```ts
import { createPrivateKeyFromBytes } from '@solana/keys';

const privateKey = await createPrivateKeyFromBytes(new Uint8Array([...]));
const extractablePrivateKey = await createPrivateKeyFromBytes(new Uint8Array([...]), true);
```

### `getPublicKeyFromPrivateKey()`

Given an extractable `CryptoKey` private key, gets the corresponding public key as a `CryptoKey`.

```ts
import { createPrivateKeyFromBytes, getPublicKeyFromPrivateKey } from '@solana/keys';

const privateKey = await createPrivateKeyFromBytes(new Uint8Array([...]), true);

const publicKey = await getPublicKeyFromPrivateKey(privateKey);
const extractablePublicKey = await getPublicKeyFromPrivateKey(privateKey, true);
```

### `isSignature()`

This is a type guard that accepts a string as input. It will both return `true` if the string conforms to the `Signature` type and will refine the type for use in your program.
Expand Down
56 changes: 56 additions & 0 deletions packages/keys/src/__tests__/public-key-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY, SolanaError } from '@solana/errors';

import { createPrivateKeyFromBytes } from '../private-key';
import { getPublicKeyFromPrivateKey } from '../public-key';

const MOCK_PRIVATE_KEY_BYTES = new Uint8Array([
0xeb, 0xfa, 0x65, 0xeb, 0x93, 0xdc, 0x79, 0x15, 0x7a, 0xba, 0xde, 0xa2, 0xf7, 0x94, 0x37, 0x9d, 0xfc, 0x07, 0x1d,
0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a,
]);

const EXPECTED_MOCK_PUBLIC_KEY_BYTES = new Uint8Array([
0x1d, 0x0e, 0x93, 0x86, 0x4d, 0xcc, 0x81, 0x5f, 0xc3, 0xf2, 0x86, 0x18, 0x09, 0x11, 0xd0, 0x0a, 0x3f, 0xd2, 0x06,
0xde, 0x31, 0xa1, 0xc9, 0x42, 0x87, 0xcb, 0x43, 0xf0, 0x5f, 0xc9, 0xf2, 0xb5,
]);

describe('getPublicKeyFromPrivateKey', () => {
describe('given an extractable private key', () => {
let privateKey: CryptoKey;
beforeEach(async () => {
privateKey = await createPrivateKeyFromBytes(MOCK_PRIVATE_KEY_BYTES, true);
});
it('gets the associated public key', async () => {
expect.assertions(1);
const publicKey = await getPublicKeyFromPrivateKey(privateKey, true);
const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', publicKey));
expect(publicKeyBytes).toEqual(EXPECTED_MOCK_PUBLIC_KEY_BYTES);
});
it('can get an extractable public key', async () => {
expect.assertions(1);
const publicKey = await getPublicKeyFromPrivateKey(privateKey, true);
expect(publicKey.extractable).toBe(true);
});
it('can get a non-extractable public key', async () => {
expect.assertions(1);
const publicKey = await getPublicKeyFromPrivateKey(privateKey, false);
expect(publicKey.extractable).toBe(false);
});
it('returns a non-extractable public key by default', async () => {
expect.assertions(1);
const publicKey = await getPublicKeyFromPrivateKey(privateKey);
expect(publicKey.extractable).toBe(false);
});
});
describe('given a non-extractable private key', () => {
let privateKey: CryptoKey;
beforeEach(async () => {
privateKey = await createPrivateKeyFromBytes(MOCK_PRIVATE_KEY_BYTES, false);
});
it('cannot get the associated public key', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for us to check if it's extractable? I'm wondering if we should add a Solana error for this case because I'm skeptical that subtle crypto will give a nice error message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout! I've added a new SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY error code.

expect.assertions(1);
await expect(() => getPublicKeyFromPrivateKey(privateKey, true)).rejects.toThrow(
new SolanaError(SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY, { key: privateKey }),
);
});
});
});
5 changes: 5 additions & 0 deletions packages/keys/src/__typetests__/public-key-typetests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getPublicKeyFromPrivateKey } from '../public-key';

getPublicKeyFromPrivateKey(new CryptoKey()) satisfies Promise<CryptoKey>;
getPublicKeyFromPrivateKey(new CryptoKey(), true) satisfies Promise<CryptoKey>;
getPublicKeyFromPrivateKey(new CryptoKey(), false) satisfies Promise<CryptoKey>;
31 changes: 31 additions & 0 deletions packages/keys/src/public-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { assertKeyExporterIsAvailable } from '@solana/assertions';
import { SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY, SolanaError } from '@solana/errors';

export async function getPublicKeyFromPrivateKey(
privateKey: CryptoKey,
extractable: boolean = false,
): Promise<CryptoKey> {
assertKeyExporterIsAvailable();

if (privateKey.extractable === false) {
throw new SolanaError(SOLANA_ERROR__SUBTLE_CRYPTO__CANNOT_EXPORT_NON_EXTRACTABLE_KEY, { key: privateKey });
}

// Export private key.
const jwk = await crypto.subtle.exportKey('jwk', privateKey);

// Import public key.
return await crypto.subtle.importKey(
'jwk',
{
crv /* curve */: 'Ed25519',
ext /* extractable */: extractable,
key_ops /* key operations */: ['verify'],
kty /* key type */: 'OKP' /* octet key pair */,
x /* public key x-coordinate */: jwk.x,
},
'Ed25519',
extractable,
['verify'],
);
}