Skip to content

Commit

Permalink
Add Tor v3 onion address support and minor code clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
ripcurlx committed Sep 8, 2021
1 parent 8755d3b commit b7769e3
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 19 deletions.
88 changes: 69 additions & 19 deletions core/src/main/java/org/bitcoinj/core/PeerAddress.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class PeerAddress extends ChildMessage {

private static final BaseEncoding BASE32 = BaseEncoding.base32().lowerCase();
private static final byte[] ONIONCAT_PREFIX = Utils.HEX.decode("fd87d87eeb43");
private static final byte[] ONIONCAT_PREFIX_V2 = Utils.HEX.decode("fd87d87eeb44");
static final int MESSAGE_SIZE = 30;

/**
Expand All @@ -64,7 +65,7 @@ public class PeerAddress extends ChildMessage {
* @param payload Bitcoin protocol formatted byte array containing message content.
* @param offset The location of the first payload byte within the array.
* @param serializer the serializer to use for this message.
* @throws ProtocolException
* @throws ProtocolException if address format is incorrect
*/
public PeerAddress(NetworkParameters params, byte[] payload, int offset, Message parent, MessageSerializer serializer) throws ProtocolException {
super(params, payload, offset, parent, serializer, UNKNOWN_LENGTH);
Expand Down Expand Up @@ -109,7 +110,16 @@ public PeerAddress(NetworkParameters params, InetAddress addr) {
* InetAddress or a String hostname. If you want to connect to a .onion, set the hostname to the .onion address.
*/
public PeerAddress(NetworkParameters params, InetSocketAddress addr) {
this(params, addr.getAddress(), addr.getPort());
super(params);
InetAddress inetAddress = addr.getAddress();
if (inetAddress != null) {
this.addr = inetAddress;
} else {
this.hostname = checkNotNull(addr.getHostString());
}
this.port = addr.getPort();
this.services = BigInteger.ZERO;
length = NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion() > 31402 ? MESSAGE_SIZE : MESSAGE_SIZE - 4;
}

/**
Expand All @@ -119,7 +129,7 @@ public PeerAddress(NetworkParameters params, InetSocketAddress addr) {
*/
public PeerAddress(InetSocketAddress addr) {
InetAddress inetAddress = addr.getAddress();
if(inetAddress != null) {
if (inetAddress != null) {
this.addr = inetAddress;
} else {
this.hostname = checkNotNull(addr.getHostString());
Expand Down Expand Up @@ -185,7 +195,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException
} else {
throw new IllegalStateException();
}
} else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) {
} else if (hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) {
byte[] onionAddress = BASE32.decode(hostname.substring(0, hostname.length() - 6));
if (onionAddress.length == 10) {
// TORv2
Expand Down Expand Up @@ -230,6 +240,29 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException
// TORv2
stream.write(ONIONCAT_PREFIX);
stream.write(onionAddress);
} else if (onionAddress.length == 32 + 2 + 1) {
/*
TORv3 onion address
The onion address of a hidden service includes its identity public key, a
version field and a basic checksum. All this information is then base32
encoded as shown below:
onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2]
where:
- PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
- VERSION is a one byte version field (default value '\x03')
- ".onion checksum" is a constant string
- CHECKSUM is truncated to two bytes before inserting it in onion_address
The ONIONCAT_PREFIX_V2 is set to be able to associate the address accessed from the stream with the
correct protocol version.
TODO: No idea why exactly ONIONCAT_PREFIX was used to detect v1 addresses.
*/
stream.write(ONIONCAT_PREFIX_V2);
stream.write(Arrays.copyOfRange(onionAddress, 0, 32));
} else {
throw new IllegalStateException();
}
Expand Down Expand Up @@ -285,12 +318,7 @@ protected void parse() throws ProtocolException {
// TORv3
if (addrLen != 32)
throw new ProtocolException("invalid length of TORv3 address: " + addrLen);
byte torVersion = 0x03;
byte[] onionAddress = new byte[35];
System.arraycopy(addrBytes, 0, onionAddress, 0, 32);
System.arraycopy(onionChecksum(addrBytes, torVersion), 0, onionAddress, 32, 2);
onionAddress[34] = torVersion;
hostname = BASE32.encode(onionAddress) + ".onion";
setTorVersion3AddressAsHostname(addrBytes);
addr = null;
} else {
// ignore unknown network IDs
Expand All @@ -300,13 +328,25 @@ protected void parse() throws ProtocolException {
} else {
services = readUint64();
length += 8;
byte[] addrBytes = readBytes(16);
length += 16;
if (Arrays.equals(ONIONCAT_PREFIX, Arrays.copyOf(addrBytes, 6))) {
byte[] onionAddress = Arrays.copyOfRange(addrBytes, 6, 16);
hostname = BASE32.encode(onionAddress) + ".onion";
byte[] addrBytesPrefix = readBytes(6);
length += 6;
if (Arrays.equals(ONIONCAT_PREFIX, addrBytesPrefix)) {
byte[] addrBytes = readBytes(10);
length += 10;
hostname = BASE32.encode(addrBytes) + ".onion";
} else if (Arrays.equals(ONIONCAT_PREFIX_V2, addrBytesPrefix)) {
byte[] addrBytes = readBytes(32);
length += 32;

setTorVersion3AddressAsHostname(addrBytes);
} else {
addr = getByAddress(addrBytes);
byte[] addrBytes = readBytes(10);
length += 10;

byte[] address = new byte[addrBytesPrefix.length + addrBytes.length];
System.arraycopy(addrBytesPrefix, 0, address, 0, addrBytesPrefix.length);
System.arraycopy(addrBytes, 0, address, addrBytesPrefix.length, addrBytes.length);
addr = getByAddress(address);
hostname = null;
}
}
Expand All @@ -315,6 +355,16 @@ protected void parse() throws ProtocolException {
length += 2;
}

private void setTorVersion3AddressAsHostname(byte[] addrBytes) {
byte torVersion = 0x03;
byte[] onionAddress = new byte[32 + 2 + 1];
System.arraycopy(addrBytes, 0, onionAddress, 0, 32);
System.arraycopy(onionChecksum(addrBytes, torVersion), 0, onionAddress, 32, 2);
onionAddress[34] = torVersion;

hostname = BASE32.encode(onionAddress) + ".onion";
}

private static InetAddress getByAddress(byte[] addrBytes) {
try {
return InetAddress.getByAddress(addrBytes);
Expand Down Expand Up @@ -376,9 +426,9 @@ public boolean equals(Object o) {

if (port != that.port) return false;
if (time != that.time) return false;
if (addr != null ? !addr.equals(that.addr) : that.addr != null) return false;
if (hostname != null ? !hostname.equals(that.hostname) : that.hostname != null) return false;
return !(services != null ? !services.equals(that.services) : that.services != null);
if (!Objects.equals(addr, that.addr)) return false;
if (!Objects.equals(hostname, that.hostname)) return false;
return Objects.equals(services, that.services);
}

public boolean equalsIgnoringMetadata(Object o) {
Expand Down
40 changes: 40 additions & 0 deletions core/src/test/java/org/bitcoinj/core/PeerAddressTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,46 @@ public void roundtrip_ipv6_versionVariant() throws Exception {
assertEquals(-1, pa2.getTime());
}

@Test
public void testOnionHostname_addressV1Variant() {
PeerAddress pa = new PeerAddress(InetSocketAddress.createUnresolved("explorernuoc63nb.onion", 8333));
assertEquals("explorernuoc63nb.onion", pa.toSocketAddress().getHostString());
assertEquals("explorernuoc63nb.onion", pa.getHostname());
assertEquals(null, pa.getAddr());
assertEquals(8333, pa.toSocketAddress().getPort());
assertEquals(8333, pa.getPort());
PeerAddress pa2 = new PeerAddress(MainNetParams.get(), InetSocketAddress.createUnresolved("explorernuoc63nb.onion", 8333));
assertPeerAddressEqualsRegardlessOfTime(pa, pa2);
PeerAddress pa3 = new PeerAddress("explorernuoc63nb.onion", 8333);
assertPeerAddressEqualsRegardlessOfTime(pa, pa3);
PeerAddress pa4 = new PeerAddress(MainNetParams.get(), "explorernuoc63nb.onion", 8333);
assertPeerAddressEqualsRegardlessOfTime(pa, pa4);
byte[] serialized = pa.unsafeBitcoinSerialize();
MessageSerializer serializer = MAINNET.getDefaultSerializer().withProtocolVersion(0);
PeerAddress paFromSerialized = new PeerAddress(MainNetParams.get(), serialized, 0, null, serializer);
assertPeerAddressEqualsRegardlessOfTime(pa, paFromSerialized);
}

@Test
public void testOnionHostname_addressV2Variant() {
PeerAddress pa = new PeerAddress(InetSocketAddress.createUnresolved("vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", 8333));
assertEquals("vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", pa.toSocketAddress().getHostString());
assertEquals("vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", pa.getHostname());
assertEquals(null, pa.getAddr());
assertEquals(8333, pa.toSocketAddress().getPort());
assertEquals(8333, pa.getPort());
PeerAddress pa2 = new PeerAddress(MainNetParams.get(), InetSocketAddress.createUnresolved("vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", 8333));
assertPeerAddressEqualsRegardlessOfTime(pa, pa2);
PeerAddress pa3 = new PeerAddress("vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", 8333);
assertPeerAddressEqualsRegardlessOfTime(pa, pa3);
PeerAddress pa4 = new PeerAddress(MainNetParams.get(), "vxbn3fftrodph7xfgu4htm7rhijv2dgtlb26emmsa2cgbxgfwyw6jfyd.onion", 8333);
assertPeerAddressEqualsRegardlessOfTime(pa, pa4);
byte[] serialized = pa.unsafeBitcoinSerialize();
MessageSerializer serializer = MAINNET.getDefaultSerializer().withProtocolVersion(0);
PeerAddress paFromSerialized = new PeerAddress(MainNetParams.get(), serialized, 0, null, serializer);
assertPeerAddressEqualsRegardlessOfTime(pa, paFromSerialized);
}

private void assertPeerAddressEqualsRegardlessOfTime(PeerAddress pa, PeerAddress pa2) {
assertEquals(pa.getPort(), pa2.getPort());
assertEquals(pa.getAddr(), pa2.getAddr());
Expand Down

0 comments on commit b7769e3

Please sign in to comment.