package io.emax.cosigner.common.crypto;
import io.emax.cosigner.common.ByteUtilities;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.security.Security;
import java.util.LinkedList;
public class Secp256k1 {
private static final Logger LOGGER = LoggerFactory.getLogger(Secp256k1.class);
private static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
private static final String RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
private static final String SECP256K1 = "secp256k1";
public static final BigInteger MAXPRIVATEKEY =
new BigInteger("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140", 16);
/**
* Generate a random private key that can be used with Secp256k1.
*/
public static byte[] generatePrivateKey() {
SecureRandom secureRandom;
try {
secureRandom =
SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM, RANDOM_NUMBER_ALGORITHM_PROVIDER);
} catch (Exception e) {
LOGGER.error(null, e);
secureRandom = new SecureRandom();
}
// Generate the key, skipping as many as desired.
byte[] privateKeyAttempt = new byte[32];
secureRandom.nextBytes(privateKeyAttempt);
BigInteger privateKeyCheck = new BigInteger(1, privateKeyAttempt);
while (privateKeyCheck.compareTo(BigInteger.ZERO) == 0
|| privateKeyCheck.compareTo(MAXPRIVATEKEY) == 1) {
secureRandom.nextBytes(privateKeyAttempt);
privateKeyCheck = new BigInteger(1, privateKeyAttempt);
}
return privateKeyAttempt;
}
/**
* Converts a private key into its corresponding public key.
*/
public static byte[] getPublicKey(byte[] privateKey) {
try {
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(SECP256K1);
ECPoint pointQ = spec.getG().multiply(new BigInteger(1, privateKey));
return pointQ.getEncoded(false);
} catch (Exception e) {
LOGGER.error(null, e);
return new byte[0];
}
}
/**
* Sign data using the ECDSA algorithm.
*/
public static byte[][] signTransaction(byte[] data, byte[] privateKey) {
try {
Security.addProvider(new BouncyCastleProvider());
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(SECP256K1);
ECDSASigner ecdsaSigner = new ECDSASigner();
ECDomainParameters domain = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN());
ECPrivateKeyParameters privateKeyParms =
new ECPrivateKeyParameters(new BigInteger(1, privateKey), domain);
ParametersWithRandom params = new ParametersWithRandom(privateKeyParms);
ecdsaSigner.init(true, params);
BigInteger[] sig = ecdsaSigner.generateSignature(data);
LinkedList<byte[]> sigData = new LinkedList<>();
byte[] publicKey = getPublicKey(privateKey);
byte recoveryId = getRecoveryId(sig[0].toByteArray(), sig[1].toByteArray(), data, publicKey);
for (BigInteger sigChunk : sig) {
sigData.add(sigChunk.toByteArray());
}
sigData.add(new byte[]{recoveryId});
return sigData.toArray(new byte[][]{});
} catch (Exception e) {
LOGGER.error(null, e);
return new byte[0][0];
}
}
/**
* Determine the recovery ID for the given signature and public key.
*
* <p>Any signed message can resolve to one of two public keys due to the nature ECDSA. The
* recovery ID provides information about which one it is, allowing confirmation that the message
* was signed by a specific key.</p>
*/
public static byte getRecoveryId(byte[] sigR, byte[] sigS, byte[] message, byte[] publicKey) {
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(SECP256K1);
BigInteger pointN = spec.getN();
for (int recoveryId = 0; recoveryId < 2; recoveryId++) {
try {
BigInteger pointX = new BigInteger(1, sigR);
X9IntegerConverter x9 = new X9IntegerConverter();
byte[] compEnc = x9.integerToBytes(pointX, 1 + x9.getByteLength(spec.getCurve()));
compEnc[0] = (byte) ((recoveryId & 1) == 1 ? 0x03 : 0x02);
ECPoint pointR = spec.getCurve().decodePoint(compEnc);
if (!pointR.multiply(pointN).isInfinity()) {
continue;
}
BigInteger pointE = new BigInteger(1, message);
BigInteger pointEInv = BigInteger.ZERO.subtract(pointE).mod(pointN);
BigInteger pointRInv = new BigInteger(1, sigR).modInverse(pointN);
BigInteger srInv = pointRInv.multiply(new BigInteger(1, sigS)).mod(pointN);
BigInteger pointEInvRInv = pointRInv.multiply(pointEInv).mod(pointN);
ECPoint pointQ = ECAlgorithms.sumOfTwoMultiplies(spec.getG(), pointEInvRInv, pointR, srInv);
byte[] pointQBytes = pointQ.getEncoded(false);
boolean matchedKeys = true;
for (int j = 0; j < publicKey.length; j++) {
if (pointQBytes[j] != publicKey[j]) {
matchedKeys = false;
break;
}
}
if (!matchedKeys) {
continue;
}
return (byte) (0xFF & recoveryId);
} catch (Exception e) {
LOGGER.error(null, e);
}
}
return (byte) 0xFF;
}
/**
* Recover the public key that corresponds to the private key, which signed this message.
*/
public static byte[] recoverPublicKey(byte[] sigR, byte[] sigS, byte[] sigV, byte[] message) {
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(SECP256K1);
BigInteger pointN = spec.getN();
try {
BigInteger pointX = new BigInteger(1, sigR);
X9IntegerConverter x9 = new X9IntegerConverter();
byte[] compEnc = x9.integerToBytes(pointX, 1 + x9.getByteLength(spec.getCurve()));
compEnc[0] = (byte) ((sigV[0] & 1) == 1 ? 0x03 : 0x02);
ECPoint pointR = spec.getCurve().decodePoint(compEnc);
if (!pointR.multiply(pointN).isInfinity()) {
return new byte[0];
}
BigInteger pointE = new BigInteger(1, message);
BigInteger pointEInv = BigInteger.ZERO.subtract(pointE).mod(pointN);
BigInteger pointRInv = new BigInteger(1, sigR).modInverse(pointN);
BigInteger srInv = pointRInv.multiply(new BigInteger(1, sigS)).mod(pointN);
BigInteger pointEInvRInv = pointRInv.multiply(pointEInv).mod(pointN);
ECPoint pointQ = ECAlgorithms.sumOfTwoMultiplies(spec.getG(), pointEInvRInv, pointR, srInv);
return pointQ.getEncoded(false);
} catch (Exception e) {
LOGGER.warn("Error recovering public key from message");
}
return new byte[0];
}
/**
* Generate a shared AES key using ECDH.
*/
public static byte[] generateSharedSecret(byte[] privateKey, byte[] publicKey) {
try {
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(SECP256K1);
ECDomainParameters domain =
new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH());
ECPublicKeyParameters pubKey =
new ECPublicKeyParameters(spec.getCurve().decodePoint(publicKey), domain);
ECPrivateKeyParameters prvkey =
new ECPrivateKeyParameters(new BigInteger(1, privateKey), domain);
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
agreement.init(prvkey);
byte[] password = agreement.calculateAgreement(pubKey).toByteArray();
return Aes.generateKey(ByteUtilities.toHexString(password), password);
} catch (Exception e) {
LOGGER.error(null, e);
return new byte[0];
}
}
}