diff --git a/packages/status-js/src/client/community/delete_handle-channel-chat-message.ts b/packages/status-js/src/client/community/delete_handle-channel-chat-message.ts index 262a64e..568637c 100644 --- a/packages/status-js/src/client/community/delete_handle-channel-chat-message.ts +++ b/packages/status-js/src/client/community/delete_handle-channel-chat-message.ts @@ -1,4 +1,4 @@ -import { recoverPublicKeyFromMetadata } from '~/src/utils/recover-public-key-from-metadata' +import { recoverPublicKey } from '~/src/utils/recover-public-key' import { ApplicationMetadataMessage } from '../../../protos/application-metadata-message' import { ChatMessage } from '../../../protos/chat-message' @@ -48,7 +48,10 @@ export function handleChannelChatMessage( return } - const publicKey = recoverPublicKeyFromMetadata(decodedMetadata) + const publicKey = recoverPublicKey( + decodedMetadata.signature, + decodedMetadata.payload + ) const decodedPayload = ChatMessage.decode(messageToDecode) diff --git a/packages/status-js/src/client/community/handle-channel-chat-message.ts b/packages/status-js/src/client/community/handle-channel-chat-message.ts index 49c2b07..4be593d 100644 --- a/packages/status-js/src/client/community/handle-channel-chat-message.ts +++ b/packages/status-js/src/client/community/handle-channel-chat-message.ts @@ -2,7 +2,7 @@ // todo?: rename to handle-message import { bytesToHex } from 'ethereum-cryptography/utils' -import { recoverPublicKeyFromMetadata } from '~/src/utils/recover-public-key-from-metadata' +import { recoverPublicKey } from '~/src/utils/recover-public-key' import { ApplicationMetadataMessage } from '../../../protos/application-metadata-message' import { @@ -63,7 +63,10 @@ export function handleChannelChatMessage( // break // } - const publicKey = recoverPublicKeyFromMetadata(decodedMetadata) + const publicKey = recoverPublicKey( + decodedMetadata.signature, + decodedMetadata.payload + ) // todo: merge and process other types of messages // TODO?: ignore messages which are messageType !== COMMUNITY_CHAT diff --git a/packages/status-js/src/utils/recover-public-key-from-metadata.ts b/packages/status-js/src/utils/recover-public-key-from-metadata.ts deleted file mode 100644 index ac860a1..0000000 --- a/packages/status-js/src/utils/recover-public-key-from-metadata.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { keccak256 } from 'ethereum-cryptography/keccak' -import { recoverPublicKey } from 'ethereum-cryptography/secp256k1' - -import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message' - -/** - * returns the public key of the signer - * msg must be the 32-byte keccak hash of the message to be signed. - * sig must be a 65-byte compact ECDSA signature containing the recovery id as the last element. - */ -export function recoverPublicKeyFromMetadata( - metadata: ApplicationMetadataMessage -): Uint8Array { - const signature = metadata.signature.slice(0, 64) - const recoveryId = metadata.signature.slice(-1) - - const pk = recoverPublicKey( - keccak256(metadata.payload), - signature, - Number(recoveryId) - ) - - return pk -} diff --git a/packages/status-js/src/utils/recover-public-key-from-metadata.test.ts b/packages/status-js/src/utils/recover-public-key.test.ts similarity index 53% rename from packages/status-js/src/utils/recover-public-key-from-metadata.test.ts rename to packages/status-js/src/utils/recover-public-key.test.ts index fff5edf..a7727e3 100644 --- a/packages/status-js/src/utils/recover-public-key-from-metadata.test.ts +++ b/packages/status-js/src/utils/recover-public-key.test.ts @@ -1,10 +1,25 @@ -import { recoverPublicKeyFromMetadata } from './recover-public-key-from-metadata' +import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils' -import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message' +import { Account } from '../account' +import { recoverPublicKey } from './recover-public-key' -describe('TODO: recoverPublicKeyFromMetadata', () => { +import type { ApplicationMetadataMessage } from '~/protos/application-metadata-message' + +describe('recoverPublicKey', () => { it('should recover public key', async () => { - const metadataFixture = { + const payload = utf8ToBytes('hello') + + const account = new Account() + const signature = await account.sign(payload) + + expect(bytesToHex(recoverPublicKey(signature, payload))).toEqual( + account.publicKey + ) + }) + + it('should recover public key from fixture', async () => { + const metadataFixture: ApplicationMetadataMessage = { + type: 'TYPE_EMOJI_REACTION' as ApplicationMetadataMessage.Type, signature: new Uint8Array([ 250, 132, 234, 119, 159, 124, 98, 93, 197, 108, 99, 52, 186, 234, 142, 101, 147, 180, 50, 190, 102, 61, 219, 189, 95, 124, 29, 74, 43, 46, 106, @@ -25,8 +40,7 @@ describe('TODO: recoverPublicKeyFromMetadata', () => { 102, 55, 99, 48, 98, 55, 55, 97, 55, 99, 48, 97, 53, 101, 98, 97, 53, 102, 97, 57, 100, 52, 100, 57, 49, 98, 97, 56, 32, 5, 40, 2, ]), - type: 'TYPE_EMOJI_REACTION', - } as unknown as ApplicationMetadataMessage + } const publicKeySnapshot = new Uint8Array([ 4, 172, 65, 157, 172, 154, 139, 187, 88, 130, 90, 60, 222, 96, 238, 240, @@ -36,8 +50,33 @@ describe('TODO: recoverPublicKeyFromMetadata', () => { 99, 24, 17, ]) - const result = recoverPublicKeyFromMetadata(metadataFixture) + const result = recoverPublicKey( + metadataFixture.signature, + metadataFixture.payload + ) expect(result).toEqual(publicKeySnapshot) }) + + it('should not recover public key with different payload', async () => { + const payload = utf8ToBytes('1') + + const account = new Account() + const signature = await account.sign(payload) + + const payload2 = utf8ToBytes('2') + expect(recoverPublicKey(signature, payload2)).not.toEqual(account.publicKey) + }) + + it('should throw error when signature length is not 65 bytes', async () => { + const payload = utf8ToBytes('hello') + const signature = new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + ]) + + // TODO: use toThrowErrorMatchingInlineSnapshot + expect(recoverPublicKey(signature, payload)).toThrow( + 'Signature must be 65 bytes long' + ) + }) }) diff --git a/packages/status-js/src/utils/recover-public-key.ts b/packages/status-js/src/utils/recover-public-key.ts new file mode 100644 index 0000000..df9c3a8 --- /dev/null +++ b/packages/status-js/src/utils/recover-public-key.ts @@ -0,0 +1,25 @@ +import { keccak256 } from 'ethereum-cryptography/keccak' +import { recoverPublicKey as secpRecoverPublicKey } from 'ethereum-cryptography/secp256k1' + +/** + * returns the public key of the signer + * msg must be the 32-byte keccak hash of the message to be signed. + * sig must be a 65-byte compact ECDSA signature containing the recovery id as the last element. + */ +export function recoverPublicKey( + sig: Uint8Array, + payload: Uint8Array +): Uint8Array { + if (sig.length !== 65) { + throw new Error('Signature must be 65 bytes long') + } + + if (sig[64] >= 4) { + throw new Error('Recovery id must be less than 4') + } + + const signature = sig.slice(0, 64) + const recoveryId = sig.slice(-1) + + return secpRecoverPublicKey(keccak256(payload), signature, Number(recoveryId)) +}