UNPKG

eth-signature-verifier

Version:

Minimal Ethereum signature verifier supporting EIP-712, EIP-191, and eth_sign. No ethers.js or web3.js.

210 lines (209 loc) 7.15 kB
import { keccak_256 } from '@noble/hashes/sha3'; import { secp256k1 } from '@noble/curves/secp256k1'; /** * Convert string to UTF-8 bytes (browser compatible) */ export const toUtf8Bytes = (str) => { return new TextEncoder().encode(str); }; /** * Convert hex string to bytes */ export const hexToBytes = (hex) => { const clean = hex.startsWith('0x') ? hex.slice(2) : hex; if (clean.length % 2 !== 0) throw new Error('Invalid hex string'); const bytes = new Uint8Array(clean.length / 2); for (let i = 0; i < clean.length; i += 2) { bytes[i / 2] = parseInt(clean.substr(i, 2), 16); } return bytes; }; /** * Convert bytes to hex string */ export const bytesToHex = (bytes) => { return '0x' + Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); }; /** * Concatenate multiple Uint8Arrays */ export const concatBytes = (...arrays) => { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; }; /** * Keccak256 hash function */ export const keccak256 = (data) => { const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; return bytesToHex(keccak_256(bytes)); }; /** * Hash a personal message (for personal_sign/eth_sign) */ export const hashPersonalMessage = (message) => { const prefix = new Uint8Array([0x19]); const text = new TextEncoder().encode(`Ethereum Signed Message:\n${message.length}`); const messageBytes = new TextEncoder().encode(message); return keccak256(concatBytes(prefix, text, messageBytes)); }; /** * Recover address from hash and signature */ export const recoverAddress = (hash, signature) => { if (!signature.startsWith('0x') || signature.length !== 132) { throw new Error('Invalid signature format'); } const sig = signature.slice(2); const r = sig.slice(0, 64); const s = sig.slice(64, 128); const v = parseInt(sig.slice(128, 130), 16); const recoveryId = v >= 27 ? v - 27 : v; const hashBytes = hexToBytes(hash); const sigBytes = concatBytes(hexToBytes(`0x${r}`), hexToBytes(`0x${s}`)); const signatureObj = secp256k1.Signature.fromCompact(sigBytes); const publicKeyPoint = signatureObj.addRecoveryBit(recoveryId).recoverPublicKey(hashBytes); const publicKeyBytes = publicKeyPoint.toRawBytes(false); if (publicKeyBytes.length !== 65 || publicKeyBytes[0] !== 0x04) { throw new Error('Invalid public key format'); } const publicKeyForHashing = publicKeyBytes.slice(1); const addressHash = keccak_256(publicKeyForHashing); const address = '0x' + bytesToHex(addressHash).slice(-40); return toChecksumAddress(address); }; /** * Convert address to checksum format */ export const toChecksumAddress = (address) => { const addr = address.toLowerCase().replace('0x', ''); const hash = keccak256(addr).replace('0x', ''); let result = '0x'; for (let i = 0; i < addr.length; i++) { const hashChar = hash[i]; const addrChar = addr[i]; if (hashChar && parseInt(hashChar, 16) >= 8) { result += addrChar?.toUpperCase() ?? ''; } else { result += addrChar ?? ''; } } return result; }; /** * Encode a type according to EIP-712 rules */ const encodeType = (name, types) => { const deps = new Set(); function findDependencies(type) { const typeFields = types[type]; if (typeFields) { deps.add(type); for (const field of typeFields) { const baseType = field.type.replace(/\[\]$/, ''); if (types[baseType] && !deps.has(baseType)) { findDependencies(baseType); } } } } findDependencies(name); deps.delete(name); const sortedDeps = [name, ...Array.from(deps).sort()]; return sortedDeps .map(type => { const typeFields = types[type]; if (!typeFields) throw new Error(`Type ${type} not found`); return `${type}(${typeFields.map(f => `${f.type} ${f.name}`).join(',')})`; }) .join(''); }; /** * Hash a type according to EIP-712 */ const hashType = (name, types) => { return keccak256(encodeType(name, types)); }; /** * Encode value according to EIP-712 rules */ const encodeValue = (type, value, types) => { if (type === 'address') { const addr = value.toLowerCase().replace('0x', '').padStart(40, '0'); return hexToBytes('0x' + '000000000000000000000000' + addr); } if (type === 'string' || type === 'bytes') { return hexToBytes(keccak256(value)); } if (type.startsWith('bytes') && type !== 'bytes') { return hexToBytes(value.padEnd(66, '0')); } if (type.startsWith('uint') || type.startsWith('int')) { const bytes = new Uint8Array(32); let bigIntValue = BigInt(value); for (let i = 31; i >= 0; i--) { bytes[i] = Number(bigIntValue & 0xffn); bigIntValue = bigIntValue >> 8n; } return bytes; } if (type === 'bool') { const bytes = new Uint8Array(32); bytes[31] = value ? 1 : 0; return bytes; } if (types[type]) { return hexToBytes(hashStruct(type, value, types)); } throw new Error(`Unsupported type: ${type}`); }; /** * Hash struct according to EIP-712 */ const hashStruct = (name, data, types) => { const typeHash = hashType(name, types); const encodedValues = [hexToBytes(typeHash)]; const typeFields = types[name]; if (!typeFields) throw new Error(`Type ${name} not found`); for (const field of typeFields) { const value = data[field.name]; if (value === undefined || value === null) { throw new Error(`Missing value for field: ${field.name}`); } encodedValues.push(encodeValue(field.type, value, types)); } return keccak256(concatBytes(...encodedValues)); }; /** * Hash typed data according to EIP-712 */ export const hashTypedData = (domain, types, value) => { const allTypes = { EIP712Domain: [ ...(domain.name !== undefined ? [{ name: 'name', type: 'string' }] : []), ...(domain.version !== undefined ? [{ name: 'version', type: 'string' }] : []), ...(domain.chainId !== undefined ? [{ name: 'chainId', type: 'uint256' }] : []), ...(domain.verifyingContract !== undefined ? [{ name: 'verifyingContract', type: 'address' }] : []), ], ...types }; const primaryType = Object.keys(types)[0]; if (!primaryType) throw new Error('No primary type found'); const domainSeparator = hashStruct('EIP712Domain', domain, allTypes); const structHash = hashStruct(primaryType, value, allTypes); const prefix = new Uint8Array([0x19, 0x01]); return keccak256(concatBytes(prefix, hexToBytes(domainSeparator), hexToBytes(structHash))); };