Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.tron.common.crypto.SignUtils;
import org.tron.common.utils.ByteArray;
import org.tron.common.utils.StringUtil;
import org.tron.core.config.args.Args;
import org.tron.core.exception.CipherException;

/**
Expand Down Expand Up @@ -168,8 +167,8 @@ private static byte[] generateMac(byte[] derivedKey, byte[] cipherText) {
return Hash.sha3(result);
}

public static SignInterface decrypt(String password, WalletFile walletFile)
throws CipherException {
public static SignInterface decrypt(String password, WalletFile walletFile,
boolean ecKey) throws CipherException {

validate(walletFile);

Expand Down Expand Up @@ -205,14 +204,14 @@ public static SignInterface decrypt(String password, WalletFile walletFile)

byte[] derivedMac = generateMac(derivedKey, cipherText);

if (!Arrays.equals(derivedMac, mac)) {
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);

return SignUtils.fromPrivate(privateKey, Args.getInstance().isECKeyCryptoEngine());
return SignUtils.fromPrivate(privateKey, ecKey);
}

static void validate(WalletFile walletFile) throws CipherException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import org.tron.common.crypto.SignInterface;
import org.tron.common.crypto.SignUtils;
import org.tron.common.utils.Utils;
import org.tron.core.config.args.Args;
import org.tron.core.exception.CipherException;

/**
Expand All @@ -32,27 +31,28 @@ public class WalletUtils {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

public static String generateFullNewWalletFile(String password, File destinationDirectory)
public static String generateFullNewWalletFile(String password, File destinationDirectory,
boolean ecKey)
throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidAlgorithmParameterException, CipherException, IOException {

return generateNewWalletFile(password, destinationDirectory, true);
return generateNewWalletFile(password, destinationDirectory, true, ecKey);
}

public static String generateLightNewWalletFile(String password, File destinationDirectory)
public static String generateLightNewWalletFile(String password, File destinationDirectory,
boolean ecKey)
throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidAlgorithmParameterException, CipherException, IOException {

return generateNewWalletFile(password, destinationDirectory, false);
return generateNewWalletFile(password, destinationDirectory, false, ecKey);
}

public static String generateNewWalletFile(
String password, File destinationDirectory, boolean useFullScrypt)
String password, File destinationDirectory, boolean useFullScrypt, boolean ecKey)
throws CipherException, IOException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchProviderException {

SignInterface ecKeyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(),
Args.getInstance().isECKeyCryptoEngine());
SignInterface ecKeyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), ecKey);
return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt);
}

Expand All @@ -75,10 +75,10 @@ public static String generateWalletFile(
return fileName;
}

public static Credentials loadCredentials(String password, File source)
public static Credentials loadCredentials(String password, File source, boolean ecKey)
throws IOException, CipherException {
WalletFile walletFile = objectMapper.readValue(source, WalletFile.class);
return Credentials.create(Wallet.decrypt(password, walletFile));
return Credentials.create(Wallet.decrypt(password, walletFile, ecKey));
}

private static String getWalletFileName(WalletFile walletFile) {
Expand Down
72 changes: 72 additions & 0 deletions crypto/src/test/java/org/tron/keystore/CredentialsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.tron.keystore;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.tron.common.crypto.SignInterface;
import org.tron.common.crypto.SignUtils;
import org.tron.common.crypto.sm2.SM2;
import org.tron.common.utils.ByteUtil;

@Slf4j
public class CredentialsTest {

@Test
public void testCreate() throws NoSuchAlgorithmException {
Credentials credentials = Credentials.create(SignUtils.getGeneratedRandomSign(
SecureRandom.getInstance("NativePRNG"), true));
Assert.assertTrue("Credentials address create failed!",
credentials.getAddress() != null && !credentials.getAddress().isEmpty());
Assert.assertNotNull("Credentials cryptoEngine create failed",
credentials.getSignInterface());
}

@Test
public void testCreateFromSM2() {
try {
Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff"
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ "fffffffffffffffffffffffffffffffffffffff")));
Assert.fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected
}
}

@Test
public void testEquals() throws NoSuchAlgorithmException {
Credentials credentials1 = Credentials.create(SignUtils.getGeneratedRandomSign(
SecureRandom.getInstance("NativePRNG"), true));
Credentials credentials2 = Credentials.create(SignUtils.getGeneratedRandomSign(
SecureRandom.getInstance("NativePRNG"), true));
Assert.assertFalse("Credentials instance should be not equal!",
credentials1.equals(credentials2));
}

@Test
public void testEqualityWithMocks() {
Object aObject = new Object();
SignInterface si = Mockito.mock(SignInterface.class);
SignInterface si2 = Mockito.mock(SignInterface.class);
SignInterface si3 = Mockito.mock(SignInterface.class);
byte[] address = "TQhZ7W1RudxFdzJMw6FvMnujPxrS6sFfmj".getBytes();
byte[] address2 = "TNCmcTdyrYKMtmE1KU2itzeCX76jGm5Not".getBytes();
Mockito.when(si.getAddress()).thenReturn(address);
Mockito.when(si2.getAddress()).thenReturn(address);
Mockito.when(si3.getAddress()).thenReturn(address2);
Credentials aCredential = Credentials.create(si);
Assert.assertFalse(aObject.equals(aCredential));
Assert.assertFalse(aCredential.equals(aObject));
Assert.assertFalse(aCredential.equals(null));
Credentials anotherCredential = Credentials.create(si);
Assert.assertTrue(aCredential.equals(anotherCredential));
Credentials aCredential2 = Credentials.create(si2);
// si and si2 are different mock objects, so credentials are not equal
Assert.assertFalse(aCredential.equals(aCredential2));
Credentials aCredential3 = Credentials.create(si3);
Assert.assertFalse(aCredential.equals(aCredential3));
}
}
165 changes: 165 additions & 0 deletions crypto/src/test/java/org/tron/keystore/CrossImplTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package org.tron.keystore;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.tron.common.crypto.SignInterface;
import org.tron.common.crypto.SignUtils;
import org.tron.common.utils.Utils;

/**
* Format compatibility tests.
*
* <p>All tests generate keystores dynamically at test time — no static
* fixtures or secrets stored in the repository. Verifies that keystore
* files can survive a full roundtrip: generate keypair, encrypt, serialize
* to JSON file, deserialize, decrypt, compare private key and address.
*/
public class CrossImplTest {

private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();

// --- Ethereum standard test vectors (from Web3 Secret Storage spec, inline) ---
// Source: web3j WalletTest.java — password and private key are public test data.

private static final String ETH_PASSWORD = "Insecure Pa55w0rd";
private static final String ETH_PRIVATE_KEY =
"a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6";

private static final String ETH_PBKDF2_KEYSTORE = "{"
+ "\"crypto\":{\"cipher\":\"aes-128-ctr\","
+ "\"cipherparams\":{\"iv\":\"02ebc768684e5576900376114625ee6f\"},"
+ "\"ciphertext\":\"7ad5c9dd2c95f34a92ebb86740b92103a5d1cc4c2eabf3b9a59e1f83f3181216\","
+ "\"kdf\":\"pbkdf2\","
+ "\"kdfparams\":{\"c\":262144,\"dklen\":32,\"prf\":\"hmac-sha256\","
+ "\"salt\":\"0e4cf3893b25bb81efaae565728b5b7cde6a84e224cbf9aed3d69a31c981b702\"},"
+ "\"mac\":\"2b29e4641ec17f4dc8b86fc8592090b50109b372529c30b001d4d96249edaf62\"},"
+ "\"id\":\"af0451b4-6020-4ef0-91ec-794a5a965b01\",\"version\":3}";

private static final String ETH_SCRYPT_KEYSTORE = "{"
+ "\"crypto\":{\"cipher\":\"aes-128-ctr\","
+ "\"cipherparams\":{\"iv\":\"3021e1ef4774dfc5b08307f3a4c8df00\"},"
+ "\"ciphertext\":\"4dd29ba18478b98cf07a8a44167acdf7e04de59777c4b9c139e3d3fa5cb0b931\","
+ "\"kdf\":\"scrypt\","
+ "\"kdfparams\":{\"dklen\":32,\"n\":262144,\"r\":8,\"p\":1,"
+ "\"salt\":\"4f9f68c71989eb3887cd947c80b9555fce528f210199d35c35279beb8c2da5ca\"},"
+ "\"mac\":\"7e8f2192767af9be18e7a373c1986d9190fcaa43ad689bbb01a62dbde159338d\"},"
+ "\"id\":\"7654525c-17e0-4df5-94b5-c7fde752c9d2\",\"version\":3}";

@Test
public void testDecryptEthPbkdf2Keystore() throws Exception {
WalletFile walletFile = MAPPER.readValue(ETH_PBKDF2_KEYSTORE, WalletFile.class);
SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true);
assertEquals("Private key must match Ethereum test vector",
ETH_PRIVATE_KEY,
org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey()));
}

@Test
public void testDecryptEthScryptKeystore() throws Exception {
WalletFile walletFile = MAPPER.readValue(ETH_SCRYPT_KEYSTORE, WalletFile.class);
SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true);
assertEquals("Private key must match Ethereum test vector",
ETH_PRIVATE_KEY,
org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey()));
}

// --- Dynamic format compatibility (no static secrets) ---

@Test
public void testKeystoreFormatCompatibility() throws Exception {
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
byte[] originalKey = keyPair.getPrivateKey();
String password = "dynamicTest123";

WalletFile walletFile = Wallet.createStandard(password, keyPair);

// Verify Web3 Secret Storage structure
assertEquals("version must be 3", 3, walletFile.getVersion());
assertNotNull("must have address", walletFile.getAddress());
assertNotNull("must have crypto", walletFile.getCrypto());
assertEquals("cipher must be aes-128-ctr",
"aes-128-ctr", walletFile.getCrypto().getCipher());
assertTrue("kdf must be scrypt or pbkdf2",
"scrypt".equals(walletFile.getCrypto().getKdf())
|| "pbkdf2".equals(walletFile.getCrypto().getKdf()));

// Write to file, read back — simulates cross-process interop
File tempFile = new File(tempFolder.getRoot(), "compat-test.json");
MAPPER.writeValue(tempFile, walletFile);
WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class);

