TorUtils: new utility for Tor/Onion addresses

Move Tor/Onion related code there.

Also adds tests.
This commit is contained in:
Andreas Schildbach 2023-08-09 18:04:54 +02:00
parent ef3d5decc0
commit 591ff3a027
4 changed files with 171 additions and 42 deletions

View File

@ -17,12 +17,11 @@
package org.bitcoinj.core; package org.bitcoinj.core;
import com.google.common.io.BaseEncoding;
import org.bitcoinj.base.VarInt; import org.bitcoinj.base.VarInt;
import org.bitcoinj.base.internal.Buffers; import org.bitcoinj.base.internal.Buffers;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.crypto.internal.CryptoUtils;
import org.bitcoinj.base.internal.ByteUtils; import org.bitcoinj.base.internal.ByteUtils;
import org.bitcoinj.crypto.internal.TorUtils;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.Inet4Address; import java.net.Inet4Address;
@ -58,7 +57,6 @@ public class PeerAddress {
private final Services services; private final Services services;
private final Instant time; private final Instant time;
private static final BaseEncoding BASE32 = BaseEncoding.base32().omitPadding().lowerCase();
private static final byte[] ONIONCAT_PREFIX = ByteUtils.parseHex("fd87d87eeb43"); private static final byte[] ONIONCAT_PREFIX = ByteUtils.parseHex("fd87d87eeb43");
// BIP-155 reserved network IDs, see: https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki // BIP-155 reserved network IDs, see: https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
@ -191,18 +189,13 @@ public class PeerAddress {
case TORV2: case TORV2:
if (addrLen != 10) if (addrLen != 10)
throw new ProtocolException("invalid length of TORv2 address: " + addrLen); throw new ProtocolException("invalid length of TORv2 address: " + addrLen);
hostname = BASE32.encode(addrBytes) + ".onion"; hostname = TorUtils.encodeOnionUrlV2(addrBytes);
addr = null; addr = null;
break; break;
case TORV3: case TORV3:
if (addrLen != 32) if (addrLen != 32)
throw new ProtocolException("invalid length of TORv3 address: " + addrLen); throw new ProtocolException("invalid length of TORv3 address: " + addrLen);
byte torVersion = 0x03; hostname = TorUtils.encodeOnionUrlV3(addrBytes);
byte[] onionAddress = new byte[35];
System.arraycopy(addrBytes, 0, onionAddress, 0, 32);
System.arraycopy(CryptoUtils.onionChecksum(addrBytes, torVersion), 0, onionAddress, 32, 2);
onionAddress[34] = torVersion;
hostname = BASE32.encode(onionAddress) + ".onion";
addr = null; addr = null;
break; break;
case I2P: case I2P:
@ -222,7 +215,7 @@ public class PeerAddress {
byte[] addrBytes = Buffers.readBytes(payload, 16); byte[] addrBytes = Buffers.readBytes(payload, 16);
if (Arrays.equals(ONIONCAT_PREFIX, Arrays.copyOf(addrBytes, 6))) { if (Arrays.equals(ONIONCAT_PREFIX, Arrays.copyOf(addrBytes, 6))) {
byte[] onionAddress = Arrays.copyOfRange(addrBytes, 6, 16); byte[] onionAddress = Arrays.copyOfRange(addrBytes, 6, 16);
hostname = BASE32.encode(onionAddress) + ".onion"; hostname = TorUtils.encodeOnionUrlV2(onionAddress);
} else { } else {
addr = getByAddress(addrBytes); addr = getByAddress(addrBytes);
hostname = null; hostname = null;
@ -273,24 +266,17 @@ public class PeerAddress {
throw new IllegalStateException(); throw new IllegalStateException();
} }
} else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) { } else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) {
byte[] onionAddress = BASE32.decode(hostname.substring(0, hostname.length() - 6)); byte[] onionAddress = TorUtils.decodeOnionUrl(hostname);
if (onionAddress.length == 10) { if (onionAddress.length == 10) {
// TORv2 // TORv2
buf.put((byte) 0x03); buf.put((byte) 0x03);
VarInt.of(10).write(buf); VarInt.of(10).write(buf);
buf.put(onionAddress); buf.put(onionAddress);
} else if (onionAddress.length == 32 + 2 + 1) { } else if (onionAddress.length == 32) {
// TORv3 // TORv3
buf.put((byte) 0x04); buf.put((byte) 0x04);
VarInt.of(32).write(buf); VarInt.of(32).write(buf);
byte[] pubkey = Arrays.copyOfRange(onionAddress, 0, 32); buf.put(onionAddress);
byte[] checksum = Arrays.copyOfRange(onionAddress, 32, 34);
byte torVersion = onionAddress[34];
if (torVersion != 0x03)
throw new IllegalStateException("version");
if (!Arrays.equals(checksum, CryptoUtils.onionChecksum(pubkey, torVersion)))
throw new IllegalStateException("checksum");
buf.put(pubkey);
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
@ -305,7 +291,7 @@ public class PeerAddress {
byte[] ipBytes = addr.getAddress(); byte[] ipBytes = addr.getAddress();
buf.put(mapIntoIPv6(ipBytes)); buf.put(mapIntoIPv6(ipBytes));
} else if (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)); byte[] onionAddress = TorUtils.decodeOnionUrl(hostname);
if (onionAddress.length == 10) { if (onionAddress.length == 10) {
// TORv2 // TORv2
buf.put(ONIONCAT_PREFIX); buf.put(ONIONCAT_PREFIX);
@ -358,13 +344,9 @@ public class PeerAddress {
throw new IllegalStateException(); throw new IllegalStateException();
} }
} else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) { } else if (addr == null && hostname != null && hostname.toLowerCase(Locale.ROOT).endsWith(".onion")) {
byte[] onionAddress = BASE32.decode(hostname.substring(0, hostname.length() - 6)); byte[] onionAddress = TorUtils.decodeOnionUrl(hostname);
if (onionAddress.length == 10) { if (onionAddress.length == 10 || onionAddress.length == 32) {
// TORv2 size += VarInt.sizeOf(onionAddress.length) + onionAddress.length;
size += VarInt.sizeOf(10) + 10;
} else if (onionAddress.length == 32 + 2 + 1) {
// TORv3
size += VarInt.sizeOf(32) + 32;
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }

View File

@ -48,17 +48,4 @@ public class CryptoUtils {
digest.doFinal(ripmemdHash, 0); digest.doFinal(ripmemdHash, 0);
return ripmemdHash; return ripmemdHash;
} }
/**
* Calculate TOR Onion Checksum (used by PeerAddress)
*/
public static 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);
}
} }

