package org.ripple.power.txns.btc; import org.ripple.bouncycastle.asn1.x9.X9ECParameters; import org.ripple.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.ripple.bouncycastle.crypto.digests.SHA256Digest; import org.ripple.bouncycastle.crypto.ec.CustomNamedCurves; import org.ripple.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.ripple.bouncycastle.crypto.params.ECDomainParameters; import org.ripple.bouncycastle.crypto.params.ECKeyGenerationParameters; import org.ripple.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.ripple.bouncycastle.crypto.params.ECPublicKeyParameters; import org.ripple.bouncycastle.crypto.signers.ECDSASigner; import org.ripple.bouncycastle.crypto.signers.HMacDSAKCalculator; import org.ripple.bouncycastle.math.ec.ECAlgorithms; import org.ripple.bouncycastle.math.ec.ECFieldElement; import org.ripple.bouncycastle.math.ec.ECPoint; import org.ripple.bouncycastle.math.ec.custom.sec.SecP256K1Curve; import org.ripple.bouncycastle.util.encoders.Base64; import org.ripple.power.Helper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.security.SecureRandom; import java.util.Arrays; /** * ECKey supports elliptic curve cryptographic operations using a public/private * key pair. A private key is required to create a signature and a public key is * required to verify a signature. The private key is always encrypted using AES * when it is serialized for storage on external media. */ public class ECKey { /** Half-curve order for generating canonical S */ public static final BigInteger HALF_CURVE_ORDER; /** Elliptic curve parameters (secp256k1 curve) */ private static final ECDomainParameters ecParams; static { X9ECParameters params = CustomNamedCurves.getByName("secp256k1"); ecParams = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); HALF_CURVE_ORDER = params.getN().shiftRight(1); } /** Strong random number generator */ private static final SecureRandom secureRandom = new SecureRandom(); /** Signed message header */ private static final String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n"; /** Key label */ private String label = ""; /** Public key */ private byte[] pubKey; /** Public key hash */ private byte[] pubKeyHash; /** Private key */ private BigInteger privKey; /** Key creation time (seconds) */ private long creationTime; /** Compressed public key */ private boolean isCompressed; /** Change key */ private boolean isChange; /** * Creates an ECKey with a new public/private key pair. Point compression is used * so the resulting public key will be 33 bytes (32 bytes for the x-coordinate and * 1 byte to represent the y-coordinate sign) */ public ECKey() { ECKeyPairGenerator keyGenerator = new ECKeyPairGenerator(); ECKeyGenerationParameters keyGenParams = new ECKeyGenerationParameters(ecParams, secureRandom); keyGenerator.init(keyGenParams); AsymmetricCipherKeyPair keyPair = keyGenerator.generateKeyPair(); ECPrivateKeyParameters privKeyParams = (ECPrivateKeyParameters)keyPair.getPrivate(); ECPublicKeyParameters pubKeyParams = (ECPublicKeyParameters)keyPair.getPublic(); privKey = privKeyParams.getD(); pubKey = pubKeyParams.getQ().getEncoded(true); creationTime = System.currentTimeMillis()/1000; isCompressed = true; } /** * Creates an ECKey with just a public key * * @param pubKey Public key */ public ECKey(byte[] pubKey) { this(pubKey, null, false); } /** * Creates an ECKey public/private key pair using the supplied private key. The * 'compressed' parameter determines the type of public key created. * * @param privKey Private key * @param compressed TRUE to create a compressed public key */ public ECKey(BigInteger privKey, boolean compressed) { this(null, privKey, compressed); } /** * Creates an ECKey with the supplied public/private key pair. The private key may be * null if you only want to use this ECKey to verify signatures. The public key will * be generated from the private key if it is not provided (the 'compressed' parameter * determines the type of public key created) * * @param pubKey Public key or null * @param privKey Private key or null * @param compressed TRUE to create a compressed public key */ public ECKey(byte[] pubKey, BigInteger privKey, boolean compressed) { this.privKey = privKey; if (pubKey != null) { this.pubKey = pubKey; isCompressed = (pubKey.length==33); } else if (privKey != null) { this.pubKey = pubKeyFromPrivKey(privKey, compressed); isCompressed = compressed; } else { throw new IllegalArgumentException("You must provide at least a private key or a public key"); } creationTime = System.currentTimeMillis()/1000; } /** * Checks if the public key is canonical * * @param pubKeyBytes Public key * @return TRUE if the key is canonical */ public static boolean isPubKeyCanonical(byte[] pubKeyBytes) { boolean isValid = false; if (pubKeyBytes.length == 33 && (pubKeyBytes[0] == (byte)0x02 || pubKeyBytes[0] == (byte)0x03)) { isValid = true; } else if (pubKeyBytes.length == 65 && pubKeyBytes[0] == (byte)0x04) { isValid = true; } return isValid; } /** * Checks if the signature is DER-encoded * * @param encodedSig Encoded signature * @return TRUE if the signature is DER-encoded */ public static boolean isSignatureCanonical(byte[] encodedSig) { // // DER-encoding requires that there is only one representation for a given // encoding. This means that no pad bytes are inserted for numeric values. // // An ASN.1 sequence is identified by 0x30 and each primitive by a type field. // An integer is identified as 0x02. Each field type is followed by a field length. // For valid R and S values, the length is a single byte since R and S are both // 32-byte or 33-byte values (a leading zero byte is added to ensure a positive // value if the sign bit would otherwise bet set). // // Bitcoin appends that hash type to the end of the DER-encoded signature. We require // this to be a single byte for a canonical signature. // // The length is encoded in the lower 7 bits for lengths between 0 and 127 and the upper bit is 0. // Longer length have the upper bit set to 1 and the lower 7 bits contain the number of bytes // in the length. // // // An ASN.1 sequence is 0x30 followed by the length // if (encodedSig.length<2 || encodedSig[0]!=(byte)0x30 || (encodedSig[1]&0x80)!=0) return false; // // Get length of sequence // int length = ((int)encodedSig[1]&0x7f) + 2; int offset = 2; // // Check R // if (offset+2>length || encodedSig[offset]!=(byte)0x02 || (encodedSig[offset+1]&0x80)!=0) return false; int rLength = (int)encodedSig[offset+1]&0x7f; if (offset+rLength+2 > length) return false; if (encodedSig[offset+2]==0x00 && (encodedSig[offset+3]&0x80)==0) return false; offset += rLength + 2; // // Check S // if (offset+2>length || encodedSig[offset]!=(byte)0x02 || (encodedSig[offset+1]&0x80)!=0) return false; int sLength = (int)encodedSig[offset+1]&0x7f; if (offset+sLength+2 > length) return false; if (encodedSig[offset+2]==0x00 && (encodedSig[offset+3]&0x80)==0) return false; offset += sLength + 2; // // There must be a single byte appended to the signature // return (offset == encodedSig.length-1); } /** * Returns the key creation time * * @return Key creation time (seconds) */ public long getCreationTime() { return creationTime; } /** * Sets the key creation time * * @param creationTime Key creation time (seconds) */ public void setCreationTime(long creationTime) { this.creationTime = creationTime; } /** * Returns the key label * * @return Key label */ public String getLabel() { return label; } /** * Sets the key label * * @param label Key label */ public void setLabel(String label) { this.label = label; } /** * Checks if this is a change key * * @return TRUE if this is a change key */ public boolean isChange() { return isChange; } /** * Sets change key status * * @param isChange TRUE if this is a change key */ public void setChange(boolean isChange) { this.isChange = isChange; } /** * Returns the public key (as used in transaction scriptSigs). A compressed * public key is 33 bytes and starts with '02' or '03' while an uncompressed * public key is 65 bytes and starts with '04'. * * @return Public key */ public byte[] getPubKey() { return pubKey; } /** * Returns the public key hash as used in addresses. The hash is 20 bytes. * * @return Public key hash */ public byte[] getPubKeyHash() { if (pubKeyHash == null) pubKeyHash = Helper.sha256Hash160(pubKey); return pubKeyHash; } /** * Returns the address for this public key * * @return Address */ public Address toAddress() { return new Address(getPubKeyHash(), label); } /** * Returns the private key * * @return Private key or null if there is no private key */ public BigInteger getPrivKey() { return privKey; } /** * Checks if there is a private key * * @return TRUE if there is a private key */ public boolean hasPrivKey() { return (privKey!=null); } /** * Returns the encoded private key in the format used by the Bitcoin reference client * * @return Dumped private key */ public DumpedPrivateKey getPrivKeyEncoded() { if (privKey == null) throw new IllegalStateException("No private key available"); return new DumpedPrivateKey(privKey, isCompressed); } /** * Checks if the public key is compressed * * @return TRUE if the public key is compressed */ public boolean isCompressed() { return isCompressed; } /** * Creates a signature for the supplied contents using the private key * * @param contents Contents to be signed * @return ECDSA signature * @throws ECException Unable to create signature */ public ECDSASignature createSignature(byte[] contents) throws ECException { if (privKey == null) throw new IllegalStateException("No private key available"); // // Get the double SHA-256 hash of the signed contents // byte[] contentsHash = Helper.doubleDigest(contents); // // Create the signature // BigInteger[] sigs; try { ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); ECPrivateKeyParameters privKeyParams = new ECPrivateKeyParameters(privKey, ecParams); signer.init(true, privKeyParams); sigs = signer.generateSignature(contentsHash); } catch (RuntimeException exc) { throw new ECException("Exception while creating signature", exc); } // // Create a canonical signature by adjusting the S component to be less than or equal to // half the curve order. // if (sigs[1].compareTo(HALF_CURVE_ORDER) > 0) sigs[1] = ecParams.getN().subtract(sigs[1]); return new ECDSASignature(sigs[0], sigs[1]); } /** * Verifies a signature for the signed contents using the public key * * @param contents The signed contents * @param signature DER-encoded signature * @return TRUE if the signature if valid, FALSE otherwise * @throws ECException Unable to verify the signature */ public boolean verifySignature(byte[] contents, byte[] signature) throws ECException { boolean isValid = false; // // Decode the DER-encoded signature and get the R and S values // ECDSASignature sig = new ECDSASignature(signature); // // Get the double SHA-256 hash of the signed contents // // A null contents will result in a hash with the first byte set to 1 and // all other bytes set to 0. This is needed to handle a bug in the reference // client where it doesn't check for an error when serializing a transaction // and instead uses the error code as the hash. // byte[] contentsHash; if (contents != null) { contentsHash = Helper.doubleDigest(contents); } else { contentsHash = new byte[32]; contentsHash[0] = 0x01; } // // Verify the signature // try { ECDSASigner signer = new ECDSASigner(); ECPublicKeyParameters params = new ECPublicKeyParameters( ecParams.getCurve().decodePoint(pubKey), ecParams); signer.init(false, params); isValid = signer.verifySignature(contentsHash, sig.getR(), sig.getS()); } catch (RuntimeException exc) { throw new ECException("Exception while verifying signature: "+exc.getMessage()); } return isValid; } /** * Signs a message using the private key * * @param message Message to be signed * @return Base64-encoded signature string * @throws ECException Unable to sign the message */ public String signMessage(String message) throws ECException { String encodedSignature; if (privKey == null) throw new IllegalStateException("No private key available"); try { // // Format the message for signing // byte[] contents; try (ByteArrayOutputStream outStream = new ByteArrayOutputStream(message.length()*2)) { byte[] headerBytes = BITCOIN_SIGNED_MESSAGE_HEADER.getBytes("UTF-8"); outStream.write(VarInt.encode(headerBytes.length)); outStream.write(headerBytes); byte[] messageBytes = message.getBytes("UTF-8"); outStream.write(VarInt.encode(messageBytes.length)); outStream.write(messageBytes); contents = outStream.toByteArray(); } // // Create the signature // ECDSASignature sig = createSignature(contents); // // Get the RecID used to recover the public key from the signature // BigInteger e = new BigInteger(1, Helper.doubleDigest(contents)); int recID = -1; for (int i=0; i<4; i++) { ECKey k = recoverFromSignature(i, sig, e, isCompressed()); if (k != null && Arrays.equals(k.getPubKey(), pubKey)) { recID = i; break; } } if (recID == -1) throw new ECException("Unable to recover public key from signature"); // // The message signature consists of a header byte followed by the R and S values // int headerByte = recID + 27 + (isCompressed() ? 4 : 0); byte[] sigData = new byte[65]; sigData[0] = (byte)headerByte; System.arraycopy(Helper.bigIntegerToBytes(sig.getR(), 32), 0, sigData, 1, 32); System.arraycopy(Helper.bigIntegerToBytes(sig.getS(), 32), 0, sigData, 33, 32); // // Create a Base-64 encoded string for the message signature // encodedSignature = new String(Base64.encode(sigData), "UTF-8"); } catch (IOException exc) { throw new IllegalStateException("Unexpected IOException", exc); } return encodedSignature; } /** * Verifies a message signature using the signing address * * @param address The address that signed the message * @param message The message that was signed * @param encodedSignature The Base64-encoded signature * @return TRUE if the signature is valid for this key * @throws SignatureException Signature is not valid */ public static boolean verifyMessage(String address, String message, String encodedSignature) throws SignatureException { // // Decode the Base64-encoded signature // byte[] decodedSignature; try { decodedSignature = Base64.decode(encodedSignature); } catch (RuntimeException exc) { throw new SignatureException("Unable to decode the signature", exc); } // // Get the selector, R and S values // // The selector byte has the following values: // 0x1B = First key, even Y // 0x1C = First key, odd Y // 0x1D = Second key, even Y // 0x1E = Second key, odd Y // // If the public key was compressed, 4 is added to the selector value // if (decodedSignature.length < 65) throw new SignatureException("Signature is too short"); int headerByte = (int)decodedSignature[0]&0xff; if (headerByte < 27 || headerByte > 34) throw new SignatureException(String.format("Header byte %d is out of range", headerByte)); BigInteger r = new BigInteger(1, Arrays.copyOfRange(decodedSignature, 1, 33)); BigInteger s = new BigInteger(1, Arrays.copyOfRange(decodedSignature, 33, 65)); ECDSASignature sig = new ECDSASignature(r, s); boolean compressed = false; if (headerByte >= 31) { compressed = true; headerByte -= 4; } int recID = headerByte - 27; ECKey key; byte[] contents; // // Format the message for signing // try { try (ByteArrayOutputStream outStream = new ByteArrayOutputStream(message.length()*2)) { byte[] headerBytes = BITCOIN_SIGNED_MESSAGE_HEADER.getBytes("UTF-8"); outStream.write(VarInt.encode(headerBytes.length)); outStream.write(headerBytes); byte[] messageBytes = message.getBytes("UTF-8"); outStream.write(VarInt.encode(messageBytes.length)); outStream.write(messageBytes); contents = outStream.toByteArray(); } BigInteger e = new BigInteger(1, Helper.doubleDigest(contents)); // // Get the public key from the signature // key = recoverFromSignature(recID, sig, e, compressed); if (key == null) throw new SignatureException("Unable to recover public key from signature"); } catch (IOException exc) { throw new IllegalStateException("Unexpected IOException", exc); } catch (IllegalArgumentException exc) { throw new SignatureException("Signature is not valid"); } // // The signature is correct if the recovered public key hash matches the supplied hash // return key.toAddress().toString().equals(address); } /** * <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 sig R and S components of the signature * @param e The double SHA-256 hash of the original message * @param compressed Whether or not the original public key was compressed * @return An ECKey containing only the public part, or null if recovery wasn't possible */ private static ECKey recoverFromSignature(int recID, ECDSASignature sig, BigInteger e, boolean compressed) { BigInteger n = ecParams.getN(); BigInteger i = BigInteger.valueOf((long)recID / 2); BigInteger x = sig.getR().add(i.multiply(n)); // // 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⌉. // 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. // // More concisely, what these points mean is to use X as a compressed public key. // SecP256K1Curve curve = (SecP256K1Curve)ecParams.getCurve(); BigInteger prime = curve.getQ(); if (x.compareTo(prime) >= 0) { return null; } // // Compressed keys require you to know an extra bit of data about the y-coordinate as // there are two possibilities. So it's encoded in the recID. // ECPoint R = decompressKey(x, (recID & 1) == 1); if (!R.multiply(n).isInfinity()) return null; // // For k from 1 to 2 do the following. (loop is outside this function via iterating recId) // 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 = sig.getR().modInverse(n); BigInteger srInv = rInv.multiply(sig.getS()).mod(n); BigInteger eInvrInv = rInv.multiply(eInv).mod(n); ECPoint q = ECAlgorithms.sumOfTwoMultiplies(ecParams.getG(), eInvrInv, R, srInv); return new ECKey(q.getEncoded(compressed)); } /** * Decompress a compressed public key (x coordinate and low-bit of y-coordinate). * * @param xBN X-coordinate * @param yBit Sign of Y-coordinate * @return Uncompressed public key */ private static ECPoint decompressKey(BigInteger xBN, boolean yBit) { SecP256K1Curve curve = (SecP256K1Curve)ecParams.getCurve(); ECFieldElement x = curve.fromBigInteger(xBN); ECFieldElement alpha = x.multiply(x.square().add(curve.getA())).add(curve.getB()); ECFieldElement beta = alpha.sqrt(); if (beta == null) throw new IllegalArgumentException("Invalid point compression"); ECPoint ecPoint; BigInteger nBeta = beta.toBigInteger(); if (nBeta.testBit(0) == yBit) { ecPoint = curve.createPoint(x.toBigInteger(), nBeta); } else { ECFieldElement y = curve.fromBigInteger(curve.getQ().subtract(nBeta)); ecPoint = curve.createPoint(x.toBigInteger(), y.toBigInteger()); } return ecPoint; } /** * Create the public key from the private key * * @param privKey Private key * @param compressed TRUE to generate a compressed public key * @return Public key */ private byte[] pubKeyFromPrivKey(BigInteger privKey, boolean compressed) { ECPoint point = ecParams.getG().multiply(privKey); return point.getEncoded(compressed); } /** * Checks if two objects are equal * * @param obj The object to check * @return TRUE if the object is equal */ @Override public boolean equals(Object obj) { return (obj!=null && (obj instanceof ECKey) && Arrays.equals(pubKey, ((ECKey)obj).pubKey)); } /** * Returns the hash code for this object * * @return Hash code */ @Override public int hashCode() { return Arrays.hashCode(pubKey); } }