Skip to content

Commit fd9e353

Browse files
BarbatosBarbatos
authored andcommitted
feat(plugins): add keystore list and update commands, deprecate --keystore-factory
Add remaining keystore subcommands to Toolkit.jar: - `keystore list`: display all keystore files and addresses in a directory - `keystore update <address>`: re-encrypt a keystore with a new password Both support --keystore-dir, --json, and --password-file options. Add deprecation warning to --keystore-factory in FullNode.jar, directing users to the new Toolkit.jar keystore commands. The old REPL continues to function normally during the transition period.
1 parent 919bef6 commit fd9e353

17 files changed

Lines changed: 880 additions & 149 deletions

File tree

crypto/src/test/java/org/tron/keystore/CrossImplTest.java

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,113 @@
33
import static org.junit.Assert.assertArrayEquals;
44
import static org.junit.Assert.assertEquals;
55
import static org.junit.Assert.assertNotNull;
6+
import static org.junit.Assert.assertTrue;
67

78
import com.fasterxml.jackson.databind.DeserializationFeature;
89
import com.fasterxml.jackson.databind.ObjectMapper;
910
import java.io.File;
11+
import java.io.InputStream;
12+
import org.junit.Rule;
1013
import org.junit.Test;
14+
import org.junit.rules.TemporaryFolder;
1115
import org.tron.common.crypto.SignInterface;
1216
import org.tron.common.crypto.SignUtils;
17+
import org.tron.common.utils.ByteArray;
1318
import org.tron.common.utils.Utils;
1419

