Skip to content

Commit a4befc5

Browse files
committed
fix: harden standard cli ledger and aliases
1 parent fd17041 commit a4befc5

11 files changed

Lines changed: 106 additions & 26 deletions

File tree

src/main/java/org/tron/ledger/listener/LedgerEventListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void hidDataReceived(HidServicesEvent event) {
220220
println("This transaction has expired, please resign and submit again.");
221221
LedgerSignResult.updateState(
222222
TransactionSignManager.getInstance().getHidDevice().getPath()
223-
, transactionId, LedgerSignResult.SIGN_RESULT_CANCEL
223+
, transactionId, LedgerSignResult.SIGN_RESULT_TIMEOUT
224224
);
225225
}
226226
doLedgerSignEnd();

src/main/java/org/tron/ledger/wrapper/LedgerSignResult.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class LedgerSignResult {
1717
public static final String SIGN_RESULT_SIGNING = "signing";
1818
public static final String SIGN_RESULT_SUCCESS = "confirmed";
1919
public static final String SIGN_RESULT_CANCEL = "cancel";
20+
public static final String SIGN_RESULT_TIMEOUT = "timeout";
2021

2122
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
2223
private static final String DIRECTORY = "Ledger";

src/main/java/org/tron/walletcli/cli/aliases/AliasEntry.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ private AliasEntry(String name, AliasType type, byte[] address,
3232
if (source == null || source.trim().isEmpty()) {
3333
throw new IllegalArgumentException("source must not be blank");
3434
}
35+
if (type == AliasType.TOKEN && (decimals < 0 || decimals > 18)) {
36+
throw new IllegalArgumentException("token decimals must be between 0 and 18");
37+
}
3538
this.name = n;
3639
this.type = type;
3740
this.address = address.clone();

src/main/java/org/tron/walletcli/cli/aliases/AliasStoreLoader.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ public AliasStore loadBuiltin(NetType network) throws IOException {
5757
}
5858

5959
public AliasStore loadUser(NetType network) throws IOException {
60+
try {
61+
return loadUserOrThrow(network);
62+
} catch (IOException e) {
63+
warnFailed("read user alias file " + userFile(network).getPath(), e);
64+
return AliasStore.empty();
65+
} catch (RuntimeException e) {
66+
warnFailed("read user alias file " + userFile(network).getPath(), e);
67+
return AliasStore.empty();
68+
}
69+
}
70+
71+
/**
72+
* Loads the user alias file and propagates read/parse failures. Mutating commands use this
73+
* path so a malformed existing alias file is never treated as an empty store and overwritten.
74+
*/
75+
public AliasStore loadUserOrThrow(NetType network) throws IOException {
6076
File file = userFile(network);
6177
if (!file.exists()) {
6278
return AliasStore.empty();
@@ -65,12 +81,6 @@ public AliasStore loadUser(NetType network) throws IOException {
6581
try {
6682
reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
6783
return readStore(reader, "user");
68-
} catch (IOException e) {
69-
warnFailed("read user alias file " + file.getPath(), e);
70-
return AliasStore.empty();
71-
} catch (RuntimeException e) {
72-
warnFailed("read user alias file " + file.getPath(), e);
73-
return AliasStore.empty();
7484
} finally {
7585
if (reader != null) {
7686
closeQuietly(reader, "close user alias file " + file.getPath());

src/main/java/org/tron/walletcli/cli/commands/AliasCommands.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private static void registerAdd(CommandRegistry registry) {
6868
+ loader.networkName(network) + " and cannot be overridden", null);
6969
return;
7070
}
71-
AliasStore user = loader.loadUser(network);
71+
AliasStore user = loader.loadUserOrThrow(network);
7272
if (user.containsName(name)) {
7373
out.usageError("Alias already exists: " + name
7474
+ ". Use alias-remove first to replace it.", null);
@@ -103,7 +103,7 @@ private static void registerRemove(CommandRegistry registry) {
103103
return;
104104
}
105105

106-
AliasStore user = loader.loadUser(network);
106+
AliasStore user = loader.loadUserOrThrow(network);
107107
List<AliasEntry> kept = new ArrayList<AliasEntry>();
108108
boolean removed = false;
109109
for (AliasEntry entry : user.listAll()) {

src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ public interface SignStateReader {
5050
void markSigning(String devicePath, String txid);
5151

5252
/**
53-
* Marks the current transaction as canceled after the standard CLI timeout path has
54-
* stopped waiting for confirmation.
53+
* Marks the current transaction as canceled or aborted so it does not remain signing.
5554
*/
5655
void markCanceled(String devicePath, String txid);
56+
57+
/**
58+
* Marks the current transaction as timed out after the standard CLI stops waiting for
59+
* confirmation.
60+
*/
61+
void markTimedOut(String devicePath, String txid);
5762
}
5863

5964
/** Drives the actual APDU exchange and waits for the on-device button. */

src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public final class NonInteractiveLedgerSigner implements LedgerSigner {
2525
*/
2626
static final String STATE_SIGNING = "signing"; // SIGN_RESULT_SIGNING — in progress
2727
static final String STATE_CONFIRMED = "confirmed"; // SIGN_RESULT_SUCCESS — user confirmed
28-
static final String STATE_CANCEL = "cancel"; // SIGN_RESULT_CANCEL — rejected or late response
28+
static final String STATE_CANCEL = "cancel"; // SIGN_RESULT_CANCEL — user rejected/aborted
29+
static final String STATE_TIMEOUT = "timeout"; // SIGN_RESULT_TIMEOUT — timed out
2930

3031
/** APDU status word: Tron app is not open on the device. */
3132
private static final byte[] APDU_APP_IS_OPEN = new byte[] { 0x65, 0x11 };
@@ -102,13 +103,16 @@ public LedgerSignOutcome sign(Chain.Transaction transaction,
102103
byte[] apdu = executor.lastSendResultBytes();
103104
if (apdu != null && apdu.length > 0) {
104105
if (matches(apdu, APDU_APP_IS_OPEN)) {
106+
stateReader.markCanceled(device.path(), txid);
105107
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.APP_NOT_OPEN,
106108
"Open the Tron app on your Ledger device and try again");
107109
}
108110
if (matches(apdu, APDU_SIGN_BY_HASH)) {
111+
stateReader.markCanceled(device.path(), txid);
109112
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.SIGN_BY_HASH_DISABLED,
110113
"Enable 'Sign By Hash' in the Ledger Tron app settings and try again");
111114
}
115+
stateReader.markCanceled(device.path(), txid);
112116
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.SIGN_FAILED,
113117
"Ledger returned APDU error " + toHex(apdu));
114118
}
@@ -134,15 +138,16 @@ public LedgerSignOutcome sign(Chain.Transaction transaction,
134138
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.SIGN_FAILED,
135139
"Ledger reported confirmation but no signature was recorded");
136140
}
137-
// STATE_CANCEL covers both the user-pressed-reject case and the rare
138-
// "device responded after our 60s window" case. From the user's perspective
139-
// both are "the sign did not complete because the user did not confirm in time".
140141
if (STATE_CANCEL.equals(postState.get())) {
141142
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.USER_REJECTED,
142143
"Transaction was rejected on the Ledger device");
143144
}
145+
if (STATE_TIMEOUT.equals(postState.get())) {
146+
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.TIMEOUT,
147+
"Timed out waiting for confirmation on Ledger device");
148+
}
144149
if (STATE_SIGNING.equals(postState.get())) {
145-
stateReader.markCanceled(device.path(), txid);
150+
stateReader.markTimedOut(device.path(), txid);
146151
return LedgerSignOutcome.failure(LedgerSignOutcome.Status.TIMEOUT,
147152
"Timed out waiting for confirmation on Ledger device");
148153
}

src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public void markCanceled(String devicePath, String txid) {
6767
LedgerSignResult.updateState(
6868
devicePath, txid, LedgerSignResult.SIGN_RESULT_CANCEL);
6969
}
70+
71+
@Override
72+
public void markTimedOut(String devicePath, String txid) {
73+
LedgerSignResult.updateState(
74+
devicePath, txid, LedgerSignResult.SIGN_RESULT_TIMEOUT);
75+
}
7076
};
7177

7278
LedgerPorts.SignExecutor executor = new LedgerPorts.SignExecutor() {
@@ -76,15 +82,20 @@ public boolean executeSignListen(LedgerPorts.DeviceHandle device, Chain.Transact
7682
HidDevice raw = ((HidDeviceAdapter) device).delegate;
7783
LedgerEventListener listener = LedgerEventListener.getInstance();
7884
listener.setStandardCliQuiet(true);
79-
listener.setLedgerSignEnd(new AtomicBoolean(false));
80-
if (raw.isClosed()) {
81-
raw.open();
82-
}
83-
boolean accepted = listener.executeSignListen(raw, tx, path, gasfree);
84-
if (listener.getLastSendResultBytes() != null || !accepted) {
85+
try {
86+
listener.setLedgerSignEnd(new AtomicBoolean(false));
87+
if (raw.isClosed()) {
88+
raw.open();
89+
}
90+
boolean accepted = listener.executeSignListen(raw, tx, path, gasfree);
91+
if (listener.getLastSendResultBytes() != null || !accepted) {
92+
listener.setStandardCliQuiet(false);
93+
}
94+
return accepted;
95+
} catch (RuntimeException e) {
8596
listener.setStandardCliQuiet(false);
97+
throw e;
8698
}
87-
return accepted;
8899
}
89100

90101
@Override

src/test/java/org/tron/walletcli/cli/aliases/AliasEntryTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,14 @@ public void addressIsCopiedDefensively() {
3737
public void rejectsWrongAddressLength() {
3838
AliasEntry.token("USDT", new byte[20], 6, "builtin");
3939
}
40+
41+
@Test(expected = IllegalArgumentException.class)
42+
public void rejectsNegativeTokenDecimals() {
43+
AliasEntry.token("USDT", addr(), -1, "builtin");
44+
}
45+
46+
@Test(expected = IllegalArgumentException.class)
47+
public void rejectsTooLargeTokenDecimals() {
48+
AliasEntry.token("USDT", addr(), 19, "builtin");
49+
}
4050
}

src/test/java/org/tron/walletcli/cli/aliases/AliasStoreLoaderTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.tron.walletserver.WalletApi;
1414

1515
import static org.junit.Assert.assertEquals;
16+
import static org.junit.Assert.fail;
1617
import static org.junit.Assert.assertNotNull;
1718
import static org.junit.Assert.assertNull;
1819

@@ -92,6 +93,18 @@ public void malformedUserFileLoadsAsEmpty() throws Exception {
9293
assertEquals(0, store.listAll().size());
9394
}
9495

96+
@Test
97+
public void malformedUserFileThrowsForStrictLoad() throws Exception {
98+
writeUserFile(NetType.SHASTA, "{ broken json");
99+
100+
try {
101+
loader.loadUserOrThrow(NetType.SHASTA);
102+
fail("Expected malformed user alias file to fail strict load");
103+
} catch (IOException e) {
104+
assertNotNull(e.getMessage());
105+
}
106+
}
107+
95108
@Test
96109
public void layeredLoadKeepsBuiltinsAuthoritative() throws Exception {
97110
writeUserFile(NetType.MAIN, "{ \"entries\": ["

0 commit comments

Comments
 (0)