From 0b61f7b7739a5c4f4d33c6571ff4242d8979ad17 Mon Sep 17 00:00:00 2001 From: Will <> Date: Tue, 19 May 2026 17:37:52 +0800 Subject: [PATCH 1/3] fix: ledger app-not-open detection and quiet-flag leak - Split LedgerAddressUtil.getTronAddress into getRawAddressResponse + parseTronAddress so callers can inspect APDU status words before parsing - Add LedgerPorts.AppNotOpenException for APDU 0x6511 (Tron app not open), distinct from a null return (device not found / address mismatch) - NonInteractiveLedgerSigner catches AppNotOpenException and returns APP_NOT_OPEN outcome instead of NOT_CONNECTED - ProductionLedgerPorts uses try-finally to unconditionally reset standardCliQuiet after executeSignListen returns, fixing permanent stdout suppression when device times out without a HID callback - AliasResolutionException overrides fillInStackTrace as a no-op to avoid unnecessary stack capture on control-flow exceptions Co-Authored-By: Claude Sonnet 4.5 --- .../org/tron/ledger/LedgerAddressUtil.java | 39 +++++++++++++++++-- .../cli/aliases/AliasResolutionException.java | 5 +++ .../walletcli/cli/ledger/LedgerPorts.java | 11 ++++++ .../ledger/NonInteractiveLedgerSigner.java | 3 ++ .../cli/ledger/ProductionLedgerPorts.java | 24 +++++++----- 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/tron/ledger/LedgerAddressUtil.java b/src/main/java/org/tron/ledger/LedgerAddressUtil.java index 4676eed24..8a7db7a6e 100644 --- a/src/main/java/org/tron/ledger/LedgerAddressUtil.java +++ b/src/main/java/org/tron/ledger/LedgerAddressUtil.java @@ -73,7 +73,12 @@ public static Map getMultiImportAddress(List paths, HidD return addressMap; } - public static String getTronAddress(String path, HidDevice hidDevice) { + /** + * Sends the "get address" APDU and returns the raw response bytes without parsing. + * Returns {@code null} on transport failure. Callers can inspect error status words + * (e.g. {@code 0x6511} = Tron app not open) before falling through to address parsing. + */ + public static byte[] getRawAddressResponse(String path, HidDevice hidDevice) { try { byte[] apdu = ApduMessageBuilder.buildTronAddressApduMessage(path); if (DebugConfig.isDebugEnabled()) { @@ -83,11 +88,25 @@ public static String getTronAddress(String path, HidDevice hidDevice) { if (DebugConfig.isDebugEnabled()) { System.out.println("Get Address Response: " + CommonUtil.bytesToHex(result)); } - if (LedgerConstant.LEDGER_LOCK.equalsIgnoreCase(CommonUtil.bytesToHex(result))) { - System.out.println(ANSI_RED + "Ledger is locked, please unlock it first"+ ANSI_RESET); - return EMPTY; + return result; + } catch (Exception e) { + if (DebugConfig.isDebugEnabled()) { + e.printStackTrace(); } + return null; + } + } + /** Parses a Tron Base58 address from a raw "get address" APDU response. Returns {@code ""} on any parse failure. */ + public static String parseTronAddress(byte[] result) { + if (result == null || result.length < 2) { + return EMPTY; + } + if (LedgerConstant.LEDGER_LOCK.equalsIgnoreCase(CommonUtil.bytesToHex(result))) { + System.out.println(ANSI_RED + "Ledger is locked, please unlock it first" + ANSI_RESET); + return EMPTY; + } + try { int offset = 0; int publicKeyLength = result[offset++] & 0xFF; byte[] publicKey = new byte[publicKeyLength]; @@ -98,6 +117,18 @@ public static String getTronAddress(String path, HidDevice hidDevice) { byte[] addressBytes = new byte[addressLength]; System.arraycopy(result, offset, addressBytes, 0, addressLength); return new String(addressBytes); + } catch (Exception e) { + if (DebugConfig.isDebugEnabled()) { + e.printStackTrace(); + } + return EMPTY; + } + } + + public static String getTronAddress(String path, HidDevice hidDevice) { + try { + byte[] result = getRawAddressResponse(path, hidDevice); + return parseTronAddress(result); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); if (DebugConfig.isDebugEnabled()) { diff --git a/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java b/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java index 536e67636..9b75314ea 100644 --- a/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java +++ b/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java @@ -4,4 +4,9 @@ public class AliasResolutionException extends IllegalArgumentException { public AliasResolutionException(String message) { super(message); } + + @Override + public Throwable fillInStackTrace() { + return this; + } } diff --git a/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java b/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java index 264588278..04775128a 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java @@ -93,4 +93,15 @@ public interface SignResultReader { public interface ContractSupport { boolean canSign(Chain.Transaction transaction); } + + /** + * Thrown by {@link HidDeviceFinder#find} when the Ledger device is physically connected + * but the Tron app is not open (APDU status word {@code 0x6511}). + * Distinct from a plain {@code null} return (device not found / address mismatch). + */ + public static final class AppNotOpenException extends RuntimeException { + public AppNotOpenException(String message) { + super(message); + } + } } diff --git a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java index 871dcdf08..a7cb11f23 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java @@ -68,6 +68,9 @@ public LedgerSignOutcome sign(Chain.Transaction transaction, try (SystemOutSuppressor ignored = SystemOutSuppressor.capture()) { try { device = finder.find(address, bip44Path); + } catch (LedgerPorts.AppNotOpenException e) { + return LedgerSignOutcome.failure(LedgerSignOutcome.Status.APP_NOT_OPEN, + e.getMessage()); } catch (RuntimeException e) { return LedgerSignOutcome.failure(LedgerSignOutcome.Status.NOT_CONNECTED, "HID transport failure: " + e.getMessage()); diff --git a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java index 2d1b417a8..b0b4c46e6 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java @@ -31,8 +31,16 @@ public static NonInteractiveLedgerSigner buildSigner(OutputFormatter formatter) if (device.isClosed()) { device.open(); } - String deviceAddress = LedgerAddressUtil.getTronAddress(path, device); - matched = address.equals(deviceAddress); + byte[] rawResponse = LedgerAddressUtil.getRawAddressResponse(path, device); + // 0x6511: Tron app is not open on the device (ISO 7816-4 "conditions not satisfied"). + // Distinguish from a genuine address mismatch so the caller can surface the right error. + if (rawResponse != null && rawResponse.length == 2 + && (rawResponse[0] & 0xFF) == 0x65 && (rawResponse[1] & 0xFF) == 0x11) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + } + String deviceAddress = LedgerAddressUtil.parseTronAddress(rawResponse); + matched = address.equals(deviceAddress) && !deviceAddress.isEmpty(); if (!matched) { return null; } @@ -87,14 +95,12 @@ public boolean executeSignListen(LedgerPorts.DeviceHandle device, Chain.Transact if (raw.isClosed()) { raw.open(); } - boolean accepted = listener.executeSignListen(raw, tx, path, gasfree); - if (listener.getLastSendResultBytes() != null || !accepted) { - listener.setStandardCliQuiet(false); - } - return accepted; - } catch (RuntimeException e) { + return listener.executeSignListen(raw, tx, path, gasfree); + } finally { + // Always reset: executeSignListen blocks until the 60-second wait completes, + // so by the time we return, the HID callback has either already reset this flag + // or it never will (silent timeout / device disconnect). listener.setStandardCliQuiet(false); - throw e; } } From bb2edd1ab9b0f8014658ff6003d532a9383b4b54 Mon Sep 17 00:00:00 2001 From: Will <> Date: Wed, 20 May 2026 12:08:45 +0800 Subject: [PATCH 2/3] fix: correctly detect app-not-open when Ledger HID open() fails On some OS/firmware combinations, HidDevice.open() returns false when the Tron app is not running, causing getLedgerHidDevice() to return null and the signer to report NOT_CONNECTED instead of APP_NOT_OPEN. - Add HidServicesWrapper.hasAnyLedgerAttached() to distinguish "no device" from "device present but not openable" - Throw AppNotOpenException in ProductionLedgerPorts when getHidDevice() returns null but a Ledger is physically attached - Also check the return value of the second device.open() call (B2 path) which was previously ignored, causing the same misclassification - Add test: returnsAppNotOpenWhenFinderThrowsAppNotOpenException Co-Authored-By: Claude Sonnet 4.5 --- .../org/tron/ledger/wrapper/HidServicesWrapper.java | 5 +++++ .../walletcli/cli/ledger/ProductionLedgerPorts.java | 9 +++++++-- .../cli/ledger/NonInteractiveLedgerSignerTest.java | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java b/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java index dccce0216..aae497e92 100644 --- a/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java +++ b/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java @@ -62,6 +62,11 @@ public HidServices initHidServices() { return hs; } + public boolean hasAnyLedgerAttached() { + return getHidServices().getAttachedHidDevices().stream() + .anyMatch(d -> d.getVendorId() == LEDGER_VENDOR_ID); + } + public static HidDevice getLedgerHidDevice(HidServices hidServices, String address, String path) { List hidDeviceList = new ArrayList<>(); HidDevice fidoDevice = null; diff --git a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java index b0b4c46e6..7746e7fe0 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java @@ -24,12 +24,17 @@ public static NonInteractiveLedgerSigner buildSigner(OutputFormatter formatter) LedgerPorts.HidDeviceFinder finder = (address, path) -> { HidDevice device = HidServicesWrapper.getInstance().getHidDevice(address, path); if (device == null) { + if (HidServicesWrapper.getInstance().hasAnyLedgerAttached()) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + } return null; } boolean matched = false; try { - if (device.isClosed()) { - device.open(); + if (device.isClosed() && !device.open()) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); } byte[] rawResponse = LedgerAddressUtil.getRawAddressResponse(path, device); // 0x6511: Tron app is not open on the device (ISO 7816-4 "conditions not satisfied"). diff --git a/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java b/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java index c566bb5f1..7262f0f71 100644 --- a/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java +++ b/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java @@ -103,6 +103,17 @@ public void returnsNotConnectedWhenFinderThrows() { Assert.assertTrue(r.getMessage().contains("transport boom")); } + @Test + public void returnsAppNotOpenWhenFinderThrowsAppNotOpenException() { + // Simulates ProductionLedgerPorts detecting that a Ledger is physically attached + // (hasAnyLedgerAttached = true) but HID open() failed because the Tron app is not running. + finder.toThrow = new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + LedgerSignOutcome r = signNonGasfree(); + Assert.assertEquals(LedgerSignOutcome.Status.APP_NOT_OPEN, r.getStatus()); + Assert.assertTrue(r.getMessage().contains("Tron app")); + } + @Test public void returnsUnsupportedContractBeforeDeviceLookup() { contractSupport.canSign = false; From 07cb5d1af4b1a1041f3e19f947ebfe3b516d5ef7 Mon Sep 17 00:00:00 2001 From: Will <> Date: Wed, 20 May 2026 14:02:58 +0800 Subject: [PATCH 3/3] fix: zero password buffers in StdinPasswordReader and rename misleading APDU constant - StdinPasswordReader.readAll wraps the read/parse in try-finally and Arrays.fill the chunk and bytes buffers before returning, matching the defensive pattern already used in StandardCliRunner.authenticate. The returned String still holds the password in its own char[], but the intermediate byte arrays no longer linger on the heap until GC. - Rename APDU_APP_IS_OPEN to APDU_APP_NOT_OPEN. The Javadoc and the error message ("Open the Tron app on your Ledger device") already treat 0x6511 as the "not open" signal; the identifier now matches. Co-Authored-By: Claude Opus 4.7 --- .../walletcli/cli/StdinPasswordReader.java | 35 +++++++++++-------- .../ledger/NonInteractiveLedgerSigner.java | 4 +-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java b/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java index d1a30ed96..2126f387e 100644 --- a/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java +++ b/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; /** * Reads MASTER_PASSWORD from an {@link InputStream} (typically {@code System.in}) once and caches @@ -37,28 +38,34 @@ public synchronized String get() { private String readAll() { ByteArrayOutputStream buf = new ByteArrayOutputStream(); byte[] chunk = new byte[256]; + byte[] bytes = null; try { int n; while ((n = in.read(chunk)) != -1) { buf.write(chunk, 0, n); } + if (buf.size() == 0) { + return null; + } + bytes = buf.toByteArray(); + int len = bytes.length; + if (len > 0 && bytes[len - 1] == '\n') { + len--; + if (len > 0 && bytes[len - 1] == '\r') { + len--; + } + } + if (len == 0) { + return null; + } + return new String(bytes, 0, len, StandardCharsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("Failed to read password from stdin: " + e.getMessage(), e); - } - if (buf.size() == 0) { - return null; - } - byte[] bytes = buf.toByteArray(); - int len = bytes.length; - if (len > 0 && bytes[len - 1] == '\n') { - len--; - if (len > 0 && bytes[len - 1] == '\r') { - len--; + } finally { + Arrays.fill(chunk, (byte) 0); + if (bytes != null) { + Arrays.fill(bytes, (byte) 0); } } - if (len == 0) { - return null; - } - return new String(bytes, 0, len, StandardCharsets.UTF_8); } } diff --git a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java index a7cb11f23..959b88272 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java @@ -29,7 +29,7 @@ public final class NonInteractiveLedgerSigner implements LedgerSigner { static final String STATE_TIMEOUT = "timeout"; // SIGN_RESULT_TIMEOUT — timed out /** APDU status word: Tron app is not open on the device. */ - private static final byte[] APDU_APP_IS_OPEN = new byte[] { 0x65, 0x11 }; + private static final byte[] APDU_APP_NOT_OPEN = new byte[] { 0x65, 0x11 }; /** APDU status word: "Sign By Hash" setting is not enabled. */ private static final byte[] APDU_SIGN_BY_HASH = new byte[] { 0x6a, (byte) 0x8c }; @@ -105,7 +105,7 @@ public LedgerSignOutcome sign(Chain.Transaction transaction, byte[] apdu = executor.lastSendResultBytes(); if (apdu != null && apdu.length > 0) { - if (matches(apdu, APDU_APP_IS_OPEN)) { + if (matches(apdu, APDU_APP_NOT_OPEN)) { stateReader.markCanceled(device.path(), txid); return LedgerSignOutcome.failure(LedgerSignOutcome.Status.APP_NOT_OPEN, "Open the Tron app on your Ledger device and try again");