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

Commit

Permalink
Add getPublicKeyFromPrivateKey helper
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Aug 5, 2024
1 parent b4a8fbf commit 526072b
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 0 deletions.
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 () => {
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'],
);
}

0 comments on commit 526072b

Please sign in to comment.