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;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.bitcoinj.base.VarInt;
|
||||
import org.bitcoinj.base.internal.Buffers;
|
||||
import org.bitcoinj.base.internal.TimeUtils;
|
||||
import org.bitcoinj.crypto.internal.CryptoUtils;
|
||||
import org.bitcoinj.base.internal.ByteUtils;
|
||||
import org.bitcoinj.crypto.internal.TorUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.Inet4Address;
|
||||
@ -58,7 +57,6 @@ public class PeerAddress {
|
||||
private final Services services;
|
||||
private final Instant time;
|
||||
|
||||
private static final BaseEncoding BASE32 = BaseEncoding.base32().omitPadding().lowerCase();
|
||||
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
|
||||
@ -191,18 +189,13 @@ public class PeerAddress {
|
||||
case TORV2:
|
||||
if (addrLen != 10)
|
||||
throw new ProtocolException("invalid length of TORv2 address: " + addrLen);
|
||||
hostname = BASE32.encode(addrBytes) + ".onion";
|
||||
hostname = TorUtils.encodeOnionUrlV2(addrBytes);
|
||||
addr = null;
|
||||
break;
|
||||
case 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(CryptoUtils.onionChecksum(addrBytes, torVersion), 0, onionAddress, 32, 2);
|
||||
onionAddress[34] = torVersion;
|
||||
hostname = BASE32.encode(onionAddress) + ".onion";
|
||||
hostname = TorUtils.encodeOnionUrlV3(addrBytes);
|
||||
addr = null;
|
||||
break;
|
||||
case I2P:
|
||||
@ -222,7 +215,7 @@ public class PeerAddress {
|
||||
byte[] addrBytes = Buffers.readBytes(payload, 16);
|
||||
if (Arrays.equals(ONIONCAT_PREFIX, Arrays.copyOf(addrBytes, 6))) {
|
||||
byte[] onionAddress = Arrays.copyOfRange(addrBytes, 6, 16);
|
||||
hostname = BASE32.encode(onionAddress) + ".onion";
|
||||
hostname = TorUtils.encodeOnionUrlV2(onionAddress);
|
||||
} else {
|
||||
addr = getByAddress(addrBytes);
|
||||
hostname = null;
|
||||
@ -273,24 +266,17 @@ public class PeerAddress {
|
||||
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));
|
||||
byte[] onionAddress = TorUtils.decodeOnionUrl(hostname);
|
||||
if (onionAddress.length == 10) {
|
||||
// TORv2
|
||||
buf.put((byte) 0x03);
|
||||
VarInt.of(10).write(buf);
|
||||
buf.put(onionAddress);
|
||||
} else if (onionAddress.length == 32 + 2 + 1) {
|
||||
} else if (onionAddress.length == 32) {
|
||||
// TORv3
|
||||
buf.put((byte) 0x04);
|
||||
VarInt.of(32).write(buf);
|
||||
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, CryptoUtils.onionChecksum(pubkey, torVersion)))
|
||||
throw new IllegalStateException("checksum");
|
||||
buf.put(pubkey);
|
||||
buf.put(onionAddress);
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
@ -305,7 +291,7 @@ public class PeerAddress {
|
||||
byte[] ipBytes = addr.getAddress();
|
||||
buf.put(mapIntoIPv6(ipBytes));
|
||||
} 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) {
|
||||
// TORv2
|
||||
buf.put(ONIONCAT_PREFIX);
|
||||
@ -358,13 +344,9 @@ public class PeerAddress {
|
||||
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
|
||||
size += VarInt.sizeOf(10) + 10;
|
||||
} else if (onionAddress.length == 32 + 2 + 1) {
|
||||
// TORv3
|
||||
size += VarInt.sizeOf(32) + 32;
|
||||
byte[] onionAddress = TorUtils.decodeOnionUrl(hostname);
|
||||
if (onionAddress.length == 10 || onionAddress.length == 32) {
|
||||
size += VarInt.sizeOf(onionAddress.length) + onionAddress.length;
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
@ -48,17 +48,4 @@ public class CryptoUtils {
|
||||
digest.doFinal(ripmemdHash, 0);
|
||||
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