Skip to content

feat(plugins): migrate keystore CLI from FullNode to Toolkit#12

Open
barbatos2011 wants to merge 6 commits intodevelopfrom
001-keystore-toolkit-migration
Open

feat(plugins): migrate keystore CLI from FullNode to Toolkit#12
barbatos2011 wants to merge 6 commits intodevelopfrom
001-keystore-toolkit-migration

Conversation

@barbatos2011
Copy link
Copy Markdown
Owner

@barbatos2011 barbatos2011 commented Apr 2, 2026

What

Migrate the --keystore-factory interactive REPL from FullNode.jar to Toolkit.jar as picocli subcommands, extract the keystore core library from framework to crypto module, and add new capabilities (list, update, SM2 support, JSON output, password-file support).

Why

The current --keystore-factory has several limitations:

  1. Coupled to FullNode.jar — operators must have the full node binary and configuration to manage keystore files, even though keystore operations have zero dependency on the node runtime
  2. Interactive REPL only — cannot be scripted, piped, or used in CI/CD pipelines; no support for --password-file or structured output
  3. Security gapsScanner-based password input echoes to terminal; no --key-file option forces private keys to be typed interactively or passed as command-line arguments (visible in ps, shell history)
  4. Module coupling — the keystore library (Wallet.java, WalletUtils.java) lives in framework and calls Args.getInstance(), preventing reuse by plugins (Toolkit.jar) without pulling in the entire framework dependency chain

Changes

Commit 1: refactor(crypto): extract keystore library from framework module

Extract the keystore package from framework to crypto module to break the framework dependency:

File Change
Wallet.java Moved to crypto/, removed Args import, added boolean ecKey parameter to decrypt(), changed MAC comparison to constant-time MessageDigest.isEqual()
WalletUtils.java Moved to crypto/, removed Args import, added boolean ecKey parameter to generateNewWalletFile(), generateFullNewWalletFile(), generateLightNewWalletFile(), loadCredentials()
Credentials.java, WalletFile.java Moved to crypto/ unchanged
WitnessInitializer.java Updated to pass Args.getInstance().isECKeyCryptoEngine() to loadCredentials()
KeystoreFactory.java Updated to pass CommonParameter.getInstance().isECKeyCryptoEngine() to keystore calls
plugins/build.gradle Added implementation project(":crypto") with exclusions (libp2p, prometheus, aspectj, httpcomponents)
CredentialsTest.java Merged two test files (fixed keystroe typo directory) into crypto module
WalletFileTest.java Moved to crypto module

Commit 2: feat(plugins): add keystore new and import commands to Toolkit

Add picocli subcommands for the two core keystore operations:

java -jar Toolkit.jar keystore new [--password-file <file>] [--keystore-dir <dir>] [--json] [--sm2]
java -jar Toolkit.jar keystore import --key-file <file> [--password-file <file>] [--keystore-dir <dir>] [--json] [--sm2]

Security design:

  • Interactive password input uses Console.readPassword() (no echo), with null check for EOF (Ctrl+D)
  • Password char[] compared directly via Arrays.equals() (avoids creating unnecessary String), zeroed in finally blocks
  • Password/key file byte[] zeroed after reading via Arrays.fill(bytes, (byte) 0)
  • Private key import reads from --key-file or interactive prompt, never from command-line arguments
  • Accepts 0x-prefixed keys (common Ethereum format)
  • Keystore files set to 600 permissions via Files.setPosixFilePermissions
  • JSON output via ObjectMapper.writeValueAsString(), not string concatenation
  • --sm2 flag for SM2 algorithm support

Shared utilities:

  • KeystoreCliUtilsreadPassword(), ensureDirectory() (uses Files.createDirectories), setOwnerOnly(), printJson()
  • ensureDirectory() uses Files.createDirectories() to avoid TOCTOU race conditions
  • Toolkit.java updated to register Keystore.class in subcommands

Test infrastructure:

  • Ethereum standard test vectors (inline JSON from Web3 Secret Storage spec, pbkdf2 + scrypt) with known password/private key — verifies exact private key recovery
  • Dynamic format compatibility tests — generate keystore, serialize to file, deserialize, decrypt, verify Web3 Secret Storage structure + TRON address format
  • Property tests: 100 random encrypt/decrypt roundtrips + 2 standard scrypt roundtrips (with timeout) + 50 wrong-password rejection tests
  • CLI tests: password-file, invalid password, empty password, special-char password, custom directory, no-TTY error, JSON output, key-file, invalid key (short, non-hex), SM2, whitespace-padded key (with roundtrip verification), duplicate address import, dir-is-file

Commit 3: feat(plugins): add keystore list and update commands, deprecate --keystore-factory

java -jar Toolkit.jar keystore list [--keystore-dir <dir>] [--json]
java -jar Toolkit.jar keystore update <address> [--password-file <file>] [--keystore-dir <dir>] [--json] [--sm2]
  • list: scans directory for keystore JSON files, displays address + filename, skips non-keystore files. JSON serialization failure returns exit code 1.
  • update: decrypts with old password, re-encrypts with new password. Atomic file write (temp file + Files.move(ATOMIC_MOVE)) prevents corruption on interrupt. Wrong old password fails without modifying the file. Supports Windows line endings in password file.
  • @Deprecated annotation on KeystoreFactory class + deprecation warning in start() + migration notice in help() method
  • Backward compatibility test: WitnessInitializerKeystoreTest verifies new tool's keystore can be loaded by WitnessInitializer.initFromKeystore()

Scope

  • Does NOT change any consensus or transaction processing logic
  • Does NOT modify protobuf definitions or public APIs
  • Does NOT break existing localwitnesskeystore config — WitnessInitializer continues to load keystore files at node startup
  • The --keystore-factory flag remains functional with a deprecation warning

Test

  • Full project test suite passes (2300+ tests, 0 failures)
  • 14 crypto module tests (roundtrip property tests, Ethereum standard vectors, format compatibility, credentials, wallet file)
  • 26 plugins module tests (all 4 subcommands: success paths, error paths, JSON output, no-TTY, SM2, special chars, whitespace, duplicate, CRLF)
  • 2 new framework tests (deprecation warning, WitnessInitializer backward compat). Existing WitnessInitializerTest and SupplementTest updated for new signatures.
  • Regression: WitnessInitializerTest, ArgsTest, SupplementTest all pass
  • Manual verification: built Toolkit.jar, tested all 4 commands end-to-end
  • Checkstyle passes (checkstyleMain + checkstyleTest)
  • Security scanning: Semgrep (OWASP + secrets, 0 findings), FindSecBugs (0 new findings in new code)

Cross-implementation compatibility

Source Format Result
Ethereum spec vector (inline) pbkdf2 c=262144 Decrypt OK, private key exact match
Ethereum spec vector (inline) scrypt n=262144 Decrypt OK, private key exact match
java-tron (dynamic) scrypt n=262144 Roundtrip OK, TRON address verified
java-tron (dynamic) scrypt n=4096 Roundtrip OK

Migration from --keystore-factory

Feature Old --keystore-factory New Toolkit.jar keystore
Create keystore GenKeystore keystore new
Import private key ImportPrivateKey keystore import
List keystores Not supported keystore list
Change password Not supported keystore update
Non-interactive mode Not supported --password-file, --key-file
JSON output Not supported --json
SM2 algorithm Not supported --sm2
Custom directory Not supported --keystore-dir
Password no-echo No (echoed to terminal) Yes (Console.readPassword)
Script/CI friendly No (interactive REPL) Yes (single command, exit codes)

The old --keystore-factory remains functional with a deprecation warning during the transition period.

Usage

java -jar Toolkit.jar keystore <command> [options]

Commands

Command Description
new Generate a new keystore file with a random keypair
import Import an existing private key into a keystore file
list List all keystore files and addresses in a directory
update Change the password of an existing keystore file

Common Options

Option Description
--keystore-dir <path> Keystore directory (default: ./Wallet)
--json Output in JSON format for scripting
--password-file <path> Read password from file (non-interactive)
--sm2 Use SM2 algorithm instead of ECDSA
--key-file <path> Read private key from file (import only)

