Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0526128
refactor(crypto): extract keystore library from framework module
Apr 2, 2026
7f0c0ba
feat(plugins): add keystore new and import commands to Toolkit
Apr 2, 2026
2bced5f
feat(plugins): add keystore list and update commands, deprecate --key…
Apr 2, 2026
1a641b4
fix(plugins): handle duplicate-address keystores and add import warning
Apr 3, 2026
791d91f
fix(plugins): block duplicate import, improve error messages and secu…
Apr 5, 2026
3c0f8eb
style(plugins): use picocli output streams and address review findings
Apr 16, 2026
cf823df
fix(plugins): secure keystore file creation and improve robustness
Apr 16, 2026
ce482cb
test(plugins): improve keystore test coverage and assertions
Apr 16, 2026
115e226
fix(plugins): unify keystore validation and fix inconsistent error me…
Apr 16, 2026
b079839
test(plugins): improve coverage for keystore validation and edge cases
Apr 17, 2026
b083fc6
test(plugins): add direct unit tests for KeystoreCliUtils
Apr 17, 2026
241590c
test(framework): expand coverage for WalletFile POJO and KeystoreFactory
Apr 17, 2026
cdb038f
ci: re-trigger build after transient apt mirror failure
Apr 17, 2026
4e19b0b
ci: re-trigger after transient infrastructure failure
Apr 17, 2026
368d104
fix(crypto): enforce keystore address consistency in Wallet.decrypt
Apr 18, 2026
c383a83
fix(plugins): prevent address spoofing in keystore update command
Apr 18, 2026
5a76ed8
docs(plugins): document keystore list address trust model
Apr 18, 2026
070316c
refactor(crypto): centralize secure keystore file writing in WalletUtils
Apr 18, 2026
40ec5ce
fix(plugins): reject symlinked password/key files to prevent file dis…
Apr 18, 2026
e5bbc20
fix(crypto): preserve whitespace in non-TTY password input
Apr 21, 2026
4bcb850
feat(plugins): hint at legacy password-truncation on keystore update …
Apr 21, 2026
f681124
feat(framework): hint at legacy password-truncation on SR startup fai…
Apr 21, 2026
99159e2
test(crypto): move keystore tests to framework for jacoco coverage
Apr 22, 2026
340669c
fix(plugins): reject symlinks in keystore directory scans
Apr 22, 2026
a5ff900
fix(plugins): reject multi-line password files in keystore new/import
Apr 22, 2026
61c155b
fix(crypto): reject JSON stubs missing cipher/KDF from keystore disco…
Apr 23, 2026
fc6ab92
refactor(crypto): rename misleading Wallet.AES_128_CTR to PBKDF2
Apr 23, 2026
File filter

Filter by extension

Filter by extension

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

/**
Expand All @@ -48,7 +47,12 @@
*/
public class Wallet {

protected static final String AES_128_CTR = "pbkdf2";
// KDF identifiers used in the Web3 Secret Storage "kdf" field.
// The old name "AES_128_CTR" was misleading — the value is the PBKDF2 KDF
// identifier, not the cipher (CIPHER below). The inner class name
// `WalletFile.Aes128CtrKdfParams` is kept for wire-format/Jackson-subtype
// backward compatibility even though it also reflects the same history.
protected static final String PBKDF2 = "pbkdf2";
protected static final String SCRYPT = "scrypt";
private static final int N_LIGHT = 1 << 12;
private static final int P_LIGHT = 6;
Expand Down Expand Up @@ -168,8 +172,8 @@ private static byte[] generateMac(byte[] derivedKey, byte[] cipherText) {
return Hash.sha3(result);
}

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

validate(walletFile);

Expand Down Expand Up @@ -205,32 +209,79 @@ public static SignInterface decrypt(String password, WalletFile walletFile)

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

if (!Arrays.equals(derivedMac, mac)) {
if (!java.security.MessageDigest.isEqual(derivedMac, mac)) {
throw new CipherException("Invalid password provided");
}

byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16);
byte[] privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText);

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

// Enforce address consistency: if the keystore declares an address, it MUST match
// the address derived from the decrypted private key. Prevents address spoofing
// where a crafted keystore displays one address but encrypts a different key.
String declared = walletFile.getAddress();
if (declared != null && !declared.isEmpty()) {
String derived = StringUtil.encode58Check(keyPair.getAddress());
if (!declared.equals(derived)) {
throw new CipherException(
"Keystore address mismatch: file declares " + declared
+ " but private key derives " + derived);
}
}

static void validate(WalletFile walletFile) throws CipherException {
WalletFile.Crypto crypto = walletFile.getCrypto();
return keyPair;
}

/**
* Returns a description of the first schema violation found in
* {@code walletFile}, or {@code null} if the file matches the supported
* V3 keystore shape (current version, known cipher, known KDF).
*
* <p>Shared by {@link #validate(WalletFile)} (which throws the message)
* and {@link #isValidKeystoreFile(WalletFile)} (which returns boolean
* for discovery-style filtering).
*/
private static String validationError(WalletFile walletFile) {
if (walletFile.getVersion() != CURRENT_VERSION) {
throw new CipherException("Wallet version is not supported");
return "Wallet version is not supported";
}

if (!crypto.getCipher().equals(CIPHER)) {
throw new CipherException("Wallet cipher is not supported");
WalletFile.Crypto crypto = walletFile.getCrypto();
if (crypto == null) {
return "Missing crypto section";
}
String cipher = crypto.getCipher();
if (cipher == null || !cipher.equals(CIPHER)) {
return "Wallet cipher is not supported";
}
String kdf = crypto.getKdf();
if (kdf == null || (!kdf.equals(PBKDF2) && !kdf.equals(SCRYPT))) {
return "KDF type is not supported";
}
return null;
}

if (!crypto.getKdf().equals(AES_128_CTR) && !crypto.getKdf().equals(SCRYPT)) {
throw new CipherException("KDF type is not supported");
static void validate(WalletFile walletFile) throws CipherException {
String error = validationError(walletFile);
if (error != null) {
throw new CipherException(error);
}
}

/**
* Returns {@code true} iff {@code walletFile} has the shape of a
* decryptable V3 keystore: non-null address, supported version, non-null
* crypto section with a supported cipher and KDF. Intended for
* discovery-style filtering (e.g. listing or duplicate detection) where
* we want to skip JSON stubs that would later fail {@link #validate}.
*/
public static boolean isValidKeystoreFile(WalletFile walletFile) {
return walletFile != null
&& walletFile.getAddress() != null
&& validationError(walletFile) == null;
}

public static byte[] generateRandomBytes(int size) {
byte[] bytes = new byte[size];
new SecureRandom().nextBytes(bytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public KdfParams getKdfparams() {
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "kdf")
@JsonSubTypes({
@JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.AES_128_CTR),
@JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.PBKDF2),
@JsonSubTypes.Type(value = ScryptKdfParams.class, name = Wallet.SCRYPT)
})
// To support my Ether Wallet keys uncomment this annotation & comment out the above
Expand Down
Loading
Loading