Skip to content

Commit

Permalink
Consolidate signTypedData and recoverTypedSignature functions
Browse files Browse the repository at this point in the history
The `signTypedData` and `recoverTypedSignature` functions each had
three different implementations (one for each version of EIP-712).
They have each been consolidated into a single function each.

The functions `signTypedData` and `recoverTypedSignature` used to be
specifically for `eth_signTypedData_v3`. Instead they now accept an
additional `version` parameter that specifies which version should be
used. This replaces `signTypedDataLegacy` and
`recoverTypedSignatureLegacy`, which were for `eth_signTypedData_v1`,
and it replaces `signTypedData_v4` and `recoverTypedSignature_v4`.

The functions `signTypedMessage` and `recoverTypedMessage` used to
provide a single interface for using any of EIP-712 types. But now that
the base functions serve that purpose instead, they are now obsolete
and have been removed.

Additionally a few `TypedDataUtils` functions were updated to accept
a `version` parameter instead of a `useV4` parameter, to bring them
in-line with the other functions. The functions changed are
`hashStruct`, `eip712Hash`, and `encodeData`.

The README and inline documentation has been updated accordingly. New
doc strings were added for `signTypedData` and `recoverTypedSignature`,
which didn't have any yet before now.

Credit to @aakilfernandes for this idea, who first implemented this in
PR #66.
  • Loading branch information