Examples

# Create a new keystore
java -jar Toolkit.jar keystore new --password-file pw.txt

# Import a private key
java -jar Toolkit.jar keystore import --key-file key.txt --password-file pw.txt

# List all keystores
java -jar Toolkit.jar keystore list --keystore-dir ./Wallet

# List in JSON format
java -jar Toolkit.jar keystore list --json

# Change password (password file: old password on line 1, new on line 2)
java -jar Toolkit.jar keystore update <address> --password-file passwords.txt

# Use SM2 algorithm
java -jar Toolkit.jar keystore new --sm2 --password-file pw.txt

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 2 times, most recently from c46573a to 730aa0a Compare April 2, 2026 09:44
implementation fileTree(dir: 'libs', include: '*.jar')
testImplementation project(":framework")
testImplementation project(":framework").sourceSets.test.output
implementation project(":crypto")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended change:

implementation(project(":crypto")) {                                                                                                                                                                                                                
      exclude(group: 'io.github.tronprotocol', module: 'libp2p')                                                                                                                                                                                      
      exclude(group: 'io.prometheus')                                                                                                                                                                                                                 
      exclude(group: 'org.aspectj')                                                                                                                                                                                                                   
      exclude(group: 'org.apache.httpcomponents')                                                                                                                                                                                                     
  }  

Without exclusions, this pulls in all transitive dependencies from the crypto module, including:

  • libp2p (P2P networking)
  • prometheus (metrics)
  • aspectj (AOP)
  • httpcomponents (HTTP client)

None of these are used by the keystore commands (new, import, list, update), which only need:

  • Bouncy Castle crypto (SCrypt, AES)
  • Signature utilities (SignUtils, SignInterface)
  • Basic utilities (ByteArray, Utils)

Impact: This should reduce Toolkit.jar from 86 MB to 56 MB.

return 1;
}

boolean ecKey = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ecKey should be a configurable parameter, defaulting to true, and it can be set to false to support the SM2 algorithm.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes,The “migration” is not behaviorally equivalent for non-EC deployments: SM2 users will generate/import keystores with the wrong engine semantics.

return 1;
}

boolean ecKey = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ecKey should be a configurable parameter, defaulting to true, and it can be set to false to support the SM2 algorithm.

return 1;
}

boolean ecKey = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

}

// Decrypt with old password
boolean ecKey = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

char[] pwd1 = console.readPassword("Enter password: ");
char[] pwd2 = console.readPassword("Confirm password: ");
String password1 = new String(pwd1);
String password2 = new String(pwd2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensitive Data in Memory. Console.readPassword() correctly returns a char[] to prevent memory leaks, but it is immediately converted into an immutable String. In Java, a String cannot be manually wiped and remains in memory until garbage collection. Recommendation: Handle passwords directly as char[] or byte[] where possible, and immediately wipe the array using Arrays.fill(pwd1, '\0') after use.

@Federico2014
Copy link
Copy Markdown

The plugins/README.md documentation has not been updated—please add usage instructions.


boolean ecKey = true;
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), ecKey);
String fileName = WalletUtils.generateWalletFile(password, keyPair, keystoreDir, true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loose File Permissions. The new Keystore file is created without explicitly setting restrictive file permissions. It will be created with the system's default umask (often 644, world-readable). Since this file contains an encrypted private key, Recommendation: Explicitly set the file permissions to 600 (owner read/write only) immediately after creation using Files.setPosixFilePermissions.

}
}

private String readPassword() throws IOException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Duplication. The readPassword() method, including the logic for handling the --password-file argument and console input, is copy-pasted across multiple classes. Recommendation: Extract this interactive input and file reading logic into a common utility class (e.g., KeystoreCliUtils) within the plugins module to reduce duplication and improve maintainability.

private static final String FilePath = "Wallet";

