Last active
September 25, 2023 06:11
-
-
Save djma/386c2dcf91fefc004b14e5044facd3a9 to your computer and use it in GitHub Desktop.
Ethereum EIP-712 message signing and address recovery with web3j
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.math.BigInteger; | |
import java.security.SignatureException; | |
import org.web3j.crypto.ECKeyPair; | |
import org.web3j.crypto.Keys; | |
import org.web3j.crypto.Sign; | |
import org.web3j.utils.Numeric; | |
/** | |
* This runnable demo summarizes my understanding of this gist after spending a | |
* lot of time getting it to work: | |
* https://gist.github.com/megamattron/94c05789e5ff410296e74dad3b528613 | |
* | |
* The main points of confusion were: | |
* 1. Whether a message needs to be prefixed with the Ethereum prefix | |
* 2. Whether the prefix msg length is 32 or the number of bytes in the message | |
* to sign. | |
* 3. Whether the message that is signed is a hash of the message or the message | |
* itself. | |
* | |
* Post EIP-712, https://eips.ethereum.org/EIPS/eip-712, the | |
* answer is: | |
* 1. The message needs to be prefixed with the Ethereum prefix. | |
* 2. The prefix msg length is the number of bytes in the message to sign. Some | |
* online sources say "It's always 32 because the message is hashed before | |
* signing." may have been true before EIP-712, but it is not true anymore. | |
* 3. The message that is signed is the original message. | |
* | |
* Different clients may not always implement EIP-712, but they should be moving | |
* towards it. I tested it with myCrypto, MetaMask, and Etherscan. | |
* | |
* This was written on 2022-10-27, with web3j 5.0.0 and java 17. | |
*/ | |
public class EthereumMessageSigningDemo { | |
public static void main(String[] args) throws Exception { | |
String msg = "foobar"; | |
System.out.println("message: " + msg); | |
// WARNING: IF YOU IMPORT THIS PRIVATE KEY, DO NOT SEND ANYTHING TO IT. IT WILL | |
// BE STOLEN IMMEDIATELY. | |
String privateKeyHexString = "0x1234567890123456789012345678901234567890123456789012345678901234"; | |
// Get the public key address from the private key. | |
ECKeyPair aPair = ECKeyPair.create(Numeric.hexStringToByteArray(privateKeyHexString)); | |
BigInteger publicKeyInBT = aPair.getPublicKey(); | |
String publicKeyHexString = publicKeyInBT.toString(16); | |
String publicAddress = Keys.getAddress(publicKeyHexString); | |
System.out.println("publicAddress: " + publicAddress + " == 0x2e988a386a799f506693793c6a5af6b54dfaabfb"); | |
// Using Sign.signPrefixedMessage is EIP-712 compliant. It: | |
// 1) adds a \x19Ethereum Signed Message\n prefix, | |
// 2) adds the byte-length of the message, and | |
// 3) Does NOT hash the message. | |
Sign.SignatureData signatureData = Sign.signPrefixedMessage(msg.getBytes(), aPair); | |
byte[] signedMessageBytes = new byte[32 + 32 + 1]; // 32 bytes for r, 32 bytes for s, 1 byte for v | |
System.arraycopy(signatureData.getR(), 0, signedMessageBytes, 0, 32); | |
System.arraycopy(signatureData.getS(), 0, signedMessageBytes, 32, 32); | |
System.arraycopy(signatureData.getV(), 0, signedMessageBytes, 64, 1); | |
String signedMessageHex = Numeric.toHexString(signedMessageBytes); | |
// Check with your preferred client (myCrypto, MM, etherscan, etc) that this is the correct signature. It may | |
// not be the same if the client does not follow EIP-712. | |
String expectedSignedMessageHex = "0xbe99c4f74b22db3f9f157d7c88b16ff064fba843b2b84d94a5c2b2653e76362126f5897bd2a559186a0dc797926c6048d7fb506c9f54f38e05e375fee15927e51c"; | |
System.out.println("Signature Hash: " + signedMessageHex + " == " + expectedSignedMessageHex); | |
// Finally, verify the signature with the reverse of the signing process. | |
String recoveredAddress = getAddressUsedToSignHashedMessage(expectedSignedMessageHex, msg); | |
System.out.println("Recovered address: " + recoveredAddress + " == " + publicAddress); | |
} | |
/** | |
* This method is the reverse of the signing process. | |
* | |
* @param signedMessageInHex | |
* The signature in hex format. It is 65 bytes long, | |
* 32 bytes for r, 32 bytes for s, and 1 byte for v. | |
* May or may not be pre-pended with "0x". | |
* @param originalMessage | |
* The original message that was signed. Not hashed. | |
* @return | |
* The address that was used to sign the message. | |
* @throws SignatureException | |
*/ | |
public static String getAddressUsedToSignHashedMessage(String signedMessageInHex, String originalMessage) | |
throws SignatureException { | |
if (signedMessageInHex.startsWith("0x")) { | |
signedMessageInHex = signedMessageInHex.substring(2); | |
} | |
// No need to prepend these strings with 0x because | |
// Numeric.hexStringToByteArray() accepts both formats | |
String r = signedMessageInHex.substring(0, 64); | |
String s = signedMessageInHex.substring(64, 128); | |
String v = signedMessageInHex.substring(128, 130); | |
// Using Sign.signedPrefixedMessageToKey for EIP-712 compliant signatures. | |
String pubkey = Sign.signedPrefixedMessageToKey(originalMessage.getBytes(), | |
new Sign.SignatureData( | |
Numeric.hexStringToByteArray(v)[0], | |
Numeric.hexStringToByteArray(r), | |
Numeric.hexStringToByteArray(s))) | |
.toString(16); | |
return Keys.getAddress(pubkey); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment