Skip to content

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

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

feat(plugins): migrate keystore CLI from FullNode to Toolkit#12
barbatos2011 wants to merge 41 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
Comment thread plugins/build.gradle Outdated
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

Comment thread framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java Outdated
Comment thread plugins/src/main/java/common/org/tron/plugins/KeystoreList.java Outdated
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

Comment thread plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java Outdated
Comment thread crypto/src/test/java/org/tron/keystore/CredentialsTest.java
Comment thread plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java
Comment thread plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java
@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch 3 times, most recently from 287ba36 to 8bdd691 Compare April 2, 2026 14:05
lvs0075 and others added 2 commits April 14, 2026 14:29
…etOwnerAddress-unpack-type

feat(actuator): correct protobuf unpack types in getOwnerAddress()
…#6631)

* feat(protocol): add protoLint script for enum validation

* feat(protocol): optimize protoLint performance and caching

* fix(proto): resolve gradle implicit task dependency warning

* docs(protocol): clarify enum discriminator in protoLint

* build(protocol): harden proto lint buf config

* docs: update comment to reflect dynamic include path derivation
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="framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java">

<violation number="1" location="framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java:123">
P2: Provide password lines before `exit`; otherwise the `exit` token is consumed by `inputPassword2Twice()` and the REPL never prints `Exit !!!`.</violation>
</file>

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

// caught and logged, and the REPL continues.
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes()));
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

P2: Provide password lines before exit; otherwise the exit token is consumed by inputPassword2Twice() and the REPL never prints Exit !!!.

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

<comment>Provide password lines before `exit`; otherwise the `exit` token is consumed by `inputPassword2Twice()` and the REPL never prints `Exit !!!`.
</comment>

<file context>
@@ -2,34 +2,146 @@
+    // 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();
</file context>
Suggested change
System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes()));
System.setIn(new ByteArrayInputStream("genkeystore\npassword123\npassword123\nexit\n".getBytes()));
Fix with Cubic

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Apr 18, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

3for and others added 20 commits April 21, 2026 11:23
…col#6614)

* test(framework): merge duplicate CredentialsTest coverage

Consolidate the misplaced keystroe CredentialsTest into org.tron.keystore.CredentialsTest.

- remove the duplicate test under the misspelled keystroe package
- add explicit equals behavior coverage for address and cryptoEngine
- normalize assertions to JUnit Assert and remove legacy TestCase usage

* test(framework): stabilize CredentialsTest fixtures

Replace random Credentials test setup with deterministic SignInterface mocks
so the suite no longer depends on platform-specific SecureRandom providers or
probabilistic retries.

- remove NativePRNG usage from CredentialsTest
- replace random key generation with fixed address fixtures via mocked SignInterface
- assert create(SignInterface) returns the expected base58check address
- keep equals/hashCode contract coverage with deterministic inputs
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.
…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.
- 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.
…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
- Replace System.out/err with spec.commandLine().getOut()/getErr() in all
  keystore commands, consistent with existing db commands pattern
- Fix description from "witness account keys" to "account keys"
- Unify permission warning for UnsupportedOperationException and IOException
- Add Keystore section to plugins/README.md with migration guide
- Update tests to use cmd.setOut()/setErr() instead of System.setOut()
- Fix TOCTOU race: keystore files were world-readable between creation
  and permission setting. Now use temp-file + setOwnerOnly + atomic-rename
  pattern (already used by update command) for new and import commands
- Fix KeystoreUpdate password file parsing: apply stripLineEndings to
  both old and new passwords to handle old-Mac line endings
- Warn on unreadable JSON files during keystore lookup instead of
  silently skipping, so users are aware of corrupted files
- Make WalletUtils.getWalletFileName public for reuse
- Extract atomicMove utility to KeystoreCliUtils for shared use
- Add tests for update: JSON output, single-line password file, no TTY,
  password file not found, SM2 keystore, multiple same-address keystores,
  large password file, BOM password file
- Add tests for import: 0x/0X prefix key, corrupted file warning,
  POSIX file permissions
- Add tests for new: large password file, BOM password file, POSIX
  file permissions
- Add tests for list: empty/nonexistent directory JSON output,
  corrupted file warning, output content verification
- Strengthen existing tests with error message assertions to prevent
  false passes from wrong failure reasons
…ssages

- Extract isValidKeystoreFile(WalletFile) to KeystoreCliUtils, requiring
  address != null, crypto != null, and version == 3 consistently across
  list, import, and update commands
- Fix update command printing both "Multiple keystores found" and
  "No keystore found" for the same address — move error messages into
  findKeystoreByAddress so each case prints exactly one message
- Add tests for update with nonexistent/file keystore-dir, old Mac CR
  line endings, and invalid-version keystore files
- Add test for import duplicate check skipping invalid-version files
- Add test for list skipping invalid-version and no-crypto keystores
- Fix password file split regex to handle old Mac CR-only line endings
Cover utility methods (stripLineEndings, jsonMap, isValidKeystoreFile,
checkFileExists, readPassword, ensureDirectory, printJson,
printSecurityTips, atomicMove, generateKeystoreFile, setOwnerOnly) with
direct unit tests, including edge cases like BOM, various line endings,
empty strings, and both ECDSA/SM2 key paths.
- Add WalletFilePojoTest covering all getter/setter, equals, hashCode
  paths for WalletFile and its inner classes (Crypto, CipherParams,
  Aes128CtrKdfParams, ScryptKdfParams), plus JSON deserialization with
  both scrypt and pbkdf2 KDF types
- Expand KeystoreFactoryDeprecationTest to cover help, invalid command,
  quit, empty line, and genKeystore/importprivatekey dispatch paths
- Tests live in framework module so their coverage data is included in
  crypto module's jacoco report (crypto:jacocoTestReport reads framework
  exec files)
Prevents address-spoofing attacks where a crafted keystore declares one
address in JSON but encrypts a different private key. After decryption,
verify that the declared address matches the address derived from the
decrypted key. Null/empty addresses are still accepted for compatibility
with Ethereum-style keystores that omit the field.
- Remove newWalletFile.setAddress(walletFile.getAddress()) — createStandard
  already sets the correctly-derived address, and propagating the JSON
  address could carry a spoofed value forward
- Use the derived address from newWalletFile for output messages instead
  of walletFile.getAddress(), keeping the output correct as a
  defense-in-depth measure even if upstream validation is weakened
- Add tests for tampered-address rejection and derived-address output
Clarify that 'keystore list' displays the declared address from each
keystore's JSON without decrypting it. A tampered keystore may claim an
address that does not correspond to its encrypted key; verification only
happens at decryption time (e.g. 'keystore update').
Move the temp-file + atomic-rename + POSIX 0600 permissions pattern from
plugins into WalletUtils.generateWalletFile and a new public
WalletUtils.writeWalletFile method. Use Files.createTempFile with
PosixFilePermissions.asFileAttribute to atomically create the temp file
with owner-only permissions, eliminating the brief world-readable window
present in the previous File.createTempFile + setOwnerOnly sequence.

All callers benefit automatically, including framework/KeystoreFactory
(which previously wrote 0644 files) and any third-party project that
consumes the crypto artifact. Removes ~68 lines of duplicated plumbing
from KeystoreCliUtils.

- Windows/non-POSIX fallback is best-effort (DOS attributes) and
  documented as such in JavaDoc
- Error path suppresses secondary IOException via addSuppressed so the
  original cause is preserved
- OWNER_ONLY wrapped with Collections.unmodifiableSet for defense
  in depth
- WalletUtilsWriteTest covers permissions, replacement, temp cleanup,
  parent-directory creation, and a failure-path test that forces move
  to fail and asserts no temp file remains
- KeystoreUpdateTest adversarially pre-loosens perms to 0644 and
  verifies update narrows back to 0600
…closure

By default java.nio.file.Files.readAllBytes follows symbolic links,
allowing an attacker who can plant files in a user-supplied path to
redirect reads to arbitrary files (e.g. /etc/shadow, SSH private keys).
The first ~1KB of the linked file would be misused as a password or
private key.

Introduces KeystoreCliUtils.readRegularFile that:
- stats the path with LinkOption.NOFOLLOW_LINKS (lstat semantics)
- rejects symlinks, directories, FIFOs and other non-regular files
- opens the byte channel with LinkOption.NOFOLLOW_LINKS too, closing
  the TOCTOU window between stat and open
- enforces a single size check via the lstat-returned attributes
  instead of a separate File.length() call

All three call sites are migrated:
- KeystoreCliUtils.readPassword (used by new/import)
- KeystoreImport.readPrivateKey (key file)
- KeystoreUpdate.call (password file for old+new passwords)

Tests:
- unit tests for readRegularFile covering success, missing file,
  too-large, symlink refused, directory refused, and empty file
- end-to-end tests in KeystoreImportTest that provide a symlinked
  --key-file and --password-file and assert refusal