public static void start() {
System.err.println("WARNING: --keystore-factory is deprecated and will be removed "
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the --keystore-factory command is officially being deprecated and now prints a warning to the console, please also add the @Deprecated annotation to the class declaration KeystoreFactory.

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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecation warning (lines 23-30) points users to the new Toolkit.jar commands:
Please use: java -jar Toolkit.jar keystore
keystore new - Generate a new keystore
keystore import - Import a private key
keystore list - List keystores
keystore update - Change password

However, the help() method inside the old REPL (lines 109-115) only shows the legacy commands without mentioning the new Toolkit.jar alternative:

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.");                                                                                                                                                                           
}                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                    
Impact: Users who continue using the old --keystore-factory REPL (during the transition period) won't see the deprecation notice when they type help, missing the opportunity to learn about the new commands.                                      
                
Suggested fix: Update the help() method to include the deprecation notice:                                                                                                                                                                          
                
private void help() {                                                                                                                                                                                                                               
    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");                                                                                                                                                                                                             
}                                                                                                                                                                                                                                                   
                
This ensures consistent messaging throughout the user experience. 


// Decrypt with old password
boolean ecKey = true;
WalletFile walletFile = MAPPER.readValue(keystoreFile, WalletFile.class);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keystore update rewrites the only keystore file in place via MAPPER.writeValue(keystoreFile, newWalletFile) after decrypting it. If serialization fails or the process dies mid-write, the user can lose the only copy of the keystore. This should write to a temp file and atomically rename on

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 3 times, most recently from 41ceeb3 to fd9e353 Compare April 2, 2026 11:45
private static final String FilePath = "Wallet";

public static void start() {
System.err.println("WARNING: --keystore-factory is deprecated and will be removed "
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intended timeline for removing KeystoreFactory from FullNode entirely?

Options:

  1. Keep indefinitely - FullNode users who have scripts/automation depending on --keystore-factory can continue using it without changes. The deprecation is just guidance.

  2. Remove in next major release - Add a timeline (e.g., "will be removed in v4.0") to give users time to migrate.

  3. Remove soon (e.g., next release) - Since Toolkit.jar provides identical functionality, there's no technical reason to maintain the code in FullNode.

Suggested clarification:

If the intent is option 1 or 2, consider updating the warning message to include a timeline or clarify that the feature will continue to work during a transition period:

WARNING: --keystore-factory is deprecated and will be removed in a future release.
Please use: java -jar Toolkit.jar keystore
(This feature will remain functional until version X.X)

This helps operators plan their migration accordingly.

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch from fd9e353 to 6643897 Compare April 2, 2026 11:55
@@ -0,0 +1,19 @@
package org.tron.plugins;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be better to place the keystore-related classes in a separate package.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 30 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java:32">
P2: `readSinglePassword` validates password strength when reading from file but skips validation when reading from console. For a method intended to accept an existing password (no confirmation prompt), the strength check on the file path would incorrectly reject valid short passwords. Remove the validation to match the console path.</violation>
</file>

<file name="framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java">

<violation number="1" location="framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java:15">
P2: Add a timeout to prevent this test from hanging CI if the REPL loop doesn't terminate. Since `KeystoreFactory.start()` enters a `Scanner`-based `while(in.hasNextLine())` loop, any unexpected input-stream behavior will block indefinitely.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreList.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreList.java:75">
P2: JSON serialization errors return exit code 0 (success). Since this tool is explicitly designed for CI/CD scripting, returning success when `--json` output could not be written will silently break downstream consumers. Return a non-zero exit code on write failure.</violation>
</file>

<file name="plugins/src/test/java/org/tron/plugins/KeystoreListTest.java">

<violation number="1" location="plugins/src/test/java/org/tron/plugins/KeystoreListTest.java:132">
P2: This assertion is a false-positive risk: `"".split("\\n")` returns `[""]` (length 1), so the test passes even when the command outputs nothing. Add a non-empty check on the output before asserting line count.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java:50">
P2: Unnecessary expensive scrypt decrypt. The `keyPair` is already in memory — use `Credentials.create(keyPair).getAddress()` to derive the address directly instead of reading and decrypting the file just written.</violation>
</file>

<file name="crypto/src/test/java/org/tron/keystore/CredentialsTest.java">

<violation number="1" location="crypto/src/test/java/org/tron/keystore/CredentialsTest.java:33">
P2: Missing `Assert.fail()` after the statement expected to throw. If `Credentials.create(...)` stops throwing, this test silently passes, hiding the regression. Add a `fail()` call, or use `@Test(expected = IllegalArgumentException.class)` instead.</violation>
</file>

<file name="plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java">

<violation number="1" location="plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java:53">
P2: `testNewKeystoreJsonOutput` doesn't verify JSON output. The `StringWriter out` is allocated but never asserted against, and the command writes JSON through `System.out` rather than picocli's writer. This test only validates that `--json` doesn't crash, not that the output is correct.

Consider having `KeystoreNew` use the picocli `spec.commandLine().getOut()` writer for output so it can be captured in tests, or redirect `System.out` in the test to verify the JSON content.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java:105">
P2: `console.readPassword()` returns `null` on EOF (e.g., Ctrl+D). This causes an NPE on `new String(key)`, and then `Arrays.fill(key, '\0')` in the `finally` block throws a second NPE that suppresses the first, making it hard to diagnose.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

if (passwordFile != null) {
byte[] bytes = Files.readAllBytes(passwordFile.toPath());
String password = new String(bytes, StandardCharsets.UTF_8).trim();
if (!WalletUtils.passwordValid(password)) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: readSinglePassword validates password strength when reading from file but skips validation when reading from console. For a method intended to accept an existing password (no confirmation prompt), the strength check on the file path would incorrectly reject valid short passwords. Remove the validation to match the console path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java, line 32:

<comment>`readSinglePassword` validates password strength when reading from file but skips validation when reading from console. For a method intended to accept an existing password (no confirmation prompt), the strength check on the file path would incorrectly reject valid short passwords. Remove the validation to match the console path.</comment>

<file context>
@@ -0,0 +1,129 @@
+    if (passwordFile != null) {
+      byte[] bytes = Files.readAllBytes(passwordFile.toPath());
+      String password = new String(bytes, StandardCharsets.UTF_8).trim();
+      if (!WalletUtils.passwordValid(password)) {
+        System.err.println("Invalid password: must be at least 6 characters.");
+        return null;
</file context>
Fix with Cubic

String output = baos.toString(StandardCharsets.UTF_8.name());
String[] lines = output.trim().split("\\n");
// Should list only the valid keystore, not the readme.json or notes.txt
assertEquals("Should list only 1 valid keystore", 1, lines.length);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This assertion is a false-positive risk: "".split("\\n") returns [""] (length 1), so the test passes even when the command outputs nothing. Add a non-empty check on the output before asserting line count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/test/java/org/tron/plugins/KeystoreListTest.java, line 132:

<comment>This assertion is a false-positive risk: `"".split("\\n")` returns `[""]` (length 1), so the test passes even when the command outputs nothing. Add a non-empty check on the output before asserting line count.</comment>

<file context>
@@ -0,0 +1,137 @@
+      String output = baos.toString(StandardCharsets.UTF_8.name());
+      String[] lines = output.trim().split("\\n");
+      // Should list only the valid keystore, not the readme.json or notes.txt
+      assertEquals("Should list only 1 valid keystore", 1, lines.length);
+    } finally {
+      System.setOut(originalOut);
</file context>
Fix with Cubic

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 7 times, most recently from cd5acbf to 21ce211 Compare April 2, 2026 14:27
@0xbigapple
Copy link
Copy Markdown

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 2, 2026

@cubic-dev-ai

@0xbigapple I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 28 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java:59">
P2: The password comparison via `String` undermines the `char[]` zeroing. `password2` is created as an immutable `String` solely for comparison, so its contents persist in memory despite zeroing `pwd2`. Compare the char arrays directly with `Arrays.equals(pwd1, pwd2)` instead.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java:115">
P2: `isValidPrivateKey` rejects keys with a `0x` prefix, but `ByteArray.fromHexString` (used to parse the key on the next line) strips that prefix. Users providing the common `0x`-prefixed format will get an unhelpful error. Either strip the prefix before validation or accept it in the regex.</violation>
</file>

<file name="plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java">

<violation number="1" location="plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java:166">
P2: This test only checks exit code but doesn't verify the imported key roundtrips correctly. Since the test's purpose is to validate whitespace trimming on the key file, it should also verify the decrypted private key matches the original — otherwise a subtle trimming bug could store the wrong key undetected. Compare with `testImportWithKeyFileAndPasswordFile` which does full roundtrip verification.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

}
try {
String password1 = new String(pwd1);
String password2 = new String(pwd2);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The password comparison via String undermines the char[] zeroing. password2 is created as an immutable String solely for comparison, so its contents persist in memory despite zeroing pwd2. Compare the char arrays directly with Arrays.equals(pwd1, pwd2) instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java, line 59:

<comment>The password comparison via `String` undermines the `char[]` zeroing. `password2` is created as an immutable `String` solely for comparison, so its contents persist in memory despite zeroing `pwd2`. Compare the char arrays directly with `Arrays.equals(pwd1, pwd2)` instead.</comment>

<file context>
@@ -0,0 +1,116 @@
+    }
+    try {
+      String password1 = new String(pwd1);
+      String password2 = new String(pwd2);
+
+      if (!password1.equals(password2)) {
</file context>
Fix with Cubic

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 3 times, most recently from 6364cbc to f655164 Compare April 2, 2026 14:58
@barbatos2011
Copy link
Copy Markdown
Owner Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 2, 2026

@cubic-dev-ai

@barbatos2011 I have started the AI code review. It will take a few minutes to complete.

@barbatos2011
Copy link
Copy Markdown
Owner Author

Code Review

Overall design is solid — module split is correct, security patterns are thorough, backward compatibility is maintained. A few items worth discussing:

1. readPassword returns String — known limitation

KeystoreCliUtils.readPassword() converts byte[]/char[] to String for the password. String is immutable in JVM and cannot be actively zeroed (relies on GC). This is an upstream constraint — WalletUtils API accepts String — but worth documenting as a known limitation.

2. No size limit on --password-file

Files.readAllBytes(passwordFile.toPath()) will read the entire file into memory. If pointed at a large file, this allocates unbounded memory. Low risk for a CLI tool, but a simple size check (e.g., reject > 1KB) would be defensive.

3. Private key validation is length-only

private static boolean isValidPrivateKey(String key) {
    return key.length() == 64 && key.matches("[0-9a-fA-F]+");

This doesn't verify the key is within the valid range for the elliptic curve (0 < key < secp256k1 order). An invalid key will fail later in SignUtils.fromPrivate(), but the error message may be confusing. Consider adding range validation or catching the downstream exception with a user-friendly message.

4. Commit 3 bundles list + update + deprecation

update involves atomic file writes and password re-encryption (complex, higher risk), while list is read-only (simple, low risk). If a regression is found in update, reverting the commit also rolls back list and the deprecation marker. Consider splitting update into its own commit if reviewers ask.

5. plugins/build.gradle dependency exclusions

implementation project(":crypto") {
    exclude ...
}

Excludes libp2p, prometheus, aspectj, httpcomponents. Need to confirm crypto module has no runtime dependency on these — compile success doesn't guarantee no ClassNotFoundException at runtime. Was this verified by running the Toolkit.jar commands end-to-end?

6. Minor: ATOMIC_MOVE fallback on non-POSIX filesystems

Files.move(tempFile.toPath(), keystoreFile.toPath(),
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.ATOMIC_MOVE);

ATOMIC_MOVE throws AtomicMoveNotSupportedException on some filesystems (e.g., across mount points, some network drives). The current code catches Exception and deletes the temp file, which is correct. But a fallback to non-atomic REPLACE_EXISTING (with a warning log) might be more resilient than failing the entire operation.


Everything else looks good — constant-time MAC comparison, Console.readPassword() with null checks, char[]/byte[] zeroing in finally blocks, POSIX file permissions, Ethereum standard test vectors, cross-implementation compatibility tests.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 28 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreList.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreList.java:86">
P2: `printEmptyJson()` hides JSON serialization failures, causing `keystore list --json` to return exit code 0 even when output generation fails in empty/missing-directory cases.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java:116">
P2: Private key handling converts secrets to immutable String values, which cannot be wiped from memory after use.</violation>
</file>

<file name="crypto/src/test/java/org/tron/keystore/CredentialsTest.java">

<violation number="1" location="crypto/src/test/java/org/tron/keystore/CredentialsTest.java:47">
P3: Remove the assertion that unequal credentials must have different hash codes; hash collisions are allowed and this can cause flaky tests.</violation>
</file>

<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java:33">
P1: Using `.trim()` on the password removes valid leading and trailing spaces, breaking backward compatibility and parity with interactive input for users whose passwords contain spaces. Use regex to strip only trailing line terminators instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

if (passwordFile != null) {
byte[] bytes = Files.readAllBytes(passwordFile.toPath());
try {
String password = new String(bytes, StandardCharsets.UTF_8).trim();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Using .trim() on the password removes valid leading and trailing spaces, breaking backward compatibility and parity with interactive input for users whose passwords contain spaces. Use regex to strip only trailing line terminators instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java, line 33:

<comment>Using `.trim()` on the password removes valid leading and trailing spaces, breaking backward compatibility and parity with interactive input for users whose passwords contain spaces. Use regex to strip only trailing line terminators instead.</comment>

<file context>
@@ -0,0 +1,117 @@
+    if (passwordFile != null) {
+      byte[] bytes = Files.readAllBytes(passwordFile.toPath());
+      try {
+        String password = new String(bytes, StandardCharsets.UTF_8).trim();
+        if (!WalletUtils.passwordValid(password)) {
+          System.err.println("Invalid password: must be at least 6 characters.");
</file context>
Fix with Cubic

return 0;
}

private void printEmptyJson() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: printEmptyJson() hides JSON serialization failures, causing keystore list --json to return exit code 0 even when output generation fails in empty/missing-directory cases.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/main/java/common/org/tron/plugins/KeystoreList.java, line 86:

<comment>`printEmptyJson()` hides JSON serialization failures, causing `keystore list --json` to return exit code 0 even when output generation fails in empty/missing-directory cases.</comment>

<file context>
@@ -0,0 +1,95 @@
+    return 0;
+  }
+
+  private void printEmptyJson() {
+    try {
+      Map<String, Object> result = new LinkedHashMap<>();
</file context>
Fix with Cubic

@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 2 times, most recently from 5c17803 to 21f53fd Compare April 2, 2026 15:38
@barbatos2011
Copy link
Copy Markdown
Owner Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 2, 2026

@cubic-dev-ai

@barbatos2011 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 28 files

Barbatos added 2 commits April 3, 2026 00:03
Move keystore package (Wallet, WalletUtils, Credentials, WalletFile)
from framework to crypto module to enable reuse by Toolkit.jar without
pulling in the entire framework dependency.

- Replace Args.getInstance().isECKeyCryptoEngine() calls with injected
  boolean ecKey parameter in Wallet.decrypt(), WalletUtils.loadCredentials(),
  generateNewWalletFile(), generateFullNewWalletFile(), generateLightNewWalletFile()
- Update callers (KeystoreFactory, WitnessInitializer) to pass ecKey
- Add implementation project(":crypto") to plugins build
- Merge two CredentialsTest files (fix keystroe typo dir) into crypto module
- Move WalletFileTest to crypto module
- CipherException already in common module, no move needed
Add picocli subcommands for keystore management in Toolkit.jar:
- `keystore new`: generate new keypair and encrypt to keystore file
- `keystore import`: import existing private key into keystore file

Both commands support:
- --password-file for non-interactive password input
- --keystore-dir for custom output directory
- --json for structured output
- Console.readPassword() for no-echo interactive input
- Clear error when no TTY available (directs to --password-file)

Import reads private key from --key-file or interactive prompt,
never from command-line arguments (security: avoids ps/history exposure).

Also adds roundtrip property tests (100 random encrypt/decrypt cycles)
and cross-implementation compatibility tests to crypto module.

Note: jqwik was planned for property testing but replaced with plain
JUnit loops due to Gradle dependency verification overhead.
@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 3 times, most recently from f30d460 to cc02b8c Compare April 2, 2026 16:49
…store-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.
@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch from cc02b8c to 1492a49 Compare April 3, 2026 08:53
- KeystoreUpdate: findKeystoreByAddress now detects multiple keystores
  with the same address and returns an error with file list, instead of
  silently picking one nondeterministically.
- KeystoreImport: scan directory before writing and print WARNING if a
  keystore for the same address already exists. Import still proceeds
  (legitimate use case) but user is made aware.
- KeystoreUpdate: strip UTF-8 BOM from password file before parsing.
- KeystoreUpdate: add comment clarifying old password skips validation.
- KeystoreList: add version != 3 check to filter non-keystore JSON.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java">

<violation number="1" location="plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java:206">
P2: When duplicates are detected, `findKeystoreByAddress` returns `null`, and the caller blindly prints `"No keystore found for address: …"`. The user sees both the duplicate warning and a contradictory "not found" message. Either return a sentinel/throw so the caller can distinguish the two cases, or move the duplicate error handling to the caller.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

System.err.println("Please remove duplicates and retry.");
return null;
}
return matches.isEmpty() ? null : matches.get(0);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When duplicates are detected, findKeystoreByAddress returns null, and the caller blindly prints "No keystore found for address: …". The user sees both the duplicate warning and a contradictory "not found" message. Either return a sentinel/throw so the caller can distinguish the two cases, or move the duplicate error handling to the caller.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java, line 206:

<comment>When duplicates are detected, `findKeystoreByAddress` returns `null`, and the caller blindly prints `"No keystore found for address: …"`. The user sees both the duplicate warning and a contradictory "not found" message. Either return a sentinel/throw so the caller can distinguish the two cases, or move the duplicate error handling to the caller.</comment>

<file context>
@@ -183,16 +183,26 @@ private File findKeystoreByAddress(String targetAddress) {
+      System.err.println("Please remove duplicates and retry.");
+      return null;
+    }
+    return matches.isEmpty() ? null : matches.get(0);
   }
 }
</file context>
Fix with Cubic

…rity tips

- Reject duplicate-address import by default, add --force flag to override
- Add friendly error messages when --password-file or --key-file not found
- Print security tips after keystore new/import (aligned with geth output)
- Add file existence checks in KeystoreCliUtils and KeystoreUpdate
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java">

<violation number="1" location="plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java:190">
P2: Hardcoded `/tmp/nonexistent-pw.txt` is fragile and platform-dependent. If this file exists on the build host the test fails; on Windows `/tmp` doesn't exist. Derive the path from `tempFolder` to guarantee it doesn't exist, consistent with every other test in this class.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

CommandLine cmd = new CommandLine(new Toolkit());
int exitCode = cmd.execute("keystore", "new",
"--keystore-dir", dir.getAbsolutePath(),
"--password-file", "/tmp/nonexistent-pw.txt");
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Hardcoded /tmp/nonexistent-pw.txt is fragile and platform-dependent. If this file exists on the build host the test fails; on Windows /tmp doesn't exist. Derive the path from tempFolder to guarantee it doesn't exist, consistent with every other test in this class.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java, line 190:

<comment>Hardcoded `/tmp/nonexistent-pw.txt` is fragile and platform-dependent. If this file exists on the build host the test fails; on Windows `/tmp` doesn't exist. Derive the path from `tempFolder` to guarantee it doesn't exist, consistent with every other test in this class.</comment>

<file context>
@@ -180,6 +180,18 @@ public void testNewKeystoreSpecialCharPassword() throws Exception {
+    CommandLine cmd = new CommandLine(new Toolkit());
+    int exitCode = cmd.execute("keystore", "new",
+        "--keystore-dir", dir.getAbsolutePath(),
+        "--password-file", "/tmp/nonexistent-pw.txt");
+
+    assertEquals("Should fail when password file not found", 1, exitCode);
</file context>
Suggested change
"--password-file", "/tmp/nonexistent-pw.txt");
"--password-file", new File(tempFolder.getRoot(), "nonexistent-pw.txt").getAbsolutePath());
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants