PeerAddress: Support Tor hidden service addresses.

This commit is contained in:
Andreas Schildbach 2021-04-23 15:02:15 +02:00
parent d511effbce
commit fd85807422
3 changed files with 118 additions and 10 deletions

View File

@ -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;
}

View File

@ -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<PeerAddress> 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()));
}

View File

@ -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<PeerAddress> 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()));
}