1520
/**
1621
* Cross-implementation compatibility tests.
17-
* Verifies that keystore files can survive a roundtrip through the
18-
* Java implementation (encrypt → serialize → deserialize → decrypt).
1922
*
20-
* Also verifies that keystore files generated by legacy --keystore-factory
21-
* code are compatible with the new library.
23+
* <p>Static fixtures (web3j-pbkdf2.json, web3j-scrypt.json) are standard
24+
* Ethereum test vectors from the Web3 Secret Storage specification.
25+
* Password and private key are publicly documented — see fixtures/README.md.
26+
*
27+
* <p>Format compatibility with geth/other clients is verified dynamically:
28+
* generate keystore at test time, serialize to file, deserialize back,
29+
* verify private key and address survive the roundtrip. No static secrets
30+
* stored in the repository.
2231
*/
2332
public class CrossImplTest {
2433

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

37+
@Rule
38+
public TemporaryFolder tempFolder = new TemporaryFolder();
39+
40+
// --- Standard Ethereum test vectors (public, from Web3 Secret Storage spec) ---
41+
42+
private static final String WEB3J_PASSWORD = "Insecure Pa55w0rd";
43+
private static final String WEB3J_SECRET =
44+
"a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6";
45+
46+
@Test
47+
public void testDecryptWeb3jPbkdf2Fixture() throws Exception {
48+
WalletFile walletFile = loadFixture("keystore-fixtures/web3j-pbkdf2.json");
49+
SignInterface recovered = Wallet.decrypt(WEB3J_PASSWORD, walletFile, true);
50+
assertEquals("Private key must match web3j test vector",
51+
WEB3J_SECRET, ByteArray.toHexString(recovered.getPrivateKey()));
52+
}
53+
2854
@Test
29-
public void testLightKeystoreRoundtrip() throws Exception {
30-
String password = "testpassword123";
55+
public void testDecryptWeb3jScryptFixture() throws Exception {
56+
WalletFile walletFile = loadFixture("keystore-fixtures/web3j-scrypt.json");
57+
SignInterface recovered = Wallet.decrypt(WEB3J_PASSWORD, walletFile, true);
58+
assertEquals("Private key must match web3j test vector",
59+
WEB3J_SECRET, ByteArray.toHexString(recovered.getPrivateKey()));
60+
}
61+
62+
// --- Dynamic format compatibility (no static secrets) ---
63+
64+
@Test
65+
public void testKeystoreFormatCompatibility() throws Exception {
66+
// Dynamically generate, serialize, deserialize — verifies the JSON format
67+
// is correct and can survive a roundtrip (same format geth/web3j produce)
3168
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
3269
byte[] originalKey = keyPair.getPrivateKey();
70+
String password = "dynamicTest123";
3371

34-
// Create keystore → write to temp file → read back → decrypt
35-
WalletFile walletFile = Wallet.createLight(password, keyPair);
36-
File tempFile = File.createTempFile("keystore-test-", ".json");
37-
tempFile.deleteOnExit();
38-
MAPPER.writeValue(tempFile, walletFile);
72+
WalletFile walletFile = Wallet.createStandard(password, keyPair);
3973

74+
// Verify the generated file has correct Web3 Secret Storage structure
75+
assertEquals("version must be 3", 3, walletFile.getVersion());
76+
assertNotNull("must have address", walletFile.getAddress());
77+
assertNotNull("must have crypto", walletFile.getCrypto());
78+
assertEquals("cipher must be aes-128-ctr",
79+
"aes-128-ctr", walletFile.getCrypto().getCipher());
80+
assertTrue("kdf must be scrypt or pbkdf2",
81+
"scrypt".equals(walletFile.getCrypto().getKdf())
82+
|| "pbkdf2".equals(walletFile.getCrypto().getKdf()));
83+
84+
// Write to file, read back — simulates cross-process interop
85+
File tempFile = new File(tempFolder.getRoot(), "compat-test.json");
86+
MAPPER.writeValue(tempFile, walletFile);
4087
WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class);
41-
SignInterface recovered = Wallet.decrypt(password, loaded, true);
4288

43-
assertArrayEquals("File roundtrip must preserve private key",
89+
// Decrypt the deserialized file
90+
SignInterface recovered = Wallet.decrypt(password, loaded, true);
91+
assertArrayEquals("Key must survive file roundtrip",
4492
originalKey, recovered.getPrivateKey());
93+
94+
// Verify TRON address format
95+
byte[] tronAddr = recovered.getAddress();
96+
assertEquals("TRON address must be 21 bytes", 21, tronAddr.length);
97+
assertEquals("First byte must be TRON prefix", 0x41, tronAddr[0] & 0xFF);
4598
}
4699

47100
@Test
48-
public void testStandardKeystoreRoundtrip() throws Exception {
49-
String password = "testpassword456";
101+
public void testLightScryptFormatCompatibility() throws Exception {
50102
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true);
51103
byte[] originalKey = keyPair.getPrivateKey();
104+
String password = "lightCompat456";
52105

53-
WalletFile walletFile = Wallet.createStandard(password, keyPair);
54-
File tempFile = File.createTempFile("keystore-std-", ".json");
55-
tempFile.deleteOnExit();
106+
WalletFile walletFile = Wallet.createLight(password, keyPair);
107+
File tempFile = new File(tempFolder.getRoot(), "light-compat.json");
56108
MAPPER.writeValue(tempFile, walletFile);
57-
58109
WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class);
59-
SignInterface recovered = Wallet.decrypt(password, loaded, true);
60110

61-
assertArrayEquals("Standard scrypt file roundtrip must preserve private key",
111+
SignInterface recovered = Wallet.decrypt(password, loaded, true);
112+
assertArrayEquals("Key must survive light scrypt file roundtrip",
62113
originalKey, recovered.getPrivateKey());
63114
}
64115

@@ -85,30 +136,22 @@ public void testLoadCredentialsIntegration() throws Exception {
85136
byte[] originalKey = keyPair.getPrivateKey();
86137
String originalAddress = Credentials.create(keyPair).getAddress();
87138

88-
// Use WalletUtils full flow
89-
File tempDir = new File(System.getProperty("java.io.tmpdir"), "keystore-test-" +
90-
System.currentTimeMillis());
91-
tempDir.mkdirs();
92-
try {
93-
String fileName = WalletUtils.generateWalletFile(password, keyPair, tempDir, false);
94-
assertNotNull(fileName);
95-
96-
File keystoreFile = new File(tempDir, fileName);
97-
Credentials loaded = WalletUtils.loadCredentials(password, keystoreFile, true);
98-
99-
assertEquals("Address must survive full WalletUtils roundtrip",
100-
originalAddress, loaded.getAddress());
101-
assertArrayEquals("Key must survive full WalletUtils roundtrip",
102-
originalKey, loaded.getSignInterface().getPrivateKey());
103-
} finally {
104-
// Cleanup
105-
File[] files = tempDir.listFiles();
106-
if (files != null) {
107-
for (File f : files) {
108-
f.delete();
109-
}
110-
}
111-
tempDir.delete();
112-
}
139+
File tempDir = tempFolder.newFolder("wallet-integration");
140+
String fileName = WalletUtils.generateWalletFile(password, keyPair, tempDir, false);
141+
assertNotNull(fileName);
142+
143+
File keystoreFile = new File(tempDir, fileName);
144+
Credentials loaded = WalletUtils.loadCredentials(password, keystoreFile, true);
145+
146+
assertEquals("Address must survive full WalletUtils roundtrip",
147+
originalAddress, loaded.getAddress());
148+
assertArrayEquals("Key must survive full WalletUtils roundtrip",
149+
originalKey, loaded.getSignInterface().getPrivateKey());
150+
}
151+
152+
private WalletFile loadFixture(String resourcePath) throws Exception {
153+
InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath);
154+
assertNotNull("Fixture not found: " + resourcePath, is);
155+
return MAPPER.readValue(is, WalletFile.class);
113156
}
114157
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Keystore Test Fixtures
2+
3+
These files are **standard Ethereum test vectors** from the
4+
[Web3 Secret Storage Definition](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition).
5+
6+
They are used to verify cross-implementation compatibility. The password
7+
and private key are publicly documented in the specification and do NOT
8+
control any real assets on any blockchain.
9+
10+
| File | KDF | Source |
11+
|------|-----|--------|
12+
| web3j-pbkdf2.json | PBKDF2 | web3j project / Ethereum wiki |
13+
| web3j-scrypt.json | Scrypt | web3j project / Ethereum wiki |
14+
15+
Password: `Insecure Pa55w0rd` (public test vector)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"crypto" : {
3+
"cipher" : "aes-128-ctr",
4+
"cipherparams" : {
5+
"iv" : "02ebc768684e5576900376114625ee6f"
6+
},
7+
"ciphertext" : "7ad5c9dd2c95f34a92ebb86740b92103a5d1cc4c2eabf3b9a59e1f83f3181216",
8+
"kdf" : "pbkdf2",
9+
"kdfparams" : {
10+
"c" : 262144,
11+
"dklen" : 32,
12+
"prf" : "hmac-sha256",
13+
"salt" : "0e4cf3893b25bb81efaae565728b5b7cde6a84e224cbf9aed3d69a31c981b702"
14+
},
15+
"mac" : "2b29e4641ec17f4dc8b86fc8592090b50109b372529c30b001d4d96249edaf62"
16+
},
17+
"id" : "af0451b4-6020-4ef0-91ec-794a5a965b01",
18+
"version" : 3
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"crypto" : {
3+
"cipher" : "aes-128-ctr",
4+
"cipherparams" : {
5+
"iv" : "3021e1ef4774dfc5b08307f3a4c8df00"
6+
},
7+
"ciphertext" : "4dd29ba18478b98cf07a8a44167acdf7e04de59777c4b9c139e3d3fa5cb0b931",
8+
"kdf" : "scrypt",
9+
"kdfparams" : {
10+
"dklen" : 32,
11+
"n" : 262144,
12+
"r" : 8,
13+
"p" : 1,
14+
"salt" : "4f9f68c71989eb3887cd947c80b9555fce528f210199d35c35279beb8c2da5ca"
15+
},
16+
"mac" : "7e8f2192767af9be18e7a373c1986d9190fcaa43ad689bbb01a62dbde159338d"
17+
},
18+
"id" : "7654525c-17e0-4df5-94b5-c7fde752c9d2",
19+
"version" : 3
20+
}

