/*
Tor Research Framework - easy to use tor client library/framework
Copyright (C) 2014 Dr Gareth Owen <drgowen@gmail.com>
www.ghowen.me / github.com/drgowen/tor-research-framework
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package tor;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.encoders.Base64;
import tor.util.MiscUtil;
import tor.util.TorCircuitException;
import tor.util.TorDocumentParser;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.TreeMap;
/**
* Created by gho on 25/07/14.
*/
public class HiddenService {
final static Logger log = LogManager.getLogger();
public static byte[] getSecretId(String onion, byte replica) {
byte[] onionbin = new Base32().decode(onion.toUpperCase());
assert onionbin.length == 10;
long curtime = System.currentTimeMillis() / 1000L;
int oid = onionbin[0] & 0xff;
long t = (curtime + (oid * 86400L / 256)) / 86400L;
ByteBuffer buf = ByteBuffer.allocate(10);
buf.putInt((int) t);
buf.put(replica);
buf.flip();
MessageDigest md = TorCrypto.getSHA1();
md.update(buf);
return md.digest();
}
// onion as base32 encoded, replica=[0,1],
public static byte[] getDescId(String onion, byte replica) {
byte[] onionbin = new Base32().decode(onion.toUpperCase());
assert onionbin.length == 10;
MessageDigest md = TorCrypto.getSHA1();
byte hashT[] = getSecretId(onion, replica);
md = TorCrypto.getSHA1();
return md.digest(ArrayUtils.addAll(onionbin, hashT)); //md.digest();
}
public static OnionRouter[] findResposibleDirectories(String onionb32) {
Consensus con = Consensus.getConsensus();
// get list of nodes with HS dir flag
TreeMap<String, OnionRouter> routers = con.getORsWithFlag("HSDir,V2Dir".split(","));
Object keys[] = routers.keySet().toArray();
Object vals[] = routers.values().toArray();
ArrayList<OnionRouter> rts = new ArrayList<>();
for (int replica = 0; replica < 2; replica++) {
// Get nodes just to right of HS's descID in the DHT
int idx = -Arrays.binarySearch(keys, Hex.encodeHexString(getDescId(onionb32, (byte) replica)));
for (int i = 0; i < 3; i++) {
rts.add((OnionRouter) vals[(idx + i) % vals.length]);
}
}
// return list containing hopefully six ORs.
return rts.toArray(new OnionRouter[0]);
}
// blocking
public static String fetchHSDescriptor(TorSocket sock, final String onion) throws IOException {
// get list of ORs with resposibility for this HS
OnionRouter ors[] = findResposibleDirectories(onion);
// loop through responsible directories until successful
for (int i = 0; i < ors.length; i++) {
OnionRouter or = ors[i];
log.debug("Trying Directory Server: {}", or);
// establish circuit to responsible director
TorCircuit circ = sock.createCircuit(true);
try {
circ.create();
circ.extend(ors[0]);
} catch(TorCircuitException e) {
log.error("HS fetched failed due to circuit failure - moving to next directory");
continue;
}
final int replica = i < 3 ? 0 : 1;
// asynchronous call
TorStream st = circ.createDirStream(new TorStream.TorStreamListener() {
@Override
public void dataArrived(TorStream s) {
}
@Override
public void connected(TorStream s) {
try {
s.sendHTTPGETRequest("/tor/rendezvous2/" + new Base32().encodeAsString(HiddenService.getDescId(onion, (byte) replica)), "dirreq");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void disconnected(TorStream s) {
synchronized (onion) {
onion.notify();
}
}
@Override
public void failure(TorStream s) {
synchronized (onion) {
onion.notify();
}
}
});
// wait for notification from the above listener that data is here! (that remote side ended connection - data could be blank
synchronized (onion) {
try {
onion.wait(1000);
if(circ.state== TorCircuit.STATES.DESTROYED) {
System.out.println("HS - Desc Fetch - Circuit Destroyed");
throw new TorCircuitException("circuit destroyed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// get HTTP response and body
String data = IOUtils.toString(st.getInputStream());
circ.destroy();
// HTTP success code
if (data.length() < 1 || !data.split(" ")[1].equals("200")) {
continue;
}
int dataIndex = data.indexOf("\r\n\r\n");
return data.substring(dataIndex);
}
log.warn("Not found hs descriptor!");
return null;
}
public static void sendIntroduce(TorSocket sock, String onion, TorCircuit rendz) throws IOException {
log.debug("Fetching Hidden Service Descriptor");
String hsdescTxt = fetchHSDescriptor(sock, onion);
OnionRouter rendzOR = rendz.getLastHop().router;
// parse the hidden service descriptor
TorDocumentParser hsdesc = new TorDocumentParser(hsdescTxt);
//decode the intro points
String intopointsb64 = new String(Base64.decode(hsdesc.map.get("introduction-points")));
// parse intro points document
TorDocumentParser intros = new TorDocumentParser(intopointsb64);
// get first intro point
String introPointIdentities[] = intros.getArrayItem("introduction-point");
int introPointNum = 0;
String ip0 = Hex.encodeHexString(new Base32().decode(introPointIdentities[introPointNum].toUpperCase()));
OnionRouter ip0or = Consensus.getConsensus().routers.get(ip0);
byte[] serviceKey = Base64.decode(intros.getArrayItem("service-key")[introPointNum]);
byte skHash[] = TorCrypto.getSHA1().digest(serviceKey);
assert (skHash.length == 20);
log.debug("Using Intro Point: {}, building circuit...", ip0or);
TorCircuit ipcirc = sock.createCircuit(true);
ipcirc.create();
ipcirc.extend(ip0or);
// outer packet
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(skHash); // service PKhash
// inner handshake
ByteBuffer handshake = ByteBuffer.allocate(1024);
handshake.put((byte) 2); //ver
handshake.put(rendzOR.ip.getAddress()); // rendz IP addr
handshake.putShort((short) rendzOR.orport);
try {
handshake.put(new Hex().decode(rendzOR.identityhash.getBytes()));
} catch (DecoderException e) {
e.printStackTrace();
}
handshake.putShort((short) rendzOR.onionKeyRaw.length); // rendz key len
handshake.put(rendzOR.onionKeyRaw); // rendz key
handshake.put(rendz.rendezvousCookie); //rend cookie
// tap handshake / create handshake
byte priv_x[] = new byte[128];
TorCrypto.rnd.nextBytes(priv_x); // g^x
rendz.temp_x = TorCrypto.byteToBN(priv_x);
rendz.temp_r = null;
BigInteger pubKey = TorCrypto.DH_G.modPow(rendz.temp_x, TorCrypto.DH_P);
byte pubKeyByte[] = TorCrypto.BNtoByte(pubKey);
handshake.put(pubKeyByte);
handshake.flip();
// convert to byte array
byte handshakeBytes[] = new byte[handshake.remaining()];
handshake.get(handshakeBytes);
// encrypt handshake
PublicKey skPK = TorCrypto.asn1GetPublicKey(serviceKey);
buf.put(TorCrypto.hybridEncrypt(handshakeBytes, skPK));
buf.flip();
byte introcell[] = new byte[buf.remaining()];
buf.get(introcell);
ipcirc.send(introcell, TorCircuit.RELAY_COMMAND_INTRODUCE1, false, (short) 0);
log.debug("waiting for introduce acknowledgement");
ipcirc.waitForState(TorCircuit.STATES.INTRODUCED, false);
log.debug("Now waiting for rendezvous connect");
rendz.waitForState(TorCircuit.STATES.RENDEZVOUS_COMPLETE, false);
ipcirc.destroy(); // no longer needed
log.debug("Hidden Service circuit built");
}
public static String publicKeyToOnion(RSAPublicKey pk) throws IOException {
byte []service = TorCrypto.getSHA1().digest(TorCrypto.publicKeyToASN1(pk));
String serviceb32 = new Base32().encodeAsString(Arrays.copyOfRange(service,0,10)).toLowerCase();
return serviceb32;
}
public static String generateHSDescriptor(byte[] privkey) throws IOException {
RSAPrivateKey pk = TorCrypto.asn1GetPrivateKey(privkey);
PublicKey puk = TorCrypto.asn1GetPrivateKeyPublic(privkey);
return generateHSDescriptor((RSAPublicKey) puk, pk);
}
public static String generateHSDescriptor(RSAPublicKey pk, RSAPrivateKey prk) throws IOException {
String serviceb32 = publicKeyToOnion(pk);
StringBuilder desc = new StringBuilder();
desc.append("rendezvous-service-descriptor "+new Base32().encodeAsString(getDescId(serviceb32, (byte)0)).toLowerCase()+"\n");
desc.append("version 2\n");
desc.append("permanent-key\n-----BEGIN RSA PUBLIC KEY-----\n");
desc.append(MiscUtil.stringMaxWidth(Base64.toBase64String(TorCrypto.publicKeyToASN1(pk)),64));
desc.append("\n-----END RSA PUBLIC KEY-----\n");
desc.append("secret-id-part "+new Base32().encodeAsString(getSecretId(serviceb32, (byte)0)).toLowerCase()+"\n");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
desc.append("publication-time "+df.format(new Date())+"\n");
desc.append("protocol-versions 2,3\n");
desc.append("introduction-points\n" +
"-----BEGIN MESSAGE-----\n");
desc.append(MiscUtil.stringMaxWidth(Base64.toBase64String("nothing to see :-)".getBytes()),64));
desc.append("\n-----END MESSAGE-----\n" +
"signature\n");
byte sig[];
try {
Signature instance = Signature.getInstance("SHA1withRSA");
instance.initSign(prk);
instance.update(desc.toString().getBytes());
sig = instance.sign();
} catch (InvalidKeyException | NoSuchAlgorithmException |SignatureException e) {
e.printStackTrace();
return null;
}
desc.append("-----BEGIN SIGNATURE-----\n");
desc.append(MiscUtil.stringMaxWidth(Base64.toBase64String(sig),64));
desc.append("\n-----END SIGNATURE-----\n");
return desc.toString();
}
}