From bf1e94dd7f5d3220a982708125eb920f02f7832d Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Fri, 18 Apr 2025 03:29:47 -0400 Subject: [PATCH 1/7] feat: update signTypedData encodeField address - refactor - adds length validation --- src/sign-typed-data.test.ts | 2 +- src/sign-typed-data.ts | 59 ++++++++++++++++++++++++++++++++----- src/utils.ts | 21 +++++++++++++ 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/sign-typed-data.test.ts b/src/sign-typed-data.test.ts index aee9e873..ff6df74f 100644 --- a/src/sign-typed-data.test.ts +++ b/src/sign-typed-data.test.ts @@ -318,7 +318,7 @@ const encodeDataErrorExamples = { { input: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB0', errorMessage: - 'Unable to encode value: Invalid address value. Expected address to be 20 bytes long, but received 21 bytes.', + 'Unable to encode field: Invalid address value. Expected address to be 20 bytes long, but received 21 bytes.', }, ], int8: [ diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index c0ce1d3c..eaf8240f 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -8,7 +8,6 @@ import { } from '@metamask/abi-utils/dist/parsers'; import { padStart } from '@metamask/abi-utils/dist/utils'; import { - add0x, assert, bigIntToBytes, bytesToHex, @@ -26,6 +25,7 @@ import { isNullish, legacyToBuffer, recoverPublicKey, + isValidEVMAddress, } from './utils'; /** @@ -220,6 +220,52 @@ function reallyStrangeAddressToBytes(address: string): Uint8Array { return padStart(bigIntToBytes(addressValue), 20); } +/** + * Validates and normalizes an address value to ensure it's a valid EVM address. + * + * @param value - The address value to validate and normalize. + * @returns The normalized address as a Uint8Array. + * @throws Error if the address is invalid. + */ +function normalizeAndValidateAddress(value: unknown): Uint8Array { + let addressBytes: Uint8Array | undefined; + let addressHex: string | undefined; + + if (typeof value === 'number') { + addressBytes = padStart(numberToBytes(value), 20); + addressHex = bytesToHex(addressBytes); + } + + if (typeof value === 'string') { + if (isStrictHexString(value)) { + addressHex = value; + addressBytes = hexToBytes(value); + } else { + // For non-hex strings, use legacy conversion + addressBytes = reallyStrangeAddressToBytes(value).subarray(0, 20); + addressHex = bytesToHex(addressBytes); + } + } + + if (!addressBytes) { + throw new Error( + `Invalid address value. Address must be a number or a string.`, + ); + } + if (addressBytes.length > 20) { + throw new Error( + `Invalid address value. Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`, + ); + } + if (!isValidEVMAddress(addressHex)) { + throw new Error( + `Invalid address value. Address must be a 0x-prefixed hex string with no more than 40 characters.`, + ); + } + + return addressBytes; +} + /** * Encode a single field. * @@ -261,12 +307,11 @@ function encodeField( } if (type === 'address') { - if (typeof value === 'number') { - return ['address', padStart(numberToBytes(value), 20)]; - } else if (isStrictHexString(value)) { - return ['address', add0x(value)]; - } else if (typeof value === 'string') { - return ['address', reallyStrangeAddressToBytes(value).subarray(0, 20)]; + try { + const addressBytes = normalizeAndValidateAddress(value); + return ['address', addressBytes]; + } catch (error) { + throw new Error(`Unable to encode field: ${String(error.message)}`); } } diff --git a/src/utils.ts b/src/utils.ts index 940b835c..c61aae73 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -55,6 +55,27 @@ export function isNullish(value) { return value === null || value === undefined; } +/** + * Validates if a string is a valid EVM address. + * A valid EVM address is a 0x-prefixed hex string of 40 characters (20 bytes). + * We have an exception for addresses with less than 40 characters in case we need to + * support addresses which may be padded with leading 0s. + * + * @param address - The address to validate. + * @returns True if the address is valid, false otherwise. + */ +export function isValidEVMAddress(address: unknown): boolean { + if (typeof address !== 'string') { + return false; + } + + if (address.length > 42) { + return false; + } + + return /^0[xX][a-fA-F0-9]{1,40}$/u.test(address); +} + /** * Convert a value to a Buffer. This function should be equivalent to the `toBuffer` function in * `ethereumjs-util@5.2.1`. From e5a7e1ebebc960c15437d4c88bcca7b84ef43afd Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:10:30 -0400 Subject: [PATCH 2/7] refactor: assert characters before legacy conversion legacy conversion coerces values. We want to detect symbols and other nonvalid chars beforehand --- src/sign-typed-data.ts | 49 +++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index eaf8240f..1ef47200 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -237,6 +237,28 @@ function normalizeAndValidateAddress(value: unknown): Uint8Array { } if (typeof value === 'string') { + const has0xPrefix = value.toLowerCase().startsWith('0x'); + const valueWithoutPrefix = has0xPrefix ? value.slice(2) : value; + + // find any invalid characters including unicode characters + // eslint-disable-next-line require-unicode-regexp + const invalidCharMatch = valueWithoutPrefix.match(/[^0-9a-fA-F]/gu); + if (invalidCharMatch) { + const invalidChar = invalidCharMatch[0]; + assert( + !invalidChar.match(/[g-zG-Z]/u), + `Invalid address value. Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`, + ); + assert( + !invalidChar.match(/[\s\n\r\t\f\v]/u), + `Invalid address value. Contains whitespace character "${invalidChar}".`, + ); + assert( + false, + `Invalid address value. Contains invalid character "${invalidChar}".`, + ); + } + if (isStrictHexString(value)) { addressHex = value; addressBytes = hexToBytes(value); @@ -247,21 +269,18 @@ function normalizeAndValidateAddress(value: unknown): Uint8Array { } } - if (!addressBytes) { - throw new Error( - `Invalid address value. Address must be a number or a string.`, - ); - } - if (addressBytes.length > 20) { - throw new Error( - `Invalid address value. Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`, - ); - } - if (!isValidEVMAddress(addressHex)) { - throw new Error( - `Invalid address value. Address must be a 0x-prefixed hex string with no more than 40 characters.`, - ); - } + assert( + addressBytes, + 'Invalid address value. Address must be a number or a string.', + ); + assert( + addressBytes.length <= 20, + `Invalid address value. Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`, + ); + assert( + addressHex?.match(/^0[xX]([a-fA-F0-9]{0,40})$/u) !== null, + `Invalid address value. Address must be a 0x-prefixed hex string with no more than 40 characters.`, + ); return addressBytes; } From 1c548af46b0d4b3915fee2087bf9e01834cf4090 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:10:49 -0400 Subject: [PATCH 3/7] test: add new signTypedData address validation cases --- .../sign-typed-data.test.ts.snap | 42 +++++++++++++++++-- src/sign-typed-data.test.ts | 32 ++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/__snapshots__/sign-typed-data.test.ts.snap b/src/__snapshots__/sign-typed-data.test.ts.snap index 039cf805..d71274ab 100644 --- a/src/__snapshots__/sign-typed-data.test.ts.snap +++ b/src/__snapshots__/sign-typed-data.test.ts.snap @@ -24,6 +24,12 @@ exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with extra dom exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with only custom domain seperator fields 1`] = `"3efa3ef0305f56ba5bba62000500e29fe82c5314bca2f958c64e31b2498560f8"`; +exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0X" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`; + +exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"`; + +exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`; + exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x0" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000000"`; exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x1fffffffffffff" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000000000000000000000000000001fffffffffffff"`; @@ -202,6 +208,12 @@ exports[`TypedDataUtils.encodeData V3 should encode data with an atomic property exports[`TypedDataUtils.encodeData V3 should encode data with custom type 1`] = `"a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"`; +exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0X" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`; + +exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"`; + +exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`; + exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x0" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000000"`; exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x1fffffffffffff" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000000000000000000000000000001fffffffffffff"`; @@ -218,7 +230,7 @@ exports[`TypedDataUtils.encodeData V4 example data type "address" should encode exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada29900000000000000000000000000000023eafa60608dacea43aa64cbe38e38e38d"`; -exports[`TypedDataUtils.encodeData V4 example data type "address" should encode array of all address example data 1`] = `"1cf487eff7ac1d95e8069104ae1f91e4ecc076c3a5c23645a8b93a413d1bc7118eb4f5f4cd3a16acaa954558d08ceb9f1a1dfd26dbf9eb2b780b871fa554bbcd"`; +exports[`TypedDataUtils.encodeData V4 example data type "address" should encode array of all address example data 1`] = `"1cf487eff7ac1d95e8069104ae1f91e4ecc076c3a5c23645a8b93a413d1bc711ff407f9cddb5e3ea5a2e1b929a5e42c275df9f78cbc1b361aab9a05c40a7769e"`; exports[`TypedDataUtils.encodeData V4 example data type "bool" should encode "-1" (type "number") 1`] = `"83478c46da931c2f6871fdb6febd69a27b0ebc8c91f3e2cf580c3bd8777690060000000000000000000000000000000000000000000000000000000000000001"`; @@ -404,6 +416,12 @@ exports[`TypedDataUtils.encodeData V4 should encode data with a recursive data t exports[`TypedDataUtils.encodeData V4 should encode data with custom type 1`] = `"a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"`; +exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0X" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`; + +exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"ab14541443c6f456616e472ecca6d4fc887f862760d3a056b348441a9e0fd1c6"`; + +exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`; + exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x0" (type "string") 1`] = `"c93aa5d5f1b1efece209c34fceee0aeb33da38ea34dbdd4df854e9708a636fea"`; exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x1fffffffffffff" (type "string") 1`] = `"edb736a49622b7860bcec9c56c2b0346870f5bf02b7e516722c51be85c740bd3"`; @@ -582,6 +600,12 @@ exports[`TypedDataUtils.hashStruct V3 should hash data with an atomic property s exports[`TypedDataUtils.hashStruct V3 should hash data with custom type 1`] = `"c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e"`; +exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0X" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`; + +exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"ab14541443c6f456616e472ecca6d4fc887f862760d3a056b348441a9e0fd1c6"`; + +exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`; + exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x0" (type "string") 1`] = `"c93aa5d5f1b1efece209c34fceee0aeb33da38ea34dbdd4df854e9708a636fea"`; exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x1fffffffffffff" (type "string") 1`] = `"edb736a49622b7860bcec9c56c2b0346870f5bf02b7e516722c51be85c740bd3"`; @@ -598,7 +622,7 @@ exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "9 exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"4d9b02d96f7647b8d3acfafc97b2faa65ef7b442ea647f8c8f6f606d4d794d34"`; -exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash array of all address example data 1`] = `"d714a2e5e9f4db8583cd86e447c95fdbb86e6fe7a8fdc576197c3f0805d1a693"`; +exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash array of all address example data 1`] = `"380bb6cbe741d8b2158a8aed568d46298719e56806d9be5c13bdd130b3ade39a"`; exports[`TypedDataUtils.hashStruct V4 example data type "bool" should hash "-1" (type "number") 1`] = `"df410941012cfe7070ec9abfdb367531856dda97faaa5134e46c313058e8415e"`; @@ -936,6 +960,12 @@ exports[`signTypedData V1 should sign data with a dynamic property set to undefi exports[`signTypedData V1 should sign data with an atomic property set to undefined 1`] = `[Function]`; +exports[`signTypedData V3 example data type "address" should sign "0X" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`; + +exports[`signTypedData V3 example data type "address" should sign "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x9dfeb4cb581e1cc8ef7f1192dbbaa05a67545116c1ecb951b8aa5e7e2167e1ca463e8260231fe82395a38a597c90537f3282f6197c17b7e04baf52c5250fa3831b"`; + +exports[`signTypedData V3 example data type "address" should sign "0x" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`; + exports[`signTypedData V3 example data type "address" should sign "0x0" (type "string") 1`] = `"0xe73f6e0860ebb2660c79396142325bd00033405615b608c757901a928f81533d78441e8c323c832e72c96bf6f95d874848336537f30081b5de518a26e9fae1891c"`; exports[`signTypedData V3 example data type "address" should sign "0x1fffffffffffff" (type "string") 1`] = `"0xd27fb914606ad217b3a238d24e86dd73499806fd45f6b229f50e242ec8eefe4b6325b3bcc8dc310c851379bc55d41b928d8b2828565f892819daccba3aeba0381b"`; @@ -1122,6 +1152,12 @@ exports[`signTypedData V3 should sign data with an atomic property set to undefi exports[`signTypedData V3 should sign data with custom type 1`] = `"0x22ee0cb3a379f3a122f7b59456ebcf404ca139320a0723560abde49cc95f4a2f69774bf94c4e776f1a9c8c8a67e9e2bdda131e04bde935f73fae376ee788920d1c"`; +exports[`signTypedData V4 example data type "address" should sign "0X" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`; + +exports[`signTypedData V4 example data type "address" should sign "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x9dfeb4cb581e1cc8ef7f1192dbbaa05a67545116c1ecb951b8aa5e7e2167e1ca463e8260231fe82395a38a597c90537f3282f6197c17b7e04baf52c5250fa3831b"`; + +exports[`signTypedData V4 example data type "address" should sign "0x" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`; + exports[`signTypedData V4 example data type "address" should sign "0x0" (type "string") 1`] = `"0xe73f6e0860ebb2660c79396142325bd00033405615b608c757901a928f81533d78441e8c323c832e72c96bf6f95d874848336537f30081b5de518a26e9fae1891c"`; exports[`signTypedData V4 example data type "address" should sign "0x1fffffffffffff" (type "string") 1`] = `"0xd27fb914606ad217b3a238d24e86dd73499806fd45f6b229f50e242ec8eefe4b6325b3bcc8dc310c851379bc55d41b928d8b2828565f892819daccba3aeba0381b"`; @@ -1138,7 +1174,7 @@ exports[`signTypedData V4 example data type "address" should sign "9007199254740 exports[`signTypedData V4 example data type "address" should sign "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x47ad9b795affe960475274f8b067225cff2451eada53b0ccf9bebd4336674b4b35b9ac6bad764f197435c7d53b6992a6d5f835796346a6ee4fd2313f8dd5b8991b"`; -exports[`signTypedData V4 example data type "address" should sign array of all address example data 1`] = `"0x4a47328b668266b3bdaa67d9c1ac201a4a2245ed185f9b81adb4acb6eb1231fe58e08ef8dd8ed85b4a264219d297e8275b761216688c8ed9bb7b367e9b99435b1c"`; +exports[`signTypedData V4 example data type "address" should sign array of all address example data 1`] = `"0x99a49e40de4b2776ac55c077b5040232a3f3b4fd6ad604c4800b6b121889748a2eef76371778e18d2ad6522997803aea41d898ff61b9ab3b99862085ca3e77bc1c"`; exports[`signTypedData V4 example data type "bool" should sign "-1" (type "number") 1`] = `"0x5266f7fdc7b8d6552656609f7160760f323a4b37ba80e41b33fdb5637349538c123e338354fd3c0fa0741725c3a273a3857d9a2f490b7ebd612b83d10b2246a11b"`; diff --git a/src/sign-typed-data.test.ts b/src/sign-typed-data.test.ts index ff6df74f..63eedc5a 100644 --- a/src/sign-typed-data.test.ts +++ b/src/sign-typed-data.test.ts @@ -269,6 +269,11 @@ const encodeDataExamples = { // atomic types supported by EIP-712: address: [ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + + // backward compatibility for non-standard Ethereum address values + '0x', + '0X', '0x0', // odd '0x10', // even 10, @@ -320,6 +325,33 @@ const encodeDataErrorExamples = { errorMessage: 'Unable to encode field: Invalid address value. Expected address to be 20 bytes long, but received 21 bytes.', }, + { + input: true, + errorMessage: + 'Invalid address value. Address must be a number or a string.', + }, + { + input: '0xa+ddress+$ymbols#', + errorMessage: 'Invalid address value. Contains invalid character "+".', + }, + { + input: '0x12345你好67890', + errorMessage: 'Invalid address value. Contains invalid character "你".', + }, + { + input: '0xbBzZBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + errorMessage: + 'Invalid address value. Contains invalid letter "z". Only a-f and A-F are valid hex letters.', + }, + { + input: '0xbBzZBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + errorMessage: + 'Invalid address value. Contains invalid letter "z". Only a-f and A-F are valid hex letters.', + }, + { + input: '0xbBbbBBB bbBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + errorMessage: 'Invalid address value. Contains whitespace character " ".', + }, ], int8: [ { From 430176509aeb9d1e0957f6c9a2d5eaedb8e65e7a Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:40:24 -0400 Subject: [PATCH 4/7] refactor: signTypedData address error handling --- src/sign-typed-data.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index 1ef47200..0cfffc41 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -247,16 +247,13 @@ function normalizeAndValidateAddress(value: unknown): Uint8Array { const invalidChar = invalidCharMatch[0]; assert( !invalidChar.match(/[g-zG-Z]/u), - `Invalid address value. Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`, + `Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`, ); assert( !invalidChar.match(/[\s\n\r\t\f\v]/u), - `Invalid address value. Contains whitespace character "${invalidChar}".`, - ); - assert( - false, - `Invalid address value. Contains invalid character "${invalidChar}".`, + `Contains whitespace character "${invalidChar}".`, ); + assert(false, `Contains invalid character "${invalidChar}".`); } if (isStrictHexString(value)) { @@ -269,17 +266,14 @@ function normalizeAndValidateAddress(value: unknown): Uint8Array { } } - assert( - addressBytes, - 'Invalid address value. Address must be a number or a string.', - ); + assert(addressBytes, 'Address must be a number or a string.'); assert( addressBytes.length <= 20, - `Invalid address value. Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`, + `Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`, ); assert( addressHex?.match(/^0[xX]([a-fA-F0-9]{0,40})$/u) !== null, - `Invalid address value. Address must be a 0x-prefixed hex string with no more than 40 characters.`, + `Address must be a 0x-prefixed hex string with no more than 40 characters.`, ); return addressBytes; @@ -329,8 +323,15 @@ function encodeField( try { const addressBytes = normalizeAndValidateAddress(value); return ['address', addressBytes]; - } catch (error) { - throw new Error(`Unable to encode field: ${String(error.message)}`); + } catch (err) { + if (typeof err?.message === 'string' && err.message.length) { + throw new Error( + `Unable to encode field: Invalid address value. ${ + err.message as string + }`, + ); + } + throw new Error(`Unable to encode field: Invalid address value.`); } } From 5d7edfde1dc6a2dc19d470656b6090fa1adac89e Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:41:02 -0400 Subject: [PATCH 5/7] refactor: rm isValidEVMAddress --- src/sign-typed-data.ts | 1 - src/utils.ts | 21 --------------------- 2 files changed, 22 deletions(-) diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index 0cfffc41..043c375d 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -25,7 +25,6 @@ import { isNullish, legacyToBuffer, recoverPublicKey, - isValidEVMAddress, } from './utils'; /** diff --git a/src/utils.ts b/src/utils.ts index c61aae73..940b835c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -55,27 +55,6 @@ export function isNullish(value) { return value === null || value === undefined; } -/** - * Validates if a string is a valid EVM address. - * A valid EVM address is a 0x-prefixed hex string of 40 characters (20 bytes). - * We have an exception for addresses with less than 40 characters in case we need to - * support addresses which may be padded with leading 0s. - * - * @param address - The address to validate. - * @returns True if the address is valid, false otherwise. - */ -export function isValidEVMAddress(address: unknown): boolean { - if (typeof address !== 'string') { - return false; - } - - if (address.length > 42) { - return false; - } - - return /^0[xX][a-fA-F0-9]{1,40}$/u.test(address); -} - /** * Convert a value to a Buffer. This function should be equivalent to the `toBuffer` function in * `ethereumjs-util@5.2.1`. From 9ff635ba0cde7da49cf5b83d8d422052a08e6a77 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:46:46 -0400 Subject: [PATCH 6/7] refactor: assertHexAddressChars --- src/sign-typed-data.ts | 46 +++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index 043c375d..a5886727 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -179,6 +179,33 @@ function parseNumber(type: string, value: string | number | bigint) { return bigIntValue; } +/** + * Asserts that the given value does not contain any invalid hex characters. + * + * @param value - The value to validate. + * @throws Error if the value contains any invalid hex characters. + */ +function assertHexAddressChars(value: string) { + const has0xPrefix = value.toLowerCase().startsWith('0x'); + const valueWithoutPrefix = has0xPrefix ? value.slice(2) : value; + + // find any invalid characters including unicode characters + // eslint-disable-next-line require-unicode-regexp + const invalidCharMatch = valueWithoutPrefix.match(/[^0-9a-fA-F]/gu); + if (invalidCharMatch) { + const invalidChar = invalidCharMatch[0]; + assert( + !invalidChar.match(/[g-zG-Z]/u), + `Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`, + ); + assert( + !invalidChar.match(/[\s\n\r\t\f\v]/u), + `Contains whitespace character "${invalidChar}".`, + ); + assert(false, `Contains invalid character "${invalidChar}".`); + } +} + /** * Parse an address string to a `Uint8Array`. The behaviour of this is quite * strange, in that it does not parse the address as hexadecimal string, nor as @@ -236,24 +263,7 @@ function normalizeAndValidateAddress(value: unknown): Uint8Array { } if (typeof value === 'string') { - const has0xPrefix = value.toLowerCase().startsWith('0x'); - const valueWithoutPrefix = has0xPrefix ? value.slice(2) : value; - - // find any invalid characters including unicode characters - // eslint-disable-next-line require-unicode-regexp - const invalidCharMatch = valueWithoutPrefix.match(/[^0-9a-fA-F]/gu); - if (invalidCharMatch) { - const invalidChar = invalidCharMatch[0]; - assert( - !invalidChar.match(/[g-zG-Z]/u), - `Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`, - ); - assert( - !invalidChar.match(/[\s\n\r\t\f\v]/u), - `Contains whitespace character "${invalidChar}".`, - ); - assert(false, `Contains invalid character "${invalidChar}".`); - } + assertHexAddressChars(value); if (isStrictHexString(value)) { addressHex = value; From 1a47039b7f6bb8db1516fe173fd4bc27c2af66e3 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:50:32 -0400 Subject: [PATCH 7/7] refactor: rn validateAndNormalizeAddress --- src/sign-typed-data.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index a5886727..a4825ad7 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -253,7 +253,7 @@ function reallyStrangeAddressToBytes(address: string): Uint8Array { * @returns The normalized address as a Uint8Array. * @throws Error if the address is invalid. */ -function normalizeAndValidateAddress(value: unknown): Uint8Array { +function validateAndNormalizeAddress(value: unknown): Uint8Array { let addressBytes: Uint8Array | undefined; let addressHex: string | undefined; @@ -330,7 +330,7 @@ function encodeField( if (type === 'address') { try { - const addressBytes = normalizeAndValidateAddress(value); + const addressBytes = validateAndNormalizeAddress(value); return ['address', addressBytes]; } catch (err) { if (typeof err?.message === 'string' && err.message.length) {