framework/src/main/java/org/tron/program/KeystoreFactory.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@
1515
import org.tron.keystore.WalletUtils;
1616

1717
@Slf4j(topic = "app")
18+
@Deprecated
1819
public class KeystoreFactory {
1920

2021
private static final String FilePath = "Wallet";
2122

2223
public static void start() {
24+
System.err.println("WARNING: --keystore-factory is deprecated and will be removed "
25+
+ "in a future release.");
26+
System.err.println("Please use: java -jar Toolkit.jar keystore <command>");
27+
System.err.println(" keystore new - Generate a new keystore");
28+
System.err.println(" keystore import - Import a private key");
29+
System.err.println(" keystore list - List keystores");
30+
System.err.println(" keystore update - Change password");
31+
System.err.println();
2332
KeystoreFactory cli = new KeystoreFactory();
2433
cli.run();
2534
}
@@ -99,11 +108,13 @@ private void importPrivateKey() throws CipherException, IOException {
99108
}
100109

101110
private void help() {
102-
System.out.println("You can enter the following command: ");
103-
System.out.println("GenKeystore");
104-
System.out.println("ImportPrivateKey");
105-
System.out.println("Exit or Quit");
106-
System.out.println("Input any one of them, you will get more tips.");
111+
System.out.println("NOTE: --keystore-factory is deprecated. Use Toolkit.jar instead:");
112+
System.out.println(" java -jar Toolkit.jar keystore new|import|list|update");
113+
System.out.println();
114+
System.out.println("Legacy commands (will be removed):");
115+
System.out.println(" GenKeystore");
116+
System.out.println(" ImportPrivateKey");
117+
System.out.println(" Exit or Quit");
107118
}
108119

109120
private void run() {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.tron.program;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.InputStream;
7+
import java.io.PrintStream;
8+
import org.junit.Test;
9+
10+
/**
11+
* Verifies that --keystore-factory prints deprecation warning to stderr.
12+
*/
13+
public class KeystoreFactoryDeprecationTest {
14+
15+
@Test
16+
public void testDeprecationWarningPrinted() throws Exception {
17+
PrintStream originalErr = System.err;
18+
InputStream originalIn = System.in;
19+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
20+
System.setErr(new PrintStream(errContent));
21+
System.setIn(new java.io.ByteArrayInputStream("exit\n".getBytes()));
22+
try {
23+
KeystoreFactory.start();
24+
} finally {
25+
System.setErr(originalErr);
26+
System.setIn(originalIn);
27+
}
28+
29+
String errOutput = errContent.toString("UTF-8");
30+
assertTrue("Should contain deprecation warning",
31+
errOutput.contains("--keystore-factory is deprecated"));
32+
assertTrue("Should point to Toolkit.jar",
33+
errOutput.contains("Toolkit.jar keystore"));
34+
}
35+
}

plugins/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ dependencies {
3434
implementation fileTree(dir: 'libs', include: '*.jar')
3535
testImplementation project(":framework")
3636
testImplementation project(":framework").sourceSets.test.output
37-
implementation project(":crypto")
37+
implementation(project(":crypto")) {
38+
exclude group: 'io.github.tronprotocol', module: 'libp2p'
39+
exclude group: 'io.prometheus'
40+
exclude group: 'org.aspectj'
41+
exclude group: 'org.apache.httpcomponents'
42+
}
3843
implementation group: 'info.picocli', name: 'picocli', version: '4.6.3'
3944
implementation group: 'com.typesafe', name: 'config', version: '1.3.2'
4045
implementation group: 'me.tongfei', name: 'progressbar', version: '0.9.3'

plugins/src/main/java/common/org/tron/plugins/Keystore.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
description = "Manage keystore files for witness account keys.",
1010
subcommands = {CommandLine.HelpCommand.class,
1111
KeystoreNew.class,
12-
KeystoreImport.class
12+
KeystoreImport.class,
13+
KeystoreList.class,
14+
KeystoreUpdate.class
1315
},
1416
commandListHeading = "%nCommands:%n%nThe most commonly used keystore commands are:%n"
1517
)

0 commit comments

Comments
 (0)