From 3e8eb1d66c8ecd4bc2e9dc47e4988b80df11465c Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sat, 16 Mar 2024 00:17:53 -0400 Subject: [PATCH 01/10] add createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION and validation --- packages/keys/src/key-pair.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index e079edff28c0..5c59fb3a6a16 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -2,6 +2,7 @@ import { assertKeyGenerationIsAvailable } from '@solana/assertions'; import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError } from '@solana/errors'; import { createPrivateKeyFromBytes } from './private-key'; +import { verifySignature, signBytes } from './signatures'; export async function generateKeyPair(): Promise { await assertKeyGenerationIsAvailable(); @@ -21,5 +22,23 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']), createPrivateKeyFromBytes(bytes.slice(0, 32), extractable), ]); + const signedData = await signBytes(privateKey, new Uint8Array([1, 2, 3, 4, 5])); + const isValid = await verifySignature(publicKey, signedData, new Uint8Array([1, 2, 3, 4, 5])); + if (!isValid) { + throw new SolanaError(SOLANA_ERROR__INVALID_KEYPAIR_BYTES); + } + + return { privateKey, publicKey } as CryptoKeyPair; +} + +export async function createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION(bytes: Uint8Array, extractable?: boolean): Promise { + if (bytes.byteLength !== 64) { + throw new SolanaError(SOLANA_ERROR__INVALID_KEYPAIR_BYTES, { byteLength: bytes.byteLength }); + } + const [publicKey, privateKey] = await Promise.all([ + crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']), + createPrivateKeyFromBytes(bytes.slice(0, 32), extractable), + ]); + return { privateKey, publicKey } as CryptoKeyPair; } From 1c3c7c0e5254436a097e94a40b9498b04ce3b8b3 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sat, 16 Mar 2024 14:49:18 -0400 Subject: [PATCH 02/10] add testcases and error codes for createKeyPairFromBytes --- packages/errors/src/codes.ts | 4 +- packages/errors/src/messages.ts | 2 + packages/keys/src/__tests__/key-pair-test.ts | 128 ++++++++++++++----- packages/keys/src/key-pair.ts | 15 ++- 4 files changed, 112 insertions(+), 37 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 9fd4bf217664..81451f16c7ae 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -96,6 +96,7 @@ export const SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH = 3704000 as const export const SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH = 3704001 as const; export const SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH = 3704002 as const; export const SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = 3704003 as const; +export const SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE = 3704004 as const; // Instruction-related errors. // Reserve error codes in the range [4128000-4128999]. @@ -417,6 +418,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE + | typeof SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE | typeof SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE | typeof SOLANA_ERROR__MALFORMED_BIGINT_STRING | typeof SOLANA_ERROR__MALFORMED_NUMBER_STRING @@ -501,4 +503,4 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_ACCOUNT_DATA_TOTAL_LIMIT | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_BLOCK_COST_LIMIT - | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT; + | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT; \ No newline at end of file diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 0085e04e3b4a..82e4fae47db5 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -121,6 +121,7 @@ import { SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH, SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE, SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, SOLANA_ERROR__MALFORMED_BIGINT_STRING, SOLANA_ERROR__MALFORMED_NUMBER_STRING, @@ -402,6 +403,7 @@ export const SolanaErrorMessages: Readonly<{ 'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.', [SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: 'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.', + [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'public key has failed to verify signature', [SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]', [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`', [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`', diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index d2448e7ea3f3..98e282d8b6d4 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -1,35 +1,101 @@ -import { generateKeyPair } from '../key-pair'; +import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE } from '@solana/errors'; +import { generateKeyPair, createKeyPairFromBytes, createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION } from '../key-pair'; -describe('generateKeyPair', () => { - it.each(['private', 'public'])('generates an ed25519 %s `CryptoKey`', async type => { - expect.assertions(1); - const keyPair = await generateKeyPair(); - expect(keyPair).toMatchObject({ - [`${type}Key`]: expect.objectContaining({ - [Symbol.toStringTag]: 'CryptoKey', - algorithm: { name: 'Ed25519' }, - type, - }), +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, + 0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 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, +]); + +const MOCK_INVALID_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, 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, 0xb1, +]); + + +describe('key-pair', () => { + describe('generateKeyPair', () => { + it.each(['private', 'public'])('generates an ed25519 %s `CryptoKey`', async type => { + expect.assertions(1); + const keyPair = await generateKeyPair(); + expect(keyPair).toMatchObject({ + [`${type}Key`]: expect.objectContaining({ + [Symbol.toStringTag]: 'CryptoKey', + algorithm: { name: 'Ed25519' }, + type, + }), + }); + }); + it('generates a non-extractable private key', async () => { + expect.assertions(1); + const { privateKey } = await generateKeyPair(); + expect(privateKey).toHaveProperty('extractable', false); + }); + it('generates a private key usable for signing operations', async () => { + expect.assertions(1); + const { privateKey } = await generateKeyPair(); + expect(privateKey).toHaveProperty('usages', ['sign']); + }); + it('generates an extractable public key', async () => { + expect.assertions(1); + const { publicKey } = await generateKeyPair(); + expect(publicKey).toHaveProperty('extractable', true); + }); + it('generates a public key usable for verifying signatures', async () => { + expect.assertions(1); + const { publicKey } = await generateKeyPair(); + expect(publicKey).toHaveProperty('usages', ['verify']); }); }); - it('generates a non-extractable private key', async () => { - expect.assertions(1); - const { privateKey } = await generateKeyPair(); - expect(privateKey).toHaveProperty('extractable', false); - }); - it('generates a private key usable for signing operations', async () => { - expect.assertions(1); - const { privateKey } = await generateKeyPair(); - expect(privateKey).toHaveProperty('usages', ['sign']); - }); - it('generates an extractable public key', async () => { - expect.assertions(1); - const { publicKey } = await generateKeyPair(); - expect(publicKey).toHaveProperty('extractable', true); - }); - it('generates a public key usable for verifying signatures', async () => { - expect.assertions(1); - const { publicKey } = await generateKeyPair(); - expect(publicKey).toHaveProperty('usages', ['verify']); + + describe('createKeyPairFromBytes', () => { + it('creates a key pair from a 64-byte array', async () => { + expect.assertions(1); + const keyPair = await createKeyPairFromBytes(MOCK_KEY_BYTES); + 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('errors when the byte array is not 64 bytes', async () => { + expect.assertions(1); + await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0,31))).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 })); + }); + it('errors when public key fails signature verification', async () => { + expect.assertions(1); + await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE)); + }) + }) + + describe('createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION', () => { + it('creates a key pair from a 64-byte array', async () => { + expect.assertions(1); + const keyPair = await createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION(MOCK_KEY_BYTES); + 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('errors when the byte array is not 64 bytes', async () => { + expect.assertions(1); + await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0,31))).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 })); + }); }); -}); +}) \ No newline at end of file diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 5c59fb3a6a16..8444ab83c27e 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -1,9 +1,11 @@ import { assertKeyGenerationIsAvailable } from '@solana/assertions'; -import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError } from '@solana/errors'; +import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE } from '@solana/errors'; import { createPrivateKeyFromBytes } from './private-key'; import { verifySignature, signBytes } from './signatures'; +const TEST_DATA = new Uint8Array([1, 2, 3, 4, 5]); + export async function generateKeyPair(): Promise { await assertKeyGenerationIsAvailable(); const keyPair = await crypto.subtle.generateKey( @@ -22,18 +24,21 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']), createPrivateKeyFromBytes(bytes.slice(0, 32), extractable), ]); - const signedData = await signBytes(privateKey, new Uint8Array([1, 2, 3, 4, 5])); - const isValid = await verifySignature(publicKey, signedData, new Uint8Array([1, 2, 3, 4, 5])); + + // Verify the key pair + const signedData = await signBytes(privateKey, TEST_DATA); + const isValid = await verifySignature(publicKey, signedData, TEST_DATA); if (!isValid) { - throw new SolanaError(SOLANA_ERROR__INVALID_KEYPAIR_BYTES); + throw new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE); } return { privateKey, publicKey } as CryptoKeyPair; } +// Similar to createKeyPairFromBytes, but does not verify the key pair export async function createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION(bytes: Uint8Array, extractable?: boolean): Promise { if (bytes.byteLength !== 64) { - throw new SolanaError(SOLANA_ERROR__INVALID_KEYPAIR_BYTES, { byteLength: bytes.byteLength }); + throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength }); } const [publicKey, privateKey] = await Promise.all([ crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']), From 4e9304b925bb4c6432df574ea17381ae883ac820 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sat, 16 Mar 2024 14:53:10 -0400 Subject: [PATCH 03/10] update error message --- packages/errors/src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 82e4fae47db5..7cc457e6eb00 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -403,7 +403,7 @@ export const SolanaErrorMessages: Readonly<{ 'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.', [SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: 'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.', - [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'public key has failed to verify signature', + [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key and public key are not a valid key pair', [SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]', [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`', [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`', From 0e7548fedd25a2b15689ad8d1b64dc9629df61e8 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sat, 16 Mar 2024 14:58:42 -0400 Subject: [PATCH 04/10] formatting --- packages/keys/src/__tests__/key-pair-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index 98e282d8b6d4..f45fd5a6fc87 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -98,4 +98,4 @@ describe('key-pair', () => { await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0,31))).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 })); }); }); -}) \ No newline at end of file +}); \ No newline at end of file From 62c7121d41b322e5571877aa48f329ead6b0a3e8 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sun, 17 Mar 2024 23:07:04 -0400 Subject: [PATCH 05/10] address feedback --- packages/errors/src/codes.ts | 4 +-- packages/errors/src/messages.ts | 2 +- packages/keys/src/__tests__/key-pair-test.ts | 28 +++----------------- packages/keys/src/key-pair.ts | 17 ++---------- 4 files changed, 8 insertions(+), 43 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 81451f16c7ae..dfb791699dd1 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -96,7 +96,7 @@ export const SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH = 3704000 as const export const SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH = 3704001 as const; export const SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH = 3704002 as const; export const SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = 3704003 as const; -export const SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE = 3704004 as const; +export const SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY = 3704004 as const; // Instruction-related errors. // Reserve error codes in the range [4128000-4128999]. @@ -418,7 +418,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE - | typeof SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE + | typeof SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY | typeof SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE | typeof SOLANA_ERROR__MALFORMED_BIGINT_STRING | typeof SOLANA_ERROR__MALFORMED_NUMBER_STRING diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 7cc457e6eb00..96fc0de71527 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -403,7 +403,7 @@ export const SolanaErrorMessages: Readonly<{ 'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.', [SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: 'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.', - [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key and public key are not a valid key pair', + [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key does not match the provided public key at address [$address].', [SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]', [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`', [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`', diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index f45fd5a6fc87..2a5805222f5d 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -1,5 +1,5 @@ -import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE } from '@solana/errors'; -import { generateKeyPair, createKeyPairFromBytes, createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION } from '../key-pair'; +import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY } from '@solana/errors'; +import { generateKeyPair, createKeyPairFromBytes } 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, @@ -72,30 +72,8 @@ describe('key-pair', () => { }); it('errors when public key fails signature verification', async () => { expect.assertions(1); - await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE)); + await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: MOCK_INVALID_KEY_BYTES.slice(32)})); }) }) - describe('createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION', () => { - it('creates a key pair from a 64-byte array', async () => { - expect.assertions(1); - const keyPair = await createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION(MOCK_KEY_BYTES); - 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('errors when the byte array is not 64 bytes', async () => { - expect.assertions(1); - await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0,31))).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 })); - }); - }); }); \ No newline at end of file diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 8444ab83c27e..f7aeba8432ce 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -1,5 +1,5 @@ import { assertKeyGenerationIsAvailable } from '@solana/assertions'; -import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE } from '@solana/errors'; +import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY } from '@solana/errors'; import { createPrivateKeyFromBytes } from './private-key'; import { verifySignature, signBytes } from './signatures'; @@ -29,21 +29,8 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo const signedData = await signBytes(privateKey, TEST_DATA); const isValid = await verifySignature(publicKey, signedData, TEST_DATA); if (!isValid) { - throw new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE); + throw new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: bytes.slice(32) }); } return { privateKey, publicKey } as CryptoKeyPair; } - -// Similar to createKeyPairFromBytes, but does not verify the key pair -export async function createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION(bytes: Uint8Array, extractable?: boolean): Promise { - if (bytes.byteLength !== 64) { - throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength }); - } - const [publicKey, privateKey] = await Promise.all([ - crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']), - createPrivateKeyFromBytes(bytes.slice(0, 32), extractable), - ]); - - return { privateKey, publicKey } as CryptoKeyPair; -} From 70611e0727bcb61780a054f758fff3ecae92ba14 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Tue, 19 Mar 2024 22:42:46 -0400 Subject: [PATCH 06/10] address feedback add assertPRNGIsAvailable --- .../assertions/src/__tests__/crypto-test.ts | 28 ++++++++++++++ packages/assertions/src/crypto.ts | 7 ++++ packages/errors/src/codes.ts | 9 ++++- packages/errors/src/messages.ts | 7 +++- packages/keys/src/__tests__/key-pair-test.ts | 37 ++++++++++++------- packages/keys/src/key-pair.ts | 18 +++++---- 6 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 packages/assertions/src/__tests__/crypto-test.ts create mode 100644 packages/assertions/src/crypto.ts diff --git a/packages/assertions/src/__tests__/crypto-test.ts b/packages/assertions/src/__tests__/crypto-test.ts new file mode 100644 index 000000000000..a676917c4e82 --- /dev/null +++ b/packages/assertions/src/__tests__/crypto-test.ts @@ -0,0 +1,28 @@ +import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; + +import { assertPRNGIsAvailable } from '../crypto'; + +describe('assertPRNGIsAvailable()', () => { + it('resolves to `undefined` without throwing', async () => { + expect.assertions(1); + await expect(assertPRNGIsAvailable()).resolves.toBeUndefined(); + }); + describe('when getRandomValues is not available', () => { + let oldCrypto: InstanceType['getRandomValues']; + beforeEach(() => { + oldCrypto = globalThis.crypto.getRandomValues; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + globalThis.crypto.getRandomValues = undefined; + }); + afterEach(() => { + globalThis.crypto.getRandomValues = oldCrypto; + }); + it('rejects', async () => { + expect.assertions(1); + await expect(() => assertPRNGIsAvailable()).rejects.toThrow( + new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED), + ); + }); + }); +}); diff --git a/packages/assertions/src/crypto.ts b/packages/assertions/src/crypto.ts new file mode 100644 index 000000000000..bd62651c5fcd --- /dev/null +++ b/packages/assertions/src/crypto.ts @@ -0,0 +1,7 @@ +import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; + +export async function assertPRNGIsAvailable() { + if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.getRandomValues !== 'function') { + throw new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED); + } +} diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index dfb791699dd1..feaac895ede2 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -90,6 +90,10 @@ export const SOLANA_ERROR__SUBTLE_CRYPTO__GENERATE_FUNCTION_UNIMPLEMENTED = 3610 export const SOLANA_ERROR__SUBTLE_CRYPTO__SIGN_FUNCTION_UNIMPLEMENTED = 3610005 as const; export const SOLANA_ERROR__SUBTLE_CRYPTO__VERIFY_FUNCTION_UNIMPLEMENTED = 3610006 as const; +// Crypto-related errors. +// Reserve error codes in the range [3611000-3611050]. +export const SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED = 3610007 as const; + // Key-related errors. // Reserve error codes in the range [3704000-3704999]. export const SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH = 3704000 as const; @@ -329,6 +333,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE | typeof SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE | typeof SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE + | typeof SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED | typeof SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_ACCOUNTS | typeof SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_DATA | typeof SOLANA_ERROR__INSTRUCTION__PROGRAM_ID_MISMATCH @@ -417,8 +422,8 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH | typeof SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH - | typeof SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE | typeof SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY + | typeof SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE | typeof SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE | typeof SOLANA_ERROR__MALFORMED_BIGINT_STRING | typeof SOLANA_ERROR__MALFORMED_NUMBER_STRING @@ -503,4 +508,4 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_ACCOUNT_DATA_TOTAL_LIMIT | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_BLOCK_COST_LIMIT - | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT; \ No newline at end of file + | typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 96fc0de71527..c1a1ce4917d6 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -32,6 +32,7 @@ import { SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE, SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, + SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_ACCOUNTS, SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_DATA, SOLANA_ERROR__INSTRUCTION__PROGRAM_ID_MISMATCH, @@ -120,8 +121,8 @@ import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH, + SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, - SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE, SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, SOLANA_ERROR__MALFORMED_BIGINT_STRING, SOLANA_ERROR__MALFORMED_NUMBER_STRING, @@ -275,6 +276,7 @@ export const SolanaErrorMessages: Readonly<{ 'Codec [$codecDescription] expected number to be in the range [$min, $max], got $value.', [SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE]: 'Codec [$codecDescription] expected offset to be in the range [0, $bytesLength], got $offset.', + [SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED]: 'No random values implementation could be found.', [SOLANA_ERROR__INSTRUCTION_ERROR__ACCOUNT_ALREADY_INITIALIZED]: 'instruction requires an uninitialized account', [SOLANA_ERROR__INSTRUCTION_ERROR__ACCOUNT_BORROW_FAILED]: 'instruction tries to borrow reference for an account which is already borrowed', @@ -401,9 +403,10 @@ export const SolanaErrorMessages: Readonly<{ 'Expected private key bytes with length 32. Actual length: $actualLength.', [SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH]: 'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.', + [SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY]: + 'The provided private key does not match the provided public key.', [SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: 'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.', - [SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key does not match the provided public key at address [$address].', [SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]', [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`', [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`', diff --git a/packages/keys/src/__tests__/key-pair-test.ts b/packages/keys/src/__tests__/key-pair-test.ts index 2a5805222f5d..df26734350aa 100644 --- a/packages/keys/src/__tests__/key-pair-test.ts +++ b/packages/keys/src/__tests__/key-pair-test.ts @@ -1,19 +1,25 @@ -import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY } from '@solana/errors'; -import { generateKeyPair, createKeyPairFromBytes } from '../key-pair'; +import { + SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, + SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, + SolanaError, +} from '@solana/errors'; + +import { createKeyPairFromBytes, 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, - 0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 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, + 0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 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, ]); const MOCK_INVALID_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, 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, 0xb1, + 0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 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, 0xb1, ]); - describe('key-pair', () => { describe('generateKeyPair', () => { it.each(['private', 'public'])('generates an ed25519 %s `CryptoKey`', async type => { @@ -65,15 +71,18 @@ describe('key-pair', () => { type: 'public', }), }); - }) + }); it('errors when the byte array is not 64 bytes', async () => { expect.assertions(1); - await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0,31))).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 })); + await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0, 31))).rejects.toThrow( + new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 }), + ); }); it('errors when public key fails signature verification', async () => { expect.assertions(1); - await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow(new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: MOCK_INVALID_KEY_BYTES.slice(32)})); - }) - }) - -}); \ No newline at end of file + await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow( + new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY), + ); + }); + }); +}); diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index f7aeba8432ce..4adc36823c17 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -1,10 +1,12 @@ import { assertKeyGenerationIsAvailable } from '@solana/assertions'; -import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SolanaError, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY } from '@solana/errors'; +import { + SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, + SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, + SolanaError, +} from '@solana/errors'; import { createPrivateKeyFromBytes } from './private-key'; -import { verifySignature, signBytes } from './signatures'; - -const TEST_DATA = new Uint8Array([1, 2, 3, 4, 5]); +import { signBytes, verifySignature } from './signatures'; export async function generateKeyPair(): Promise { await assertKeyGenerationIsAvailable(); @@ -26,10 +28,12 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo ]); // Verify the key pair - const signedData = await signBytes(privateKey, TEST_DATA); - const isValid = await verifySignature(publicKey, signedData, TEST_DATA); + const randomBytes = new Uint8Array(32); + crypto.getRandomValues(randomBytes); + const signedData = await signBytes(privateKey, randomBytes); + const isValid = await verifySignature(publicKey, signedData, randomBytes); if (!isValid) { - throw new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: bytes.slice(32) }); + throw new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY); } return { privateKey, publicKey } as CryptoKeyPair; From 99ff817026e556c6b7d38d7b95e1faa52514c545 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Tue, 19 Mar 2024 22:56:06 -0400 Subject: [PATCH 07/10] update --- packages/errors/src/codes.ts | 2 +- packages/keys/src/key-pair.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index feaac895ede2..5bac1bfeaafa 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -92,7 +92,7 @@ export const SOLANA_ERROR__SUBTLE_CRYPTO__VERIFY_FUNCTION_UNIMPLEMENTED = 361000 // Crypto-related errors. // Reserve error codes in the range [3611000-3611050]. -export const SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED = 3610007 as const; +export const SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED = 3611000 as const; // Key-related errors. // Reserve error codes in the range [3704000-3704999]. diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 4adc36823c17..a3b5d9507b27 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -1,4 +1,4 @@ -import { assertKeyGenerationIsAvailable } from '@solana/assertions'; +import { assertKeyGenerationIsAvailable, assertPRNGIsAvailable, assert } from '@solana/assertions'; import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, @@ -19,6 +19,8 @@ export async function generateKeyPair(): Promise { } export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: boolean): Promise { + await assertPRNGIsAvailable(); + if (bytes.byteLength !== 64) { throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength }); } From 9292acfd885eccacafcbe5daf7b3130576bcb6bd Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Fri, 22 Mar 2024 23:12:41 -0400 Subject: [PATCH 08/10] address feedback --- packages/assertions/src/__tests__/crypto-test.ts | 15 +++++++++------ packages/assertions/src/crypto.ts | 2 +- packages/assertions/src/index.ts | 1 + packages/keys/src/key-pair.ts | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/assertions/src/__tests__/crypto-test.ts b/packages/assertions/src/__tests__/crypto-test.ts index a676917c4e82..5357cd790ff6 100644 --- a/packages/assertions/src/__tests__/crypto-test.ts +++ b/packages/assertions/src/__tests__/crypto-test.ts @@ -3,9 +3,13 @@ import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError import { assertPRNGIsAvailable } from '../crypto'; describe('assertPRNGIsAvailable()', () => { - it('resolves to `undefined` without throwing', async () => { - expect.assertions(1); - await expect(assertPRNGIsAvailable()).resolves.toBeUndefined(); + describe('when getRandomValues is available', () => { + it('does not throw', () => { + expect(assertPRNGIsAvailable).not.toThrow(); + }); + it('returns `undefined`', () => { + expect(assertPRNGIsAvailable()).toBeUndefined(); + }); }); describe('when getRandomValues is not available', () => { let oldCrypto: InstanceType['getRandomValues']; @@ -18,9 +22,8 @@ describe('assertPRNGIsAvailable()', () => { afterEach(() => { globalThis.crypto.getRandomValues = oldCrypto; }); - it('rejects', async () => { - expect.assertions(1); - await expect(() => assertPRNGIsAvailable()).rejects.toThrow( + it('throws', () => { + expect(assertPRNGIsAvailable).toThrow( new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED), ); }); diff --git a/packages/assertions/src/crypto.ts b/packages/assertions/src/crypto.ts index bd62651c5fcd..8752465e83b2 100644 --- a/packages/assertions/src/crypto.ts +++ b/packages/assertions/src/crypto.ts @@ -1,6 +1,6 @@ import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; -export async function assertPRNGIsAvailable() { +export function assertPRNGIsAvailable() { if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.getRandomValues !== 'function') { throw new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED); } diff --git a/packages/assertions/src/index.ts b/packages/assertions/src/index.ts index b3383c7a9ca8..f91038910746 100644 --- a/packages/assertions/src/index.ts +++ b/packages/assertions/src/index.ts @@ -1 +1,2 @@ export * from './subtle-crypto'; +export * from './crypto'; \ No newline at end of file diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index a3b5d9507b27..06100b586b62 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -1,4 +1,4 @@ -import { assertKeyGenerationIsAvailable, assertPRNGIsAvailable, assert } from '@solana/assertions'; +import { assertKeyGenerationIsAvailable, assertPRNGIsAvailable } from '@solana/assertions'; import { SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, @@ -19,7 +19,7 @@ export async function generateKeyPair(): Promise { } export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: boolean): Promise { - await assertPRNGIsAvailable(); + assertPRNGIsAvailable(); if (bytes.byteLength !== 64) { throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength }); From 0f1a0eaaf0cb947104cdfd809ac0855116b99004 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Sat, 23 Mar 2024 00:32:45 -0700 Subject: [PATCH 09/10] Create thick-radios-search.md --- .changeset/thick-radios-search.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/thick-radios-search.md diff --git a/.changeset/thick-radios-search.md b/.changeset/thick-radios-search.md new file mode 100644 index 000000000000..b9c504112dfb --- /dev/null +++ b/.changeset/thick-radios-search.md @@ -0,0 +1,7 @@ +--- +"@solana/assertions": patch +"@solana/errors": patch +"@solana/keys": patch +--- + +`createKeyPairFromBytes()` now validates that the public key imported is the one that would be derived from the private key imported From 6e50c492c72d31a74bdfc816f07267ca40607718 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Sat, 23 Mar 2024 12:46:47 -0400 Subject: [PATCH 10/10] run prettier --- packages/assertions/src/index.ts | 3 ++- packages/keys/src/key-pair.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/assertions/src/index.ts b/packages/assertions/src/index.ts index f91038910746..98dfc6aced80 100644 --- a/packages/assertions/src/index.ts +++ b/packages/assertions/src/index.ts @@ -1,2 +1,3 @@ export * from './subtle-crypto'; -export * from './crypto'; \ No newline at end of file + +export * from './crypto'; diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts index 06100b586b62..32c3264e8510 100644 --- a/packages/keys/src/key-pair.ts +++ b/packages/keys/src/key-pair.ts @@ -20,7 +20,7 @@ export async function generateKeyPair(): Promise { export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: boolean): Promise { assertPRNGIsAvailable(); - + if (bytes.byteLength !== 64) { throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength }); }