web3j icon indicating copy to clipboard operation
web3j copied to clipboard

[SOLVED]Unable to recover address from MetaMask signature

Open doit-ceo opened this issue 3 years ago • 5 comments

Java vs. JavaScript

I'm trying to implement login with MetaMask (web3.js)

  • Sign a message with MetaMask (for simplicity its '1') via (web3.js).
  • Web3.js is working correctly and I can get the key back when invoking (personal_ecRecover) method.
  • Java/Server address recovery fail (server produce different address).

JS signing

var msg = "1";
var msgHex = "0x" + toHex( msg );	
const signture = await window.ethereum.request({ method: 'personal_sign', params: [msgHex,ethereum.selectedAddress]});
var recoverAddress = await window.ethereum.request({ method: 'personal_ecRecover', params: [msgHex,signture]});
const signatureBuffer = EthJS.Util.toBuffer(signture);
const signatureParams = EthJS.Util.fromRpcSig(signatureBuffer);    		
console.log("address:" + ethereum.selectedAddress);
console.log("recoverAddress:" + recoverAddress);
console.log("r:" + EthJS.Util.bufferToHex(signatureParams.r));
console.log("s:" + EthJS.Util.bufferToHex(signatureParams.s));
console.log("v:" + signatureParams.v);

JS signing -- output

address:0x1a43d9d9f10058e03feda50c2aa657b18f1e1181
recoverAddress:0x1a43d9d9f10058e03feda50c2aa657b18f1e1181
r:0xce531debaf59e357c860591bacbcf8dfc3c390aa4a548365c05e9cad0982c0dd
s:0x4b0c4603302398739835498e5ba2b231d45c38fe0d3b3914f53cc380ead14da8
v:28

Java recover (on server)

// Removing 0x
public static BigInteger bInt(String h) {
	BigInteger b = new BigInteger(h.substring(2, h.length()), 16);
	return b;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
	String r = "0xce531debaf59e357c860591bacbcf8dfc3c390aa4a548365c05e9cad0982c0dd";
	String s = "0x4b0c4603302398739835498e5ba2b231d45c38fe0d3b3914f53cc380ead14da8";
	String msg = "1";
	ECDSASignature es = new ECDSASignature(bInt(r), bInt(s));
	System.out.println("0x" + Keys.getAddress(Sign.recoverFromSignature(1, es, msg.getBytes())));
}

Java output (on server)

Here we fail to recover correct address !

0x1dc2fa223cc7f271be98019189b8bfbe53f0ff0e

doit-ceo avatar Dec 20 '21 20:12 doit-ceo

Problem was solved , I wish this will be included as a method in the API with clear name, i.e. recoverFromSignatureETH, I see the usage of prefix message, but newbies will not understand it.

Code

public static void main(String[] args) throws ExecutionException, InterruptedException {
	// r & s & v from web3.js 
	String r = "0xfdd697c2024e40fcb3707875b3d64f7db68f3729df358f53f24de0dadc4ae0d2";
	String s = "0x435ce19292d9d2972c456136d75d1130e62562f4da63cc26522370733821ef25";
	int v = 27;
	String msg = "HI";
	String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n";
	String message = MESSAGE_PREFIX + msg.length() + msg;
	byte[] hash = Hash.sha3(message.getBytes(StandardCharsets.UTF_8));
	ECDSASignature es = new ECDSASignature(bInt(r), bInt(s));
	System.out.println("0x1a43d9d9f10058e03feda50c2aa657b18f1e1181"); //Expected Address
	System.out.println("0x" + Keys.getAddress(Sign.recoverFromSignature(v - 27, es, hash))); //recovered Address
}

Output

0x1a43d9d9f10058e03feda50c2aa657b18f1e1181
0x1a43d9d9f10058e03feda50c2aa657b18f1e1181

doit-ceo avatar Dec 21 '21 08:12 doit-ceo

This saved my day! Would be nice to have the implementation also for a signature created with eth_signTypedData_v4

simonlucalandi avatar Jan 19 '22 16:01 simonlucalandi

Great. I've been looking for a way to log in safely. Thank you very much

kutasms avatar Jan 26 '22 20:01 kutasms

