mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-01-18 21:32:35 +01:00
TorUtils: new utility for Tor/Onion addresses
Move Tor/Onion related code there. Also adds tests.
This commit is contained in:
parent
ef3d5decc0
commit
591ff3a027
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
108
core/src/main/java/org/bitcoinj/crypto/internal/TorUtils.java
Normal file
108
core/src/main/java/org/bitcoinj/crypto/internal/TorUtils.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user