UNPKG

eth-sig-util

Version:

A few useful functions for signing ethereum data

512 lines 21.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.recoverTypedSignature_v4 = exports.recoverTypedSignature = exports.signTypedData_v4 = exports.signTypedData = exports.recoverTypedMessage = exports.signTypedMessage = exports.getEncryptionPublicKey = exports.decryptSafely = exports.decrypt = exports.encryptSafely = exports.encrypt = exports.recoverTypedSignatureLegacy = exports.signTypedDataLegacy = exports.typedSignatureHash = exports.extractPublicKey = exports.recoverPersonalSignature = exports.personalSign = exports.normalize = exports.concatSig = exports.TypedDataUtils = exports.TYPED_MESSAGE_SCHEMA = void 0; const ethUtil = __importStar(require("ethereumjs-util")); const ethAbi = __importStar(require("ethereumjs-abi")); const nacl = __importStar(require("tweetnacl")); const naclUtil = __importStar(require("tweetnacl-util")); const TYPED_MESSAGE_SCHEMA = { type: 'object', properties: { types: { type: 'object', additionalProperties: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, type: { type: 'string' }, }, required: ['name', 'type'], }, }, }, primaryType: { type: 'string' }, domain: { type: 'object' }, message: { type: 'object' }, }, required: ['types', 'primaryType', 'domain', 'message'], }; exports.TYPED_MESSAGE_SCHEMA = TYPED_MESSAGE_SCHEMA; /** * A collection of utility functions used for signing typed data */ const TypedDataUtils = { /** * Encodes an object by encoding and concatenating each of its members * * @param {string} primaryType - Root type * @param {Object} data - Object to encode * @param {Object} types - Type definitions * @returns {Buffer} - Encoded representation of an object */ encodeData(primaryType, data, types, useV4 = true) { const encodedTypes = ['bytes32']; const encodedValues = [this.hashType(primaryType, types)]; if (useV4) { const encodeField = (name, type, value) => { if (types[type] !== undefined) { return [ 'bytes32', value == null // eslint-disable-line no-eq-null ? '0x0000000000000000000000000000000000000000000000000000000000000000' : ethUtil.keccak(this.encodeData(type, value, types, useV4)), ]; } if (value === undefined) { throw new Error(`missing value for field ${name} of type ${type}`); } if (type === 'bytes') { return ['bytes32', ethUtil.keccak(value)]; } if (type === 'string') { // convert string to buffer - prevents ethUtil from interpreting strings like '0xabcd' as hex if (typeof value === 'string') { value = Buffer.from(value, 'utf8'); } return ['bytes32', ethUtil.keccak(value)]; } if (type.lastIndexOf(']') === type.length - 1) { const parsedType = type.slice(0, type.lastIndexOf('[')); const typeValuePairs = value.map((item) => encodeField(name, parsedType, item)); return [ 'bytes32', ethUtil.keccak(ethAbi.rawEncode(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v))), ]; } return [type, value]; }; for (const field of types[primaryType]) { const [type, value] = encodeField(field.name, field.type, data[field.name]); encodedTypes.push(type); encodedValues.push(value); } } else { for (const field of types[primaryType]) { let value = data[field.name]; if (value !== undefined) { if (field.type === 'bytes') { encodedTypes.push('bytes32'); value = ethUtil.keccak(value); encodedValues.push(value); } else if (field.type === 'string') { encodedTypes.push('bytes32'); // convert string to buffer - prevents ethUtil from interpreting strings like '0xabcd' as hex if (typeof value === 'string') { value = Buffer.from(value, 'utf8'); } value = ethUtil.keccak(value); encodedValues.push(value); } else if (types[field.type] !== undefined) { encodedTypes.push('bytes32'); value = ethUtil.keccak(this.encodeData(field.type, value, types, useV4)); encodedValues.push(value); } else if (field.type.lastIndexOf(']') === field.type.length - 1) { throw new Error('Arrays are unimplemented in encodeData; use V4 extension'); } else { encodedTypes.push(field.type); encodedValues.push(value); } } } } return ethAbi.rawEncode(encodedTypes, encodedValues); }, /** * Encodes the type of an object by encoding a comma delimited list of its members * * @param {string} primaryType - Root type to encode * @param {Object} types - Type definitions * @returns {string} - Encoded representation of the type of an object */ encodeType(primaryType, types) { let result = ''; let deps = this.findTypeDependencies(primaryType, types).filter((dep) => dep !== primaryType); deps = [primaryType].concat(deps.sort()); for (const type of deps) { const children = types[type]; if (!children) { throw new Error(`No type definition specified: ${type}`); } result += `${type}(${types[type] .map(({ name, type: t }) => `${t} ${name}`) .join(',')})`; } return result; }, /** * Finds all types within a type definition object * * @param {string} primaryType - Root type * @param {Object} types - Type definitions * @param {Array} results - current set of accumulated types * @returns {Array} - Set of all types found in the type definition */ findTypeDependencies(primaryType, types, results = []) { [primaryType] = primaryType.match(/^\w*/u); if (results.includes(primaryType) || types[primaryType] === undefined) { return results; } results.push(primaryType); for (const field of types[primaryType]) { for (const dep of this.findTypeDependencies(field.type, types, results)) { !results.includes(dep) && results.push(dep); } } return results; }, /** * Hashes an object * * @param {string} primaryType - Root type * @param {Object} data - Object to hash * @param {Object} types - Type definitions * @returns {Buffer} - Hash of an object */ hashStruct(primaryType, data, types, useV4 = true) { return ethUtil.keccak(this.encodeData(primaryType, data, types, useV4)); }, /** * Hashes the type of an object * * @param {string} primaryType - Root type to hash * @param {Object} types - Type definitions * @returns {Buffer} - Hash of an object */ hashType(primaryType, types) { return ethUtil.keccak(this.encodeType(primaryType, types)); }, /** * Removes properties from a message object that are not defined per EIP-712 * * @param {Object} data - typed message object * @returns {Object} - typed message object with only allowed fields */ sanitizeData(data) { const sanitizedData = {}; for (const key in TYPED_MESSAGE_SCHEMA.properties) { if (data[key]) { sanitizedData[key] = data[key]; } } if ('types' in sanitizedData) { sanitizedData.types = Object.assign({ EIP712Domain: [] }, sanitizedData.types); } return sanitizedData; }, /** * Signs a typed message as per EIP-712 and returns its keccak hash * * @param {Object} typedData - Types message data to sign * @returns {Buffer} - keccak hash of the resulting signed message */ sign(typedData, useV4 = true) { const sanitizedData = this.sanitizeData(typedData); const parts = [Buffer.from('1901', 'hex')]; parts.push(this.hashStruct('EIP712Domain', sanitizedData.domain, sanitizedData.types, useV4)); if (sanitizedData.primaryType !== 'EIP712Domain') { parts.push(this.hashStruct(sanitizedData.primaryType, sanitizedData.message, sanitizedData.types, useV4)); } return ethUtil.keccak(Buffer.concat(parts)); }, }; exports.TypedDataUtils = TypedDataUtils; function concatSig(v, r, s) { const rSig = ethUtil.fromSigned(r); const sSig = ethUtil.fromSigned(s); const vSig = ethUtil.bufferToInt(v); const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64); const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64); const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig)); return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex'); } exports.concatSig = concatSig; function normalize(input) { if (!input) { return undefined; } if (typeof input === 'number') { const buffer = ethUtil.toBuffer(input); input = ethUtil.bufferToHex(buffer); } if (typeof input !== 'string') { let msg = 'eth-sig-util.normalize() requires hex string or integer input.'; msg += ` received ${typeof input}: ${input}`; throw new Error(msg); } return ethUtil.addHexPrefix(input.toLowerCase()); } exports.normalize = normalize; function personalSign(privateKey, msgParams) { const message = ethUtil.toBuffer(msgParams.data); const msgHash = ethUtil.hashPersonalMessage(message); const sig = ethUtil.ecsign(msgHash, privateKey); const serialized = ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s)); return serialized; } exports.personalSign = personalSign; function recoverPersonalSignature(msgParams) { const publicKey = getPublicKeyFor(msgParams); const sender = ethUtil.publicToAddress(publicKey); const senderHex = ethUtil.bufferToHex(sender); return senderHex; } exports.recoverPersonalSignature = recoverPersonalSignature; function extractPublicKey(msgParams) { const publicKey = getPublicKeyFor(msgParams); return `0x${publicKey.toString('hex')}`; } exports.extractPublicKey = extractPublicKey; function externalTypedSignatureHash(typedData) { const hashBuffer = typedSignatureHash(typedData); return ethUtil.bufferToHex(hashBuffer); } exports.typedSignatureHash = externalTypedSignatureHash; function signTypedDataLegacy(privateKey, msgParams) { const msgHash = typedSignatureHash(msgParams.data); const sig = ethUtil.ecsign(msgHash, privateKey); return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s)); } exports.signTypedDataLegacy = signTypedDataLegacy; function recoverTypedSignatureLegacy(msgParams) { const msgHash = typedSignatureHash(msgParams.data); const publicKey = recoverPublicKey(msgHash, msgParams.sig); const sender = ethUtil.publicToAddress(publicKey); return ethUtil.bufferToHex(sender); } exports.recoverTypedSignatureLegacy = recoverTypedSignatureLegacy; function encrypt(receiverPublicKey, msgParams, version) { switch (version) { case 'x25519-xsalsa20-poly1305': { if (typeof msgParams.data !== 'string') { throw new Error('Cannot detect secret message, message params should be of the form {data: "secret message"} '); } // generate ephemeral keypair const ephemeralKeyPair = nacl.box.keyPair(); // assemble encryption parameters - from string to UInt8 let pubKeyUInt8Array; try { pubKeyUInt8Array = naclUtil.decodeBase64(receiverPublicKey); } catch (err) { throw new Error('Bad public key'); } const msgParamsUInt8Array = naclUtil.decodeUTF8(msgParams.data); const nonce = nacl.randomBytes(nacl.box.nonceLength); // encrypt const encryptedMessage = nacl.box(msgParamsUInt8Array, nonce, pubKeyUInt8Array, ephemeralKeyPair.secretKey); // handle encrypted data const output = { version: 'x25519-xsalsa20-poly1305', nonce: naclUtil.encodeBase64(nonce), ephemPublicKey: naclUtil.encodeBase64(ephemeralKeyPair.publicKey), ciphertext: naclUtil.encodeBase64(encryptedMessage), }; // return encrypted msg data return output; } default: throw new Error('Encryption type/version not supported'); } } exports.encrypt = encrypt; function encryptSafely(receiverPublicKey, msgParams, version) { const DEFAULT_PADDING_LENGTH = 2 ** 11; const NACL_EXTRA_BYTES = 16; const { data } = msgParams; if (!data) { throw new Error('Cannot encrypt empty msg.data'); } if (typeof data === 'object' && 'toJSON' in data) { // remove toJSON attack vector // TODO, check all possible children throw new Error('Cannot encrypt with toJSON property. Please remove toJSON property'); } // add padding const dataWithPadding = { data, padding: '', }; // calculate padding const dataLength = Buffer.byteLength(JSON.stringify(dataWithPadding), 'utf-8'); const modVal = dataLength % DEFAULT_PADDING_LENGTH; let padLength = 0; // Only pad if necessary if (modVal > 0) { padLength = DEFAULT_PADDING_LENGTH - modVal - NACL_EXTRA_BYTES; // nacl extra bytes } dataWithPadding.padding = '0'.repeat(padLength); const paddedMsgParams = { data: JSON.stringify(dataWithPadding) }; return encrypt(receiverPublicKey, paddedMsgParams, version); } exports.encryptSafely = encryptSafely; function decrypt(encryptedData, receiverPrivateKey) { switch (encryptedData.version) { case 'x25519-xsalsa20-poly1305': { // string to buffer to UInt8Array const recieverPrivateKeyUint8Array = nacl_decodeHex(receiverPrivateKey); const recieverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey(recieverPrivateKeyUint8Array).secretKey; // assemble decryption parameters const nonce = naclUtil.decodeBase64(encryptedData.nonce); const ciphertext = naclUtil.decodeBase64(encryptedData.ciphertext); const ephemPublicKey = naclUtil.decodeBase64(encryptedData.ephemPublicKey); // decrypt const decryptedMessage = nacl.box.open(ciphertext, nonce, ephemPublicKey, recieverEncryptionPrivateKey); // return decrypted msg data let output; try { output = naclUtil.encodeUTF8(decryptedMessage); } catch (err) { throw new Error('Decryption failed.'); } if (output) { return output; } throw new Error('Decryption failed.'); } default: throw new Error('Encryption type/version not supported.'); } } exports.decrypt = decrypt; function decryptSafely(encryptedData, receiverPrivateKey) { const dataWithPadding = JSON.parse(decrypt(encryptedData, receiverPrivateKey)); return dataWithPadding.data; } exports.decryptSafely = decryptSafely; function getEncryptionPublicKey(privateKey) { const privateKeyUint8Array = nacl_decodeHex(privateKey); const encryptionPublicKey = nacl.box.keyPair.fromSecretKey(privateKeyUint8Array).publicKey; return naclUtil.encodeBase64(encryptionPublicKey); } exports.getEncryptionPublicKey = getEncryptionPublicKey; /** * A generic entry point for all typed data methods to be passed, includes a version parameter. */ function signTypedMessage(privateKey, msgParams, version = 'V4') { switch (version) { case 'V1': return signTypedDataLegacy(privateKey, msgParams); case 'V3': return signTypedData(privateKey, msgParams); case 'V4': default: return signTypedData_v4(privateKey, msgParams); } } exports.signTypedMessage = signTypedMessage; function recoverTypedMessage(msgParams, version = 'V4') { switch (version) { case 'V1': return recoverTypedSignatureLegacy(msgParams); case 'V3': return recoverTypedSignature(msgParams); case 'V4': default: return recoverTypedSignature_v4(msgParams); } } exports.recoverTypedMessage = recoverTypedMessage; function signTypedData(privateKey, msgParams) { const message = TypedDataUtils.sign(msgParams.data, false); const sig = ethUtil.ecsign(message, privateKey); return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s)); } exports.signTypedData = signTypedData; function signTypedData_v4(privateKey, msgParams) { const message = TypedDataUtils.sign(msgParams.data); const sig = ethUtil.ecsign(message, privateKey); return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s)); } exports.signTypedData_v4 = signTypedData_v4; function recoverTypedSignature(msgParams) { const message = TypedDataUtils.sign(msgParams.data, false); const publicKey = recoverPublicKey(message, msgParams.sig); const sender = ethUtil.publicToAddress(publicKey); return ethUtil.bufferToHex(sender); } exports.recoverTypedSignature = recoverTypedSignature; function recoverTypedSignature_v4(msgParams) { const message = TypedDataUtils.sign(msgParams.data); const publicKey = recoverPublicKey(message, msgParams.sig); const sender = ethUtil.publicToAddress(publicKey); return ethUtil.bufferToHex(sender); } exports.recoverTypedSignature_v4 = recoverTypedSignature_v4; /** * @param typedData - Array of data along with types, as per EIP712. * @returns Buffer */ function typedSignatureHash(typedData) { const error = new Error('Expect argument to be non-empty array'); if (typeof typedData !== 'object' || !('length' in typedData) || !typedData.length) { throw error; } const data = typedData.map(function (e) { return e.type === 'bytes' ? ethUtil.toBuffer(e.value) : e.value; }); const types = typedData.map(function (e) { return e.type; }); const schema = typedData.map(function (e) { if (!e.name) { throw error; } return `${e.type} ${e.name}`; }); return ethAbi.soliditySHA3(['bytes32', 'bytes32'], [ ethAbi.soliditySHA3(new Array(typedData.length).fill('string'), schema), ethAbi.soliditySHA3(types, data), ]); } function recoverPublicKey(hash, sig) { const signature = ethUtil.toBuffer(sig); const sigParams = ethUtil.fromRpcSig(signature); return ethUtil.ecrecover(hash, sigParams.v, sigParams.r, sigParams.s); } function getPublicKeyFor(msgParams) { const message = ethUtil.toBuffer(msgParams.data); const msgHash = ethUtil.hashPersonalMessage(message); return recoverPublicKey(msgHash, msgParams.sig); } function padWithZeroes(number, length) { let myString = `${number}`; while (myString.length < length) { myString = `0${myString}`; } return myString; } // converts hex strings to the Uint8Array format used by nacl function nacl_decodeHex(msgHex) { const msgBase64 = Buffer.from(msgHex, 'hex').toString('base64'); return naclUtil.decodeBase64(msgBase64); } //# sourceMappingURL=index.js.map