diff --git a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java index 7d42907e254..9cbdc05c4ab 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -86,6 +86,9 @@ private void validate(String privateKey) { throw new TronError("private key must be hex string", TronError.ErrCode.WITNESS_INIT); } + if (!ECKey.isValidPrivateKey(ByteArray.fromHexString(privateKey))) { + throw new TronError("Invalid ECDSA witness private key", TronError.ErrCode.WITNESS_INIT); + } } public void addPrivateKeys(String privateKey) { diff --git a/crypto/src/main/java/org/tron/common/crypto/ECKey.java b/crypto/src/main/java/org/tron/common/crypto/ECKey.java index a23933a3a90..f2a59854bc3 100644 --- a/crypto/src/main/java/org/tron/common/crypto/ECKey.java +++ b/crypto/src/main/java/org/tron/common/crypto/ECKey.java @@ -23,15 +23,11 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; -import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; import java.security.SignatureException; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; -import java.util.Objects; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.sec.SECNamedCurves; @@ -58,6 +54,14 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; +/** + * A secp256k1 key pair and ECDSA signing utility. + * + *

ECDSA signatures are malleable: for a valid {@code (r, s)} pair, {@code (r, n - s)} is + * valid as well. New signatures are canonicalized to low-S form, but verification accepts both + * forms because transaction identifiers do not include signatures. See BIP-62 for background on + * low-S signatures. + */ @Slf4j(topic = "crypto") public class ECKey implements Serializable, SignInterface { @@ -77,6 +81,9 @@ public class ECKey implements Serializable, SignInterface { */ public static final BigInteger HALF_CURVE_ORDER; + private static final int MAX_PRIVATE_KEY_LENGTH = 32; + private static final int COMPRESSED_PUBLIC_KEY_LENGTH = 33; + private static final int UNCOMPRESSED_PUBLIC_KEY_LENGTH = 65; private static final BigInteger SECP256K1N = new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16); private static final SecureRandom secureRandom; @@ -100,16 +107,10 @@ public class ECKey implements Serializable, SignInterface { // TODO: Redesign this class to use consistent internals and more // efficient serialization. private final PrivateKey privKey; - // the Java Cryptographic Architecture provider to use for Signature - // this is set along with the PrivateKey privKey and must be compatible - // this provider will be used when selecting a Signature instance - // https://docs.oracle.com/javase/8/docs/technotes/guides/security - // /SunProviders.html - private final Provider provider; // Transient because it's calculated on demand. - private transient byte[] pubKeyHash; - private transient byte[] nodeId; + private transient volatile byte[] pubKeyHash; + private transient volatile byte[] nodeId; /** * Generates an entirely new keypair. @@ -121,78 +122,58 @@ public ECKey() { } /** - * Generate a new keypair using the given Java Security Provider. + * Generates an entirely new keypair with the given {@link SecureRandom} object.

BouncyCastle + * will be used as the Java Security Provider * - *

All private key operations will use the provider. + * @param secureRandom - */ - public ECKey(Provider provider, SecureRandom secureRandom) { - this.provider = provider; - - final KeyPairGenerator keyPairGen = ECKeyPairGenerator.getInstance(provider, secureRandom); + public ECKey(SecureRandom secureRandom) { + final KeyPairGenerator keyPairGen = ECKeyPairGenerator.getInstance( + TronCastleProvider.getInstance(), secureRandom); final KeyPair keyPair = keyPairGen.generateKeyPair(); this.privKey = keyPair.getPrivate(); final PublicKey pubKey = keyPair.getPublic(); - if (pubKey instanceof BCECPublicKey) { - pub = ((BCECPublicKey) pubKey).getQ(); - } else if (pubKey instanceof ECPublicKey) { - pub = extractPublicKey((ECPublicKey) pubKey); - } else { + if (!(pubKey instanceof BCECPublicKey)) { throw new AssertionError( - "Expected Provider " + provider.getName() - + " to produce a subtype of ECPublicKey, found " - + pubKey.getClass()); + "Expected Bouncy Castle EC public key, found " + pubKey.getClass()); } + this.pub = ((BCECPublicKey) pubKey).getQ(); } - /** - * Generates an entirely new keypair with the given {@link SecureRandom} object.

BouncyCastle - * will be used as the Java Security Provider - * - * @param secureRandom - - */ - public ECKey(SecureRandom secureRandom) { - this(TronCastleProvider.getInstance(), secureRandom); - } - - /** - * Pair a private key with a public EC point. - * - *

All private key operations will use the provider. - */ - public ECKey(byte[] key, boolean isPrivateKey) { if (isPrivateKey) { + check(isValidPrivateKey(key), "Invalid private key"); BigInteger pk = new BigInteger(1, key); this.privKey = privateKeyFromBigInteger(pk); this.pub = CURVE.getG().multiply(pk); } else { this.privKey = null; - this.pub = CURVE.getCurve().decodePoint(key); + this.pub = decodePublicKey(key); + check(isValidPublicPoint(this.pub), "Invalid public key"); } - this.provider = TronCastleProvider.getInstance(); } - public ECKey(Provider provider, @Nullable PrivateKey privKey, ECPoint pub) { - this.provider = provider; + private ECKey(BigInteger privateKey) { + this.privKey = privateKeyFromBigInteger(privateKey); + this.pub = CURVE.getG().multiply(privateKey); + } - if (privKey == null || isECPrivateKey(privKey)) { + private ECKey(@Nullable PrivateKey privKey, ECPoint pub) { + if (privKey == null || privKey instanceof BCECPrivateKey) { this.privKey = privKey; } else { throw new IllegalArgumentException( - "Expected EC private key, given a private key object with" + - " class " + "Expected Bouncy Castle EC private key, given a private key object with class " + privKey.getClass().toString() + " and algorithm " + privKey.getAlgorithm()); } - if (pub == null) { - throw new IllegalArgumentException("Public key may not be null"); - } else { - this.pub = pub; - } + check(isValidPublicPoint(pub), "Invalid public key"); + checkPrivateKeyMatchesPublic(privKey, pub); + this.pub = pub; } /** @@ -200,32 +181,17 @@ public ECKey(Provider provider, @Nullable PrivateKey privKey, ECPoint pub) { * Security Provider */ public ECKey(@Nullable BigInteger priv, ECPoint pub) { - this( - TronCastleProvider.getInstance(), - privateKeyFromBigInteger(priv), - pub - ); + this(privateKeyFromBigInteger(priv), pub); } - /* Convert a Java JCE ECPublicKey into a BouncyCastle ECPoint - */ - private static ECPoint extractPublicKey(final ECPublicKey ecPublicKey) { - final java.security.spec.ECPoint publicPointW = ecPublicKey.getW(); - final BigInteger xCoord = publicPointW.getAffineX(); - final BigInteger yCoord = publicPointW.getAffineY(); - - return CURVE.getCurve().createPoint(xCoord, yCoord); - } - - /* Test if a generic private key is an EC private key - * - * it is not sufficient to check that privKey is a subtype of ECPrivateKey - * as the SunPKCS11 Provider will return a generic PrivateKey instance - * a fallback that covers this case is to check the key algorithm - */ - private static boolean isECPrivateKey(PrivateKey privKey) { - return privKey instanceof ECPrivateKey || privKey.getAlgorithm() - .equals("EC"); + private static void checkPrivateKeyMatchesPublic( + @Nullable PrivateKey privateKey, ECPoint publicPoint) { + if (privateKey instanceof BCECPrivateKey) { + BigInteger privateScalar = ((BCECPrivateKey) privateKey).getD(); + check(isValidPrivateKey(privateScalar), "Invalid private key"); + check(CURVE.getG().multiply(privateScalar).equals(publicPoint), + "Private key does not match public key"); + } } /* Convert a BigInteger into a PrivateKey object @@ -234,6 +200,7 @@ private static PrivateKey privateKeyFromBigInteger(BigInteger priv) { if (priv == null) { return null; } else { + check(isValidPrivateKey(priv), "Invalid private key"); try { return ECKeyFactory .getInstance(TronCastleProvider.getInstance()) @@ -245,6 +212,57 @@ private static PrivateKey privateKeyFromBigInteger(BigInteger priv) { } } + /** + * Returns whether the supplied key bytes represent a valid secp256k1 private scalar. + * Accepts unsigned encodings up to 32 bytes and Java {@link BigInteger} encodings with one + * leading zero sign byte. + */ + public static boolean isValidPrivateKey(byte[] privateKey) { + // BigInteger.toByteArray() prepends a zero sign byte only when the magnitude's high bit is set. + return !ByteArray.isEmpty(privateKey) + && (privateKey.length <= MAX_PRIVATE_KEY_LENGTH + || (privateKey.length == MAX_PRIVATE_KEY_LENGTH + 1 + && privateKey[0] == 0 + && (privateKey[1] & 0x80) != 0)) + && isValidPrivateKey(new BigInteger(1, privateKey)); + } + + /** + * Returns whether the supplied value is in the secp256k1 private-key range {@code [1, n)}. + */ + public static boolean isValidPrivateKey(BigInteger privateKey) { + return privateKey != null + && privateKey.signum() > 0 + && privateKey.compareTo(SECP256K1N) < 0; + } + + /** + * Returns whether the encoded key is a non-infinity point on its elliptic curve. + */ + public static boolean isValidPublicKey(byte[] publicKey) { + try { + return isValidPublicPoint(decodePublicKey(publicKey)); + } catch (RuntimeException e) { + return false; + } + } + + private static boolean isValidPublicPoint(ECPoint publicPoint) { + return publicPoint != null + && CURVE.getCurve().equals(publicPoint.getCurve()) + && !publicPoint.isInfinity() + && publicPoint.isValid(); + } + + private static ECPoint decodePublicKey(byte[] publicKey) { + check(isPubKeyCanonical(publicKey), "Invalid public key"); + try { + return CURVE.getCurve().decodePoint(publicKey); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Invalid public key", e); + } + } + /** * Utility for compressing an elliptic curve point. Returns the same point if it's already * compressed. See the ECKey class docs for a discussion of point compression. @@ -276,7 +294,8 @@ public static ECPoint decompressPoint(ECPoint compressed) { * @return - */ public static ECKey fromPrivate(BigInteger privKey) { - return new ECKey(privKey, CURVE.getG().multiply(privKey)); + check(isValidPrivateKey(privKey), "Invalid private key"); + return new ECKey(privKey); } /** @@ -286,41 +305,8 @@ public static ECKey fromPrivate(BigInteger privKey) { * @return - */ public static ECKey fromPrivate(byte[] privKeyBytes) { - if (ByteArray.isEmpty(privKeyBytes)) { - return null; - } - return fromPrivate(new BigInteger(1, privKeyBytes)); - } - - /** - * Creates an ECKey that simply trusts the caller to ensure that point is really the result of - * multiplying the generator point by the private key. This is used to speed things up when you - * know you have the right values already. The compression state of pub will be preserved. - * - * @param priv - - * @param pub - - * @return - - */ - public static ECKey fromPrivateAndPrecalculatedPublic(BigInteger priv, - ECPoint pub) { - return new ECKey(priv, pub); - } - - /** - * Creates an ECKey that simply trusts the caller to ensure that point is really the result of - * multiplying the generator point by the private key. This is used to speed things up when you - * know you have the right values already. The compression state of the point will be preserved. - * - * @param priv - - * @param pub - - * @return - - */ - public static ECKey fromPrivateAndPrecalculatedPublic(byte[] priv, byte[] - pub) { - check(priv != null, "Private key must not be null"); - check(pub != null, "Public key must not be null"); - return new ECKey(new BigInteger(1, priv), CURVE.getCurve() - .decodePoint(pub)); + check(isValidPrivateKey(privKeyBytes), "Invalid private key"); + return new ECKey(new BigInteger(1, privKeyBytes)); } /** @@ -331,7 +317,7 @@ public static ECKey fromPrivateAndPrecalculatedPublic(byte[] priv, byte[] * @return - */ public static ECKey fromPublicOnly(ECPoint pub) { - return new ECKey(null, pub); + return new ECKey((PrivateKey) null, pub); } /** @@ -342,7 +328,7 @@ public static ECKey fromPublicOnly(ECPoint pub) { * @return - */ public static ECKey fromPublicOnly(byte[] pub) { - return new ECKey(null, CURVE.getCurve().decodePoint(pub)); + return new ECKey((PrivateKey) null, decodePublicKey(pub)); } /** @@ -355,6 +341,7 @@ public static ECKey fromPublicOnly(byte[] pub) { */ public static byte[] publicKeyFromPrivate(BigInteger privKey, boolean compressed) { + check(isValidPrivateKey(privKey), "Invalid private key"); ECPoint point = CURVE.getG().multiply(privKey); return point.getEncoded(compressed); } @@ -481,12 +468,15 @@ public static ECKey signatureToKey(byte[] messageHash, String * @return - */ public static boolean isPubKeyCanonical(byte[] pubkey) { + if (ByteArray.isEmpty(pubkey)) { + return false; + } if (pubkey[0] == 0x04) { // Uncompressed pubkey - return pubkey.length == 65; + return pubkey.length == UNCOMPRESSED_PUBLIC_KEY_LENGTH; } else if (pubkey[0] == 0x02 || pubkey[0] == 0x03) { // Compressed pubkey - return pubkey.length == 33; + return pubkey.length == COMPRESSED_PUBLIC_KEY_LENGTH; } else { return false; } @@ -669,10 +659,12 @@ public boolean hasPrivKey() { * @return 21-byte address */ public byte[] getAddress() { - if (pubKeyHash == null) { - pubKeyHash = Hash.computeAddress(this.pub); + byte[] address = pubKeyHash; + if (address == null) { + address = Hash.computeAddress(this.pub); + pubKeyHash = address; } - return pubKeyHash; + return Arrays.copyOf(address, address.length); } @Override @@ -691,10 +683,12 @@ public byte[] Base64toBytes(String signature) { * Generates the NodeID based on this key, that is the public key without first format byte */ public byte[] getNodeId() { - if (nodeId == null) { - nodeId = pubBytesWithoutFormat(this.pub); + byte[] id = nodeId; + if (id == null) { + id = pubBytesWithoutFormat(this.pub); + nodeId = id; } - return nodeId; + return Arrays.copyOf(id, id.length); } @@ -744,22 +738,6 @@ public String toString() { return b.toString(); } - /** - * Produce a string rendering of the ECKey INCLUDING the private key. Unless you absolutely need - * the private key it is better for security reasons to just use toString(). - * - * @return - - */ - public String toStringWithPrivate() { - StringBuilder b = new StringBuilder(); - b.append(toString()); - if (privKey != null && privKey instanceof BCECPrivateKey) { - b.append(" priv:").append(Hex.toHexString(((BCECPrivateKey) - privKey).getD().toByteArray())); - } - return b.toString(); - } - /** * Signs the given hash and returns the R and S components as BigIntegers and putData them in * ECDSASignature @@ -841,8 +819,6 @@ public byte[] getPrivKeyBytes() { return null; } else if (privKey instanceof BCECPrivateKey) { return ByteUtil.bigIntegerToBytes(((BCECPrivateKey) privKey).getD(), 32); - } else if (privKey instanceof ECPrivateKey) { - return ByteUtil.bigIntegerToBytes(((ECPrivateKey) privKey).getS(), 32); } else { return null; } @@ -860,10 +836,7 @@ public boolean equals(Object o) { ECKey ecKey = (ECKey) o; - if (privKey != null && !privKey.equals(ecKey.privKey)) { - return false; - } - return pub == null || pub.equals(ecKey.pub); + return pub.equals(ecKey.pub); } @Override diff --git a/crypto/src/main/java/org/tron/common/crypto/Hash.java b/crypto/src/main/java/org/tron/common/crypto/Hash.java index eca769a8dad..7af0f2c06fb 100644 --- a/crypto/src/main/java/org/tron/common/crypto/Hash.java +++ b/crypto/src/main/java/org/tron/common/crypto/Hash.java @@ -68,8 +68,8 @@ public class Hash { private static final int SIZE_THRESHOLD = 56; static { - Security.addProvider(TronCastleProvider.getInstance()); - CRYPTO_PROVIDER = Security.getProvider("BC"); + CRYPTO_PROVIDER = TronCastleProvider.getInstance(); + Security.addProvider(CRYPTO_PROVIDER); HASH_256_ALGORITHM_NAME = "TRON-KECCAK-256"; HASH_512_ALGORITHM_NAME = "TRON-KECCAK-512"; EMPTY_TRIE_HASH = sha3(encodeElement(EMPTY_BYTE_ARRAY)); diff --git a/crypto/src/main/java/org/tron/common/crypto/jce/TronCastleProvider.java b/crypto/src/main/java/org/tron/common/crypto/jce/TronCastleProvider.java index ad8e51e65ac..b65317deda9 100644 --- a/crypto/src/main/java/org/tron/common/crypto/jce/TronCastleProvider.java +++ b/crypto/src/main/java/org/tron/common/crypto/jce/TronCastleProvider.java @@ -19,7 +19,6 @@ package org.tron.common.crypto.jce; import java.security.Provider; -import java.security.Security; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.tron.common.crypto.cryptohash.Keccak256; import org.tron.common.crypto.cryptohash.Keccak512; @@ -35,9 +34,8 @@ private static class Holder { private static final Provider INSTANCE; static { - Provider p = Security.getProvider("BC"); - - INSTANCE = (p != null) ? p : new BouncyCastleProvider(); + // Use our own instance instead of trusting a globally registered provider named BC. + INSTANCE = new BouncyCastleProvider(); INSTANCE.put("MessageDigest.TRON-KECCAK-256", Keccak256.class.getName()); INSTANCE.put("MessageDigest.TRON-KECCAK-512", Keccak512.class.getName()); } diff --git a/crypto/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java index a1f74c7891b..d0051764490 100644 --- a/crypto/src/main/java/org/tron/keystore/Wallet.java +++ b/crypto/src/main/java/org/tron/keystore/Wallet.java @@ -207,31 +207,48 @@ public static SignInterface decrypt(String password, WalletFile walletFile) throw new CipherException("Unable to deserialize params: " + crypto.getKdf()); } - byte[] derivedMac = generateMac(derivedKey, cipherText); + byte[] encryptKey = null; + byte[] privateKey = null; + try { + byte[] derivedMac = generateMac(derivedKey, cipherText); - if (!java.security.MessageDigest.isEqual(derivedMac, mac)) { - throw new CipherException("Invalid password provided"); - } + if (!java.security.MessageDigest.isEqual(derivedMac, mac)) { + throw new CipherException("Invalid password provided"); + } - byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16); - byte[] privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText); - - SignInterface keyPair = SignUtils.fromPrivate(privateKey); - - // Enforce address consistency: if the keystore declares an address, it MUST match - // the address derived from the decrypted private key. Prevents address spoofing - // where a crafted keystore displays one address but encrypts a different key. - String declared = walletFile.getAddress(); - if (declared != null && !declared.isEmpty()) { - String derived = StringUtil.encode58Check(keyPair.getAddress()); - if (!declared.equals(derived)) { - throw new CipherException( - "Keystore address mismatch: file declares " + declared - + " but private key derives " + derived); + encryptKey = Arrays.copyOfRange(derivedKey, 0, 16); + privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText); + + SignInterface keyPair; + try { + keyPair = SignUtils.fromPrivate(privateKey); + } catch (IllegalArgumentException e) { + throw new CipherException("Invalid private key in keystore", e); } - } - return keyPair; + // Enforce address consistency: if the keystore declares an address, it MUST match + // the address derived from the decrypted private key. Prevents address spoofing + // where a crafted keystore displays one address but encrypts a different key. + String declared = walletFile.getAddress(); + if (declared != null && !declared.isEmpty()) { + String derived = StringUtil.encode58Check(keyPair.getAddress()); + if (!declared.equals(derived)) { + throw new CipherException( + "Keystore address mismatch: file declares " + declared + + " but private key derives " + derived); + } + } + + return keyPair; + } finally { + Arrays.fill(derivedKey, (byte) 0); + if (encryptKey != null) { + Arrays.fill(encryptKey, (byte) 0); + } + if (privateKey != null) { + Arrays.fill(privateKey, (byte) 0); + } + } } /** diff --git a/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java b/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java index 273672e8342..f847ef30bd8 100644 --- a/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java +++ b/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java @@ -5,17 +5,16 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.tron.common.utils.client.utils.AbiUtil.generateOccupationConstantPrivateKey; import java.math.BigInteger; -import java.security.KeyPairGenerator; -import java.security.Security; import java.security.SignatureException; import java.util.Arrays; import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.sec.SECNamedCurves; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import org.tron.common.crypto.ECKey.ECDSASignature; @@ -69,10 +68,76 @@ public void testFromPrivateKey() { assertTrue(key.hasPrivKey()); assertArrayEquals(pubKey, key.getPubKey()); - key = ECKey.fromPrivate((byte[]) null); - assertNull(key); - key = ECKey.fromPrivate(new byte[0]); - assertNull(key); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPrivate((byte[]) null)); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPrivate(new byte[0])); + } + + @Test + public void testValidatePrivateKey() { + assertTrue(ECKey.isValidPrivateKey(privateKey)); + assertTrue(ECKey.isValidPrivateKey(Hex.decode(privString))); + assertFalse(ECKey.isValidPrivateKey((BigInteger) null)); + assertFalse(ECKey.isValidPrivateKey((byte[]) null)); + assertFalse(ECKey.isValidPrivateKey(new byte[0])); + assertFalse(ECKey.isValidPrivateKey(new byte[33])); + assertFalse(ECKey.isValidPrivateKey(BigInteger.ZERO)); + assertFalse(ECKey.isValidPrivateKey(ECKey.CURVE.getN())); + + BigInteger highBitPrivateKey = ECKey.CURVE.getN().subtract(BigInteger.ONE); + byte[] signPaddedPrivateKey = highBitPrivateKey.toByteArray(); + assertEquals(33, signPaddedPrivateKey.length); + assertEquals(0, signPaddedPrivateKey[0]); + assertTrue(ECKey.isValidPrivateKey(signPaddedPrivateKey)); + assertEquals(highBitPrivateKey, ECKey.fromPrivate(signPaddedPrivateKey).getPrivKey()); + + byte[] redundantSignPaddedPrivateKey = new byte[33]; + redundantSignPaddedPrivateKey[32] = 1; + assertFalse(ECKey.isValidPrivateKey(redundantSignPaddedPrivateKey)); + + byte[] nonZeroLeadingPrivateKey = Arrays.copyOf(signPaddedPrivateKey, + signPaddedPrivateKey.length); + nonZeroLeadingPrivateKey[0] = 1; + assertFalse(ECKey.isValidPrivateKey(nonZeroLeadingPrivateKey)); + + byte[] doubleSignPaddedPrivateKey = new byte[34]; + System.arraycopy(signPaddedPrivateKey, 0, doubleSignPaddedPrivateKey, 1, + signPaddedPrivateKey.length); + assertFalse(ECKey.isValidPrivateKey(doubleSignPaddedPrivateKey)); + + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPrivate(BigInteger.ZERO)); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPrivate(ECKey.CURVE.getN())); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPrivate(new byte[1024 * 1024])); + assertThrows(IllegalArgumentException.class, + () -> ECKey.publicKeyFromPrivate(null, false)); + } + + @Test + public void testRejectInvalidPublicKey() { + assertTrue(ECKey.isValidPublicKey(pubKey)); + assertTrue(ECKey.isValidPublicKey(compressedPubKey)); + assertFalse(ECKey.isValidPublicKey(null)); + assertFalse(ECKey.isValidPublicKey(new byte[0])); + assertFalse(ECKey.isValidPublicKey(new byte[]{0})); + assertFalse(ECKey.isValidPublicKey(new byte[66])); + + byte[] oversizedPublicKey = new byte[1024 * 1024]; + oversizedPublicKey[0] = 0x04; + assertFalse(ECKey.isValidPublicKey(oversizedPublicKey)); + + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPublicOnly((byte[]) null)); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPublicOnly(ECKey.CURVE.getCurve().getInfinity())); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPublicOnly( + ECKey.CURVE.getCurve().createPoint(BigInteger.ZERO, BigInteger.ZERO))); + assertThrows(IllegalArgumentException.class, + () -> ECKey.fromPublicOnly(SECNamedCurves.getByName("secp256r1").getG())); } @Test(expected = IllegalArgumentException.class) @@ -81,12 +146,21 @@ public void testPrivatePublicKeyBytesNoArg() { fail("Expecting an IllegalArgumentException for using only null-parameters"); } - @Test(expected = IllegalArgumentException.class) - public void testInvalidPrivateKey() throws Exception { - new ECKey(Security.getProvider("SunEC"), - KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate(), - ECKey.fromPublicOnly(pubKey).getPubKeyPoint()); - fail("Expecting an IllegalArgumentException for using an non EC private key"); + @Test + public void testRejectMismatchedKeyPair() { + BigInteger otherPrivateKey = privateKey.add(BigInteger.ONE); + ECKey otherKey = ECKey.fromPrivate(otherPrivateKey); + + assertThrows(IllegalArgumentException.class, + () -> new ECKey(privateKey, otherKey.getPubKeyPoint())); + } + + @Test + public void testAcceptMatchingKeyPair() { + ECKey key = new ECKey(privateKey, ECKey.CURVE.getG().multiply(privateKey)); + + assertArrayEquals(pubKey, key.getPubKey()); + assertTrue(key.hasPrivKey()); } @Test @@ -201,6 +275,8 @@ public void testIsPubKeyCanonicalWrongPrefix() { // Test wrong prefix 3, right length 33 byte[] nonCanonicalPubkey6 = new byte[33]; assertFalse(ECKey.isPubKeyCanonical(nonCanonicalPubkey6)); + assertFalse(ECKey.isPubKeyCanonical(null)); + assertFalse(ECKey.isPubKeyCanonical(new byte[0])); } @Test @@ -215,10 +291,28 @@ public void testEqualsObject() { ECKey key0 = new ECKey(); ECKey key1 = ECKey.fromPrivate(privateKey); ECKey key2 = ECKey.fromPrivate(privateKey); + ECKey publicOnlyKey = ECKey.fromPublicOnly(key1.getPubKey()); assertFalse(key0.equals(key1)); assertTrue(key1.equals(key1)); assertTrue(key1.equals(key2)); + assertTrue(key1.equals(publicOnlyKey)); + assertTrue(publicOnlyKey.equals(key1)); + assertEquals(key1.hashCode(), publicOnlyKey.hashCode()); + } + + @Test + public void testDefensiveCopy() { + ECKey key = ECKey.fromPrivate(privateKey); + byte[] expectedAddress = Arrays.copyOf(key.getAddress(), key.getAddress().length); + byte[] returnedAddress = key.getAddress(); + returnedAddress[0] ^= 1; + assertArrayEquals(expectedAddress, key.getAddress()); + + byte[] expectedNodeId = Arrays.copyOf(key.getNodeId(), key.getNodeId().length); + byte[] returnedNodeId = key.getNodeId(); + returnedNodeId[0] ^= 1; + assertArrayEquals(expectedNodeId, key.getNodeId()); } diff --git a/framework/src/test/java/org/tron/common/crypto/jce/TronCastleProviderTest.java b/framework/src/test/java/org/tron/common/crypto/jce/TronCastleProviderTest.java new file mode 100644 index 00000000000..81fd0e3b952 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/jce/TronCastleProviderTest.java @@ -0,0 +1,98 @@ +package org.tron.common.crypto.jce; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.net.URL; +import java.net.URLClassLoader; +import java.security.MessageDigest; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Arrays; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.common.crypto.Hash; +import org.tron.common.crypto.cryptohash.Keccak512; + +public class TronCastleProviderTest { + + @Test + public void testExistingProvider() throws Exception { + assertProviderIsolation(new Provider("BC", 1.0, "Unrelated test provider") { }); + } + + @Test + public void testNoRegisteredProvider() throws Exception { + assertProviderIsolation(null); + } + + private void assertProviderIsolation(Provider existing) throws Exception { + Provider original = Security.getProvider("BC"); + int position = Arrays.asList(Security.getProviders()).indexOf(original) + 1; + Security.removeProvider("BC"); + try (URLClassLoader loader = newCryptoClassLoader()) { + if (existing != null) { + Security.addProvider(existing); + } + Class providerClass = loader.loadClass(TronCastleProvider.class.getName()); + Provider provider = (Provider) providerClass.getMethod("getInstance").invoke(null); + assertEquals(BouncyCastleProvider.class, provider.getClass()); + assertNotSame(existing, provider); + assertSame(provider, providerClass.getMethod("getInstance").invoke(null)); + assertSame(existing, Security.getProvider("BC")); + + byte[] empty = new byte[0]; + byte[] expected = Hex.decode( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + assertArrayEquals(expected, + MessageDigest.getInstance("TRON-KECCAK-256", provider).digest(empty)); + assertArrayEquals(new Keccak512().digest(empty), + MessageDigest.getInstance("TRON-KECCAK-512", provider).digest(empty)); + assertNotNull(ECKeyPairGenerator.getInstance(provider, new SecureRandom()).generateKeyPair()); + + Class hashClass = loader.loadClass(Hash.class.getName()); + assertArrayEquals(expected, + (byte[]) hashClass.getMethod("sha3", byte[].class).invoke(null, empty)); + if (existing != null) { + assertSame(existing, Security.getProvider("BC")); + assertNull(existing.get("MessageDigest.TRON-KECCAK-256")); + assertNull(existing.get("MessageDigest.TRON-KECCAK-512")); + } + } finally { + Security.removeProvider("BC"); + if (original != null) { + Security.insertProviderAt(original, position); + } + } + } + + private URLClassLoader newCryptoClassLoader() { + URL location = TronCastleProvider.class.getProtectionDomain().getCodeSource().getLocation(); + // Reload static initializers regardless of which crypto tests ran earlier in this JVM. + return new URLClassLoader(new URL[]{location}, getClass().getClassLoader()) { + @Override + protected synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.equals(Hash.class.getName()) + || name.equals(TronCastleProvider.class.getName()) + || name.startsWith(TronCastleProvider.class.getName() + "$")) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + loaded = findClass(name); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + return super.loadClass(name, resolve); + } + }; + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java b/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java index 28feadbd1db..1b613dc3320 100644 --- a/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java +++ b/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java @@ -22,7 +22,9 @@ import com.google.common.collect.Lists; import java.io.IOException; +import java.math.BigInteger; import java.security.SecureRandom; +import java.util.Arrays; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; @@ -30,6 +32,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; import org.tron.common.utils.ByteArray; import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; @@ -132,6 +135,22 @@ public void testInvalidPrivateKey() { assertTronError(localWitnesses, privateKey2, expectedMessage2); } + @Test + public void testPrivateKeyRange() { + for (BigInteger scalar : Arrays.asList(BigInteger.ZERO, ECKey.CURVE.getN())) { + String key = String.format("%064x", scalar); + assertTronError(localWitness, key, "Invalid ECDSA witness private key"); + TronError error = assertThrows(TronError.class, + () -> localWitness.setPrivateKeys(Arrays.asList(PRIVATE_KEY, key))); + assertEquals(ErrCode.WITNESS_INIT, error.getErrCode()); + } + for (BigInteger scalar : Arrays.asList(BigInteger.ONE, + ECKey.CURVE.getN().subtract(BigInteger.ONE))) { + String key = String.format("%064x", scalar); + assertEquals(key, new LocalWitnesses(key).getPrivateKey()); + } + } + private void assertTronError(LocalWitnesses localWitnesses, String privateKey, String expectedMessage) { TronError thrown = assertThrows(TronError.class, diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java index 2ee4331b99b..988f20c7acb 100644 --- a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java @@ -5,6 +5,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -12,19 +14,25 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; +import java.math.BigInteger; import java.security.SecureRandom; +import java.util.Collections; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; import org.slf4j.LoggerFactory; +import org.tron.common.crypto.ECKey; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; import org.tron.common.utils.ByteArray; import org.tron.common.utils.LocalWitnesses; +import org.tron.core.exception.CipherException; import org.tron.core.exception.TronError; import org.tron.keystore.Credentials; +import org.tron.keystore.Wallet; import org.tron.keystore.WalletFile; import org.tron.keystore.WalletUtils; @@ -183,6 +191,25 @@ public void testTamperedKeystoreRejectedAtSrLoading() throws Exception { } } + @Test + public void testInvalidPrivateKeyLoadError() throws Exception { + // Only mock the source of bytes for encryption. Decryption and key validation are real. + SignInterface invalidKey = mock(SignInterface.class); + when(invalidKey.getPrivateKey()).thenReturn(new byte[32]); + when(invalidKey.getAddress()).thenReturn(ECKey.fromPrivate(BigInteger.ONE).getAddress()); + WalletFile walletFile = Wallet.createLight(PASSWORD, invalidKey); + String fileName = DIR_NAME + "/invalid-key.json"; + new ObjectMapper().writeValue(new File(System.getProperty("user.dir"), fileName), walletFile); + + TronError err = assertThrows(TronError.class, + () -> WitnessInitializer.initFromKeystore( + Collections.singletonList(fileName), PASSWORD, null)); + assertEquals(TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + assertTrue(err.getCause() instanceof CipherException); + assertEquals("Invalid private key in keystore", err.getCause().getMessage()); + assertTrue(err.getCause().getCause() instanceof IllegalArgumentException); + } + private static ListAppender attachAppender() { ListAppender appender = new ListAppender<>(); appender.start(); diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java index 3ecef5b10c9..362738a56e8 100644 --- a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import java.io.File; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -93,6 +94,20 @@ public void testInitFromConfig() { assertEquals(ErrCode.WITNESS_INIT, err.getErrCode()); } + @Test + public void testRejectInvalidPlaintextPrivateKeys() { + String key = String.format("%064x", BigInteger.ZERO); + for (String witnessAddress : Arrays.asList(null, address)) { + TronError cliError = assertThrows(TronError.class, + () -> WitnessInitializer.initFromCLIPrivateKey(key, witnessAddress)); + assertEquals(ErrCode.WITNESS_INIT, cliError.getErrCode()); + TronError configError = assertThrows(TronError.class, + () -> WitnessInitializer.initFromCFGPrivateKey( + Collections.singletonList(key), witnessAddress)); + assertEquals(ErrCode.WITNESS_INIT, configError.getErrCode()); + } + } + @Test public void testInitFromKeystore() { List keystores = Arrays.asList("keystore1.json", "keystore2.json"); diff --git a/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java index 52a828c8b31..1135ac65c19 100644 --- a/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java +++ b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java @@ -1,13 +1,24 @@ package org.tron.keystore; +import static java.nio.charset.StandardCharsets.UTF_8; + +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mockStatic; +import java.util.concurrent.atomic.AtomicReference; +import org.bouncycastle.crypto.generators.SCrypt; import org.junit.Test; +import org.mockito.MockedStatic; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; import org.tron.common.utils.Utils; import org.tron.core.exception.CipherException; @@ -75,4 +86,88 @@ public void testDecryptAllowsEmptyAddress() throws Exception { assertNotNull(recovered); } + @Test + public void testDecryptClearsTemporaryKeys() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom()); + WalletFile walletFile = Wallet.createLight(password, keyPair); + byte[] expectedPrivateKey = keyPair.getPrivateKey(); + byte[] derivedKey = deriveKey(password, walletFile); + AtomicReference decryptedPrivateKey = new AtomicReference<>(); + + try (MockedStatic mockedScrypt = mockStatic(SCrypt.class); + MockedStatic mockedSignUtils = mockStatic(SignUtils.class)) { + mockedScrypt.when(() -> SCrypt.generate(any(byte[].class), any(byte[].class), + anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(derivedKey); + mockedSignUtils.when(() -> SignUtils.fromPrivate(any(byte[].class))) + .thenAnswer(invocation -> { + byte[] privateKey = invocation.getArgument(0); + assertArrayEquals(expectedPrivateKey, privateKey); + decryptedPrivateKey.set(privateKey); + return invocation.callRealMethod(); + }); + + SignInterface recovered = Wallet.decrypt(password, walletFile); + assertArrayEquals(expectedPrivateKey, recovered.getPrivateKey()); + assertArrayEquals(keyPair.getAddress(), recovered.getAddress()); + } + + assertArrayEquals(new byte[derivedKey.length], derivedKey); + byte[] clearedPrivateKey = decryptedPrivateKey.get(); + assertNotNull(clearedPrivateKey); + assertArrayEquals(new byte[clearedPrivateKey.length], clearedPrivateKey); + } + + @Test + public void testDecryptWrapsInvalidPrivateKey() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom()); + WalletFile walletFile = Wallet.createLight(password, keyPair); + byte[] expectedPrivateKey = keyPair.getPrivateKey(); + AtomicReference decryptedPrivateKey = new AtomicReference<>(); + + try (MockedStatic mockedSignUtils = mockStatic(SignUtils.class)) { + mockedSignUtils.when(() -> SignUtils.fromPrivate(any(byte[].class))) + .thenAnswer(invocation -> { + byte[] privateKey = invocation.getArgument(0); + assertArrayEquals(expectedPrivateKey, privateKey); + decryptedPrivateKey.set(privateKey); + throw new IllegalArgumentException("Invalid private key"); + }); + + CipherException exception = assertThrows(CipherException.class, + () -> Wallet.decrypt(password, walletFile)); + assertTrue(exception.getMessage().contains("Invalid private key")); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + } + + byte[] clearedPrivateKey = decryptedPrivateKey.get(); + assertNotNull(clearedPrivateKey); + assertArrayEquals(new byte[clearedPrivateKey.length], clearedPrivateKey); + } + + @Test + public void testDecryptClearsDerivedKeyOnIncorrectPassword() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom()); + WalletFile walletFile = Wallet.createLight(password, keyPair); + byte[] derivedKey = deriveKey("wrong", walletFile); + + try (MockedStatic mockedScrypt = mockStatic(SCrypt.class)) { + mockedScrypt.when(() -> SCrypt.generate(any(byte[].class), any(byte[].class), + anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(derivedKey); + CipherException exception = assertThrows(CipherException.class, + () -> Wallet.decrypt("wrong", walletFile)); + assertEquals("Invalid password provided", exception.getMessage()); + } + + assertArrayEquals(new byte[derivedKey.length], derivedKey); + } + + private static byte[] deriveKey(String password, WalletFile walletFile) { + WalletFile.ScryptKdfParams params = + (WalletFile.ScryptKdfParams) walletFile.getCrypto().getKdfparams(); + return SCrypt.generate(password.getBytes(UTF_8), ByteArray.fromHexString(params.getSalt()), + params.getN(), params.getR(), params.getP(), params.getDklen()); + } }