I used web3j api to verify the signature, working code is in the class Web3SignatureVerification https://github.com/sridharreddyu/web3-java-snippets

sridharreddyu avatar Mar 09 '22 06:03 sridharreddyu

Here's an implementation I wrote that works exactly like the JavaScript ethers.utils.verifyMessage(message, signature) and ethers.utils.recoverAddress(digest, signature) methods.

EthersUtils.java

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.Hash;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.crypto.Sign.SignatureData;
import org.web3j.utils.Numeric;

public class EthersUtils {
  private static final String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n";

  public static String verifyMessage(String message, String signature) {
    return EthersUtils.recoverAddress(EthersUtils.hashMessage(message), signature);
  }

  public static String hashMessage(String message) {
    return Hash.sha3(
        Numeric.toHexStringNoPrefix(
            (EthersUtils.MESSAGE_PREFIX + message.length() + message).getBytes(StandardCharsets.UTF_8)));
  }

  public static String recoverAddress(String digest, String signature) {
    SignatureData signatureData = EthersUtils.getSignatureData(signature);
    int header = 0;
    for (byte b : signatureData.getV()) {
      header = (header << 8) + (b & 0xFF);
    }
    if (header < 27 || header > 34) {
      return null;
    }
    int recId = header - 27;
    BigInteger key = Sign.recoverFromSignature(
        recId,
        new ECDSASignature(
            new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())),
        Numeric.hexStringToByteArray(digest));
    if (key == null) {
      return null;
    }
    return ("0x" + Keys.getAddress(key)).trim();
  }

  private static SignatureData getSignatureData(String signature) {
    byte[] signatureBytes = Numeric.hexStringToByteArray(signature);
    byte v = signatureBytes[64];
    if (v < 27) {
      v += 27;
    }
    byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32);
    byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64);
    return new SignatureData(v, r, s);
  }
}

clayrisser avatar Mar 26 '22 14:03 clayrisser

Here's an implementation I wrote that works exactly like the JavaScript ethers.utils.verifyMessage(message, signature) and ethers.utils.recoverAddress(digest, signature) methods.

EthersUtils.java

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.Hash;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.crypto.Sign.SignatureData;
import org.web3j.utils.Numeric;

public class EthersUtils {
  private static final String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n";

  public static String verifyMessage(String message, String signature) {
    return EthersUtils.recoverAddress(EthersUtils.hashMessage(message), signature);
  }

  public static String hashMessage(String message) {
    return Hash.sha3(
        Numeric.toHexStringNoPrefix(
            (EthersUtils.MESSAGE_PREFIX + message.length() + message).getBytes(StandardCharsets.UTF_8)));
  }

  public static String recoverAddress(String digest, String signature) {
    SignatureData signatureData = EthersUtils.getSignatureData(signature);
    int header = 0;
    for (byte b : signatureData.getV()) {
      header = (header << 8) + (b & 0xFF);
    }
    if (header < 27 || header > 34) {
      return null;
    }
    int recId = header - 27;
    BigInteger key = Sign.recoverFromSignature(
        recId,
        new ECDSASignature(
            new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())),
        Numeric.hexStringToByteArray(digest));
    if (key == null) {
      return null;
    }
    return ("0x" + Keys.getAddress(key)).trim();
  }

  private static SignatureData getSignatureData(String signature) {
    byte[] signatureBytes = Numeric.hexStringToByteArray(signature);
    byte v = signatureBytes[64];
    if (v < 27) {
      v += 27;
    }
    byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32);
    byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64);
    return new SignatureData(v, r, s);
  }
}

Hello Sir, I tried your code but it seems to not work. So I get the nonce as message along with the prefix sent to the user then it gets signed by the user after signing signature is sent back to backend for validation so upon validation I recreate the same message it was sent to user and recover it from signature by doing recoverAddress(message, signature). What could be that I am doing wrong here?

Note: I tried to pass a hash of message instead of message itself but I keep getting wrong address I even tried to send a some "hello world" message instead of nonce message but I keep getting wrong address while recovering address from the js web3js returns correct address.

rgouzal avatar Oct 03 '22 14:10 rgouzal