diff --git a/packages/status-js/package.json b/packages/status-js/package.json index 434e3960..c4fad170 100644 --- a/packages/status-js/package.json +++ b/packages/status-js/package.json @@ -35,7 +35,8 @@ "dependencies": { "@bufbuild/protobuf": "^1.0.0", "ethereum-cryptography": "^1.0.3", - "js-waku": "^0.30.0" + "js-waku": "^0.30.0", + "multiformats": "^11.0.1" }, "devDependencies": { "@bufbuild/protoc-gen-es": "^1.0.0" diff --git a/packages/status-js/src/utils/deserialize-public-key.test.ts b/packages/status-js/src/utils/deserialize-public-key.test.ts new file mode 100644 index 00000000..daca7dcd --- /dev/null +++ b/packages/status-js/src/utils/deserialize-public-key.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from 'vitest' + +import { deserializePublicKey } from './deserialize-public-key' + +test('should deserialize public key from compressed base58btc encoding', () => { + expect( + deserializePublicKey('zQ3shY7r4cAdg4eUF5dfcuCqCFzWmdjHW4SX5hspM9ucAarfU') + ).toEqual( + '0x029f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb4542955133' + ) +}) + +test('should deserialize public key from compressed hexadecimal encoding', () => { + expect( + deserializePublicKey( + '0x029f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb4542955133' + ) + ).toEqual( + '0x029f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb4542955133' + ) +}) + +test('should deserialize public key from uncompressed hexadecimal encoding', () => { + expect( + deserializePublicKey( + '0x049f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb454295513305b23fcf11d005ee622144fc402b713a8928f80d705781e2e78d701c6e01bfc4' + ) + ).toEqual( + '0x029f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb4542955133' + ) +}) + +test('should throw when deserializing unsupported multibase encoding', () => { + expect(() => + deserializePublicKey('ZQ3shY7r4cAdg4eUF5dfcuCqCFzWmdjHW4SX5hspM9ucAarfU') + ).toThrowError() +}) + +test('should throw when deserializing invalid public key', () => { + expect(() => + deserializePublicKey( + '0x019f196bbfef4fa6a5eb81dd802133a63498325445ca1af1d154b1bb4542955133' + ) + ).toThrowError() +}) diff --git a/packages/status-js/src/utils/deserialize-public-key.ts b/packages/status-js/src/utils/deserialize-public-key.ts new file mode 100644 index 00000000..989562a0 --- /dev/null +++ b/packages/status-js/src/utils/deserialize-public-key.ts @@ -0,0 +1,73 @@ +import { Point } from 'ethereum-cryptography/secp256k1' +import { + toHex, + // utf8ToBytes as toBytes, // see https://github.com/paulmillr/noble-hashes/blob/d76eb7c818931d290c4c27abb778e8e269895154/src/utils.ts#L91-L96 +} from 'ethereum-cryptography/utils' +import { varint } from 'multiformats' +import { base58btc } from 'multiformats/bases/base58' + +/** + * @see https://github.com/multiformats/multibase/blob/af2d36bdfaeaca453d20b18542ca57bd56b51f6c/README.md#multibase-table + */ +const VALID_MULTIBASE_CODES = [ + 'f', // hexadecimal + 'z', // base58btc +] as const + +type MultibaseCode = typeof VALID_MULTIBASE_CODES[number] + +/** + * @see https://pkg.go.dev/github.com/multiformats/go-multicodec#pkg-types + */ +const VALID_MULTICODEC_CODES = [ + 231, // secp256k1-pub (compressed) (0xe7) +] as const + +type MulticodecCode = typeof VALID_MULTICODEC_CODES[number] + +/** + * @see https://specs.status.im/spec/2#public-key-serialization for specification + */ +export function deserializePublicKey( + publicKey: string, // uncompressed, compressed, or compressed & encoded + options = { compress: true } +): string { + const multibasePublicKey = publicKey.replace(/^0[xX]/, 'f') // ensure multibase code for hexadecimal encoding + const multibaseCode = multibasePublicKey[0] as MultibaseCode + + if (!VALID_MULTIBASE_CODES.includes(multibaseCode)) { + throw new Error('Invalid public key multibase code') + } + + let hexadecimalPublicKey: string + switch (multibaseCode) { + case 'z': { + const base58btcPublicKey = base58btc.decode(multibasePublicKey) + const multicodec = varint.decode(base58btcPublicKey) + const multicodecCode = multicodec[0] as MulticodecCode + const multicodecCodeByteLength = multicodec[1] + + if (!VALID_MULTICODEC_CODES.includes(multicodecCode)) { + throw new Error('Invalid public key multicodec code') + } + + hexadecimalPublicKey = toHex( + base58btcPublicKey.slice(multicodecCodeByteLength) + ) + + break + } + + case 'f': { + hexadecimalPublicKey = multibasePublicKey.slice(1) + + break + } + + default: { + throw new Error('Unsupported public key multicodec code') + } + } + + return `0x${Point.fromHex(hexadecimalPublicKey).toHex(options.compress)}` // validates and sets compression +} diff --git a/yarn.lock b/yarn.lock index cd9ff53d..6a89a170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5429,6 +5429,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multiformats@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.1.tgz#ba58c3f69f032ab67dab4b48cc70f01ac2ca07fe" + integrity sha512-atWruyH34YiknSdL5yeIir00EDlJRpHzELYQxG7Iy29eCyL+VrZHpPrX5yqlik3jnuqpLpRKVZ0SGVb9UzKaSA== + multiformats@^9.4.2, multiformats@^9.4.5: version "9.6.4" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.4.tgz#5dce1f11a407dbb69aa612cb7e5076069bb759ca"