View File

@ -0,0 +1,108 @@
/*
* Copyright by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.crypto.internal;
import com.google.common.io.BaseEncoding;
import org.bouncycastle.jcajce.provider.digest.SHA3;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Locale;
import static org.bitcoinj.base.internal.Preconditions.checkArgument;
/**
* Utilities for encoding and decoding Onion addresses.
*/
public class TorUtils {
private static final BaseEncoding BASE32 = BaseEncoding.base32().omitPadding().lowerCase();
/**
* Encode an Onion URL from a Tor V2 address.
* <p>
* See <a href="https://github.com/torproject/torspec/blob/main/address-spec.txt">address-spec.txt</a>
*
* @param onionAddrBytes Tor V2 address to encode
* @return encoded Onion URL
*/
public static String encodeOnionUrlV2(byte[] onionAddrBytes) {
checkArgument(onionAddrBytes.length == 10);
return BASE32.encode(onionAddrBytes) + ".onion";
}
/**
* Encode an Onion URL from a Tor V3 address (pubkey).
* <p>
* See <a href="https://github.com/torproject/torspec/blob/main/address-spec.txt">address-spec.txt</a>
*
* @param onionAddrBytes Tor V3 address to encode
* @return encoded Onion URL
*/
public static String encodeOnionUrlV3(byte[] onionAddrBytes) {
checkArgument(onionAddrBytes.length == 32);
byte torVersion = 0x03;
byte[] onionAddress = new byte[35];
System.arraycopy(onionAddrBytes, 0, onionAddress, 0, 32);
System.arraycopy(onionChecksum(onionAddrBytes, torVersion), 0, onionAddress, 32, 2);
onionAddress[34] = torVersion;
return BASE32.encode(onionAddress) + ".onion";
}
/**
* Decode an Onion URL into a Tor V2 or V3 address.
* <p>
* See <a href="https://github.com/torproject/torspec/blob/main/address-spec.txt">address-spec.txt</a>
*
* @param onionUrl Onion URL to decode
* @return decoded Tor address
*/
public static byte[] decodeOnionUrl(String onionUrl) {
if (!onionUrl.toLowerCase(Locale.ROOT).endsWith(".onion"))
throw new IllegalArgumentException("not an onion URL: " + onionUrl);
byte[] onionAddress = BASE32.decode(onionUrl.substring(0, onionUrl.length() - 6));
if (onionAddress.length == 10) {
// TORv2
return onionAddress;
} else if (onionAddress.length == 32 + 2 + 1) {
// TORv3
byte[] pubkey = Arrays.copyOfRange(onionAddress, 0, 32);
byte[] checksum = Arrays.copyOfRange(onionAddress, 32, 34);
byte torVersion = onionAddress[34];
if (torVersion != 0x03)
throw new IllegalArgumentException("unknown version: " + onionUrl);
if (!Arrays.equals(checksum, onionChecksum(pubkey, torVersion)))
throw new IllegalArgumentException("bad checksum: " + onionUrl);
return pubkey;
} else {
throw new IllegalArgumentException("unrecognizable length: " + onionUrl);
}
}
/**
* Calculate Onion Checksum
*/
private static 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);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.crypto.internal;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
public class TorUtilsTest {
@Test
public void roundtripOnionV2() {
byte[] onionAddr = new byte[10];
byte[] onionAddrCopy = TorUtils.decodeOnionUrl(TorUtils.encodeOnionUrlV2(onionAddr));
assertArrayEquals(onionAddr, onionAddrCopy);
}
@Test
public void roundtripOnionV3() {
byte[] onionAddr = new byte[32];
byte[] onionAddrCopy = TorUtils.decodeOnionUrl(TorUtils.encodeOnionUrlV3(onionAddr));
assertArrayEquals(onionAddr, onionAddrCopy);
}
@Test(expected = IllegalArgumentException.class)
public void encodeOnionUrlV2_badLength() {
TorUtils.encodeOnionUrlV2(new byte[11]);
}
@Test(expected = IllegalArgumentException.class)
public void encodeOnionUrlV3_badLength() {
TorUtils.encodeOnionUrlV2(new byte[33]);
}
@Test(expected = IllegalArgumentException.class)
public void decodeOnionUrl_badLength() {
TorUtils.decodeOnionUrl("aaa.onion");
}
}