Skip to content

Commit 0902e5b

Browse files
committed
add web3.eth.personal.ecRecover()
1 parent cdc2794 commit 0902e5b

5 files changed

Lines changed: 192 additions & 44 deletions

File tree

etherspace-java-example/src/main/java/cc/etherspace/example/JavaExample.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import cc.etherspace.Options;
1010
import cc.etherspace.Send;
1111
import cc.etherspace.TransactionHash;
12+
import cc.etherspace.TransactionReceipt;
1213

1314
public class JavaExample {
1415
public static void main(String[] args) throws IOException {
@@ -25,7 +26,7 @@ public static void main(String[] args) throws IOException {
2526
System.out.println("Updating greeting to: Hello World");
2627

2728
TransactionHash hash = greeter.newGreeting("Hello World");
28-
hash.requestTransactionReceipt();
29+
TransactionReceipt receipt = hash.requestTransactionReceipt();
2930

3031
System.out.println("Transaction returned with hash: " + hash.getHash());
3132

@@ -37,7 +38,7 @@ public static void main(String[] args) throws IOException {
3738

3839
Options options = new Options(BigInteger.ZERO, BigInteger.valueOf(5_300_000), BigInteger.valueOf(24_000_000_000L));
3940
hash = greeter.newGreeting("Hello World", options);
40-
hash.requestTransactionReceipt();
41+
receipt = hash.requestTransactionReceipt();
4142

4243
System.out.println("Transaction returned with hash: " + hash.getHash());
4344
}

etherspace-java/src/main/java/cc/etherspace/Web3.kt

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.web3j.utils.Numeric
66
import java.io.IOException
77
import java.lang.reflect.Type
88
import java.math.BigInteger
9+
import java.nio.ByteBuffer
910

1011
@Suppress("unused")
1112
interface Web3 {
@@ -24,6 +25,7 @@ interface Web3 {
2425

2526
interface Eth {
2627
val accounts: Accounts
28+
val personal: Personal
2729

2830
@Throws(IOException::class)
2931
fun sign(dataToSign: String, address: String): String
@@ -57,11 +59,32 @@ interface Web3 {
5759
fun sign(messageHash: ByteArray, privateKey: String): Signature
5860
}
5961

60-
data class Signature(val messageHash: String,
61-
val v: String,
62-
val r: String,
63-
val s: String,
64-
val signature: String)
62+
interface Personal {
63+
@Throws(IOException::class)
64+
fun ecRecover(dataThatWasSigned: String, signature: Signature): String
65+
66+
@Throws(IOException::class)
67+
fun ecRecover(dataThatWasSigned: ByteArray, signature: Signature): String
68+
}
69+
70+
data class Signature(val v: Byte,
71+
val r: ByteArray,
72+
val s: ByteArray) {
73+
val signature: String
74+
get() = Numeric.toHexString(signatureEncode(v, r, s))
75+
76+
private fun signatureEncode(v: Byte, r: ByteArray, s: ByteArray): ByteArray {
77+
assert(r.size == 32)
78+
assert(s.size == 32)
79+
assert(v.toInt() == 27 || v.toInt() == 28)
80+
val buffer = ByteBuffer.allocate(SIGNATURE_LENGTH)
81+
buffer.put(r)
82+
buffer.put(s)
83+
buffer.put(v)
84+
assert(buffer.position() == SIGNATURE_LENGTH)
85+
return buffer.array()
86+
}
87+
}
6588

6689
data class DefaultBlock(val value: String) {
6790
constructor(blockNumber: BigInteger) : this(Numeric.encodeQuantity(blockNumber))
@@ -93,4 +116,7 @@ interface Web3 {
93116
nonce)
94117
}
95118

119+
companion object {
120+
private const val SIGNATURE_LENGTH = 65
121+
}
96122
}

etherspace-java/src/main/java/cc/etherspace/web3j/Web3jAdapter.kt

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import org.web3j.protocol.http.HttpService
1515
import org.web3j.utils.Numeric
1616
import java.io.IOException
1717
import java.math.BigInteger
18-
import java.nio.ByteBuffer
1918

2019

2120
class Web3jAdapter(val web3j: Web3j) : Web3 {
@@ -25,44 +24,41 @@ class Web3jAdapter(val web3j: Web3j) : Web3 {
2524

2625
override val eth: Web3jEth = Web3jEth()
2726

28-
class Web3jAccounts : Web3.Accounts {
29-
override fun sign(message: String, privateKey: String): Web3.Signature {
30-
return sign(message.toByteArray(Charsets.UTF_8), privateKey)
27+
class Web3jPersonal : Web3.Personal {
28+
override fun ecRecover(dataThatWasSigned: String, signature: Web3.Signature): String =
29+
ecRecover(dataThatWasSigned.toByteArray(Charsets.UTF_8), signature)
30+
31+
override fun ecRecover(dataThatWasSigned: ByteArray, signature: Web3.Signature): String {
32+
val prefixMsgHash = attachEthereumSignedMessage(dataThatWasSigned)
33+
val ecdsaSignature = ECDSASignature(Numeric.toBigInt(signature.r),
34+
Numeric.toBigInt(signature.s))
35+
val publicKey = RecoverFromSignature.recoverFromSignature(signature.v - 27,
36+
ecdsaSignature,
37+
prefixMsgHash)
38+
return Keys.toChecksumAddress(Numeric.prependHexPrefix(Keys.getAddress(publicKey)))
3139
}
40+
}
41+
42+
class Web3jAccounts : Web3.Accounts {
43+
override fun sign(message: String, privateKey: String): Web3.Signature =
44+
sign(message.toByteArray(Charsets.UTF_8), privateKey)
3245

3346
/**
3447
* from: https://github.com/EuroHsu/TestLibrary/blob/6883528dbcddb283f14955620f042b4ea3253624/src/main/java/com/example/testlibrary/cryptos/signer.java
3548
*/
3649
override fun sign(messageHash: ByteArray, privateKey: String): Web3.Signature {
37-
val prefix = "\u0019Ethereum Signed Message:\n".toByteArray(Charsets.UTF_8)
38-
val prefixSize = ByteUtils.concatenate(prefix, messageHash.size.toString().toByteArray(Charsets.UTF_8))
39-
val prefixMsgHash = ByteUtils.concatenate(prefixSize, messageHash)
50+
val prefixMsgHash = attachEthereumSignedMessage(messageHash)
4051
val ecKeyPair = ECKeyPair.create(Numeric.toBigInt(privateKey))
4152
val signatureData = Sign.signMessage(prefixMsgHash, ecKeyPair)
42-
return Web3.Signature(Numeric.toHexString(Hash.sha3(prefixMsgHash)),
43-
toHexString(signatureData.v),
44-
Numeric.toHexString(signatureData.r),
45-
Numeric.toHexString(signatureData.s),
46-
Numeric.toHexString(signatureEncode(signatureData)))
47-
}
48-
49-
private fun toHexString(byte: Byte) = String.format("0x%02x", byte)
50-
51-
private fun signatureEncode(signatureData: Sign.SignatureData): ByteArray {
52-
assert(signatureData.r.size == 32)
53-
assert(signatureData.s.size == 32)
54-
assert(signatureData.v.toInt() == 27 || signatureData.v.toInt() == 28)
55-
val buffer = ByteBuffer.allocate(SIGNATURE_LENGTH)
56-
buffer.put(signatureData.r)
57-
buffer.put(signatureData.s)
58-
buffer.put(signatureData.v)
59-
assert(buffer.position() == SIGNATURE_LENGTH)
60-
return buffer.array()
53+
return Web3.Signature(signatureData.v,
54+
signatureData.r,
55+
signatureData.s)
6156
}
6257
}
6358

6459
inner class Web3jEth : Web3.Eth {
6560
override val accounts: Web3.Accounts = Web3jAccounts()
61+
override val personal: Web3.Personal = Web3jPersonal()
6662

6763
override fun sign(dataToSign: String, address: String): String {
6864
return web3j.ethSign(address, dataToSign).send().signature
@@ -213,8 +209,6 @@ class Web3jAdapter(val web3j: Web3j) : Web3 {
213209
}
214210

215211
companion object {
216-
private const val SIGNATURE_LENGTH = 65
217-
218212
private fun createWeb3j(client: OkHttpClient?, provider: String): Web3j {
219213
val httpService = if (client != null) HttpService(provider, client, false) else HttpService(provider)
220214
return try {
@@ -227,5 +221,11 @@ class Web3jAdapter(val web3j: Web3j) : Web3 {
227221
Web3j.build(httpService)
228222
}
229223
}
224+
225+
private fun attachEthereumSignedMessage(messageHash: ByteArray): ByteArray? {
226+
val prefix = "\u0019Ethereum Signed Message:\n".toByteArray(Charsets.UTF_8)
227+
val prefixSize = ByteUtils.concatenate(prefix, messageHash.size.toString().toByteArray(Charsets.UTF_8))
228+
return ByteUtils.concatenate(prefixSize, messageHash)
229+
}
230230
}
231231
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.web3j.crypto;
2+
3+
import org.bouncycastle.asn1.x9.X9IntegerConverter;
4+
import org.bouncycastle.math.ec.ECAlgorithms;
5+
import org.bouncycastle.math.ec.ECPoint;
6+
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
7+
8+
import java.math.BigInteger;
9+
import java.util.Arrays;
10+
11+
import static org.web3j.utils.Assertions.verifyPrecondition;
12+
13+
public class RecoverFromSignature {
14+
/**
15+
* <p>Given the components of a signature and a selector value, recover and return the public
16+
* key that generated the signature according to the algorithm in SEC1v2 section 4.1.6.</p>
17+
*
18+
* <p>The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the
19+
* correct one. Because the key recovery operation yields multiple potential keys, the correct
20+
* key must either be stored alongside the
21+
* signature, or you must be willing to try each recId in turn until you find one that outputs
22+
* the key you are expecting.</p>
23+
*
24+
* <p>If this method returns null it means recovery was not possible and recId should be
25+
* iterated.</p>
26+
*
27+
* <p>Given the above two points, a correct usage of this method is inside a for loop from
28+
* 0 to 3, and if the output is null OR a key that is not the one you expect, you try again
29+
* with the next recId.</p>
30+
*
31+
* @param recId Which possible key to recover.
32+
* @param sig the R and S components of the signature, wrapped.
33+
* @param message Hash of the data that was signed.
34+
* @return An ECKey containing only the public part, or null if recovery wasn't possible.
35+
*/
36+
public static BigInteger recoverFromSignature(int recId, ECDSASignature sig, byte[] message) {
37+
verifyPrecondition(recId >= 0, "recId must be positive");
38+
verifyPrecondition(sig.r.signum() >= 0, "r must be positive");
39+
verifyPrecondition(sig.s.signum() >= 0, "s must be positive");
40+
verifyPrecondition(message != null, "message cannot be null");
41+
42+
// 1.0 For j from 0 to h (h == recId here and the loop is outside this function)
43+
// 1.1 Let x = r + jn
44+
BigInteger n = Sign.CURVE.getN(); // Curve order.
45+
BigInteger i = BigInteger.valueOf((long) recId / 2);
46+
BigInteger x = sig.r.add(i.multiply(n));
47+
// 1.2. Convert the integer x to an octet string X of length mlen using the conversion
48+
// routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉.
49+
// 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R
50+
// using the conversion routine specified in Section 2.3.4. If this conversion
51+
// routine outputs "invalid", then do another iteration of Step 1.
52+
//
53+
// More concisely, what these points mean is to use X as a compressed public key.
54+
BigInteger prime = SecP256K1Curve.q;
55+
if (x.compareTo(prime) >= 0) {
56+
// Cannot have point co-ordinates larger than this as everything takes place modulo Q.
57+
return null;
58+
}
59+
// Compressed keys require you to know an extra bit of data about the y-coord as there are
60+
// two possibilities. So it's encoded in the recId.
61+
ECPoint R = decompressKey(x, (recId & 1) == 1);
62+
// 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers
63+
// responsibility).
64+
if (!R.multiply(n).isInfinity()) {
65+
return null;
66+
}
67+
// 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification.
68+
BigInteger e = new BigInteger(1, message);
69+
// 1.6. For k from 1 to 2 do the following. (loop is outside this function via
70+
// iterating recId)
71+
// 1.6.1. Compute a candidate public key as:
72+
// Q = mi(r) * (sR - eG)
73+
//
74+
// Where mi(x) is the modular multiplicative inverse. We transform this into the following:
75+
// Q = (mi(r) * s ** R) + (mi(r) * -e ** G)
76+
// Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n).
77+
// In the above equation ** is point multiplication and + is point addition (the EC group
78+
// operator).
79+
//
80+
// We can find the additive inverse by subtracting e from zero then taking the mod. For
81+
// example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and
82+
// -3 mod 11 = 8.
83+
BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
84+
BigInteger rInv = sig.r.modInverse(n);
85+
BigInteger srInv = rInv.multiply(sig.s).mod(n);
86+
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
87+
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(Sign.CURVE.getG(), eInvrInv, R, srInv);
88+
89+
byte[] qBytes = q.getEncoded(false);
90+
// We remove the prefix
91+
return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length));
92+
}
93+
94+
/** Decompress a compressed public key (x co-ord and low-bit of y-coord). */
95+
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
96+
X9IntegerConverter x9 = new X9IntegerConverter();
97+
byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(Sign.CURVE.getCurve()));
98+
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
99+
return Sign.CURVE.getCurve().decodePoint(compEnc);
100+
}
101+
}
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package cc.etherspace.web3j
22

3+
import cc.etherspace.Web3
34
import okhttp3.OkHttpClient
45
import okhttp3.logging.HttpLoggingInterceptor
56
import org.amshove.kluent.`should be equal to`
67
import org.junit.Before
78
import org.junit.Test
9+
import org.web3j.crypto.Keys
10+
import org.web3j.crypto.Sign
811
import org.web3j.utils.Numeric
12+
import java.math.BigInteger
913

1014
class Web3jAdapterTest {
1115
lateinit var web3jAdapter: Web3jAdapter
16+
lateinit var privateKey: String
17+
lateinit var publicKey: BigInteger
18+
lateinit var address: String
1219

1320
@Before
1421
fun setUp() {
@@ -18,6 +25,10 @@ class Web3jAdapterTest {
1825
.addInterceptor(interceptor)
1926
.build()
2027
web3jAdapter = Web3jAdapter("http://localhost:8545", okHttpClient)
28+
29+
privateKey = "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d"
30+
publicKey = Sign.publicKeyFromPrivate(Numeric.toBigInt(privateKey))
31+
address = Keys.toChecksumAddress(Numeric.prependHexPrefix(Keys.getAddress(publicKey)))
2132
}
2233

2334
@Test
@@ -26,23 +37,32 @@ class Web3jAdapterTest {
2637
// sig.`should be equal to`("0x342ddadac7d370278559a5cbf0b80a600a2945828434b1d5a7dfb0d7f9f64e3270757c5a9bad86a90c89951c078ff45e0830c99aa313576d7dba349568251a9c01")
2738

2839
val data = Numeric.hexStringToByteArray("0x48540bdede62ae1b3868651336d31d36c29100af9db1559945879d14532a8067")
29-
val signature = web3jAdapter.eth.accounts.sign(data, "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d")
40+
val signature = web3jAdapter.eth.accounts.sign(data, privateKey)
3041

31-
signature.messageHash.`should be equal to`("0x29b207dbaf344538cb132c7910ab110160b5de4e212a35b870a3fb40d24c4754")
32-
signature.v.`should be equal to`("0x1c")
33-
signature.r.`should be equal to`("0x342ddadac7d370278559a5cbf0b80a600a2945828434b1d5a7dfb0d7f9f64e32")
34-
signature.s.`should be equal to`("0x70757c5a9bad86a90c89951c078ff45e0830c99aa313576d7dba349568251a9c")
42+
signature.v.`should be equal to`(28)
43+
Numeric.toHexString(signature.r).`should be equal to`("0x342ddadac7d370278559a5cbf0b80a600a2945828434b1d5a7dfb0d7f9f64e32")
44+
Numeric.toHexString(signature.s).`should be equal to`("0x70757c5a9bad86a90c89951c078ff45e0830c99aa313576d7dba349568251a9c")
3545
signature.signature.`should be equal to`("0x342ddadac7d370278559a5cbf0b80a600a2945828434b1d5a7dfb0d7f9f64e3270757c5a9bad86a90c89951c078ff45e0830c99aa313576d7dba349568251a9c1c")
3646
}
3747

48+
@Test
49+
fun ecRecover() {
50+
val signature = Web3.Signature(28,
51+
Numeric.hexStringToByteArray("0x342ddadac7d370278559a5cbf0b80a600a2945828434b1d5a7dfb0d7f9f64e32"),
52+
Numeric.hexStringToByteArray("0x70757c5a9bad86a90c89951c078ff45e0830c99aa313576d7dba349568251a9c"))
53+
54+
val data = Numeric.hexStringToByteArray("0x48540bdede62ae1b3868651336d31d36c29100af9db1559945879d14532a8067")
55+
val address = web3jAdapter.eth.personal.ecRecover(data, signature)
56+
address.`should be equal to`(address)
57+
}
58+
3859
@Test
3960
fun sign_jsexample() {
4061
val signature = web3jAdapter.eth.accounts.sign("Some data", "0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318")
4162

42-
signature.messageHash.`should be equal to`("0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655")
43-
signature.v.`should be equal to`("0x1c")
44-
signature.r.`should be equal to`("0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd")
45-
signature.s.`should be equal to`("0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029")
63+
signature.v.`should be equal to`(28)
64+
Numeric.toHexString(signature.r).`should be equal to`("0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd")
65+
Numeric.toHexString(signature.s).`should be equal to`("0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029")
4666
signature.signature.`should be equal to`("0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c")
4767
}
4868
}

0 commit comments

Comments
 (0)