From fd85807422f6e986229bb40018b72f7210e42361 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Fri, 23 Apr 2021 15:02:15 +0200 Subject: [PATCH] PeerAddress: Support Tor hidden service addresses. --- .../java/org/bitcoinj/core/PeerAddress.java | 75 ++++++++++++++++++- .../bitcoinj/core/AddressV1MessageTest.java | 19 ++++- .../bitcoinj/core/AddressV2MessageTest.java | 34 ++++++++- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/PeerAddress.java b/core/src/main/java/org/bitcoinj/core/PeerAddress.java index 9fda06fe3..adbabd370 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerAddress.java +++ b/core/src/main/java/org/bitcoinj/core/PeerAddress.java @@ -17,6 +17,8 @@ package org.bitcoinj.core; +import com.google.common.io.BaseEncoding; +import org.bouncycastle.jcajce.provider.digest.SHA3; import java.io.IOException; import java.io.OutputStream; @@ -26,6 +28,9 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Locale; import java.util.Objects; import static com.google.common.base.Preconditions.checkNotNull; @@ -46,6 +51,9 @@ public class PeerAddress extends ChildMessage { private BigInteger services; private long time; + private static final BaseEncoding BASE32 = BaseEncoding.base32().lowerCase(); + private static final byte[] ONIONCAT_PREFIX = Utils.HEX.decode("fd87d87eeb43"); + /** * Construct a peer address from a serialized payload. * @param params NetworkParameters object. @@ -138,6 +146,28 @@ public class PeerAddress extends ChildMessage { } else { throw new IllegalStateException(); } + } else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) { + byte[] onionAddress = BASE32.decode(hostname.substring(0, hostname.length() - 6)); + if (onionAddress.length == 10) { + // TORv2 + stream.write(0x03); + stream.write(new VarInt(10).encode()); + stream.write(onionAddress); + } else if (onionAddress.length == 32 + 2 + 1) { + // TORv3 + stream.write(0x04); + stream.write(new VarInt(32).encode()); + byte[] pubkey = Arrays.copyOfRange(onionAddress, 0, 32); + byte[] checksum = Arrays.copyOfRange(onionAddress, 32, 34); + byte torVersion = onionAddress[34]; + if (torVersion != 0x03) + throw new IllegalStateException("version"); + if (!Arrays.equals(checksum, onionChecksum(pubkey, torVersion))) + throw new IllegalStateException("checksum"); + stream.write(pubkey); + } else { + throw new IllegalStateException(); + } } else { throw new IllegalStateException(); } @@ -155,6 +185,15 @@ public class PeerAddress extends ChildMessage { ipBytes = v6addr; } stream.write(ipBytes); + } 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 + stream.write(ONIONCAT_PREFIX); + stream.write(onionAddress); + } else { + throw new IllegalStateException(); + } } else { throw new IllegalStateException(); } @@ -197,6 +236,23 @@ public class PeerAddress extends ChildMessage { throw new ProtocolException("invalid length of IPv6 address: " + addrLen); addr = getByAddress(addrBytes); hostname = null; + } else if (networkId == 0x03) { + // TORv2 + if (addrLen != 10) + throw new ProtocolException("invalid length of TORv2 address: " + addrLen); + hostname = BASE32.encode(addrBytes) + ".onion"; + addr = null; + } else if (networkId == 0x04) { + // 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"; + addr = null; } else { // ignore unknown network IDs addr = null; @@ -207,8 +263,13 @@ public class PeerAddress extends ChildMessage { length += 8; byte[] addrBytes = readBytes(16); length += 16; - addr = getByAddress(addrBytes); - hostname = null; + if (Arrays.equals(ONIONCAT_PREFIX, Arrays.copyOf(addrBytes, 6))) { + byte[] onionAddress = Arrays.copyOfRange(addrBytes, 6, 16); + hostname = BASE32.encode(onionAddress) + ".onion"; + } else { + addr = getByAddress(addrBytes); + hostname = null; + } } port = Utils.readUint16BE(payload, cursor); cursor += 2; @@ -223,6 +284,16 @@ public class PeerAddress extends ChildMessage { } } + private byte[] onionChecksum(byte[] pubkey, byte version) { + if (pubkey.length != 32) + throw new IllegalArgumentException(); + SHA3.Digest256 digest256 = new SHA3.Digest256(); + digest256.update(".onion checksum".getBytes(StandardCharsets.US_ASCII)); + digest256.update(pubkey); + digest256.update(version); + return Arrays.copyOf(digest256.digest(), 2); + } + public String getHostname() { return hostname; } diff --git a/core/src/test/java/org/bitcoinj/core/AddressV1MessageTest.java b/core/src/test/java/org/bitcoinj/core/AddressV1MessageTest.java index 07db7e485..f773e4284 100644 --- a/core/src/test/java/org/bitcoinj/core/AddressV1MessageTest.java +++ b/core/src/test/java/org/bitcoinj/core/AddressV1MessageTest.java @@ -31,9 +31,9 @@ import static org.junit.Assert.assertTrue; public class AddressV1MessageTest { private static final NetworkParameters UNITTEST = UnitTestParams.get(); - // mostly copied from src/test/netbase_tests.cpp#stream_addrv1_hex + // mostly copied from src/test/netbase_tests.cpp#stream_addrv1_hex and src/test/net_tests.cpp private static final String MESSAGE_HEX = - "03" // number of entries + "04" // number of entries + "61bc6649" // time, Fri Jan 9 02:54:25 UTC 2009 + "0000000000000000" // service flags, NODE_NONE @@ -48,14 +48,19 @@ public class AddressV1MessageTest { + "ffffffff" // time, Sun Feb 7 06:28:15 UTC 2106 + "4804000000000000" // service flags, NODE_WITNESS | NODE_COMPACT_FILTERS | NODE_NETWORK_LIMITED + "00000000000000000000000000000001" // address, fixed 16 bytes (IPv6) - + "f1f2"; // port + + "f1f2" // port + + + "00000000" // time + + "0000000000000000" // service flags, NODE_NONE + + "fd87d87eeb43f1f2f3f4f5f6f7f8f9fa" // address, fixed 16 bytes (TORv2) + + "0000"; // port @Test public void roundtrip() { AddressMessage message = new AddressV1Message(UNITTEST, HEX.decode(MESSAGE_HEX)); List addresses = message.getAddresses(); - assertEquals(3, addresses.size()); + assertEquals(4, addresses.size()); PeerAddress a0 = addresses.get(0); assertEquals("2009-01-09T02:54:25Z", Utils.dateTimeFormat(a0.getTime() * 1000)); assertEquals(0, a0.getServices().intValue()); @@ -78,6 +83,12 @@ public class AddressV1MessageTest { assertEquals("0:0:0:0:0:0:0:1", a2.getAddr().getHostAddress()); assertNull(a2.getHostname()); assertEquals(0xf1f2, a2.getPort()); + PeerAddress a3 = addresses.get(3); + assertEquals("1970-01-01T00:00:00Z", Utils.dateTimeFormat(a3.getTime() * 1000)); + assertEquals(0, a3.getServices().intValue()); + assertNull(a3.getAddr()); + assertEquals("6hzph5hv6337r6p2.onion", a3.getHostname()); + assertEquals(0, a3.getPort()); assertEquals(MESSAGE_HEX, HEX.encode(message.bitcoinSerialize())); } diff --git a/core/src/test/java/org/bitcoinj/core/AddressV2MessageTest.java b/core/src/test/java/org/bitcoinj/core/AddressV2MessageTest.java index bc0aaf1c4..321998962 100644 --- a/core/src/test/java/org/bitcoinj/core/AddressV2MessageTest.java +++ b/core/src/test/java/org/bitcoinj/core/AddressV2MessageTest.java @@ -31,9 +31,9 @@ import static org.junit.Assert.assertTrue; public class AddressV2MessageTest { private static final NetworkParameters UNITTEST = UnitTestParams.get(); - // mostly copied from src/test/netbase_tests.cpp#stream_addrv2_hex + // mostly copied from src/test/netbase_tests.cpp#stream_addrv2_hex and src/test/net_tests.cpp private static final String MESSAGE_HEX = - "03" // number of entries + "05" // number of entries + "61bc6649" // time, Fri Jan 9 02:54:25 UTC 2009 + "00" // service flags, COMPACTSIZE(NODE_NONE) @@ -54,14 +54,28 @@ public class AddressV2MessageTest { + "02" // network id, IPv6 + "10" // address length, COMPACTSIZE(16) + "00000000000000000000000000000001" // address - + "f1f2"; // port + + "f1f2" // port + + + "00000000" // time + + "00" // service flags, COMPACTSIZE(NODE_NONE) + + "03" // network id, TORv2 + + "0a" // address length, COMPACTSIZE(10) + + "f1f2f3f4f5f6f7f8f9fa" // address + + "0000" // port + + + "00000000" // time + + "00" // service flags, COMPACTSIZE(NODE_NONE) + + "04" // network id, TORv3 + + "20"// address length, COMPACTSIZE(32) + + "53cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd88" // address + + "0000"; // port @Test public void roundtrip() { AddressMessage message = new AddressV2Message(UNITTEST, HEX.decode(MESSAGE_HEX)); List addresses = message.getAddresses(); - assertEquals(3, addresses.size()); + assertEquals(5, addresses.size()); PeerAddress a0 = addresses.get(0); assertEquals("2009-01-09T02:54:25Z", Utils.dateTimeFormat(a0.getTime() * 1000)); assertEquals(0, a0.getServices().intValue()); @@ -84,6 +98,18 @@ public class AddressV2MessageTest { assertEquals("0:0:0:0:0:0:0:1", a2.getAddr().getHostAddress()); assertNull(a2.getHostname()); assertEquals(0xf1f2, a2.getPort()); + PeerAddress a3 = addresses.get(3); + assertEquals("1970-01-01T00:00:00Z", Utils.dateTimeFormat(a3.getTime() * 1000)); + assertEquals(0, a3.getServices().intValue()); + assertNull(a3.getAddr()); + assertEquals("6hzph5hv6337r6p2.onion", a3.getHostname()); + assertEquals(0, a3.getPort()); + PeerAddress a4 = addresses.get(4); + assertEquals("1970-01-01T00:00:00Z", Utils.dateTimeFormat(a4.getTime() * 1000)); + assertEquals(0, a4.getServices().intValue()); + assertNull(a4.getAddr()); + assertEquals("kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion", a4.getHostname()); + assertEquals(0, a4.getPort()); assertEquals(MESSAGE_HEX, HEX.encode(message.bitcoinSerialize())); }