package one.block.eosiojava.utilities; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.primitives.Bytes; import java.io.CharArrayReader; import java.io.Reader; import java.math.BigInteger; import java.util.Arrays; import one.block.eosiojava.enums.AlgorithmEmployed; import one.block.eosiojava.error.ErrorConstants; import one.block.eosiojava.error.utilities.*; import org.bitcoinj.core.Base58; import org.bitcoinj.core.Sha256Hash; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.sec.SECNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.asn1.x9.X9IntegerConverter; import org.bouncycastle.crypto.digests.RIPEMD160Digest; import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.math.ec.ECAlgorithms; import org.bouncycastle.math.ec.ECCurve; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.math.ec.FixedPointUtil; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; import org.jetbrains.annotations.NotNull; /** * This class provides a number of helper methods that can be used to convert certain objects to and * from formats that are germane to EOS blockchain transactions and the PEM (Privacy Enhanced Mail) * format for those objects. It also provides methods to format a serialized transaction into a * format that can be submitted to an EOS blockchain. */ public class EOSFormatter { /* EOS Format Prefixes - The prefixes below are all used to preface the EOS format of certain types of keys and signatures. For instance, 'EOS' is used to preface a legacy form of a public key that was generated using the secp256k1 algorithm. The prefixes and there associated objects are as follows: EOS - Public Key generated with secp256k1 algorithm formatted for use on EOS blockchain. PUB_R1_ - Public Key generated with secp256r1 or prime256v1 algorithm formatted for use on EOS blockchain. PUB_K1_ - Public Key generated with secp256k1 algorithm formatted for use on EOS blockchain. PVT_R1_ - Private Key generated with secp256r1 algorithm formatted for use on EOS blockchain. SIG_R1_ - Signature signed with key generated with secp256r1 algorithm. SIG_K1_ - Signature signed with key generated with secp256k1 algorithm. */ private static final String PATTERN_STRING_EOS_PREFIX_EOS = "EOS"; private static final String PATTERN_STRING_EOS_PREFIX_PUB_R1 = "PUB_R1_"; private static final String PATTERN_STRING_EOS_PREFIX_PUB_K1 = "PUB_K1_"; private static final String PATTERN_STRING_EOS_PREFIX_PVT_R1 = "PVT_R1_"; private static final String PATTERN_STRING_EOS_PREFIX_SIG_R1 = "SIG_R1_"; private static final String PATTERN_STRING_EOS_PREFIX_SIG_K1 = "SIG_K1_"; //PEM FORMAT PREFIXES private static final String PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256R1 = "30770201010420"; private static final String PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256K1 = "302E0201010420"; private static final String PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_UNCOMPRESSED = "3059301306072a8648ce3d020106082a8648ce3d030107034200"; private static final String PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_UNCOMPRESSED = "3056301006072a8648ce3d020106052b8104000a034200"; private static final String PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_COMPRESSED = "3039301306072a8648ce3d020106082a8648ce3d030107032200"; private static final String PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_COMPRESSED = "3036301006072a8648ce3d020106052b8104000a032200"; //PEM FORMAT SUFFIXES private static final String PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256K1 = "A00706052B8104000A"; private static final String PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256R1 = "A00A06082A8648CE3D030107"; //PEM HEADERS & FOOTERS private static final String PEM_HEADER_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----"; private static final String PEM_FOOTER_PUBLIC_KEY = "-----END PUBLIC KEY-----"; private static final String PEM_HEADER_PRIVATE_KEY = "-----BEGIN EC PRIVATE KEY-----"; private static final String PEM_FOOTER_PRIVATE_KEY = "-----END EC PRIVATE KEY-----"; private static final String PEM_HEADER_EC_PRIVATE_KEY = "EC PRIVATE KEY"; private static final String PEM_HEADER_EC_PUBLIC_KEY = "PUBLIC KEY"; //CHECKSUM RELATED private static final String SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX = "R1"; private static final String SECP256K1_CHECKSUM_VALIDATION_SUFFIX = "K1"; private static final String LEGACY_CHECKSUM_VALIDATION_SUFFIX = ""; //CONSTANTS USED DURING DECODING AND CHECKSUM VALIDATION private static final int STANDARD_KEY_LENGTH = 32; private static final int CHECKSUM_BYTES = 4; private static final int FIRST_TWO_BYTES_OF_KEY = 4; private static final int DATA_SEQUENCE_LENGTH_BYTE_POSITION = 2; //CONSTANTS USED DURING EOS ENCODING private static final int EOS_SECP256K1_HEADER_BYTE = 0x80; //CONSTANTS USED DURING EOS DECODING private static final byte UNCOMPRESSED_PUBLIC_KEY_BYTE_INDICATOR = 0x04; private static final byte COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_POSITIVE_Y = 0x02; private static final byte COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_NEGATIVE_Y = 0x03; private static final int CHAIN_ID_LENGTH = 64; /** * Minimum length of signable transaction: Chain id length + 32 bytes of 0's length + 1 (minimum length for serialized transaction) */ private static final int MINIMUM_SIGNABLE_TRANSACTION_LENGTH = CHAIN_ID_LENGTH + 64 + 1; //SIGNATURE RELATED CONSTANTS private static final int VALUE_TO_ADD_TO_SIGNATURE_HEADER = 31; private static final int EXPECTED_R_OR_S_LENGTH = 32; private static final int NUMBER_OF_POSSIBLE_PUBLIC_KEYS = 4; /* Covers the PEM objects currently supported by this class (i.e. The class allows for PEM formatting of public keys, private keys, and signatures). */ private enum PEMObjectType { PUBLICKEY("PUBLIC KEY"), PRIVATEKEY("PRIVATE KEY"), SIGNATURE("SIGNATURE"); private String value; PEMObjectType(String value) { this.value = value; } public String getString() { return value; } } /** * Const name of secp256r1 curves */ private static final String SECP256_R1 = "secp256r1"; /** * Const name of secp256k1 curves */ private static final String SECP256_K1 = "secp256k1"; /** * EC domain parameters of R1 key */ private static final ECDomainParameters ecParamsR1; /** * EC domain parameters of K1 key */ private static final ECDomainParameters ecParamsK1; /** * EC parameters holder of R1 key type */ private static final X9ECParameters CURVE_PARAMS_R1 = CustomNamedCurves.getByName(SECP256_R1); /** * EC parameters holder of K1 key type */ private static final X9ECParameters CURVE_PARAMS_K1 = CustomNamedCurves.getByName(SECP256_K1); /** * EC holder of R1 key type */ private static final ECDomainParameters CURVE_R1; /** * Half curve value of R1 key type (to calculate low S) */ private static final BigInteger HALF_CURVE_ORDER_R1; /** * EC holder of K1 key type */ private static final ECDomainParameters CURVE_K1; /** * Half curve value of K1 key type (to calculate low S) */ private static final BigInteger HALF_CURVE_ORDER_K1; static { X9ECParameters paramsR1 = SECNamedCurves.getByName(SECP256_R1); ecParamsR1 = new ECDomainParameters(paramsR1.getCurve(), paramsR1.getG(), paramsR1.getN(), paramsR1.getH()); X9ECParameters paramsK1 = SECNamedCurves.getByName(SECP256_K1); ecParamsK1 = new ECDomainParameters(paramsK1.getCurve(), paramsK1.getG(), paramsK1.getN(), paramsK1.getH()); // secp256r1 FixedPointUtil.precompute(CURVE_PARAMS_R1.getG()); CURVE_R1 = new ECDomainParameters( CURVE_PARAMS_R1.getCurve(), CURVE_PARAMS_R1.getG(), CURVE_PARAMS_R1.getN(), CURVE_PARAMS_R1.getH()); HALF_CURVE_ORDER_R1 = CURVE_PARAMS_R1.getN().shiftRight(1); // secp256k1 CURVE_K1 = new ECDomainParameters( CURVE_PARAMS_K1.getCurve(), CURVE_PARAMS_K1.getG(), CURVE_PARAMS_K1.getN(), CURVE_PARAMS_K1.getH()); HALF_CURVE_ORDER_K1 = CURVE_PARAMS_K1.getN().shiftRight(1); } /** * This method converts a PEM formatted public key to the EOS format. * * @param publicKeyPEM Public key in the PEM format * @param requireLegacyFormOfSecp256k1Key - If the developer prefers a legacy version of a * secp256k1 key that uses a "EOS" prefix. * @return EOS formatted public key as string * @throws EOSFormatterError if PEM conversion to EOS format fails. */ @NotNull public static String convertPEMFormattedPublicKeyToEOSFormat(@NotNull String publicKeyPEM, boolean requireLegacyFormOfSecp256k1Key) throws EOSFormatterError { String eosFormattedPublicKey = publicKeyPEM; AlgorithmEmployed algorithmEmployed; PemObject pemObject; /* Validate that key type in PEM object is 'EC PUBLIC KEY'. */ String type; try (Reader reader = new CharArrayReader(eosFormattedPublicKey.toCharArray()); PemReader pemReader = new PemReader(reader);) { pemObject = pemReader.readPemObject(); type = pemObject.getType(); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.INVALID_PEM_PRIVATE_KEY, e); } //Perform a case-insensitive search for the 'EC PRIVATE KEY' string if (type.matches("(?i:.*" + PEM_HEADER_EC_PUBLIC_KEY + ".*)")) { //Get Base64 encoded public key from PEM object eosFormattedPublicKey = Hex.toHexString(pemObject.getContent()); //Determine algorithm used to generate key and remove DER header if (eosFormattedPublicKey .toUpperCase().contains( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_UNCOMPRESSED .toUpperCase())) { eosFormattedPublicKey = eosFormattedPublicKey.toUpperCase() .replace( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_UNCOMPRESSED .toUpperCase(), ""); algorithmEmployed = AlgorithmEmployed.SECP256R1; } else if (eosFormattedPublicKey .toUpperCase().contains( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_COMPRESSED .toUpperCase())) { eosFormattedPublicKey = eosFormattedPublicKey.toUpperCase() .replace(PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_COMPRESSED .toUpperCase(), ""); algorithmEmployed = AlgorithmEmployed.SECP256R1; } else if (eosFormattedPublicKey .toUpperCase().contains( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_UNCOMPRESSED .toUpperCase())) { eosFormattedPublicKey = eosFormattedPublicKey.toUpperCase() .replace( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_UNCOMPRESSED .toUpperCase(), ""); algorithmEmployed = AlgorithmEmployed.SECP256K1; } else if (eosFormattedPublicKey .toUpperCase().contains( PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_COMPRESSED .toUpperCase())) { eosFormattedPublicKey = eosFormattedPublicKey.toUpperCase() .replace(PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_COMPRESSED .toUpperCase(), ""); algorithmEmployed = AlgorithmEmployed.SECP256K1; } else { throw new EOSFormatterError(ErrorConstants.INVALID_DER_PRIVATE_KEY); } /* Compress the public key if necessary. Compression is only necessary if the key has a value of 0x04 for the first byte, which indicates it is uncompressed. */ byte[] eosFormattedPublicKeyBytes = Hex.decode(eosFormattedPublicKey); if (eosFormattedPublicKeyBytes[0] == UNCOMPRESSED_PUBLIC_KEY_BYTE_INDICATOR) { try { eosFormattedPublicKey = Hex.toHexString( compressPublickey(Hex.decode(eosFormattedPublicKey), algorithmEmployed)); } catch (Exception e) { throw new EOSFormatterError(e); } } try { //Add checksum,Base58 encode key, and add prefix eosFormattedPublicKey = encodePublicKey(Hex.decode(eosFormattedPublicKey), algorithmEmployed, requireLegacyFormOfSecp256k1Key); } catch (Base58ManipulationError e) { throw new EOSFormatterError(e); } } else { throw new EOSFormatterError(ErrorConstants.INVALID_PEM_PRIVATE_KEY); } return eosFormattedPublicKey; } /** * This method converts an EOS formatted public key to the PEM format. * * @param publicKeyEOS Public key in the EOS format * @return PEM formatted public key as string * @throws EOSFormatterError if EOS conversion to PEM format fails. */ @NotNull public static String convertEOSPublicKeyToPEMFormat(@NotNull String publicKeyEOS) throws EOSFormatterError { String pemFormattedPublickKey = publicKeyEOS; AlgorithmEmployed algorithmEmployed; String keyPrefix; /* The public key will contain an EOS prefix indicating which algorithm was used to generate it. Below we split the prefix from the key. */ if (pemFormattedPublickKey.toUpperCase() .contains(PATTERN_STRING_EOS_PREFIX_PUB_R1.toUpperCase())) { algorithmEmployed = AlgorithmEmployed.SECP256R1; keyPrefix = PATTERN_STRING_EOS_PREFIX_PUB_R1; pemFormattedPublickKey = pemFormattedPublickKey .replace(PATTERN_STRING_EOS_PREFIX_PUB_R1, ""); } else if (pemFormattedPublickKey.toUpperCase() .contains(PATTERN_STRING_EOS_PREFIX_PUB_K1.toUpperCase())) { algorithmEmployed = AlgorithmEmployed.SECP256K1; keyPrefix = PATTERN_STRING_EOS_PREFIX_PUB_K1; pemFormattedPublickKey = pemFormattedPublickKey .replace(PATTERN_STRING_EOS_PREFIX_PUB_K1, ""); } else if (pemFormattedPublickKey.toUpperCase() .contains(PATTERN_STRING_EOS_PREFIX_EOS.toUpperCase())) { algorithmEmployed = AlgorithmEmployed.SECP256K1; keyPrefix = PATTERN_STRING_EOS_PREFIX_EOS; pemFormattedPublickKey = pemFormattedPublickKey .replace(PATTERN_STRING_EOS_PREFIX_EOS, ""); } else { throw new EOSFormatterError(ErrorConstants.INVALID_EOS_PUBLIC_KEY); } //Base58 decode the key byte[] base58DecodedPublicKey; try { base58DecodedPublicKey = decodePublicKey(pemFormattedPublickKey, keyPrefix); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.BASE58_DECODING_ERROR, e); } //Convert decoded array to string pemFormattedPublickKey = Hex.toHexString(base58DecodedPublicKey); /* Compress the public key if necessary. Compression is only necessary if the key has a value of 0x04 for the first byte, which indicates it is uncompressed. */ if (base58DecodedPublicKey[0] == UNCOMPRESSED_PUBLIC_KEY_BYTE_INDICATOR) { try { pemFormattedPublickKey = Hex.toHexString( compressPublickey(Hex.decode(pemFormattedPublickKey), algorithmEmployed)); } catch (Exception e) { throw new EOSFormatterError(e); } } //Add DER header switch (algorithmEmployed) { case SECP256R1: pemFormattedPublickKey = PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256R1_COMPRESSED + pemFormattedPublickKey; break; case SECP256K1: pemFormattedPublickKey = PATTERN_STRING_PEM_PREFIX_PUBLIC_KEY_SECP256K1_COMPRESSED + pemFormattedPublickKey; break; default: throw new EOSFormatterError(ErrorConstants.UNSUPPORTED_ALGORITHM); } /* Correct the sequence length value. According to the ASN.1 specification. For a DER encoded public key the second byte reflects the number of bytes following the second byte. Here we take the length of the entire string, subtract 4 to remove the first two bytes, divide by 2 (i.e. two characters per byte) and replace the second byte in the string with the corrected length. */ if (pemFormattedPublickKey.length() > FIRST_TWO_BYTES_OF_KEY) { int i = (pemFormattedPublickKey.length() - FIRST_TWO_BYTES_OF_KEY) / 2; String correctedLength = Integer.toHexString(i); pemFormattedPublickKey = pemFormattedPublickKey.substring(0, DATA_SEQUENCE_LENGTH_BYTE_POSITION) + correctedLength + pemFormattedPublickKey.substring(FIRST_TWO_BYTES_OF_KEY); } else { throw new EOSFormatterError(ErrorConstants.INVALID_EOS_PUBLIC_KEY); } try { pemFormattedPublickKey = derToPEM(Hex.decode(pemFormattedPublickKey), PEMObjectType.PUBLICKEY); } catch (Exception e) { throw new EOSFormatterError(e); } return pemFormattedPublickKey; } /** * This method converts a signature to a EOS compliant form. The signature to be converted must * be an The ECDSA signature that is a DER encoded ASN.1 sequence of two integer fields (see * ECDSA-Sig-Value in rfc3279 section 2.2.3). * * The DER encoded ECDSA signature follows the following format: Byte 1 - Sequence (Should be * 30) Byte 2 - Signature length Byte 3 - R Marker (0x02) Byte 4 - R length Bytes 5 to 37 or 38- * R Byte After R - S Marker (0x02) Byte After S Marker - S Length Bytes After S Length - S * (always 32-33 bytes) Byte Final - Hash Type * * @param signatureDER ECDSA DER encoded signature as byte array * @param signableTransaction Transaction in signable format * @param publicKeyPEM public key in PEM format * @return EOS format of signature * @throws EOSFormatterError if DER conversion to EOS format fails. */ @NotNull public static String convertDERSignatureToEOSFormat(@NotNull byte[] signatureDER, @NotNull byte[] signableTransaction, @NotNull String publicKeyPEM) throws EOSFormatterError { String eosFormattedSignature = ""; try (ASN1InputStream asn1InputStream = new ASN1InputStream(signatureDER)) { PEMProcessor publicKey = new PEMProcessor(publicKeyPEM); AlgorithmEmployed algorithmEmployed = publicKey.getAlgorithm(); byte[] keyData = publicKey.getKeyData(); DLSequence sequence = (DLSequence) asn1InputStream.readObject(); BigInteger r = ((ASN1Integer) sequence.getObjectAt(0)).getPositiveValue(); BigInteger s = ((ASN1Integer) sequence.getObjectAt(1)).getPositiveValue(); s = checkAndHandleLowS(s, algorithmEmployed); /* Get recovery ID. This is the index of the public key (0-3) that represents the expected public key used to sign the transaction. */ int recoverId = getRecoveryId(r, s, Sha256Hash.of(signableTransaction), keyData, algorithmEmployed); if (recoverId < 0) { throw new IllegalStateException( ErrorConstants.COULD_NOT_RECOVER_PUBLIC_KEY_FROM_SIG); } //Add RecoveryID + 27 + 4 to create the header byte recoverId += VALUE_TO_ADD_TO_SIGNATURE_HEADER; byte headerByte = ((Integer) recoverId).byteValue(); byte[] decodedSignature = Bytes .concat(new byte[]{headerByte}, org.bitcoinj.core.Utils.bigIntegerToBytes(r,EXPECTED_R_OR_S_LENGTH), org.bitcoinj.core.Utils.bigIntegerToBytes(s,EXPECTED_R_OR_S_LENGTH)); if (algorithmEmployed.equals(AlgorithmEmployed.SECP256K1) && !isCanonical(decodedSignature)) { throw new IllegalArgumentException(ErrorConstants.NON_CANONICAL_SIGNATURE); } //Add checksum to signature byte[] signatureWithCheckSum; String signaturePrefix; switch (algorithmEmployed) { case SECP256R1: signatureWithCheckSum = addCheckSumToSignature(decodedSignature, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); signaturePrefix = PATTERN_STRING_EOS_PREFIX_SIG_R1; break; case SECP256K1: signatureWithCheckSum = addCheckSumToSignature(decodedSignature, SECP256K1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); signaturePrefix = PATTERN_STRING_EOS_PREFIX_SIG_K1; break; default: throw new EOSFormatterError(ErrorConstants.UNSUPPORTED_ALGORITHM); } //Base58 encode signature and add pertinent EOS prefix eosFormattedSignature = signaturePrefix.concat(Base58.encode(signatureWithCheckSum)); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.SIGNATURE_FORMATTING_ERROR, e); } return eosFormattedSignature; } /** * This method converts a signature to a EOS compliant form. The signature to be converted must * be an The ECDSA signature that is a DER encoded ASN.1 sequence of two integer fields (see * ECDSA-Sig-Value in rfc3279 section 2.2.3). This method should be used when only the R and S * values of the signature are available. * * The DER encoded ECDSA signature follows the following format: Byte 1 - Sequence (Should be * 30) Byte 2 - Signature length Byte 3 - R Marker (0x02) Byte 4 - R length Bytes 5 to 37 or 38- * R Byte After R - S Marker (0x02) Byte After S Marker - S Length Bytes After S Length - S * (always 32-33 bytes) Byte Final - Hash Type * * @param signatureR R value as BigInteger in string format * @param signatureS S value as BigInteger in string format * @param signableTransaction Transaction in signable format * @param publicKeyPEM Public Key used to sign in PEM format * @return EOS format of signature * @throws EOSFormatterError if conversion to EOS format fails. */ @NotNull public static String convertRawRandSofSignatureToEOSFormat(@NotNull String signatureR, String signatureS, @NotNull byte[] signableTransaction, @NotNull String publicKeyPEM) throws EOSFormatterError { String eosFormattedSignature = ""; try { PEMProcessor publicKey = new PEMProcessor(publicKeyPEM); AlgorithmEmployed algorithmEmployed = publicKey.getAlgorithm(); byte[] keyData = publicKey.getKeyData(); BigInteger r = new BigInteger(signatureR); BigInteger s = new BigInteger(signatureS); s = checkAndHandleLowS(s, algorithmEmployed); /* Get recovery ID. This is the index of the public key (0-3) that represents the expected public key used to sign the transaction. */ int recoverId = getRecoveryId(r, s, Sha256Hash.of(signableTransaction), keyData, algorithmEmployed); if (recoverId < 0) { throw new IllegalStateException( ErrorConstants.COULD_NOT_RECOVER_PUBLIC_KEY_FROM_SIG); } //Add RecoveryID + 27 + 4 to create the header byte recoverId += VALUE_TO_ADD_TO_SIGNATURE_HEADER; byte headerByte = ((Integer) recoverId).byteValue(); byte[] decodedSignature = Bytes .concat(new byte[]{headerByte}, org.bitcoinj.core.Utils.bigIntegerToBytes(r,EXPECTED_R_OR_S_LENGTH), org.bitcoinj.core.Utils.bigIntegerToBytes(s,EXPECTED_R_OR_S_LENGTH)); if (algorithmEmployed.equals(AlgorithmEmployed.SECP256K1) && !isCanonical(decodedSignature)) { throw new EosFormatterSignatureIsNotCanonicalError(ErrorConstants.NON_CANONICAL_SIGNATURE); } //Add checksum to signature byte[] signatureWithCheckSum; String signaturePrefix; switch (algorithmEmployed) { case SECP256R1: signatureWithCheckSum = addCheckSumToSignature(decodedSignature, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); signaturePrefix = PATTERN_STRING_EOS_PREFIX_SIG_R1; break; case SECP256K1: signatureWithCheckSum = addCheckSumToSignature(decodedSignature, SECP256K1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); signaturePrefix = PATTERN_STRING_EOS_PREFIX_SIG_K1; break; default: throw new EOSFormatterError(ErrorConstants.UNSUPPORTED_ALGORITHM); } //Base58 encode signature and add pertinent EOS prefix eosFormattedSignature = signaturePrefix.concat(Base58.encode(signatureWithCheckSum)); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.SIGNATURE_FORMATTING_ERROR, e); } return eosFormattedSignature; } /** * This method converts a PEM formatted private key to the EOS format. * * @param privateKeyPEM Private key in PEM format * @return EOS formatted private key as string * @throws EOSFormatterError if PEM conversion to EOS format fails. */ @NotNull public static String convertPEMFormattedPrivateKeyToEOSFormat(@NotNull String privateKeyPEM) throws EOSFormatterError { String eosFormattedPrivateKey = privateKeyPEM; AlgorithmEmployed algorithmEmployed; PemObject pemObject; /* Validate that key type in PEM object is 'EC PRIVATE KEY'. */ String type; try (Reader reader = new CharArrayReader(eosFormattedPrivateKey.toCharArray()); PemReader pemReader = new PemReader(reader);) { pemObject = pemReader.readPemObject(); type = pemObject.getType(); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.INVALID_PEM_PRIVATE_KEY, e); } //Perform a case-insensitive search for the 'EC PRIVATE KEY' string if (type.matches("(?i:.*" + PEM_HEADER_EC_PRIVATE_KEY + ".*)")) { //Get Base64 encoded private key from PEM object eosFormattedPrivateKey = Hex.toHexString(pemObject.getContent()); //Determine algorithm used to generate key if (eosFormattedPrivateKey .matches("(?i:.*" + PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256R1 + ".*)")) { algorithmEmployed = AlgorithmEmployed.SECP256R1; } else if (eosFormattedPrivateKey .matches("(?i:.*" + PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256K1 + ".*)")) { algorithmEmployed = AlgorithmEmployed.SECP256K1; } else { throw new EOSFormatterError(ErrorConstants.INVALID_DER_PRIVATE_KEY); } //Strip away the DER header and footer switch (algorithmEmployed) { case SECP256R1: eosFormattedPrivateKey = eosFormattedPrivateKey .substring(PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256R1.length(), eosFormattedPrivateKey.length() - PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256R1 .length()); break; case SECP256K1: eosFormattedPrivateKey = eosFormattedPrivateKey .substring(PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256K1.length(), eosFormattedPrivateKey.length() - PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256K1 .length()); break; default: throw new EOSFormatterError(ErrorConstants.UNSUPPORTED_ALGORITHM); } try { //Add checksum and Base58 encode key eosFormattedPrivateKey = encodePrivateKey(Hex.decode(eosFormattedPrivateKey), algorithmEmployed); } catch (Base58ManipulationError e) { throw new EOSFormatterError(e); } //Add prefix StringBuilder builder = new StringBuilder(eosFormattedPrivateKey); switch (algorithmEmployed) { case SECP256K1: //K1 keys do not currently use prefixes break; case SECP256R1: builder.insert(0, PATTERN_STRING_EOS_PREFIX_PVT_R1); break; default: break; } eosFormattedPrivateKey = builder.toString(); } else { throw new EOSFormatterError(ErrorConstants.INVALID_PEM_PRIVATE_KEY); } return eosFormattedPrivateKey; } /** * This method converts an EOS formatted private key to the PEM format. * * @param privateKeyEOS Private key in EOS format * @return PEM formatted private key as a string * @throws EOSFormatterError if EOS conversion to PEM format fails. */ @NotNull public static String convertEOSPrivateKeyToPEMFormat(@NotNull String privateKeyEOS) throws EOSFormatterError { String pemFormattedPrivateKey = privateKeyEOS; AlgorithmEmployed algorithmEmployed; /* If the private key was encrypted using the secp256R1 algorithm it will have a 'PVT_R1_' prefix that needs to be removed. */ if (pemFormattedPrivateKey.toUpperCase() .contains(PATTERN_STRING_EOS_PREFIX_PVT_R1.toUpperCase())) { algorithmEmployed = AlgorithmEmployed.SECP256R1; /* Split the prefix from the key and take the second half of string. The second half contains the key minus the prefix. */ pemFormattedPrivateKey = pemFormattedPrivateKey .split(PATTERN_STRING_EOS_PREFIX_PVT_R1)[1]; } else { algorithmEmployed = AlgorithmEmployed.SECP256K1; } //Base58 decode the key byte[] base58DecodedPrivateKey; try { base58DecodedPrivateKey = decodePrivateKey(pemFormattedPrivateKey, algorithmEmployed); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.BASE58_DECODING_ERROR, e); } //Convert decoded array to string pemFormattedPrivateKey = Hex.toHexString(base58DecodedPrivateKey); //Add header and footer switch (algorithmEmployed) { case SECP256R1: pemFormattedPrivateKey = PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256R1 + pemFormattedPrivateKey + PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256R1; break; case SECP256K1: pemFormattedPrivateKey = PATTERN_STRING_PEM_PREFIX_PRIVATE_KEY_SECP256K1 + pemFormattedPrivateKey + PATTERN_STRING_PEM_SUFFIX_PRIVATE_KEY_SECP256K1; break; default: throw new EOSFormatterError(ErrorConstants.UNSUPPORTED_ALGORITHM); } /* Correct the sequence length value. According to the ASN.1 specification. For a DER encoded private key the second byte reflects the number of bytes following the second byte. Here we take the length of the entire string, subtract 4 to remove the first two bytes, divide by 2 (i.e. two characters per byte) and replace the second byte in the string with the corrected length. */ if (pemFormattedPrivateKey.length() > FIRST_TWO_BYTES_OF_KEY) { int i = (pemFormattedPrivateKey.length() - FIRST_TWO_BYTES_OF_KEY) / 2; String correctedLength = Integer.toHexString(i); pemFormattedPrivateKey = pemFormattedPrivateKey.substring(0, DATA_SEQUENCE_LENGTH_BYTE_POSITION) + correctedLength + pemFormattedPrivateKey.substring(FIRST_TWO_BYTES_OF_KEY); } else { throw new EOSFormatterError(ErrorConstants.INVALID_EOS_PRIVATE_KEY); } try { pemFormattedPrivateKey = derToPEM(Hex.decode(pemFormattedPrivateKey), PEMObjectType.PRIVATEKEY); } catch (DerToPemConversionError e) { throw new EOSFormatterError(e); } return pemFormattedPrivateKey; } /** * Extract serialized transaction from a signable transaction * <p> * Signable signature structure: * <p> * chainId (64 characters) + serialized transaction + 32 bytes of 0 * * @param eosTransaction - the input signable transaction * @return - extracted serialized transaction from the input signable transaction * @throws EOSFormatterError if input is invalid */ public static String extractSerializedTransactionFromSignable(@NotNull String eosTransaction) throws EOSFormatterError { if (eosTransaction.isEmpty()) { throw new EOSFormatterError(ErrorConstants.EMPTY_INPUT_EXTRACT_SERIALIZIED_TRANS_FROM_SIGNABLE); } if (eosTransaction.length() <= MINIMUM_SIGNABLE_TRANSACTION_LENGTH) { throw new EOSFormatterError(String.format(ErrorConstants.INVALID_INPUT_SIGNABLE_TRANS_LENGTH_EXTRACT_SERIALIZIED_TRANS_FROM_SIGNABLE, MINIMUM_SIGNABLE_TRANSACTION_LENGTH)); } if (!eosTransaction.endsWith(Hex.toHexString(new byte[32]))) { throw new EOSFormatterError(ErrorConstants.INVALID_INPUT_SIGNABLE_TRANS_EXTRACT_SERIALIZIED_TRANS_FROM_SIGNABLE); } try { String cutChainId = eosTransaction.substring(CHAIN_ID_LENGTH); return cutChainId.substring(0, cutChainId.length() - Hex.toHexString(new byte[32]).length()); } catch (Exception ex) { throw new EOSFormatterError(ErrorConstants.EXTRACT_SERIALIZIED_TRANS_FROM_SIGNABLE_ERROR, ex); } } /** * Preparing signable transaction for signing. * <p> * Signable signature structure: * <p> * chainId + serialized transaction + 32 bytes of 0 * * @param serializedTransaction - the serialized transaction to be converted to signable transaction * @param chainId - the chain id will be used inside the signature transaction structure. * @return - Signable transaction * @throws EOSFormatterError if inputs are invalid */ public static String prepareSerializedTransactionForSigning(@NotNull String serializedTransaction, @NotNull String chainId) throws EOSFormatterError { if (serializedTransaction.isEmpty() || chainId.isEmpty()) { throw new EOSFormatterError(ErrorConstants.EMPTY_INPUT_PREPARE_SERIALIZIED_TRANS_FOR_SIGNING); } String signableTransaction = chainId + serializedTransaction + Hex.toHexString(new byte[32]); if (signableTransaction.length() <= MINIMUM_SIGNABLE_TRANSACTION_LENGTH) { throw new EOSFormatterError(String.format(ErrorConstants.INVALID_INPUT_SIGNABLE_TRANS_LENGTH_EXTRACT_SERIALIZIED_TRANS_FROM_SIGNABLE, MINIMUM_SIGNABLE_TRANSACTION_LENGTH)); } return signableTransaction; } /** * This method converts a DER encoded private key, public key, or signature into the PEM * format. * * Example of a PEM formatted private key: -----BEGIN EC PRIVATE KEY----- * MDECAQEEIEJSCKmyR0kmxy2pgkEwkqrodn2jG9mhXRhhxgsneuBsoAoGCCqGSM49AwEH -----END EC PRIVATE * KEY----- * * The key data between the header and footer is Base64 encoded. * * @param derEncodedByteArray DER encoded byte array to convert to PEM format * @param pemObjectType The type of PEM object being created (i.e. Private Key, Public Key, * Signature). * @return PEM format as string. * @throws DerToPemConversionError if DER to PEM conversion fails. */ @NotNull private static String derToPEM(@NotNull byte[] derEncodedByteArray, @NotNull PEMObjectType pemObjectType) throws DerToPemConversionError { StringBuilder pemForm = new StringBuilder(); try { //Build Header if (pemObjectType.equals(PEMObjectType.PRIVATEKEY)) { pemForm.append(PEM_HEADER_PRIVATE_KEY); } else if (pemObjectType.equals(PEMObjectType.PUBLICKEY)) { pemForm.append(PEM_HEADER_PUBLIC_KEY); } else { throw new DerToPemConversionError(ErrorConstants.DER_TO_PEM_CONVERSION); } pemForm.append("\n"); //Base64 Encode DER Encoded Byte Array And Add to PEM Object String base64EncodedByteArray = new String(Base64.encode(derEncodedByteArray)); pemForm.append(base64EncodedByteArray); pemForm.append("\n"); //Build Footer if (pemObjectType.equals(PEMObjectType.PRIVATEKEY)) { pemForm.append(PEM_FOOTER_PRIVATE_KEY); } else if (pemObjectType.equals(PEMObjectType.PUBLICKEY)) { pemForm.append(PEM_FOOTER_PUBLIC_KEY); } else { throw new DerToPemConversionError(ErrorConstants.DER_TO_PEM_CONVERSION); } } catch (Exception e) { throw new DerToPemConversionError(ErrorConstants.DER_TO_PEM_CONVERSION, e); } return pemForm.toString(); } /** * This method Base58 decodes the private key and validates its checksum. * * @param strKey Base58 value of the key * @param keyType key type * @return Base58 decoded key minus checksum * @throws Base58ManipulationError if private key decoding fails. */ @NotNull private static byte[] decodePrivateKey(@NotNull String strKey, AlgorithmEmployed keyType) throws Base58ManipulationError { if (strKey.isEmpty()) { throw new IllegalArgumentException(ErrorConstants.BASE58_EMPTY_KEY); } byte[] decodedKey; try { byte[] base58Decoded = Base58.decode(strKey); byte[] firstCheckSum = Arrays .copyOfRange(base58Decoded, base58Decoded.length - CHECKSUM_BYTES, base58Decoded.length); decodedKey = Arrays .copyOfRange(base58Decoded, 0, base58Decoded.length - CHECKSUM_BYTES); switch (keyType) { case SECP256R1: byte[] secp256r1Suffix = SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX .getBytes(); if (invalidRipeMD160CheckSum(decodedKey, firstCheckSum, secp256r1Suffix)) { throw new IllegalArgumentException(ErrorConstants.BASE58_INVALID_CHECKSUM); } break; case PRIME256V1: byte[] prime256v1Suffix = SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX .getBytes(); if (invalidRipeMD160CheckSum(decodedKey, firstCheckSum, prime256v1Suffix)) { throw new IllegalArgumentException(ErrorConstants.BASE58_INVALID_CHECKSUM); } break; case SECP256K1: if (invalidSha256x2CheckSum(decodedKey, firstCheckSum)) { throw new IllegalArgumentException(ErrorConstants.BASE58_INVALID_CHECKSUM); } break; default: throw new Base58ManipulationError(ErrorConstants.UNSUPPORTED_ALGORITHM); } // trim 0x80 out if the key size is more than 32 bytes // this code apply for key has more than 32 byte and non R1 key if (decodedKey.length > STANDARD_KEY_LENGTH && keyType != AlgorithmEmployed.SECP256R1) { // Slice out the first byte decodedKey = Arrays.copyOfRange(decodedKey, 1, decodedKey.length); if (decodedKey.length > STANDARD_KEY_LENGTH && decodedKey[STANDARD_KEY_LENGTH] == ((Integer) 1).byteValue()) { // Slice out last byte decodedKey = Arrays.copyOfRange(decodedKey, 0, decodedKey.length - 1); } } } catch (Exception ex) { throw new Base58ManipulationError(ErrorConstants.BASE58_DECODING_ERROR, ex); } return decodedKey; } /** * Base58 encodes a private key after calculating and appending the checksum. * * @param pemKey - Private key as byte[] to encode * @param keyType - input key type * @return Base58 encoded private key as byte[] * @throws Base58ManipulationError it private key encoding fails. */ @NotNull public static String encodePrivateKey(@NotNull byte[] pemKey, @NotNull AlgorithmEmployed keyType) throws Base58ManipulationError { byte[] checkSum; String base58Key = ""; switch (keyType) { case SECP256R1: checkSum = extractCheckSumRIPEMD160(pemKey, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); break; case PRIME256V1: checkSum = extractCheckSumRIPEMD160(pemKey, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); break; case SECP256K1: pemKey = Bytes.concat(new byte[]{((Integer) EOS_SECP256K1_HEADER_BYTE).byteValue()}, pemKey); checkSum = extractCheckSumSha256x2(pemKey); break; default: throw new Base58ManipulationError(ErrorConstants.CHECKSUM_GENERATION_ERROR); } base58Key = Base58.encode(Bytes.concat(pemKey, checkSum)); if (base58Key.isEmpty()) { throw new Base58ManipulationError(ErrorConstants.BASE58_ENCODING_ERROR); } else { return base58Key; } } /** * Encoding PEM public key to EOS format. * * @param pemKey - PEM key as byte[] to encode * @param keyType - Algorithm type used to create key * @param isLegacy - If the developer prefers a legacy version of a secp256k1 key that uses an * "EOS" prefix. * @return - EOS format of public key * @throws Base58ManipulationError if public key encoding fails. */ @NotNull public static String encodePublicKey(@NotNull byte[] pemKey, @NotNull AlgorithmEmployed keyType, boolean isLegacy) throws Base58ManipulationError { String base58Key = ""; if (pemKey.length == 0) { throw new IllegalArgumentException(ErrorConstants.PUBLIC_KEY_IS_EMPTY); } try { byte[] checkSum; switch (keyType) { case SECP256K1: if (isLegacy) { checkSum = extractCheckSumRIPEMD160(pemKey, LEGACY_CHECKSUM_VALIDATION_SUFFIX.getBytes()); } else { checkSum = extractCheckSumRIPEMD160(pemKey, SECP256K1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); } break; case SECP256R1: checkSum = extractCheckSumRIPEMD160(pemKey, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes()); break; default: throw new Base58ManipulationError(ErrorConstants.UNSUPPORTED_ALGORITHM); } base58Key = Base58.encode(Bytes.concat(pemKey, checkSum)); if (base58Key.equals("")) { throw new Base58ManipulationError(ErrorConstants.BASE58_ENCODING_ERROR); } } catch (Exception ex) { throw new Base58ManipulationError(ErrorConstants.BASE58_ENCODING_ERROR, ex); } //Add prefix StringBuilder builder = new StringBuilder(base58Key); switch (keyType) { case SECP256K1: if (isLegacy) { builder.insert(0, PATTERN_STRING_EOS_PREFIX_EOS); } else { builder.insert(0, PATTERN_STRING_EOS_PREFIX_PUB_K1); } break; case SECP256R1: builder.insert(0, PATTERN_STRING_EOS_PREFIX_PUB_R1); break; default: break; } base58Key = builder.toString(); return base58Key; } /** * Base58 decodes a public key and validates checksum. * * @param strKey Base58 encoded public key in string format. * @param keyPrefix EOS specific key type prefix (i.e. PUB_R1_, PUB_K1_, or EOS). * @return Base58 decoded public key as byte[] * @throws Base58ManipulationError if public key decoding fails. */ @NotNull public static byte[] decodePublicKey(@NotNull String strKey, String keyPrefix) throws Base58ManipulationError { if (strKey.isEmpty()) { throw new IllegalArgumentException("Input key to decode can't be empty."); } byte[] decodedKey = null; try { byte[] base58Decoded = Base58.decode(strKey); byte[] firstCheckSum = Arrays .copyOfRange(base58Decoded, base58Decoded.length - CHECKSUM_BYTES, base58Decoded.length); decodedKey = Arrays .copyOfRange(base58Decoded, 0, base58Decoded.length - CHECKSUM_BYTES); switch (keyPrefix) { case PATTERN_STRING_EOS_PREFIX_PUB_R1: if (invalidRipeMD160CheckSum(decodedKey, firstCheckSum, SECP256R1_AND_PRIME256V1_CHECKSUM_VALIDATION_SUFFIX.getBytes())) { throw new IllegalArgumentException( ErrorConstants.BASE58_INVALID_CHECKSUM); } break; case PATTERN_STRING_EOS_PREFIX_PUB_K1: if (invalidRipeMD160CheckSum(decodedKey, firstCheckSum, SECP256K1_CHECKSUM_VALIDATION_SUFFIX.getBytes())) { throw new IllegalArgumentException( ErrorConstants.BASE58_INVALID_CHECKSUM); } break; case PATTERN_STRING_EOS_PREFIX_EOS: if (invalidRipeMD160CheckSum(decodedKey, firstCheckSum, LEGACY_CHECKSUM_VALIDATION_SUFFIX.getBytes())) { throw new IllegalArgumentException( ErrorConstants.BASE58_INVALID_CHECKSUM); } break; default: break; } } catch (Exception ex) { throw new Base58ManipulationError(ErrorConstants.BASE58_DECODING_ERROR, ex); } return decodedKey; } /** * Validate checksum by RipeMD160 digestion * * @param inputKey - input key to validate * @param checkSumToValidate - checksum to validate with the checksum inside input key * @param keyTypeByteArray - byte[] of key type used for checksum validation (e.g. * "K1".getBytes()) * @return This checksum returns whether the checksum comparison was invalid. */ private static boolean invalidRipeMD160CheckSum(@NotNull byte[] inputKey, @NotNull byte[] checkSumToValidate, @NotNull byte[] keyTypeByteArray) { if (inputKey.length == 0 || checkSumToValidate.length == 0 ) { throw new IllegalArgumentException( ErrorConstants.BASE58_EMPTY_CHECKSUM_OR_KEY_OR_KEY_TYPE); } byte[] keyWithType = Bytes.concat(inputKey, keyTypeByteArray); byte[] digestRIPEMD160 = digestRIPEMD160(keyWithType); byte[] checkSumFromInputKey = Arrays.copyOfRange(digestRIPEMD160, 0, CHECKSUM_BYTES); //This checksum returns whether the checksum comparison was invalid. return !Arrays.equals(checkSumToValidate, checkSumFromInputKey); } /** * Validate checksum by double Sha256 * * @param inputKey - input key to validate * @param checkSumToValidate - checksum to validate with the checksum inside input key * @return This checksum returns whether the checksum comparison was invalid. */ private static boolean invalidSha256x2CheckSum(@NotNull byte[] inputKey, @NotNull byte[] checkSumToValidate) { if (inputKey.length == 0 || checkSumToValidate.length == 0) { throw new IllegalArgumentException(ErrorConstants.BASE58_EMPTY_CHECKSUM_OR_KEY); } byte[] sha256x2 = Sha256Hash.hashTwice(inputKey); byte[] checkSumFromInputKey = Arrays.copyOfRange(sha256x2, 0, CHECKSUM_BYTES); //This checksum returns whether the checksum comparison was invalid. return !Arrays.equals(checkSumToValidate, checkSumFromInputKey); } /** * Digesting input byte[] to RIPEMD160 format * * @param input - input byte[] * @return RIPEMD160 format */ @NotNull private static byte[] digestRIPEMD160(@NotNull byte[] input) { RIPEMD160Digest digest = new RIPEMD160Digest(); byte[] output = new byte[digest.getDigestSize()]; digest.update(input, 0, input.length); digest.doFinal(output, 0); return output; } /** * Extracting Checksum for RIPEMD160 digest format * * @param pemKey - input PEM key * @return checksum */ @NotNull private static byte[] extractCheckSumRIPEMD160(@NotNull byte[] pemKey, byte[] keyTypeByteArray) { if (keyTypeByteArray != null) { pemKey = Bytes.concat(pemKey, keyTypeByteArray); } byte[] ripemd160Digest = digestRIPEMD160(pemKey); return Arrays.copyOfRange(ripemd160Digest, 0, CHECKSUM_BYTES); } /** * Extracting checksum for Sha256x2 format * * @param pemKey - input pem key * @return checksum */ @NotNull private static byte[] extractCheckSumSha256x2(@NotNull byte[] pemKey) { byte[] sha256x2 = Sha256Hash.hashTwice(pemKey); return Arrays.copyOfRange(sha256x2, 0, CHECKSUM_BYTES); } /** * Decompresses a public key based on the algorithm used to generate it. * * @param compressedPublicKey Compressed public key as byte[] * @param algorithmEmployed Algorithm used during key creation * @return Decompressed public key as byte[] * @throws EOSFormatterError when public key decompression fails. */ @NotNull private static byte[] decompressPublickey(byte[] compressedPublicKey, AlgorithmEmployed algorithmEmployed) throws EOSFormatterError { try { ECParameterSpec parameterSpec = ECNamedCurveTable .getParameterSpec(algorithmEmployed.getString()); ECPoint ecPoint = parameterSpec.getCurve().decodePoint(compressedPublicKey); byte[] x = ecPoint.getXCoord().getEncoded(); byte[] y = ecPoint.getYCoord().getEncoded(); if (y.length > STANDARD_KEY_LENGTH) { y = Arrays.copyOfRange(y, 1, y.length); } return Bytes.concat(new byte[]{UNCOMPRESSED_PUBLIC_KEY_BYTE_INDICATOR}, x, y); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.PUBLIC_KEY_DECOMPRESSION_ERROR, e); } } /** * Compresses a public key based on the algorithm used to generate it. * * @param compressedPublicKey Decompressed public key as byte[] * @param algorithmEmployed Algorithm used during key creation * @return Compressed public key as byte[] * @throws EOSFormatterError when public key compression fails. */ @NotNull private static byte[] compressPublickey(byte[] compressedPublicKey, AlgorithmEmployed algorithmEmployed) throws EOSFormatterError { byte compressionPrefix; try { ECParameterSpec parameterSpec = ECNamedCurveTable .getParameterSpec(algorithmEmployed.getString()); ECPoint ecPoint = parameterSpec.getCurve().decodePoint(compressedPublicKey); byte[] x = ecPoint.getXCoord().getEncoded(); byte[] y = ecPoint.getYCoord().getEncoded(); //Check whether y is negative(odd in field) or positive(even in field) and assign compressionPrefix BigInteger bigIntegerY = new BigInteger(Hex.toHexString(y), 16); BigInteger bigIntegerTwo = BigInteger.valueOf(2); BigInteger remainder = bigIntegerY.mod(bigIntegerTwo); if (remainder.equals(BigInteger.ZERO)) { compressionPrefix = COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_POSITIVE_Y; } else { compressionPrefix = COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_NEGATIVE_Y; } return Bytes.concat(new byte[]{compressionPrefix}, x); } catch (Exception e) { throw new EOSFormatterError(ErrorConstants.PUBLIC_KEY_COMPRESSION_ERROR, e); } } /** * Takes the S value of an ECDSA DER encoded signature and converts it to a low value. * * @param s S value from signature * @param keyType Algorithm used to generate private key that signed the message. * @return Low S value * @throws LowSVerificationError when the S value determination fails. */ private static BigInteger checkAndHandleLowS(BigInteger s, AlgorithmEmployed keyType) throws LowSVerificationError { if (!isLowS(s, keyType)) { switch (keyType) { case SECP256R1: return CURVE_R1.getN().subtract(s); default: return CURVE_K1.getN().subtract(s); } } return s; } /** * Takes the S value of an ECDSA DER encoded signature and determines whether the value is low. * * @param s S value from signature * @param keyType Algorithm used to generate private key that signed the message. * @return boolean indicating whether S value is low * @throws LowSVerificationError when the S value determination fails. */ private static boolean isLowS(BigInteger s, AlgorithmEmployed keyType) throws LowSVerificationError { int compareResult; switch (keyType) { case SECP256R1: compareResult = s.compareTo(HALF_CURVE_ORDER_R1); break; case SECP256K1: compareResult = s.compareTo(HALF_CURVE_ORDER_K1); break; default: throw new LowSVerificationError(ErrorConstants.UNSUPPORTED_ALGORITHM); } return compareResult == 0 || compareResult == -1; } /** * Adding checksum to signature * * @param signature - signature to get checksum added */ private static byte[] addCheckSumToSignature(byte[] signature, byte[] keyTypeByteArray) { byte[] signatureWithKeyType = Bytes.concat(signature, keyTypeByteArray); byte[] signatureRipemd160 = digestRIPEMD160(signatureWithKeyType); byte[] checkSum = Arrays.copyOfRange(signatureRipemd160, 0, CHECKSUM_BYTES); return Bytes.concat(signature, checkSum); } /** * Check if the input signature is canonical * * @param signature - signature to check for canonical * @return whether the input signature is canonical */ private static boolean isCanonical(byte[] signature) { return (signature[1] & ((Integer) 0x80).byteValue()) == ((Integer) 0x00).byteValue() && !(signature[1] == ((Integer) 0x00).byteValue() && ((signature[2] & ((Integer) 0x80).byteValue()) == ((Integer) 0x00).byteValue())) && (signature[33] & ((Integer) 0x80).byteValue()) == ((Integer) 0x00).byteValue() && !(signature[33] == ((Integer) 0x00).byteValue() && ((signature[34] & ((Integer) 0x80).byteValue()) == ((Integer) 0x00) .byteValue())); } /** * Getting recovery id from R and S * * @param r - R in DER of Signature * @param s - S in DER of Signature * @param sha256HashMessage - Sha256Hash of signed message * @param publicKey - public key to validate * @param keyType - key type * @return - Recovery id of the signature. From 0 to 3. Return -1 if find nothing. */ private static int getRecoveryId(BigInteger r, BigInteger s, Sha256Hash sha256HashMessage, byte[] publicKey, AlgorithmEmployed keyType) { for (int i = 0; i < NUMBER_OF_POSSIBLE_PUBLIC_KEYS; i++) { byte[] recoveredPublicKey = recoverPublicKeyFromSignature(i, r, s, sha256HashMessage, true, keyType); if (Arrays.equals(publicKey, recoveredPublicKey)) { return i; } } return -1; } /** * * Copyright 2011 Google Inc. * Copyright 2014 Andreas Schildbach * Copyright 2014-2016 the * libsecp256k1 contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by * applicable law or agreed to in writing, software * distributed under the License is * distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. * See the License for the specific language governing permissions and * * limitations under the License. * <p> * The method was modified to match what we need * * <p>Given the components of a signature and a selector value, recover and return the public * key that generated the signature according to the algorithm in SEC1v2 section 4.1.6.</p> * * <p>The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the * correct one. Because the key recovery operation yields multiple potential keys, the correct * key must either be stored alongside the signature, or you must be willing to try each recId * in turn until you find one that outputs the key you are expecting.</p> * * <p>If this method returns null it means recovery was not possible and recId should be * iterated.</p> * * <p>Given the above two points, a correct usage of this method is inside a for loop from 0 to * 3, and if the output is null OR a key that is not the one you expect, you try again with the * next recId.</p> * * @param recId Which possible key to recover. * @param r the R components of the signature, wrapped. * @param s the S components of the signature, wrapped. * @param message Hash of the data that was signed. * @param compressed Whether or not the original pubkey was compressed. * @param keyType key type * @return An ECKey containing only the public part, or null if recovery wasn't possible. */ private static byte[] recoverPublicKeyFromSignature(int recId, BigInteger r, BigInteger s, @NotNull Sha256Hash message, boolean compressed, AlgorithmEmployed keyType) { checkArgument(recId >= 0, "recId must be positive"); checkArgument(r.signum() >= 0, "r must be positive"); checkArgument(s.signum() >= 0, "s must be positive"); // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) // 1.1 Let x = r + jn BigInteger n; // Curve order. ECPoint g; ECCurve.Fp curve; switch (keyType) { case SECP256R1: n = ecParamsR1.getN(); g = ecParamsR1.getG(); curve = (ECCurve.Fp) ecParamsR1.getCurve(); break; default: n = ecParamsK1.getN(); g = ecParamsK1.getG(); curve = (ECCurve.Fp) ecParamsK1.getCurve(); break; } BigInteger i = BigInteger.valueOf((long) recId / 2); BigInteger x = r.add(i.multiply(n)); // 1.2. Convert the integer x to an octet string X of length mlen using the conversion routine // specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R using the // conversion routine specified in Section 2.3.4. If this conversion routine outputs “invalid”, then // do another iteration of Step 1. // // More concisely, what these points mean is to use X as a compressed public key. BigInteger prime = curve.getQ(); if (x.compareTo(prime) >= 0) { // Cannot have point co-ordinates larger than this as everything takes place modulo Q. return null; } // Compressed keys require you to know an extra bit of data about the y-coord as there are two possibilities. // So it's encoded in the recId. ECPoint R = decompressKey(x, (recId & 1) == 1, keyType); // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers responsibility). if (!R.multiply(n).isInfinity()) { return null; } // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. BigInteger e = message.toBigInteger(); // 1.6. For k from 1 to 2 do the following. (loop is outside this function via iterating recId) // 1.6.1. Compute a candidate public key as: // Q = mi(r) * (sR - eG) // // Where mi(x) is the modular multiplicative inverse. We transform this into the following: // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). In the above equation // ** is point multiplication and + is point addition (the EC group operator). // // We can find the additive inverse by subtracting e from zero then taking the mod. For example the additive // inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and -3 mod 11 = 8. BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); BigInteger rInv = r.modInverse(n); BigInteger srInv = rInv.multiply(s).mod(n); BigInteger eInvrInv = rInv.multiply(eInv).mod(n); ECPoint q = ECAlgorithms.sumOfTwoMultiplies(g, eInvrInv, R, srInv); return q.getEncoded(compressed); } /** * * Copyright 2011 Google Inc. * Copyright 2014 Andreas Schildbach * Copyright 2014-2016 the * libsecp256k1 contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by * applicable law or agreed to in writing, software * distributed under the License is * distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. * See the License for the specific language governing permissions and * * limitations under the License. * <p> * The method was modified to match what we need * <p> * Decompress a compressed public key (x co-ord and low-bit of y-coord). */ private static ECPoint decompressKey(BigInteger xBN, boolean yBit, AlgorithmEmployed keyType) { ECCurve.Fp curve; switch (keyType) { case SECP256R1: curve = (ECCurve.Fp) ecParamsR1.getCurve(); break; default: curve = (ECCurve.Fp) ecParamsK1.getCurve(); break; } X9IntegerConverter x9 = new X9IntegerConverter(); byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(curve)); compEnc[0] = (byte) (yBit ? COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_NEGATIVE_Y : COMPRESSED_PUBLIC_KEY_BYTE_INDICATOR_POSITIVE_Y); return curve.decodePoint(compEnc); } }