@barbatos2011 barbatos2011 force-pushed the 001-keystore-toolkit-migration branch from ba01b3b to 40ec5ce Compare April 21, 2026 09:13
Barbatos added 5 commits April 21, 2026 17:31
WalletUtils.inputPassword() previously applied trim() + split("\\s+")[0]
on the non-TTY (stdin) branch, silently truncating passphrases like
"correct horse battery staple" to "correct" when piped via stdin. This
produced keystores encrypted with only the first word, which would then
fail to decrypt under new CLIs that correctly preserve the full password.

- Replace trim() + split("\\s+")[0] with stripPasswordLine() that only
  strips the UTF-8 BOM and trailing line terminators, preserving internal
  whitespace
- Expose stripPasswordLine as the canonical public helper and consolidate
  KeystoreCliUtils.stripLineEndings into it (plugins now delegates)
- Fix a pre-existing Scanner lifecycle bug: inputPassword() used to
  allocate a fresh Scanner(System.in) on every call, so the second call
  in inputPassword2Twice() lost bytes the first Scanner had buffered.
  The Scanner is now lazily-cached and shared across calls, with a
  resetSharedStdinScanner() hook for tests
- KeystoreFactory.importPrivateKey keeps trim()+split for private-key
  input (hex strings have no legitimate whitespace)

New tests cover: internal whitespace preservation, tabs, CRLF, BOM,
leading/trailing spaces, the inputPassword2Twice piped double-read path,
and direct unit tests of stripPasswordLine (null, empty, only-BOM,
only-terminators, BOM-then-terminator).
…failure

When `keystore update` decryption fails and the provided old password
contains whitespace, print a hint pointing users at the
`--keystore-factory` non-TTY truncation bug: keystores created with
`echo PASS | java -jar FullNode.jar --keystore-factory` were encrypted
using only the first whitespace-separated word of the password.

The hint is gated on whitespace in the supplied password so that the
overwhelmingly common case (user simply mistyped) does not get noise
about an unrelated legacy bug. Wording explicitly tells the user to
re-run with just the first word as the current password and offers the
full phrase as the new password.

Two tests cover both branches of the gate (hint fires when password
contains whitespace, suppressed otherwise).

Deliberately not adding automatic fallback to split("\\s+")[0] during
decryption — that would lower effective password entropy by giving an
attacker a free first-token brute-force channel. Recovery stays explicit
and user-driven.
…lure

When WitnessInitializer fails to load the SR keystore due to a
CipherException and the provided password contains whitespace, log a
tip pointing the operator at the same legacy `--keystore-factory`
non-TTY truncation bug handled in the Toolkit plugins CLI.

Gives SR operators a breadcrumb when their node refuses to start after
upgrading past the inputPassword() fix: their keystore may have been
encrypted using only the first word of their password, and they can
recover by using that first word temporarily, then resetting with
Toolkit.jar keystore update.

The tip only fires when the password has whitespace (truncation cannot
explain a failure otherwise), keeping the common wrong-password case
quiet.
crypto/build.gradle's jacocoTestReport reads exec data from
../framework/build/jacoco only — so tests in crypto/src/test itself
produce no signal in the crypto coverage report that the Coverage Gate
job checks. WalletFilePojoTest was already placed in framework for
this reason; this commit makes the other seven keystore tests
consistent.

Moves (pure git mv, no code changes needed — same package
`org.tron.keystore`, all test deps available in framework):
- CredentialsTest
- CrossImplTest
- WalletAddressValidationTest
- WalletFileTest
- WalletPropertyTest
- WalletUtilsInputPasswordTest
- WalletUtilsWriteTest

After the move, crypto/src/test/java/org/tron/keystore/ is empty and
all keystore test classes live in framework/src/test/java/org/tron/
keystore/ alongside WalletFilePojoTest.
Earlier commits hardened --password-file / --key-file reads against
symlink-following, but the keystore-directory scans in list, import
(duplicate check), and update (findKeystoreByAddress) still called
MAPPER.readValue(file, ...) on every *.json entry without a symlink
guard. In a hostile or group-writable keystore directory, a planted
symlink could redirect deserialization to arbitrary files, and FIFOs
or devices could block the scan indefinitely.

- Add KeystoreCliUtils.isSafeRegularFile(file, err) helper that stats
  with LinkOption.NOFOLLOW_LINKS and rejects symbolic links,
  directories, FIFOs, and devices with a warning to err
- Apply it before MAPPER.readValue in KeystoreList.call,
  KeystoreImport.findExistingKeystore, and
  KeystoreUpdate.findKeystoreByAddress
- Add three end-to-end tests (one per command) that plant a .json
  symlink in the keystore dir and verify each command warns and
  continues without reading the symlink target
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.