diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000000..085821d3217
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,96 @@
+# CODEOWNERS — java-tron PR Review assignments
+#
+# Rule: when multiple rules match the same file, the last one takes precedence.
+# Recommendation: put more specific paths further down.
+
+# Default owner — applies to any file not matched by a more specific rule below.
+* @zeusoo001 @317787106
+
+# ────────────────────────────────────────────────────────────────
+# 1. Top-level Gradle modules (corresponds to the "Level-1/Level-2 module" column)
+# ────────────────────────────────────────────────────────────────
+
+/framework/ @xxo1shine @bladehan1
+/chainbase/ @halibobo1205 @bladehan1
+/actuator/ @Sunny6889 @lxcmyf
+/consensus/ @kuny0707 @xxo1shine
+/protocol/ @lvs0075 @waynercheung
+/common/ @lvs0075 @lxcmyf
+/crypto/ @Federico2014 @3for
+/plugins/ @halibobo1205 @warku123
+/docker/ @3for @kuny0707
+
+# ────────────────────────────────────────────────────────────────
+# 2. CI / Build configuration
+# ────────────────────────────────────────────────────────────────
+
+/.github/ @317787106 @halibobo1205
+*.gradle @317787106 @halibobo1205
+/gradle/ @317787106 @halibobo1205
+
+# ────────────────────────────────────────────────────────────────
+# 3. Sub-module paths (more specific than top-level; placed later to override rules above)
+# ────────────────────────────────────────────────────────────────
+
+# db — covers the db, db2, and storage packages inside chainbase
+/chainbase/src/main/java/org/tron/core/db/ @halibobo1205 @bladehan1
+/chainbase/src/main/java/org/tron/core/db2/ @halibobo1205 @bladehan1
+/chainbase/src/main/java/org/tron/common/storage/ @halibobo1205 @bladehan1
+
+# trie
+/framework/src/main/java/org/tron/core/trie/ @halibobo1205 @317787106
+
+# net
+/framework/src/main/java/org/tron/core/net/ @317787106 @xxo1shine
+
+# vm / tvm
+/actuator/src/main/java/org/tron/core/vm/ @yanghang8612 @CodeNinjaEvan
+
+# jsonrpc
+/framework/src/main/java/org/tron/core/services/jsonrpc/ @0xbigapple @waynercheung
+/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/ @0xbigapple @waynercheung
+
+# rpc (gRPC) — non-HTTP parts of interfaceOnPBFT / interfaceOnSolidity + RpcService
+/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/ @317787106 @waynercheung
+/framework/src/main/java/org/tron/common/application/ @317787106 @waynercheung
+
+# http (REST) — overrides the rpc rule above for the http sub-directories inside interfaceOnXxx
+/framework/src/main/java/org/tron/core/services/http/ @Sunny6889 @waynercheung
+/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/ @Sunny6889 @waynercheung
+
+# event
+/framework/src/main/java/org/tron/core/services/event/ @xxo1shine @0xbigapple
+/framework/src/main/java/org/tron/common/logsfilter/ @xxo1shine @0xbigapple
+
+# config
+/framework/src/main/java/org/tron/core/config/ @317787106 @kuny0707
+/chainbase/src/main/java/org/tron/core/config/ @317787106 @kuny0707
+
+# backup
+/framework/src/main/java/org/tron/common/backup/ @xxo1shine @317787106
+/framework/src/main/java/org/tron/core/db/backup/ @xxo1shine @317787106
+
+# metrics
+/framework/src/main/java/org/tron/core/metrics/ @halibobo1205 @Sunny6889
+
+# rewards — logic is spread across chainbase service/store
+/chainbase/src/main/java/org/tron/core/service/ @Sunny6889 @kuny0707
+/chainbase/src/main/java/org/tron/core/store/ @Sunny6889 @kuny0707
+
+# lite — DbLite in plugins module; lite-related filters in framework
+/plugins/src/main/java/common/org/tron/plugins/DbLite.java @bladehan1 @halibobo1205
+/plugins/src/main/java/common/org/tron/plugins/DbCopy.java @bladehan1 @halibobo1205
+/framework/src/main/java/org/tron/core/services/filter/ @bladehan1 @halibobo1205
+
+# ────────────────────────────────────────────────────────────────
+# 4. Test code — mirrors the module ownership above
+# ────────────────────────────────────────────────────────────────
+
+/framework/src/test/ @xxo1shine @bladehan1
+/chainbase/src/test/ @halibobo1205 @bladehan1
+/actuator/src/test/ @Sunny6889 @lxcmyf
+/consensus/src/test/ @kuny0707 @xxo1shine
+/protocol/src/test/ @lvs0075 @waynercheung
+/common/src/test/ @lvs0075 @lxcmyf
+/crypto/src/test/ @Federico2014 @3for
+/plugins/src/test/ @halibobo1205 @warku123
diff --git a/framework/src/main/java/org/tron/keystore/Credentials.java b/crypto/src/main/java/org/tron/keystore/Credentials.java
similarity index 100%
rename from framework/src/main/java/org/tron/keystore/Credentials.java
rename to crypto/src/main/java/org/tron/keystore/Credentials.java
diff --git a/framework/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java
similarity index 97%
rename from framework/src/main/java/org/tron/keystore/Wallet.java
rename to crypto/src/main/java/org/tron/keystore/Wallet.java
index d38b1c74984..b5cb37a23ab 100644
--- a/framework/src/main/java/org/tron/keystore/Wallet.java
+++ b/crypto/src/main/java/org/tron/keystore/Wallet.java
@@ -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;
/**
@@ -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);
@@ -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 {
diff --git a/framework/src/main/java/org/tron/keystore/WalletFile.java b/crypto/src/main/java/org/tron/keystore/WalletFile.java
similarity index 100%
rename from framework/src/main/java/org/tron/keystore/WalletFile.java
rename to crypto/src/main/java/org/tron/keystore/WalletFile.java
diff --git a/framework/src/main/java/org/tron/keystore/WalletUtils.java b/crypto/src/main/java/org/tron/keystore/WalletUtils.java
similarity index 94%
rename from framework/src/main/java/org/tron/keystore/WalletUtils.java
rename to crypto/src/main/java/org/tron/keystore/WalletUtils.java
index 8bcc68cbab0..ad4f434005f 100644
--- a/framework/src/main/java/org/tron/keystore/WalletUtils.java
+++ b/crypto/src/main/java/org/tron/keystore/WalletUtils.java
@@ -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;
/**
@@ -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);
}
@@ -75,13 +75,13 @@ 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) {
+ public static String getWalletFileName(WalletFile walletFile) {
DateTimeFormatter format = DateTimeFormatter.ofPattern(
"'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'");
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
diff --git a/crypto/src/test/java/org/tron/keystore/CredentialsTest.java b/crypto/src/test/java/org/tron/keystore/CredentialsTest.java
new file mode 100644
index 00000000000..7d90679f0d9
--- /dev/null
+++ b/crypto/src/test/java/org/tron/keystore/CredentialsTest.java
@@ -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));
+ }
+}
diff --git a/crypto/src/test/java/org/tron/keystore/CrossImplTest.java b/crypto/src/test/java/org/tron/keystore/CrossImplTest.java
new file mode 100644
index 00000000000..6b00c57c1f9
--- /dev/null
+++ b/crypto/src/test/java/org/tron/keystore/CrossImplTest.java
@@ -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.
+ *
+ *
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());
+ }
+}
diff --git a/framework/src/test/java/org/tron/keystore/WalletFileTest.java b/crypto/src/test/java/org/tron/keystore/WalletFileTest.java
similarity index 100%
rename from framework/src/test/java/org/tron/keystore/WalletFileTest.java
rename to crypto/src/test/java/org/tron/keystore/WalletFileTest.java
diff --git a/crypto/src/test/java/org/tron/keystore/WalletPropertyTest.java b/crypto/src/test/java/org/tron/keystore/WalletPropertyTest.java
new file mode 100644
index 00000000000..3028d2a7799
--- /dev/null
+++ b/crypto/src/test/java/org/tron/keystore/WalletPropertyTest.java
@@ -0,0 +1,77 @@
+package org.tron.keystore;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.security.SecureRandom;
+import org.junit.Test;
+import org.tron.common.crypto.SignInterface;
+import org.tron.common.crypto.SignUtils;
+import org.tron.common.utils.Utils;
+import org.tron.core.exception.CipherException;
+
+/**
+ * Property-based roundtrip tests: decrypt(encrypt(privateKey, password)) == privateKey.
+ * Uses randomized inputs via loop instead of jqwik to avoid dependency verification overhead.
+ */
+public class WalletPropertyTest {
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+ private static final String CHARS =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ @Test
+ public void encryptDecryptRoundtripLight() throws Exception {
+ for (int i = 0; i < 100; i++) {
+ String password = randomPassword(6, 32);
+ SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
+ byte[] originalKey = keyPair.getPrivateKey();
+
+ WalletFile walletFile = Wallet.createLight(password, keyPair);
+ SignInterface recovered = Wallet.decrypt(password, walletFile, true);
+
+ assertArrayEquals("Roundtrip failed at iteration " + i,
+ originalKey, recovered.getPrivateKey());
+ }
+ }
+
+ @Test(timeout = 120000)
+ public void encryptDecryptRoundtripStandard() throws Exception {
+ // Fewer iterations for standard scrypt (slow, ~10s each)
+ for (int i = 0; i < 2; i++) {
+ String password = randomPassword(6, 16);
+ SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
+ byte[] originalKey = keyPair.getPrivateKey();
+
+ WalletFile walletFile = Wallet.createStandard(password, keyPair);
+ SignInterface recovered = Wallet.decrypt(password, walletFile, true);
+
+ assertArrayEquals("Standard roundtrip failed at iteration " + i,
+ originalKey, recovered.getPrivateKey());
+ }
+ }
+
+ @Test
+ public void wrongPasswordFailsDecrypt() throws Exception {
+ for (int i = 0; i < 50; i++) {
+ String password = randomPassword(6, 16);
+ SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
+ WalletFile walletFile = Wallet.createLight(password, keyPair);
+
+ try {
+ Wallet.decrypt(password + "X", walletFile, true);
+ throw new AssertionError("Expected CipherException at iteration " + i);
+ } catch (CipherException e) {
+ // Expected
+ }
+ }
+ }
+
+ private String randomPassword(int minLen, int maxLen) {
+ int len = minLen + RANDOM.nextInt(maxLen - minLen + 1);
+ StringBuilder sb = new StringBuilder(len);
+ for (int i = 0; i < len; i++) {
+ sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));
+ }
+ return sb.toString();
+ }
+}
diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
index 30711eb6190..0ef242e2e01 100644
--- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
+++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
@@ -79,7 +79,8 @@ public static LocalWitnesses initFromKeystore(
List privateKeys = new ArrayList<>();
try {
- Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName));
+ Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName),
+ Args.getInstance().isECKeyCryptoEngine());
SignInterface sign = credentials.getSignInterface();
String prikey = ByteArray.toHexString(sign.getPrivateKey());
privateKeys.add(prikey);
diff --git a/framework/src/main/java/org/tron/program/KeystoreFactory.java b/framework/src/main/java/org/tron/program/KeystoreFactory.java
index 8199d7e9076..a88cdca904a 100755
--- a/framework/src/main/java/org/tron/program/KeystoreFactory.java
+++ b/framework/src/main/java/org/tron/program/KeystoreFactory.java
@@ -15,11 +15,20 @@
import org.tron.keystore.WalletUtils;
@Slf4j(topic = "app")
+@Deprecated
public class KeystoreFactory {
private static final String FilePath = "Wallet";
public static void start() {
+ System.err.println("WARNING: --keystore-factory is deprecated and will be removed "
+ + "in a future release.");
+ System.err.println("Please use: java -jar Toolkit.jar keystore ");
+ System.err.println(" keystore new - Generate a new keystore");
+ System.err.println(" keystore import - Import a private key");
+ System.err.println(" keystore list - List keystores");
+ System.err.println(" keystore update - Change password");
+ System.err.println();
KeystoreFactory cli = new KeystoreFactory();
cli.run();
}
@@ -57,15 +66,16 @@ private void fileCheck(File file) throws IOException {
private void genKeystore() throws CipherException, IOException {
+ boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine();
String password = WalletUtils.inputPassword2Twice();
- SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random,
- CommonParameter.getInstance().isECKeyCryptoEngine());
+ SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random, ecKey);
File file = new File(FilePath);
fileCheck(file);
String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true);
System.out.println("Gen a keystore its name " + fileName);
- Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName));
+ Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName),
+ ecKey);
System.out.println("Your address is " + credentials.getAddress());
}
@@ -84,22 +94,25 @@ private void importPrivateKey() throws CipherException, IOException {
String password = WalletUtils.inputPassword2Twice();
- SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey),
- CommonParameter.getInstance().isECKeyCryptoEngine());
+ boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine();
+ SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey), ecKey);
File file = new File(FilePath);
fileCheck(file);
String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true);
System.out.println("Gen a keystore its name " + fileName);
- Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName));
+ Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName),
+ ecKey);
System.out.println("Your address is " + credentials.getAddress());
}
private void help() {
- System.out.println("You can enter the following command: ");
- System.out.println("GenKeystore");
- System.out.println("ImportPrivateKey");
- System.out.println("Exit or Quit");
- System.out.println("Input any one of them, you will get more tips.");
+ System.out.println("NOTE: --keystore-factory is deprecated. Use Toolkit.jar instead:");
+ System.out.println(" java -jar Toolkit.jar keystore new|import|list|update");
+ System.out.println();
+ System.out.println("Legacy commands (will be removed):");
+ System.out.println(" GenKeystore");
+ System.out.println(" ImportPrivateKey");
+ System.out.println(" Exit or Quit");
}
private void run() {
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
new file mode 100644
index 00000000000..0a7717cb1a0
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java
@@ -0,0 +1,86 @@
+package org.tron.core.config.args;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.File;
+import java.security.SecureRandom;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+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.ByteArray;
+import org.tron.common.utils.LocalWitnesses;
+import org.tron.keystore.Credentials;
+import org.tron.keystore.WalletUtils;
+
+/**
+ * Backward compatibility: verifies that keystore files generated by
+ * the new Toolkit code path can be loaded by WitnessInitializer
+ * (used by FullNode at startup via localwitnesskeystore config).
+ */
+public class WitnessInitializerKeystoreTest {
+
+ @ClassRule
+ public static final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ // WitnessInitializer prepends user.dir to the filename, so we must
+ // create the keystore dir relative to user.dir. Use unique name to
+ // avoid collisions with parallel test runs.
+ private static final String DIR_NAME =
+ ".test-keystore-" + System.currentTimeMillis();
+
+ private static String keystoreFileName;
+ private static String expectedPrivateKey;
+ private static final String PASSWORD = "backcompat123";
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ Args.setParam(new String[]{"-d", tempFolder.newFolder().toString()},
+ "config-test.conf");
+
+ SignInterface keyPair = SignUtils.getGeneratedRandomSign(
+ SecureRandom.getInstance("NativePRNG"), true);
+ expectedPrivateKey = ByteArray.toHexString(keyPair.getPrivateKey());
+
+ File dir = new File(System.getProperty("user.dir"), DIR_NAME);
+ dir.mkdirs();
+ String generatedName =
+ WalletUtils.generateWalletFile(PASSWORD, keyPair, dir, true);
+ keystoreFileName = DIR_NAME + "/" + generatedName;
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ Args.clearParam();
+ File dir = new File(System.getProperty("user.dir"), DIR_NAME);
+ if (dir.exists()) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ f.delete();
+ }
+ }
+ dir.delete();
+ }
+ }
+
+ @Test
+ public void testNewKeystoreLoadableByWitnessInitializer() {
+ java.util.List keystores =
+ java.util.Collections.singletonList(keystoreFileName);
+
+ LocalWitnesses result = WitnessInitializer.initFromKeystore(
+ keystores, PASSWORD, null);
+
+ assertNotNull("WitnessInitializer should load new keystore", result);
+ assertFalse("Should have at least one private key",
+ result.getPrivateKeys().isEmpty());
+ assertEquals("Private key must match original",
+ expectedPrivateKey, result.getPrivateKeys().get(0));
+ }
+}
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..e0aa2606473 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
@@ -6,6 +6,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -106,7 +107,7 @@ public void testInitFromKeystore() {
byte[] keyBytes = Hex.decode(privateKey);
when(signInterface.getPrivateKey()).thenReturn(keyBytes);
mockedWallet.when(() -> WalletUtils.loadCredentials(
- anyString(), any(File.class))).thenReturn(credentials);
+ anyString(), any(File.class), anyBoolean())).thenReturn(credentials);
mockedByteArray.when(() -> ByteArray.toHexString(any()))
.thenReturn(privateKey);
mockedByteArray.when(() -> ByteArray.fromHexString(anyString()))
diff --git a/framework/src/test/java/org/tron/keystore/CredentialsTest.java b/framework/src/test/java/org/tron/keystore/CredentialsTest.java
deleted file mode 100644
index 3fe2ce02b63..00000000000
--- a/framework/src/test/java/org/tron/keystore/CredentialsTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.tron.keystore;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import junit.framework.TestCase;
-import lombok.extern.slf4j.Slf4j;
-import org.junit.Test;
-import org.springframework.util.Assert;
-import org.tron.common.crypto.SignUtils;
-import org.tron.common.crypto.sm2.SM2;
-import org.tron.common.utils.ByteUtil;
-
-@Slf4j
-public class CredentialsTest extends TestCase {
-
- @Test
- public void testCreate() throws NoSuchAlgorithmException {
- Credentials credentials = Credentials.create(SignUtils.getGeneratedRandomSign(
- SecureRandom.getInstance("NativePRNG"),true));
- Assert.hasText(credentials.getAddress(),"Credentials address create failed!");
- Assert.notNull(credentials.getSignInterface(),
- "Credentials cryptoEngine create failed");
- }
-
- @Test
- public void testCreateFromSM2() {
- try {
- Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff"
- + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
- + "fffffffffffffffffffffffffffffffffffffff")));
- } catch (Exception e) {
- Assert.isInstanceOf(IllegalArgumentException.class, e);
- }
- }
-
- @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.isTrue(!credentials1.equals(credentials2),
- "Credentials instance should be not equal!");
- Assert.isTrue(!(credentials1.hashCode() == credentials2.hashCode()),
- "Credentials instance hashcode should be not equal!");
- }
-
-}
\ No newline at end of file
diff --git a/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java
new file mode 100644
index 00000000000..83c7096665b
--- /dev/null
+++ b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java
@@ -0,0 +1,389 @@
+package org.tron.keystore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+public class WalletFilePojoTest {
+
+ @Test
+ public void testWalletFileGettersSetters() {
+ WalletFile wf = new WalletFile();
+ wf.setAddress("TAddr");
+ wf.setId("uuid-123");
+ wf.setVersion(3);
+ WalletFile.Crypto c = new WalletFile.Crypto();
+ wf.setCrypto(c);
+
+ assertEquals("TAddr", wf.getAddress());
+ assertEquals("uuid-123", wf.getId());
+ assertEquals(3, wf.getVersion());
+ assertEquals(c, wf.getCrypto());
+ }
+
+ @Test
+ public void testWalletFileCryptoV1Setter() {
+ WalletFile wf = new WalletFile();
+ WalletFile.Crypto c = new WalletFile.Crypto();
+ wf.setCryptoV1(c);
+ assertEquals(c, wf.getCrypto());
+ }
+
+ @Test
+ public void testWalletFileEqualsAllBranches() {
+ WalletFile a = new WalletFile();
+ a.setAddress("TAddr");
+ a.setId("id1");
+ a.setVersion(3);
+ WalletFile.Crypto c = new WalletFile.Crypto();
+ a.setCrypto(c);
+
+ WalletFile b = new WalletFile();
+ b.setAddress("TAddr");
+ b.setId("id1");
+ b.setVersion(3);
+ b.setCrypto(c);
+
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertTrue(a.equals(a));
+ assertFalse(a.equals(null));
+ assertFalse(a.equals("string"));
+
+ // Different address
+ b.setAddress("TOther");
+ assertNotEquals(a, b);
+ b.setAddress("TAddr");
+
+ // Different id
+ b.setId("id2");
+ assertNotEquals(a, b);
+ b.setId("id1");
+
+ // Different version
+ b.setVersion(4);
+ assertNotEquals(a, b);
+ b.setVersion(3);
+
+ // Different crypto
+ b.setCrypto(new WalletFile.Crypto());
+ // Still equal since Cryptos are equal (both empty)
+ assertEquals(a, b);
+
+ // Null fields
+ WalletFile empty = new WalletFile();
+ WalletFile empty2 = new WalletFile();
+ assertEquals(empty, empty2);
+ assertEquals(empty.hashCode(), empty2.hashCode());
+
+ // One side null
+ empty2.setAddress("X");
+ assertNotEquals(empty, empty2);
+ }
+
+ @Test
+ public void testCryptoGettersSetters() {
+ WalletFile.Crypto c = new WalletFile.Crypto();
+ c.setCipher("aes-128-ctr");
+ c.setCiphertext("ciphertext");
+ c.setKdf("scrypt");
+ c.setMac("mac-value");
+
+ WalletFile.CipherParams cp = new WalletFile.CipherParams();
+ cp.setIv("ivvalue");
+ c.setCipherparams(cp);
+
+ WalletFile.ScryptKdfParams kp = new WalletFile.ScryptKdfParams();
+ c.setKdfparams(kp);
+
+ assertEquals("aes-128-ctr", c.getCipher());
+ assertEquals("ciphertext", c.getCiphertext());
+ assertEquals("scrypt", c.getKdf());
+ assertEquals("mac-value", c.getMac());
+ assertEquals(cp, c.getCipherparams());
+ assertEquals(kp, c.getKdfparams());
+ }
+
+ @Test
+ public void testCryptoEqualsAllBranches() {
+ WalletFile.Crypto a = new WalletFile.Crypto();
+ a.setCipher("c1");
+ a.setCiphertext("txt");
+ a.setKdf("kdf");
+ a.setMac("mac");
+ WalletFile.CipherParams cp = new WalletFile.CipherParams();
+ cp.setIv("iv");
+ a.setCipherparams(cp);
+ WalletFile.Aes128CtrKdfParams kp = new WalletFile.Aes128CtrKdfParams();
+ a.setKdfparams(kp);
+
+ WalletFile.Crypto b = new WalletFile.Crypto();
+ b.setCipher("c1");
+ b.setCiphertext("txt");
+ b.setKdf("kdf");
+ b.setMac("mac");
+ b.setCipherparams(cp);
+ b.setKdfparams(kp);
+
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertTrue(a.equals(a));
+ assertFalse(a.equals(null));
+ assertFalse(a.equals("string"));
+
+ // cipher differs
+ b.setCipher("c2");
+ assertNotEquals(a, b);
+ b.setCipher("c1");
+
+ // ciphertext differs
+ b.setCiphertext("other");
+ assertNotEquals(a, b);
+ b.setCiphertext("txt");
+
+ // kdf differs
+ b.setKdf("other");
+ assertNotEquals(a, b);
+ b.setKdf("kdf");
+
+ // mac differs
+ b.setMac("other");
+ assertNotEquals(a, b);
+ b.setMac("mac");
+
+ // cipherparams differs
+ WalletFile.CipherParams cp2 = new WalletFile.CipherParams();
+ cp2.setIv("other");
+ b.setCipherparams(cp2);
+ assertNotEquals(a, b);
+ b.setCipherparams(cp);
+
+ // kdfparams differs
+ WalletFile.Aes128CtrKdfParams kp2 = new WalletFile.Aes128CtrKdfParams();
+ kp2.setC(5);
+ b.setKdfparams(kp2);
+ assertNotEquals(a, b);
+ }
+
+ @Test
+ public void testCryptoNullFields() {
+ WalletFile.Crypto a = new WalletFile.Crypto();
+ WalletFile.Crypto b = new WalletFile.Crypto();
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+
+ a.setCipher("x");
+ assertNotEquals(a, b);
+ }
+
+ @Test
+ public void testCipherParamsGettersSetters() {
+ WalletFile.CipherParams cp = new WalletFile.CipherParams();
+ cp.setIv("ivvalue");
+ assertEquals("ivvalue", cp.getIv());
+ }
+
+ @Test
+ public void testCipherParamsEquals() {
+ WalletFile.CipherParams a = new WalletFile.CipherParams();
+ WalletFile.CipherParams b = new WalletFile.CipherParams();
+ assertEquals(a, b);
+ a.setIv("iv");
+ assertNotEquals(a, b);
+ b.setIv("iv");
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ b.setIv("other");
+ assertNotEquals(a, b);
+ assertTrue(a.equals(a));
+ assertFalse(a.equals(null));
+ assertFalse(a.equals("string"));
+ }
+
+ @Test
+ public void testAes128CtrKdfParamsAllAccessors() {
+ WalletFile.Aes128CtrKdfParams p = new WalletFile.Aes128CtrKdfParams();
+ p.setDklen(32);
+ p.setC(262144);
+ p.setPrf("hmac-sha256");
+ p.setSalt("saltvalue");
+
+ assertEquals(32, p.getDklen());
+ assertEquals(262144, p.getC());
+ assertEquals("hmac-sha256", p.getPrf());
+ assertEquals("saltvalue", p.getSalt());
+ }
+
+ @Test
+ public void testAes128CtrKdfParamsEquals() {
+ WalletFile.Aes128CtrKdfParams a = new WalletFile.Aes128CtrKdfParams();
+ a.setDklen(32);
+ a.setC(262144);
+ a.setPrf("hmac-sha256");
+ a.setSalt("salt");
+
+ WalletFile.Aes128CtrKdfParams b = new WalletFile.Aes128CtrKdfParams();
+ b.setDklen(32);
+ b.setC(262144);
+ b.setPrf("hmac-sha256");
+ b.setSalt("salt");
+
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertTrue(a.equals(a));
+ assertFalse(a.equals(null));
+ assertFalse(a.equals("string"));
+
+ b.setDklen(64);
+ assertNotEquals(a, b);
+ b.setDklen(32);
+
+ b.setC(1);
+ assertNotEquals(a, b);
+ b.setC(262144);
+
+ b.setPrf("other");
+ assertNotEquals(a, b);
+ b.setPrf("hmac-sha256");
+
+ b.setSalt("other");
+ assertNotEquals(a, b);
+ b.setSalt("salt");
+
+ // null fields
+ WalletFile.Aes128CtrKdfParams x = new WalletFile.Aes128CtrKdfParams();
+ WalletFile.Aes128CtrKdfParams y = new WalletFile.Aes128CtrKdfParams();
+ assertEquals(x, y);
+ x.setPrf("x");
+ assertNotEquals(x, y);
+ }
+
+ @Test
+ public void testScryptKdfParamsAllAccessors() {
+ WalletFile.ScryptKdfParams p = new WalletFile.ScryptKdfParams();
+ p.setDklen(32);
+ p.setN(262144);
+ p.setP(1);
+ p.setR(8);
+ p.setSalt("saltvalue");
+
+ assertEquals(32, p.getDklen());
+ assertEquals(262144, p.getN());
+ assertEquals(1, p.getP());
+ assertEquals(8, p.getR());
+ assertEquals("saltvalue", p.getSalt());
+ }
+
+ @Test
+ public void testScryptKdfParamsEquals() {
+ WalletFile.ScryptKdfParams a = new WalletFile.ScryptKdfParams();
+ a.setDklen(32);
+ a.setN(262144);
+ a.setP(1);
+ a.setR(8);
+ a.setSalt("salt");
+
+ WalletFile.ScryptKdfParams b = new WalletFile.ScryptKdfParams();
+ b.setDklen(32);
+ b.setN(262144);
+ b.setP(1);
+ b.setR(8);
+ b.setSalt("salt");
+
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertTrue(a.equals(a));
+ assertFalse(a.equals(null));
+ assertFalse(a.equals("string"));
+
+ b.setDklen(64);
+ assertNotEquals(a, b);
+ b.setDklen(32);
+
+ b.setN(1);
+ assertNotEquals(a, b);
+ b.setN(262144);
+
+ b.setP(2);
+ assertNotEquals(a, b);
+ b.setP(1);
+
+ b.setR(16);
+ assertNotEquals(a, b);
+ b.setR(8);
+
+ b.setSalt("other");
+ assertNotEquals(a, b);
+
+ // null salt
+ WalletFile.ScryptKdfParams x = new WalletFile.ScryptKdfParams();
+ WalletFile.ScryptKdfParams y = new WalletFile.ScryptKdfParams();
+ assertEquals(x, y);
+ x.setSalt("x");
+ assertNotEquals(x, y);
+ }
+
+ @Test
+ public void testJsonDeserializeWithScryptKdf() throws Exception {
+ String json = "{"
+ + "\"address\":\"TAddr\","
+ + "\"version\":3,"
+ + "\"id\":\"uuid\","
+ + "\"crypto\":{"
+ + " \"cipher\":\"aes-128-ctr\","
+ + " \"ciphertext\":\"ct\","
+ + " \"cipherparams\":{\"iv\":\"iv\"},"
+ + " \"kdf\":\"scrypt\","
+ + " \"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"salt\"},"
+ + " \"mac\":\"mac\""
+ + "}}";
+
+ WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class);
+ assertEquals("TAddr", wf.getAddress());
+ assertEquals(3, wf.getVersion());
+ assertNotNull(wf.getCrypto());
+ assertNotNull(wf.getCrypto().getKdfparams());
+ assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.ScryptKdfParams);
+ }
+
+ @Test
+ public void testJsonDeserializeWithAes128Kdf() throws Exception {
+ String json = "{"
+ + "\"address\":\"TAddr\","
+ + "\"version\":3,"
+ + "\"crypto\":{"
+ + " \"cipher\":\"aes-128-ctr\","
+ + " \"ciphertext\":\"ct\","
+ + " \"cipherparams\":{\"iv\":\"iv\"},"
+ + " \"kdf\":\"pbkdf2\","
+ + " \"kdfparams\":{\"dklen\":32,\"c\":262144,\"prf\":\"hmac-sha256\",\"salt\":\"salt\"},"
+ + " \"mac\":\"mac\""
+ + "}}";
+
+ WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class);
+ assertNotNull(wf.getCrypto().getKdfparams());
+ assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.Aes128CtrKdfParams);
+ }
+
+ @Test
+ public void testJsonDeserializeCryptoV1Field() throws Exception {
+ // Legacy files may use "Crypto" instead of "crypto"
+ String json = "{"
+ + "\"address\":\"TAddr\","
+ + "\"version\":3,"
+ + "\"Crypto\":{"
+ + " \"cipher\":\"aes-128-ctr\","
+ + " \"kdf\":\"scrypt\","
+ + " \"kdfparams\":{\"dklen\":32,\"n\":1,\"p\":1,\"r\":8,\"salt\":\"s\"}"
+ + "}}";
+
+ WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class);
+ assertNotNull(wf.getCrypto());
+ assertEquals("aes-128-ctr", wf.getCrypto().getCipher());
+ }
+}
diff --git a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java
deleted file mode 100644
index 2642129e00a..00000000000
--- a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.tron.keystroe;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.tron.common.crypto.SignInterface;
-import org.tron.keystore.Credentials;
-
-public class CredentialsTest {
-
- @Test
- public void test_equality() {
- 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);
- Assert.assertTrue(aCredential.equals(anotherCredential));
- Credentials aCredential3 = Credentials.create(si3);
- Assert.assertFalse(aCredential.equals(aCredential3));
- }
-}
diff --git a/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java
new file mode 100644
index 00000000000..860980d21e5
--- /dev/null
+++ b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java
@@ -0,0 +1,147 @@
+package org.tron.program;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.PrintStream;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.tron.common.TestConstants;
+import org.tron.core.config.args.Args;
+
+/**
+ * Verifies the deprecated --keystore-factory CLI.
+ */
+public class KeystoreFactoryDeprecationTest {
+
+ private PrintStream originalOut;
+ private PrintStream originalErr;
+ private InputStream originalIn;
+
+ @Before
+ public void setup() {
+ originalOut = System.out;
+ originalErr = System.err;
+ originalIn = System.in;
+ Args.setParam(new String[] {}, TestConstants.TEST_CONF);
+ }
+
+ @After
+ public void teardown() throws Exception {
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ System.setIn(originalIn);
+ Args.clearParam();
+ // Clean up Wallet dir
+ File wallet = new File("Wallet");
+ if (wallet.exists()) {
+ if (wallet.isDirectory() && wallet.listFiles() != null) {
+ for (File f : wallet.listFiles()) {
+ f.delete();
+ }
+ }
+ wallet.delete();
+ }
+ }
+
+ @Test(timeout = 10000)
+ public void testDeprecationWarningPrinted() throws Exception {
+ ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ System.setErr(new PrintStream(errContent));
+ System.setIn(new ByteArrayInputStream("exit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String errOutput = errContent.toString("UTF-8");
+ assertTrue("Should contain deprecation warning",
+ errOutput.contains("--keystore-factory is deprecated"));
+ assertTrue("Should point to Toolkit.jar",
+ errOutput.contains("Toolkit.jar keystore"));
+ }
+
+ @Test(timeout = 10000)
+ public void testHelpCommand() throws Exception {
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("help\nexit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("Should show legacy commands", out.contains("GenKeystore"));
+ assertTrue("Should show ImportPrivateKey", out.contains("ImportPrivateKey"));
+ }
+
+ @Test(timeout = 10000)
+ public void testInvalidCommand() throws Exception {
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("badcommand\nexit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("Should report invalid cmd",
+ out.contains("Invalid cmd: badcommand"));
+ }
+
+ @Test(timeout = 10000)
+ public void testEmptyLineSkipped() throws Exception {
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("\n\nexit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("Should exit cleanly", out.contains("Exit"));
+ }
+
+ @Test(timeout = 10000)
+ public void testQuitCommand() throws Exception {
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("quit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("Quit should terminate", out.contains("Exit"));
+ }
+
+ @Test(timeout = 10000)
+ public void testGenKeystoreTriggersError() throws Exception {
+ // genkeystore reads password via a nested Scanner, which conflicts
+ // with the outer Scanner and throws "No line found". The error is
+ // caught and logged, and the REPL continues.
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("genKeystore should prompt for password",
+ out.contains("Please input password"));
+ assertTrue("REPL should continue to exit", out.contains("Exit"));
+ }
+
+ @Test(timeout = 10000)
+ public void testImportPrivateKeyTriggersPrompt() throws Exception {
+ // importprivatekey reads via nested Scanner — same limitation as above,
+ // but we at least hit the dispatch logic.
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setIn(new ByteArrayInputStream("importprivatekey\nexit\n".getBytes()));
+
+ KeystoreFactory.start();
+
+ String out = outContent.toString("UTF-8");
+ assertTrue("importprivatekey should prompt for key",
+ out.contains("Please input private key"));
+ }
+}
diff --git a/framework/src/test/java/org/tron/program/SupplementTest.java b/framework/src/test/java/org/tron/program/SupplementTest.java
index 38a1b8426dd..d5557614c46 100644
--- a/framework/src/test/java/org/tron/program/SupplementTest.java
+++ b/framework/src/test/java/org/tron/program/SupplementTest.java
@@ -54,8 +54,8 @@ public void testGet() throws Exception {
String p = dbPath + File.separator;
dbBackupConfig.initArgs(true, p + "propPath", p + "bak1path/", p + "bak2path/", 1);
- WalletUtils.generateFullNewWalletFile("123456", new File(dbPath));
- WalletUtils.generateLightNewWalletFile("123456", new File(dbPath));
+ WalletUtils.generateFullNewWalletFile("123456", new File(dbPath), true);
+ WalletUtils.generateLightNewWalletFile("123456", new File(dbPath), true);
WalletUtils.getDefaultKeyDirectory();
WalletUtils.getTestnetKeyDirectory();
WalletUtils.getMainnetKeyDirectory();
diff --git a/plugins/README.md b/plugins/README.md
index db25811882f..dc16b3ecf35 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -143,3 +143,72 @@ NOTE: large db may GC overhead limit exceeded.
- ``: Source path for database. Default: output-directory/database
- `--db`: db name.
- `-h | --help`: provide the help info
+
+## Keystore
+
+Keystore provides commands for managing account keystore files (Web3 Secret Storage format).
+
+> **Migrating from `--keystore-factory`**: The legacy `FullNode.jar --keystore-factory` interactive mode is deprecated. Use the Toolkit keystore commands below instead. The mapping is:
+> - `GenKeystore` → `keystore new`
+> - `ImportPrivateKey` → `keystore import`
+> - (new) `keystore list` — list all keystores in a directory
+> - (new) `keystore update` — change the password of a keystore
+
+### Subcommands
+
+#### keystore new
+
+Generate a new keystore file with a random keypair.
+
+```shell script
+# full command
+ java -jar Toolkit.jar keystore new [-h] [--keystore-dir=] [--password-file=] [--sm2] [--json]
+# examples
+ java -jar Toolkit.jar keystore new # interactive prompt
+ java -jar Toolkit.jar keystore new --keystore-dir /data/keystores # custom directory
+ java -jar Toolkit.jar keystore new --password-file pass.txt --json # non-interactive with JSON output
+```
+
+#### keystore import
+
+Import a private key into a new keystore file.
+
+```shell script
+# full command
+ java -jar Toolkit.jar keystore import [-h] [--keystore-dir=] [--password-file=] [--private-key-file=] [--sm2] [--json]
+# examples
+ java -jar Toolkit.jar keystore import # interactive prompt
+ java -jar Toolkit.jar keystore import --private-key-file key.txt --json # from file with JSON output
+```
+
+#### keystore list
+
+List all keystore files in a directory.
+
+```shell script
+# full command
+ java -jar Toolkit.jar keystore list [-h] [--keystore-dir=] [--json]
+# examples
+ java -jar Toolkit.jar keystore list # list default ./Wallet directory
+ java -jar Toolkit.jar keystore list --keystore-dir /data/keystores # custom directory
+```
+
+#### keystore update
+
+Change the password of a keystore file.
+
+```shell script
+# full command
+ java -jar Toolkit.jar keystore update [-h] [--keystore-dir=] [--password-file=] [--new-password-file=] [--json]
+# examples
+ java -jar Toolkit.jar keystore update TXyz...abc # interactive prompt
+ java -jar Toolkit.jar keystore update TXyz...abc --keystore-dir /data/ks # custom directory
+```
+
+### Common Options
+
+- `--keystore-dir`: Keystore directory, default: `./Wallet`.
+- `--password-file`: Read password from a file instead of interactive prompt.
+- `--sm2`: Use SM2 algorithm instead of ECDSA (for `new` and `import`).
+- `--json`: Output in JSON format for scripting.
+- `-h | --help`: Provide the help info.
diff --git a/plugins/build.gradle b/plugins/build.gradle
index 85dcdd2342d..2e358a884a3 100644
--- a/plugins/build.gradle
+++ b/plugins/build.gradle
@@ -34,6 +34,12 @@ dependencies {
implementation fileTree(dir: 'libs', include: '*.jar')
testImplementation project(":framework")
testImplementation project(":framework").sourceSets.test.output
+ implementation(project(":crypto")) {
+ exclude group: 'io.github.tronprotocol', module: 'libp2p'
+ exclude group: 'io.prometheus'
+ exclude group: 'org.aspectj'
+ exclude group: 'org.apache.httpcomponents'
+ }
implementation group: 'info.picocli', name: 'picocli', version: '4.6.3'
implementation group: 'com.typesafe', name: 'config', version: '1.3.2'
implementation group: 'me.tongfei', name: 'progressbar', version: '0.9.3'
diff --git a/plugins/src/main/java/common/org/tron/plugins/Keystore.java b/plugins/src/main/java/common/org/tron/plugins/Keystore.java
new file mode 100644
index 00000000000..6929bb406ea
--- /dev/null
+++ b/plugins/src/main/java/common/org/tron/plugins/Keystore.java
@@ -0,0 +1,19 @@
+package org.tron.plugins;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "keystore",
+ mixinStandardHelpOptions = true,
+ version = "keystore command 1.0",
+ description = "Manage keystore files for account keys.",
+ subcommands = {CommandLine.HelpCommand.class,
+ KeystoreNew.class,
+ KeystoreImport.class,
+ KeystoreList.class,
+ KeystoreUpdate.class
+ },
+ commandListHeading = "%nCommands:%n%nThe most commonly used keystore commands are:%n"
+)
+public class Keystore {
+}
diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java
new file mode 100644
index 00000000000..961f28eb552
--- /dev/null
+++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java
@@ -0,0 +1,240 @@
+package org.tron.plugins;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.Console;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.tron.common.crypto.SignInterface;
+import org.tron.core.exception.CipherException;
+import org.tron.keystore.Wallet;
+import org.tron.keystore.WalletFile;
+import org.tron.keystore.WalletUtils;
+
+/**
+ * Shared utilities for keystore CLI commands.
+ */
+final class KeystoreCliUtils {
+
+ private static final Set OWNER_ONLY = EnumSet.of(
+ PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
+
+ private static final long MAX_FILE_SIZE = 1024;
+
+ private KeystoreCliUtils() {
+ }
+
+ /**
+ * Generate a keystore file using temp-file + atomic-rename to avoid
+ * a TOCTOU window where the file is world-readable before permissions are set.
+ *
+ * @return the final keystore file name (not the full path)
+ */
+ static String generateKeystoreFile(String password, SignInterface keyPair,
+ File destDir, boolean useFullScrypt, PrintWriter err)
+ throws CipherException, IOException {
+
+ WalletFile walletFile;
+ if (useFullScrypt) {
+ walletFile = Wallet.createStandard(password, keyPair);
+ } else {
+ walletFile = Wallet.createLight(password, keyPair);
+ }
+
+ String fileName = WalletUtils.getWalletFileName(walletFile);
+ File destination = new File(destDir, fileName);
+
+ File tempFile = File.createTempFile("keystore-", ".tmp", destDir);
+ try {
+ setOwnerOnly(tempFile, err);
+ MAPPER.writeValue(tempFile, walletFile);
+ atomicMove(tempFile, destination);
+ } catch (Exception e) {
+ if (!tempFile.delete()) {
+ err.println("Warning: could not delete temp file: " + tempFile.getName());
+ }
+ throw e;
+ }
+
+ return fileName;
+ }
+
+ /**
+ * Atomic move with fallback for filesystems that don't support it.
+ */
+ static void atomicMove(File source, File target) throws IOException {
+ try {
+ Files.move(source.toPath(), target.toPath(),
+ StandardCopyOption.REPLACE_EXISTING,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (java.nio.file.AtomicMoveNotSupportedException e) {
+ Files.move(source.toPath(), target.toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ static String readPassword(File passwordFile, PrintWriter err) throws IOException {
+ if (passwordFile != null) {
+ if (!passwordFile.exists()) {
+ err.println("Password file not found: " + passwordFile.getPath()
+ + ". Omit --password-file for interactive input.");
+ return null;
+ }
+ if (passwordFile.length() > MAX_FILE_SIZE) {
+ err.println("Password file too large (max 1KB).");
+ return null;
+ }
+ byte[] bytes = Files.readAllBytes(passwordFile.toPath());
+ try {
+ String password = stripLineEndings(
+ new String(bytes, StandardCharsets.UTF_8));
+ if (!WalletUtils.passwordValid(password)) {
+ err.println("Invalid password: must be at least 6 characters.");
+ return null;
+ }
+ return password;
+ } finally {
+ Arrays.fill(bytes, (byte) 0);
+ }
+ }
+
+ Console console = System.console();
+ if (console == null) {
+ err.println("No interactive terminal available. "
+ + "Use --password-file to provide password.");
+ return null;
+ }
+
+ char[] pwd1 = console.readPassword("Enter password: ");
+ if (pwd1 == null) {
+ err.println("Password input cancelled.");
+ return null;
+ }
+ char[] pwd2 = console.readPassword("Confirm password: ");
+ if (pwd2 == null) {
+ Arrays.fill(pwd1, '\0');
+ err.println("Password input cancelled.");
+ return null;
+ }
+ try {
+ if (!Arrays.equals(pwd1, pwd2)) {
+ err.println("Passwords do not match.");
+ return null;
+ }
+ String password = new String(pwd1);
+ if (!WalletUtils.passwordValid(password)) {
+ err.println("Invalid password: must be at least 6 characters.");
+ return null;
+ }
+ return password;
+ } finally {
+ Arrays.fill(pwd1, '\0');
+ Arrays.fill(pwd2, '\0');
+ }
+ }
+
+ static void ensureDirectory(File dir) throws IOException {
+ Path path = dir.toPath();
+ if (Files.exists(path) && !Files.isDirectory(path)) {
+ throw new IOException(
+ "Path exists but is not a directory: " + dir.getAbsolutePath());
+ }
+ Files.createDirectories(path);
+ }
+
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .configure(
+ com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ static ObjectMapper mapper() {
+ return MAPPER;
+ }
+
+ static void printJson(PrintWriter out, PrintWriter err, Map fields) {
+ try {
+ out.println(MAPPER.writeValueAsString(fields));
+ } catch (Exception e) {
+ err.println("Error writing JSON output");
+ }
+ }
+
+ static Map jsonMap(String... keyValues) {
+ Map map = new LinkedHashMap<>();
+ for (int i = 0; i < keyValues.length - 1; i += 2) {
+ map.put(keyValues[i], keyValues[i + 1]);
+ }
+ return map;
+ }
+
+ static String stripLineEndings(String s) {
+ // Strip UTF-8 BOM if present (Windows Notepad adds this)
+ if (s.length() > 0 && s.charAt(0) == '\uFEFF') {
+ s = s.substring(1);
+ }
+ int end = s.length();
+ while (end > 0) {
+ char c = s.charAt(end - 1);
+ if (c == '\n' || c == '\r') {
+ end--;
+ } else {
+ break;
+ }
+ }
+ return s.substring(0, end);
+ }
+
+ static boolean checkFileExists(File file, String label, PrintWriter err) {
+ if (file != null && !file.exists()) {
+ err.println(label + " not found: " + file.getPath());
+ return false;
+ }
+ return true;
+ }
+
+ static void printSecurityTips(PrintWriter out, String address, String fileName) {
+ out.println();
+ out.println("Public address of the key: " + address);
+ out.println("Path of the secret key file: " + fileName);
+ out.println();
+ out.println(
+ "- You can share your public address with anyone."
+ + " Others need it to interact with you.");
+ out.println(
+ "- You must NEVER share the secret key with anyone!"
+ + " The key controls access to your funds!");
+ out.println(
+ "- You must BACKUP your key file!"
+ + " Without the key, it's impossible to access account funds!");
+ out.println(
+ "- You must REMEMBER your password!"
+ + " Without the password, it's impossible to decrypt the key!");
+ }
+
+ /**
+ * Check if a WalletFile represents a valid V3 keystore.
+ */
+ static boolean isValidKeystoreFile(WalletFile wf) {
+ return wf.getAddress() != null
+ && wf.getCrypto() != null
+ && wf.getVersion() == 3;
+ }
+
+ static void setOwnerOnly(File file, PrintWriter err) {
+ try {
+ Files.setPosixFilePermissions(file.toPath(), OWNER_ONLY);
+ } catch (UnsupportedOperationException | IOException e) {
+ err.println("Warning: could not set file permissions on " + file.getName()
+ + ". Please manually restrict access to this file.");
+ }
+ }
+}
diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java
new file mode 100644
index 00000000000..7b3589c11f6
--- /dev/null
+++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java
@@ -0,0 +1,185 @@
+package org.tron.plugins;
+
+import java.io.Console;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import org.apache.commons.lang3.StringUtils;
+import org.tron.common.crypto.SignInterface;
+import org.tron.common.crypto.SignUtils;
+import org.tron.common.utils.ByteArray;
+import org.tron.core.exception.CipherException;
+import org.tron.keystore.Credentials;
+import org.tron.keystore.WalletFile;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Spec;
+
+@Command(name = "import",
+ mixinStandardHelpOptions = true,
+ description = "Import a private key into a new keystore file.")
+public class KeystoreImport implements Callable {
+
+ @Spec
+ private CommandSpec spec;
+
+ @Option(names = {"--keystore-dir"},
+ description = "Keystore directory (default: ./Wallet)",
+ defaultValue = "Wallet")
+ private File keystoreDir;
+
+ @Option(names = {"--json"},
+ description = "Output in JSON format")
+ private boolean json;
+
+ @Option(names = {"--key-file"},
+ description = "Read private key from file instead of interactive prompt")
+ private File keyFile;
+
+ @Option(names = {"--password-file"},
+ description = "Read password from file instead of interactive prompt")
+ private File passwordFile;
+
+ @Option(names = {"--sm2"},
+ description = "Use SM2 algorithm instead of ECDSA")
+ private boolean sm2;
+
+ @Option(names = {"--force"},
+ description = "Allow import even if address already exists")
+ private boolean force;
+
+ @Override
+ public Integer call() {
+ PrintWriter out = spec.commandLine().getOut();
+ PrintWriter err = spec.commandLine().getErr();
+ try {
+ if (!KeystoreCliUtils.checkFileExists(keyFile, "Key file", err)) {
+ return 1;
+ }
+ KeystoreCliUtils.ensureDirectory(keystoreDir);
+
+ String privateKey = readPrivateKey(err);
+ if (privateKey == null) {
+ return 1;
+ }
+
+ if (privateKey.startsWith("0x") || privateKey.startsWith("0X")) {
+ privateKey = privateKey.substring(2);
+ }
+ if (!isValidPrivateKey(privateKey)) {
+ err.println("Invalid private key: must be 64 hex characters.");
+ return 1;
+ }
+
+ String password = KeystoreCliUtils.readPassword(passwordFile, err);
+ if (password == null) {
+ return 1;
+ }
+
+ boolean ecKey = !sm2;
+ SignInterface keyPair;
+ try {
+ keyPair = SignUtils.fromPrivate(
+ ByteArray.fromHexString(privateKey), ecKey);
+ } catch (Exception e) {
+ err.println("Invalid private key: not a valid key"
+ + " for the selected algorithm.");
+ return 1;
+ }
+ String address = Credentials.create(keyPair).getAddress();
+ String existingFile = findExistingKeystore(keystoreDir, address, err);
+ if (existingFile != null && !force) {
+ err.println("Keystore for address " + address
+ + " already exists: " + existingFile
+ + ". Use --force to import anyway.");
+ return 1;
+ }
+ String fileName = KeystoreCliUtils.generateKeystoreFile(
+ password, keyPair, keystoreDir, true, err);
+ if (json) {
+ KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap(
+ "address", address, "file", fileName));
+ } else {
+ out.println("Imported keystore successfully");
+ KeystoreCliUtils.printSecurityTips(out, address,
+ new File(keystoreDir, fileName).getPath());
+ }
+ return 0;
+ } catch (CipherException e) {
+ err.println("Encryption error: " + e.getMessage());
+ return 1;
+ } catch (Exception e) {
+ err.println("Error: " + e.getMessage());
+ return 1;
+ }
+ }
+
+ private String readPrivateKey(PrintWriter err) throws IOException {
+ if (keyFile != null) {
+ if (keyFile.length() > 1024) {
+ err.println("Key file too large (max 1KB).");
+ return null;
+ }
+ byte[] bytes = Files.readAllBytes(keyFile.toPath());
+ try {
+ return new String(bytes, StandardCharsets.UTF_8).trim();
+ } finally {
+ Arrays.fill(bytes, (byte) 0);
+ }
+ }
+
+ Console console = System.console();
+ if (console == null) {
+ err.println("No interactive terminal available. "
+ + "Use --key-file to provide private key.");
+ return null;
+ }
+
+ char[] key = console.readPassword("Enter private key (hex): ");
+ if (key == null) {
+ err.println("Input cancelled.");
+ return null;
+ }
+ try {
+ return new String(key);
+ } finally {
+ Arrays.fill(key, '\0');
+ }
+ }
+
+ private static final java.util.regex.Pattern HEX_PATTERN =
+ java.util.regex.Pattern.compile("[0-9a-fA-F]{64}");
+
+ private boolean isValidPrivateKey(String key) {
+ return !StringUtils.isEmpty(key) && HEX_PATTERN.matcher(key).matches();
+ }
+
+ private String findExistingKeystore(File dir, String address, PrintWriter err) {
+ if (!dir.exists() || !dir.isDirectory()) {
+ return null;
+ }
+ File[] files = dir.listFiles((d, name) -> name.endsWith(".json"));
+ if (files == null) {
+ return null;
+ }
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ KeystoreCliUtils.mapper();
+ for (File file : files) {
+ try {
+ WalletFile wf = mapper.readValue(file, WalletFile.class);
+ if (KeystoreCliUtils.isValidKeystoreFile(wf)
+ && address.equals(wf.getAddress())) {
+ return file.getName();
+ }
+ } catch (Exception e) {
+ err.println("Warning: skipping unreadable file: " + file.getName());
+ }
+ }
+ return null;
+ }
+}
diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java
new file mode 100644
index 00000000000..52a82ba6527
--- /dev/null
+++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java
@@ -0,0 +1,106 @@
+package org.tron.plugins;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import org.tron.keystore.WalletFile;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Spec;
+
+@Command(name = "list",
+ mixinStandardHelpOptions = true,
+ description = "List all keystore files in a directory.")
+public class KeystoreList implements Callable {
+
+ private static final ObjectMapper MAPPER = KeystoreCliUtils.mapper();
+
+ @Spec
+ private CommandSpec spec;
+
+ @Option(names = {"--keystore-dir"},
+ description = "Keystore directory (default: ./Wallet)",
+ defaultValue = "Wallet")
+ private File keystoreDir;
+
+ @Option(names = {"--json"},
+ description = "Output in JSON format")
+ private boolean json;
+
+ @Override
+ public Integer call() {
+ PrintWriter out = spec.commandLine().getOut();
+ PrintWriter err = spec.commandLine().getErr();
+
+ if (!keystoreDir.exists() || !keystoreDir.isDirectory()) {
+ if (json) {
+ return printEmptyJson(out, err);
+ } else {
+ out.println("No keystores found in: " + keystoreDir.getAbsolutePath());
+ }
+ return 0;
+ }
+
+ File[] files = keystoreDir.listFiles((dir, name) -> name.endsWith(".json"));
+ if (files == null || files.length == 0) {
+ if (json) {
+ return printEmptyJson(out, err);
+ } else {
+ out.println("No keystores found in: " + keystoreDir.getAbsolutePath());
+ }
+ return 0;
+ }
+
+ List