Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 35 additions & 4 deletions src/main/java/org/tron/ledger/LedgerAddressUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ public static Map<String, String> getMultiImportAddress(List<String> 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()) {
Expand All @@ -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];
Expand All @@ -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()) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<HidDevice> hidDeviceList = new ArrayList<>();
HidDevice fidoDevice = null;
Expand Down
35 changes: 21 additions & 14 deletions src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ public class AliasResolutionException extends IllegalArgumentException {
public AliasResolutionException(String message) {
super(message);
}

@Override
public Throwable fillInStackTrace() {
return this;
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -102,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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,28 @@ 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").
// 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.getTronAddress(path, device);
matched = address.equals(deviceAddress);
String deviceAddress = LedgerAddressUtil.parseTronAddress(rawResponse);
matched = address.equals(deviceAddress) && !deviceAddress.isEmpty();
if (!matched) {
return null;
}
Expand Down Expand Up @@ -87,14 +100,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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down