SignInterface recovered = Wallet.decrypt(password, loaded, true);
assertArrayEquals("Key must survive file roundtrip",
originalKey, recovered.getPrivateKey());

// Verify TRON address format
byte[] tronAddr = recovered.getAddress();
assertEquals("TRON address must be 21 bytes", 21, tronAddr.length);
assertEquals("First byte must be TRON prefix", 0x41, tronAddr[0] & 0xFF);
}

@Test
public void testLightScryptFormatCompatibility() throws Exception {
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
byte[] originalKey = keyPair.getPrivateKey();
String password = "lightCompat456";

WalletFile walletFile = Wallet.createLight(password, keyPair);
File tempFile = new File(tempFolder.getRoot(), "light-compat.json");
MAPPER.writeValue(tempFile, walletFile);
WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class);

SignInterface recovered = Wallet.decrypt(password, loaded, true);
assertArrayEquals("Key must survive light scrypt file roundtrip",
originalKey, recovered.getPrivateKey());
}

@Test
public void testKeystoreAddressConsistency() throws Exception {
String password = "addresscheck";
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
Credentials original = Credentials.create(keyPair);

WalletFile walletFile = Wallet.createLight(password, keyPair);
assertEquals("WalletFile address must match credentials address",
original.getAddress(), walletFile.getAddress());

SignInterface recovered = Wallet.decrypt(password, walletFile, true);
Credentials recoveredCreds = Credentials.create(recovered);
assertEquals("Recovered address must match original",
original.getAddress(), recoveredCreds.getAddress());
}

@Test
public void testLoadCredentialsIntegration() throws Exception {
String password = "integration789";
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
byte[] originalKey = keyPair.getPrivateKey();
String originalAddress = Credentials.create(keyPair).getAddress();

File tempDir = tempFolder.newFolder("wallet-integration");
String fileName = WalletUtils.generateWalletFile(password, keyPair, tempDir, false);
assertNotNull(fileName);

File keystoreFile = new File(tempDir, fileName);
Credentials loaded = WalletUtils.loadCredentials(password, keystoreFile, true);

assertEquals("Address must survive full WalletUtils roundtrip",
originalAddress, loaded.getAddress());
assertArrayEquals("Key must survive full WalletUtils roundtrip",
originalKey, loaded.getSignInterface().getPrivateKey());
}
}
Loading
Loading