diff --git a/packages/web3-errors/src/error_codes.ts b/packages/web3-errors/src/error_codes.ts index 1db99b30ea7..dc718eb2063 100644 --- a/packages/web3-errors/src/error_codes.ts +++ b/packages/web3-errors/src/error_codes.ts @@ -29,6 +29,7 @@ export const ERR_ABI_ENCODING = 205; export const ERR_EXISTING_PLUGIN_NAMESPACE = 206; export const ERR_INVALID_METHOD_PARAMS = 207; export const ERR_MULTIPLE_ERRORS = 208; +export const ERR_INSECURE_CONTEXT = 209; // Contract error codes export const ERR_CONTRACT = 300; diff --git a/packages/web3-errors/src/errors/generic_errors.ts b/packages/web3-errors/src/errors/generic_errors.ts index 1e246bf88ff..c572f9b2cc3 100644 --- a/packages/web3-errors/src/errors/generic_errors.ts +++ b/packages/web3-errors/src/errors/generic_errors.ts @@ -26,6 +26,7 @@ import { ERR_PARAM, ERR_EXISTING_PLUGIN_NAMESPACE, ERR_INVALID_METHOD_PARAMS, + ERR_INSECURE_CONTEXT, } from '../error_codes.js'; import { BaseWeb3Error } from '../web3_error_base.js'; @@ -81,6 +82,10 @@ export class OperationAbortError extends BaseWeb3Error { public code = ERR_OPERATION_ABORT; } +export class InsecureContextError extends BaseWeb3Error { + public code = ERR_INSECURE_CONTEXT; +} + export class AbiError extends BaseWeb3Error { public code = ERR_ABI_ENCODING; public readonly props: Record & { name?: string }; diff --git a/packages/web3-eth-accounts/src/account.ts b/packages/web3-eth-accounts/src/account.ts index 8b8dcce7fe5..4ef62aae189 100644 --- a/packages/web3-eth-accounts/src/account.ts +++ b/packages/web3-eth-accounts/src/account.ts @@ -49,6 +49,7 @@ import { import { pbkdf2Sync } from 'ethereum-cryptography/pbkdf2.js'; import { scryptSync } from 'ethereum-cryptography/scrypt.js'; import { + InsecureContextError, InvalidKdfError, InvalidPasswordError, InvalidPrivateKeyError, @@ -126,7 +127,10 @@ export const parseAndValidatePrivateKey = (data: Bytes, ignoreLength?: boolean): } try { - privateKeyUint8Array = data instanceof Uint8Array ? data : bytesToUint8Array(data); + privateKeyUint8Array = + data instanceof Uint8Array || data?.constructor?.name === 'Uint8Array' + ? (data as Uint8Array) + : bytesToUint8Array(data); } catch { throw new InvalidPrivateKeyError(); } @@ -406,7 +410,7 @@ export const recover = ( const V_INDEX = 130; // r = first 32 bytes, s = second 32 bytes, v = last byte of signature const hashedMessage = prefixedOrR ? data : hashMessage(data); - let v = parseInt(signatureOrV.substring(V_INDEX),16); // 0x + r + s + v + let v = parseInt(signatureOrV.substring(V_INDEX), 16); // 0x + r + s + v if (v > 26) { v -= 27; } @@ -421,7 +425,7 @@ export const recover = ( const address = toChecksumAddress(`0x${publicHash.slice(-40)}`); return address; -}; +};; /** * Get the ethereum Address from a private key @@ -456,7 +460,7 @@ export const privateKeyToAddress = (privateKey: Bytes): string => { * Get the public key from a private key * * @param privateKey - String or Uint8Array of 32 bytes - * @param isCompressed - if true, will generate a 33 byte compressed public key instead of a 65 byte public key + * @param isCompressed - if true, will generate a 33 byte compressed public key instead of a 65 byte public key * @returns The public key * @example * ```ts @@ -465,7 +469,7 @@ export const privateKeyToAddress = (privateKey: Bytes): string => { * > "0x42beb65f179720abaa3ec9a70a539629cbbc5ec65bb57e7fc78977796837e537662dd17042e6449dc843c281067a4d6d8d1a1775a13c41901670d5de7ee6503a" // uncompressed public key * ``` */ - export const privateKeyToPublicKey = (privateKey: Bytes, isCompressed: boolean): string => { +export const privateKeyToPublicKey = (privateKey: Bytes, isCompressed: boolean): string => { const privateKeyUint8Array = parseAndValidatePrivateKey(privateKey); // Get public key from private key in compressed format @@ -618,6 +622,13 @@ export const encrypt = async ( throw new InvalidKdfError(); } + // https://stackoverflow.com/a/46468377/8303489 + // > crypto.subtle is supposed to be undefined in insecure contexts + if (typeof window !== 'undefined' && !window?.crypto?.subtle) { + throw new InsecureContextError( + 'crypto.subtle is supposed to be undefined in insecure contexts', + ); + } const cipher = await createCipheriv( privateKeyUint8Array, derivedKey.slice(0, 16), diff --git a/packages/web3-eth-accounts/src/tx/transactionFactory.ts b/packages/web3-eth-accounts/src/tx/transactionFactory.ts index e4da9fcfe6e..7434a861d89 100644 --- a/packages/web3-eth-accounts/src/tx/transactionFactory.ts +++ b/packages/web3-eth-accounts/src/tx/transactionFactory.ts @@ -134,8 +134,8 @@ export class TransactionFactory { * @param txOptions - The transaction options */ public static fromBlockBodyData(data: Uint8Array | Uint8Array[], txOptions: TxOptions = {}) { - if (data instanceof Uint8Array) { - return this.fromSerializedData(data, txOptions); + if (data instanceof Uint8Array || data?.constructor?.name === 'Uint8Array') { + return this.fromSerializedData(data as Uint8Array, txOptions); } if (Array.isArray(data)) { // It is a legacy transaction diff --git a/packages/web3-eth-accounts/test/unit/encrypt-jsdom.test.ts b/packages/web3-eth-accounts/test/unit/encrypt-jsdom.test.ts new file mode 100644 index 00000000000..9ba5d0071cb --- /dev/null +++ b/packages/web3-eth-accounts/test/unit/encrypt-jsdom.test.ts @@ -0,0 +1,42 @@ +/** + * @jest-environment jsdom + */ + +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +// ignore the rule `header/header` to allow keeping `@jest-environment jsdom` on top: +// eslint-disable-next-line header/header +import { TextEncoder } from 'util'; + +/* eslint-disable import/first */ +global.TextEncoder = TextEncoder; // polyfill TextEncoder for jsdom + +import { InsecureContextError } from 'web3-errors'; +import { encrypt } from '../../src/account'; +import { validEncryptData } from '../fixtures/account'; + +describe('encrypt/decrypt in jsdom', () => { + describe('encrypt', () => { + describe('valid cases', () => { + it.each(validEncryptData)('%s', async input => { + const result = encrypt(input[0], input[1], input[2]); + await expect(result).rejects.toThrow(InsecureContextError); + }); + }); + }); +}); diff --git a/packages/web3-utils/src/converters.ts b/packages/web3-utils/src/converters.ts index ef7f1307a77..3b4cc6ce3dc 100644 --- a/packages/web3-utils/src/converters.ts +++ b/packages/web3-utils/src/converters.ts @@ -90,8 +90,8 @@ export type EtherUnits = keyof typeof ethUnitMap; export const bytesToUint8Array = (data: Bytes): Uint8Array | never => { validator.validate(['bytes'], [data]); - if (data instanceof Uint8Array) { - return data; + if (data instanceof Uint8Array || data?.constructor?.name === 'Uint8Array') { + return data as Uint8Array; } if (Array.isArray(data)) { @@ -588,7 +588,11 @@ export const toChecksumAddress = (address: Address): string => { const lowerCaseAddress = address.toLowerCase().replace(/^0x/i, ''); - const hash = utils.uint8ArrayToHexString(keccak256(utf8ToBytes(lowerCaseAddress))); + // calling `Uint8Array.from` because `noble-hashes` checks with `instanceof Uint8Array` that fails in some edge cases: + // https://github.com/paulmillr/noble-hashes/issues/25#issuecomment-1750106284 + const hash = utils.uint8ArrayToHexString( + keccak256(Uint8Array.from(utf8ToBytes(lowerCaseAddress))), + ); if ( isNullish(hash) || diff --git a/packages/web3-validator/src/validation/bytes.ts b/packages/web3-validator/src/validation/bytes.ts index ef15fbc0120..5cc4bcf63d8 100644 --- a/packages/web3-validator/src/validation/bytes.ts +++ b/packages/web3-validator/src/validation/bytes.ts @@ -22,7 +22,8 @@ import { isHexStrict } from './string.js'; /** * checks input if typeof data is valid Uint8Array input */ -export const isUint8Array = (data: ValidInputTypes) => data instanceof Uint8Array; +export const isUint8Array = (data: ValidInputTypes) => + data instanceof Uint8Array || data?.constructor?.name === 'Uint8Array'; export const isBytes = ( value: ValidInputTypes | Uint8Array | number[], @@ -30,7 +31,12 @@ export const isBytes = ( abiType: 'bytes', }, ) => { - if (typeof value !== 'string' && !Array.isArray(value) && !(value instanceof Uint8Array)) { + if ( + typeof value !== 'string' && + !Array.isArray(value) && + !(value instanceof Uint8Array) && + value?.constructor?.name !== 'Uint8Array' + ) { return false; } @@ -57,7 +63,7 @@ export const isBytes = ( } valueToCheck = new Uint8Array(value); } else { - valueToCheck = value; + valueToCheck = value as Uint8Array; } if (options?.abiType) {