Skip to content

Commit 88df0ff

Browse files
BarbatosBarbatos
authored andcommitted
fix(plugins): secure keystore file creation and improve robustness
- 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
1 parent ba232f4 commit 88df0ff

6 files changed

Lines changed: 70 additions & 27 deletions

File tree

crypto/src/main/java/org/tron/keystore/WalletUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public static Credentials loadCredentials(String password, File source, boolean
8181
return Credentials.create(Wallet.decrypt(password, walletFile, ecKey));
8282
}
8383

84-
private static String getWalletFileName(WalletFile walletFile) {
84+
public static String getWalletFileName(WalletFile walletFile) {
8585
DateTimeFormatter format = DateTimeFormatter.ofPattern(
8686
"'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'");
8787
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
import java.nio.charset.StandardCharsets;
99
import java.nio.file.Files;
1010
import java.nio.file.Path;
11+
import java.nio.file.StandardCopyOption;
1112
import java.nio.file.attribute.PosixFilePermission;
1213
import java.util.Arrays;
1314
import java.util.EnumSet;
1415
import java.util.LinkedHashMap;
1516
import java.util.Map;
1617
import java.util.Set;
18+
import org.tron.common.crypto.SignInterface;
19+
import org.tron.core.exception.CipherException;
20+
import org.tron.keystore.Wallet;
21+
import org.tron.keystore.WalletFile;
1722
import org.tron.keystore.WalletUtils;
1823

1924
/**
@@ -29,6 +34,55 @@ final class KeystoreCliUtils {
2934
private KeystoreCliUtils() {
3035
}
3136

37+
/**
38+
* Generate a keystore file using temp-file + atomic-rename to avoid
39+
* a TOCTOU window where the file is world-readable before permissions are set.
40+
*
41+
* @return the final keystore file name (not the full path)
42+
*/
43+
static String generateKeystoreFile(String password, SignInterface keyPair,
44+
File destDir, boolean useFullScrypt, PrintWriter err)
45+
throws CipherException, IOException {
46+
47+
WalletFile walletFile;
48+
if (useFullScrypt) {
49+
walletFile = Wallet.createStandard(password, keyPair);
50+
} else {
51+
walletFile = Wallet.createLight(password, keyPair);
52+
}
53+
54+
String fileName = WalletUtils.getWalletFileName(walletFile);
55+
File destination = new File(destDir, fileName);
56+
57+
File tempFile = File.createTempFile("keystore-", ".tmp", destDir);
58+
try {
59+
setOwnerOnly(tempFile, err);
60+
MAPPER.writeValue(tempFile, walletFile);
61+
atomicMove(tempFile, destination);
62+
} catch (Exception e) {
63+
if (!tempFile.delete()) {
64+
err.println("Warning: could not delete temp file: " + tempFile.getName());
65+
}
66+
throw e;
67+
}
68+
69+
return fileName;
70+
}
71+
72+
/**
73+
* Atomic move with fallback for filesystems that don't support it.
74+
*/
75+
static void atomicMove(File source, File target) throws IOException {
76+
try {
77+
Files.move(source.toPath(), target.toPath(),
78+
StandardCopyOption.REPLACE_EXISTING,
79+
StandardCopyOption.ATOMIC_MOVE);
80+
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
81+
Files.move(source.toPath(), target.toPath(),
82+
StandardCopyOption.REPLACE_EXISTING);
83+
}
84+
}
85+
3286
static String readPassword(File passwordFile, PrintWriter err) throws IOException {
3387
if (passwordFile != null) {
3488
if (!passwordFile.exists()) {
@@ -171,7 +225,7 @@ static void setOwnerOnly(File file, PrintWriter err) {
171225
Files.setPosixFilePermissions(file.toPath(), OWNER_ONLY);
172226
} catch (UnsupportedOperationException | IOException e) {
173227
err.println("Warning: could not set file permissions on " + file.getName()
174-
+ ". Please manually restrict access (e.g. chmod 600).");
228+
+ ". Please manually restrict access to this file.");
175229
}
176230
}
177231
}

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import org.tron.common.utils.ByteArray;
1515
import org.tron.core.exception.CipherException;
1616
import org.tron.keystore.Credentials;
17-
import org.tron.keystore.WalletUtils;
17+
import org.tron.keystore.WalletFile;
1818
import picocli.CommandLine.Command;
1919
import picocli.CommandLine.Model.CommandSpec;
2020
import picocli.CommandLine.Option;
@@ -92,15 +92,15 @@ public Integer call() {
9292
return 1;
9393
}
9494
String address = Credentials.create(keyPair).getAddress();
95-
String existingFile = findExistingKeystore(keystoreDir, address);
95+
String existingFile = findExistingKeystore(keystoreDir, address, err);
9696
if (existingFile != null && !force) {
9797
err.println("Keystore for address " + address
9898
+ " already exists: " + existingFile
9999
+ ". Use --force to import anyway.");
100100
return 1;
101101
}
102-
String fileName = WalletUtils.generateWalletFile(password, keyPair, keystoreDir, true);
103-
KeystoreCliUtils.setOwnerOnly(new File(keystoreDir, fileName), err);
102+
String fileName = KeystoreCliUtils.generateKeystoreFile(
103+
password, keyPair, keystoreDir, true, err);
104104
if (json) {
105105
KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap(
106106
"address", address, "file", fileName));
@@ -159,7 +159,7 @@ private boolean isValidPrivateKey(String key) {
159159
return !StringUtils.isEmpty(key) && HEX_PATTERN.matcher(key).matches();
160160
}
161161

162-
private String findExistingKeystore(File dir, String address) {
162+
private String findExistingKeystore(File dir, String address, PrintWriter err) {
163163
if (!dir.exists() || !dir.isDirectory()) {
164164
return null;
165165
}
@@ -171,13 +171,12 @@ private String findExistingKeystore(File dir, String address) {
171171
KeystoreCliUtils.mapper();
172172
for (File file : files) {
173173
try {
174-
org.tron.keystore.WalletFile wf =
175-
mapper.readValue(file, org.tron.keystore.WalletFile.class);
174+
WalletFile wf = mapper.readValue(file, WalletFile.class);
176175
if (address.equals(wf.getAddress())) {
177176
return file.getName();
178177
}
179178
} catch (Exception e) {
180-
// Skip invalid files
179+
err.println("Warning: skipping unreadable file: " + file.getName());
181180
}
182181
}
183182
return null;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public Integer call() {
7171
entry.put("file", file.getName());
7272
entries.add(entry);
7373
} catch (Exception e) {
74-
// Skip files that aren't valid keystore JSON
74+
err.println("Warning: skipping unreadable file: " + file.getName());
7575
}
7676
}
7777

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.tron.common.utils.Utils;
99
import org.tron.core.exception.CipherException;
1010
import org.tron.keystore.Credentials;
11-
import org.tron.keystore.WalletUtils;
1211
import picocli.CommandLine.Command;
1312
import picocli.CommandLine.Model.CommandSpec;
1413
import picocli.CommandLine.Option;
@@ -53,8 +52,8 @@ public Integer call() {
5352

5453
boolean ecKey = !sm2;
5554
SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), ecKey);
56-
String fileName = WalletUtils.generateWalletFile(password, keyPair, keystoreDir, true);
57-
KeystoreCliUtils.setOwnerOnly(new File(keystoreDir, fileName), err);
55+
String fileName = KeystoreCliUtils.generateKeystoreFile(
56+
password, keyPair, keystoreDir, true, err);
5857

5958
String address = Credentials.create(keyPair).getAddress();
6059
if (json) {

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import java.io.PrintWriter;
88
import java.nio.charset.StandardCharsets;
99
import java.nio.file.Files;
10-
import java.nio.file.StandardCopyOption;
1110
import java.util.Arrays;
1211
import java.util.concurrent.Callable;
1312
import org.tron.common.crypto.SignInterface;
@@ -90,8 +89,8 @@ public Integer call() {
9089
+ " on separate lines.");
9190
return 1;
9291
}
93-
oldPassword = lines[0];
94-
newPassword = lines[1];
92+
oldPassword = KeystoreCliUtils.stripLineEndings(lines[0]);
93+
newPassword = KeystoreCliUtils.stripLineEndings(lines[1]);
9594
} finally {
9695
Arrays.fill(bytes, (byte) 0);
9796
}
@@ -153,15 +152,7 @@ public Integer call() {
153152
try {
154153
KeystoreCliUtils.setOwnerOnly(tempFile, err);
155154
MAPPER.writeValue(tempFile, newWalletFile);
156-
try {
157-
Files.move(tempFile.toPath(), keystoreFile.toPath(),
158-
StandardCopyOption.REPLACE_EXISTING,
159-
StandardCopyOption.ATOMIC_MOVE);
160-
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
161-
// Fallback for NFS, FAT32, cross-partition
162-
Files.move(tempFile.toPath(), keystoreFile.toPath(),
163-
StandardCopyOption.REPLACE_EXISTING);
164-
}
155+
KeystoreCliUtils.atomicMove(tempFile, keystoreFile);
165156
} catch (Exception e) {
166157
if (!tempFile.delete()) {
167158
err.println("Warning: could not delete temp file: "
@@ -204,7 +195,7 @@ private File findKeystoreByAddress(String targetAddress, PrintWriter err) {
204195
matches.add(file);
205196
}
206197
} catch (Exception e) {
207-
// Skip invalid files
198+
err.println("Warning: skipping unreadable file: " + file.getName());
208199
}
209200
}
210201
if (matches.size() > 1) {

0 commit comments

Comments
 (0)