Gudahtt committed Jul 15, 2021
1 parent 278415f commit 99816a7
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 296 deletions.
34 changes: 9 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,21 @@ msgParams should have a `data` key that is hex-encoded data unsigned, and a `sig

Returns a hex-encoded sender address.

### signTypedData (privateKeyBuffer, msgParams)
### signTypedData (privateKeyBuffer, msgParams, version)

Signs typed data as per [an early draft of EIP 712](https://github.com/ethereum/EIPs/pull/712/commits/21abe254fe0452d8583d5b132b1d7be87c0439ca).
Sign typed data according to EIP-712. The signing differs based upon the `version`, which is explained in the table below.

Data should be under `data` key of `msgParams`. The method returns prefixed signature.

### signTypedData_v3 (privateKeyBuffer, msgParams)
| Version | Description |
| ------- | ------------------------------------------------------------ |
| `V1` | This is based on [an early version of EIP-712](https://github.com/ethereum/EIPs/pull/712/commits/21abe254fe0452d8583d5b132b1d7be87c0439ca) that lacked some later security improvements, and should generally be neglected in favor of `V3`. |
| `V3` | Currently represents the latest version of the [EIP 712 spec](https://eips.ethereum.org/EIPS/eip-712), making it the most secure method for signing cheap-to-verify data on-chain that we have yet. |
| `V4` | Currently represents the latest version of the [EIP 712 spec](https://eips.ethereum.org/EIPS/eip-712), with added support for arrays and with a breaking fix for the way structs are encoded |

Signs typed data as per the published version of [EIP 712](https://github.com/ethereum/EIPs/pull/712).
### recoverTypedSignature ({data, sig}, version)

Data should be under `data` key of `msgParams`. The method returns prefixed signature.

### signTypedData_v4 (privateKeyBuffer, msgParams)

Signs typed data as per an extension of the published version of [EIP 712](https://github.com/MetaMask/eth-sig-util/pull/54).

This extension adds support for arrays and recursive data types.

Data should be under `data` key of `msgParams`. The method returns prefixed signature.

### recoverTypedSignature ({data, sig})

Return address of a signer that did `signTypedData`.

Expects the same data that were used for signing. `sig` is a prefixed signature.

### recoverTypedSignature_V4 ({data, sig})

Return address of a signer that did `signTypedData` as per an extension of the published version of [EIP 712](https://github.com/MetaMask/eth-sig-util/pull/54).

This extension adds support for arrays and recursive data types.
Return address of a signer that did `signTypedData`. The `version` parameter must match the version the signature was created with.

Expects the same data that were used for signing. `sig` is a prefixed signature.

Expand Down
137 changes: 50 additions & 87 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,26 @@ const TypedDataUtils = {
* @param {string} primaryType - Root type
* @param {Object} data - Object to encode
* @param {Object} types - Type definitions
* @param {Version} version - The EIP-712 version the encoding should comply with
* @returns {Buffer} - Encoded representation of an object
*/
encodeData(
primaryType: string,
data: Record<string, unknown>,
types: Record<string, MessageTypeProperty[]>,
useV4 = true,
version: Version,
): Buffer {
const encodedTypes = ['bytes32'];
const encodedValues = [this.hashType(primaryType, types)];

if (useV4) {
if (version === 'V4') {
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)),
: ethUtil.keccak(this.encodeData(type, value, types, version)),
];
}

Expand Down Expand Up @@ -168,7 +169,7 @@ const TypedDataUtils = {
} else if (types[field.type] !== undefined) {
encodedTypes.push('bytes32');
value = ethUtil.keccak(
this.encodeData(field.type, value, types, useV4),
this.encodeData(field.type, value, types, version),
);
encodedValues.push(value);
} else if (field.type.lastIndexOf(']') === field.type.length - 1) {
Expand Down Expand Up @@ -246,15 +247,16 @@ const TypedDataUtils = {
* @param {string} primaryType - Root type
* @param {Object} data - Object to hash
* @param {Object} types - Type definitions
* @param {Version} version - The EIP-712 version the hash should comply with
* @returns {Buffer} - Hash of an object
*/
hashStruct(
primaryType: string,
data: Record<string, unknown>,
types: Record<string, unknown>,
useV4 = true,
version: Version,
): Buffer {
return ethUtil.keccak(this.encodeData(primaryType, data, types, useV4));
return ethUtil.keccak(this.encodeData(primaryType, data, types, version));
},

/**
Expand Down Expand Up @@ -293,11 +295,12 @@ const TypedDataUtils = {
* Signs a typed message as per EIP-712 and returns its keccak hash
*
* @param {Object} typedData - Types message data to hash as per eip-712
* @param {Version} version - The EIP-712 version the hash should comply with
* @returns {Buffer} - keccak hash of the resulting signed message
*/
eip712Hash<T extends MessageTypes>(
typedData: Partial<TypedData | TypedMessage<T>>,
useV4 = true,
version: Version,
): Buffer {
const sanitizedData = this.sanitizeData(typedData);
const parts = [Buffer.from('1901', 'hex')];
Expand All @@ -306,7 +309,7 @@ const TypedDataUtils = {
'EIP712Domain',
sanitizedData.domain,
sanitizedData.types,
useV4,
version,
),
);
if (sanitizedData.primaryType !== 'EIP712Domain') {
Expand All @@ -315,7 +318,7 @@ const TypedDataUtils = {
sanitizedData.primaryType,
sanitizedData.message,
sanitizedData.types,
useV4,
version,
),
);
}
Expand Down Expand Up @@ -384,24 +387,6 @@ function externalTypedSignatureHash(typedData: EIP712TypedData[]): string {
return ethUtil.bufferToHex(hashBuffer);
}

function signTypedDataLegacy<T extends MessageTypes>(
privateKey: Buffer,
msgParams: MsgParams<TypedData | TypedMessage<T>>,
): string {
const msgHash = typedSignatureHash(msgParams.data);
const sig = ethUtil.ecsign(msgHash, privateKey);
return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s));
}

function recoverTypedSignatureLegacy<T extends MessageTypes>(
msgParams: SignedMsgParams<TypedData | TypedMessage<T>>,
): string {
const msgHash = typedSignatureHash(msgParams.data);
const publicKey = recoverPublicKey(msgHash, msgParams.sig);
const sender = ethUtil.publicToAddress(publicKey);
return ethUtil.bufferToHex(sender);
}

function encrypt<T extends MessageTypes>(
receiverPublicKey: string,
msgParams: MsgParams<TypedData | TypedMessage<T>>,
Expand Down Expand Up @@ -560,71 +545,55 @@ function getEncryptionPublicKey(privateKey: string): string {
}

/**
* A generic entry point for all typed data methods to be passed, includes a version parameter.
* Sign typed data according to EIP-712. The signing differs based upon the `version`.
*
* V1 is based upon [an early version of EIP-712](https://github.com/ethereum/EIPs/pull/712/commits/21abe254fe0452d8583d5b132b1d7be87c0439ca)
* that lacked some later security improvements, and should generally be
* neglected in favor of later versions.
*
* V3 represents the latest version of EIP-712.
*
* V4 is similar to V3 except that it also supports arrays and recursive data
* structures.
*
* @param privateKey - The private key to sign with.
* @param msgParams - Signing parameters.
* @param msgParams.data - The typed data to sign.
* @param version - The signing version to use.
* @returns The signature
*/
function signTypedMessage<T extends MessageTypes>(
privateKey: Buffer,
msgParams: MsgParams<TypedData | TypedMessage<T>>,
version: Version = 'V4',
): string {
switch (version) {
case 'V1':
return signTypedDataLegacy(privateKey, msgParams);
case 'V3':
return signTypedData(privateKey, msgParams);
case 'V4':
default:
return signTypedData_v4(privateKey, msgParams);
}
}

function recoverTypedMessage<T extends MessageTypes>(
msgParams: SignedMsgParams<TypedData | TypedMessage<T>>,
version: Version = 'V4',
): string {
switch (version) {
case 'V1':
return recoverTypedSignatureLegacy(msgParams);
case 'V3':
return recoverTypedSignature(msgParams);
case 'V4':
default:
return recoverTypedSignature_v4(msgParams);
}
}

function signTypedData<T extends MessageTypes>(
privateKey: Buffer,
msgParams: MsgParams<TypedData | TypedMessage<T>>,
version: Version,
): string {
const message = TypedDataUtils.eip712Hash(msgParams.data, false);
const sig = ethUtil.ecsign(message, privateKey);
return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s));
}

function signTypedData_v4<T extends MessageTypes>(
privateKey: Buffer,
msgParams: MsgParams<TypedData | TypedMessage<T>>,
): string {
const message = TypedDataUtils.eip712Hash(msgParams.data);
const sig = ethUtil.ecsign(message, privateKey);
const messageHash =
version === 'V1'
? typedSignatureHash(msgParams.data)
: TypedDataUtils.eip712Hash(msgParams.data, version);
const sig = ethUtil.ecsign(messageHash, privateKey);
return ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s));
}

/**
* Recover the address of the account that created the given EIP-712
* signature. The version provided must match the version used to
* create the signature.
*
* @param msgParams - Signing parameters.
* @param msgParams.data - The data that was signed.
* @param version - The signing version to use.
* @returns The address of the signer.
*/
function recoverTypedSignature<T extends MessageTypes>(
msgParams: SignedMsgParams<TypedData | TypedMessage<T>>,
version: Version,
): string {
const message = TypedDataUtils.eip712Hash(msgParams.data, false);
const publicKey = recoverPublicKey(message, msgParams.sig);
const sender = ethUtil.publicToAddress(publicKey);
return ethUtil.bufferToHex(sender);
}

function recoverTypedSignature_v4<T extends MessageTypes>(
msgParams: SignedMsgParams<TypedData | TypedMessage<T>>,
): string {
const message = TypedDataUtils.eip712Hash(msgParams.data);
const publicKey = recoverPublicKey(message, msgParams.sig);
const messageHash =
version === 'V1'
? typedSignatureHash(msgParams.data)
: TypedDataUtils.eip712Hash(msgParams.data, version);
const publicKey = recoverPublicKey(messageHash, msgParams.sig);
const sender = ethUtil.publicToAddress(publicKey);
return ethUtil.bufferToHex(sender);
}
Expand All @@ -638,19 +607,13 @@ export {
recoverPersonalSignature,
extractPublicKey,
externalTypedSignatureHash as typedSignatureHash,
signTypedDataLegacy,
recoverTypedSignatureLegacy,
encrypt,
encryptSafely,
decrypt,
decryptSafely,
getEncryptionPublicKey,
signTypedMessage,
recoverTypedMessage,
signTypedData,
signTypedData_v4,
recoverTypedSignature,
recoverTypedSignature_v4,
};

/**
Expand Down
Loading

0 comments on commit 99816a7

Please sign in to comment.