Java tutorial
/* 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.lang.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.util.encoders.Hex; import tor.util.TorCircuitException; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.TreeMap; public class TorCircuit { public static final int RELAY_BEGIN = 1; public static final int RELAY_DATA = 2; public static final int RELAY_END = 3; public static final int RELAY_CONNECTED = 4; public static final int RELAY_SENDME = 5; public static final int RELAY_EXTEND = 6; public static final int RELAY_EXTENDED = 7; public static final int RELAY_TRUNCATE = 8; public static final int RELAY_TRUNCATED = 9; public static final int RELAY_DROP = 10; public static final int RELAY_RESOLVE = 11; public static final int RELAY_RESOLVED = 12; public static final int RELAY_BEGIN_DIR = 13; public static final int RELAY_COMMAND_ESTABLISH_INTRO = 32; public static final int RELAY_COMMAND_ESTABLISH_RENDEZVOUS = 33; public static final int RELAY_COMMAND_INTRODUCE1 = 34; public static final int RELAY_COMMAND_INTRODUCE2 = 35; public static final int RELAY_COMMAND_RENDEZVOUS1 = 36; public static final int RELAY_COMMAND_RENDEZVOUS2 = 37; public static final int RELAY_COMMAND_INTRO_ESTABLISHED = 38; public static final int RELAY_COMMAND_RENDEZVOUS_ESTABLISHED = 39; public static final int RELAY_COMMAND_INTRODUCE_ACK = 40; final static Logger log = LogManager.getLogger(); public static short streamId_counter = 1; public static String[] DESTROY_ERRORS = { "NONE", "PROTOCOL", "INTERNAL", "REQUESTED", "HIBERNATING", "RESOURCELIMIT", "CONNECTFAILED", "OR_IDENTITY", "OR_CONN_CLOSED", "FINISHED", "TIMEOUT", "DESTROYED", "NOSUCHSERVICE" }; public static String[] STREAM_ERRORS = { "-", "REASON_MISC", "REASON_RESOLVEFAILED", "REASON_CONNECTREFUSED", "REASON_EXITPOLICY", "REASON_DESTROY", "REASON_DONE", "REASON_TIMEOUT", "REASON_NOROUTE", "REASON_HIBERNATING", "REASON_INTERNAL", "REASON_RESOURCELIMIT", "REASON_CONNRESET", "REASON_TORPROTOCOL", "REASON_NOTDIRECTORY" }; private static int circId_counter = 1; // temp vars for created/extended public BigInteger temp_x; public OnionRouter temp_r; public STATES state = STATES.NONE; public byte[] rendezvousCookie = new byte[20]; /** * Handles cell for this circuit * * @param c Cell to handle * @return Successfully handled */ public long receiveWindow = 1000; public long sendWindow = 1000; long circId = 0; boolean blocking = false; // list of active streams for this circuit TreeMap<Integer, TorStream> streams = new TreeMap<>(); // streams with packets to send /** * */ //UniqueQueue <TorStream> streamsSending = new UniqueQueue<TorStream>(); TorSocket sock; /** * Gererates a relay cell, encrypts and sends it * * @param payload Relay payload * @param relaytype Type of relay cell (see RELAY_) * @param early Whether to use an early cell (needed for EXTEND only) * @param stream Stream ID */ long sentPackets = 0; long sentBytes = 0; // this circuit hop private LinkedList<OnionRouter> circuitToBuild = new LinkedList<>(); private ArrayList<TorHop> hops = new ArrayList<>(); private Object stateNotify = new Object(); public TorCircuit(TorSocket sock) { circId = circId_counter++; // in proto version 4 or higher, the MSB bit of the circId must be one for the initiator (aka, us). if (sock.PROTOCOL_VERSION >= 4) circId |= 0x80000000; this.sock = sock; } public TorCircuit(int cid, TorSocket sock) { circId = cid; this.sock = sock; } public void setBlocking(boolean blocking) { this.blocking = blocking; } public void setState(STATES newState) { log.trace("[Circ {}] New Circuit state {} (oldState {})", circId, newState, state); synchronized (stateNotify) { state = newState; stateNotify.notifyAll(); } } public void waitForState(STATES desired, boolean waitIfAlready) throws IOException { if (!waitIfAlready && state.equals(desired)) return; while (true) { synchronized (stateNotify) { try { stateNotify.wait(1000); if (state == STATES.DESTROYED && desired != STATES.DESTROYED) { log.error("Waiting for unreachable state - circuit destroyed"); throw new TorCircuitException("circuit destroyed - waiting for unreachable state"); } } catch (InterruptedException e) { e.printStackTrace(); } } if (state.equals(desired)) return; } } /** * Utility function to create routes * * @param hopList Comma separated list on onion router names * @throws IOException */ public void createRoute(String hopList) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } String hops[] = hopList.split(","); for (String s : hops) { circuitToBuild.add(Consensus.getConsensus().getRouterByName(s)); } create(sock.firstHop); // must go to first hop first if (blocking) waitForState(STATES.READY, false); } public void create() throws IOException { create(sock.firstHop); } /** * Sends a create cell to specified hop (usually first hop that we're already connected to) * * @param r Hop */ public void create(OnionRouter r) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } setState(STATES.CREATING); sock.sendCell(circId, Cell.CREATE, createPayload(r)); if (blocking) waitForState(STATES.READY, true); } /** * Builds create cell payload (e.g. tap handshake) * * @param r Hop to create to * @return Payload * @throws IOException */ private byte[] createPayload(OnionRouter r) throws IOException { byte privkey[] = new byte[40]; // generate priv key TorCrypto.rnd.nextBytes(privkey); temp_x = TorCrypto.byteToBN(privkey); temp_r = r; // generate pub key BigInteger pubKey = TorCrypto.DH_G.modPow(temp_x, TorCrypto.DH_P); byte pubKeyByte[] = TorCrypto.BNtoByte(pubKey); return TorCrypto.hybridEncrypt(pubKeyByte, r.getOnionKey()); } /** * Builds a relay cell payload (not including cell header, only relay header) * * @param toHop Hop that it's destined for * @param cmd Command ID, see RELAY_ * @param stream Stream ID * @param payload Relay cell data * @return Constructed relay payload */ protected synchronized byte[] buildRelay(TorHop toHop, int cmd, short stream, byte[] payload) { byte[] fnl = new byte[509]; ByteBuffer buf = ByteBuffer.wrap(fnl); buf.put((byte) cmd); buf.putShort((short) 0); // recognised buf.putShort(stream); buf.putInt(0); // digest if (payload != null) { buf.putShort((short) payload.length); buf.put(payload); } else { buf.putShort((short) 0); } toHop.df_md.update(fnl); MessageDigest md; try { md = (MessageDigest) toHop.df_md.clone(); byte digest[] = md.digest(); //byte [] fnl_final = new byte[509]; System.arraycopy(digest, 0, fnl, 5, 4); return fnl; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } /** * Wraps data in onion skins for sending down circuit * * @param data Data to wrap/encrypt * @return Wrapped/encrypted data */ private byte[] encrypt(byte[] data) { for (int i = hops.size() - 1; i >= 0; i--) { data = hops.get(i).encrypt(data); } return data; } /** * Removes onion skins for received data * * @param data Encrypted data for onion skin removal. * @return Decrypted data. */ // TODO: should check digest in this function too - otherwise might miss packets with 1/65535 probability. private byte[] decrypt(byte[] data) { for (TorHop hop : hops) { data = hop.decrypt(data); } return data; } /** * Return the last TorHop in this circuit, excluding the first hop. * * @return the last TorHop, or null if there are no TorHops in the circuit. */ public TorHop getLastHop() { if (hops.size() < 1) return null; else return hops.get(hops.size() - 1); } /** * Sends an extend cell to extend the circuit to specified hop * * @param nextHop Hop to extend to * @throws IOException */ public void extend(OnionRouter nextHop) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } // Unused, throws ArrayIndexOutOfBoundsException when extend() is called after createCircuit() // without the fix to getLastHop() which returns null when hops.size() == 0 //TorHop lastHop = getLastHop(); byte create[] = createPayload(nextHop); byte extend[] = new byte[4 + 2 + create.length + TorCrypto.HASH_LEN]; ByteBuffer buf = ByteBuffer.wrap(extend); buf.put(nextHop.ip.getAddress()); buf.putShort((short) nextHop.orport); buf.put(create); buf.put(Hex.decode(nextHop.identityhash)); send(extend, RELAY_EXTEND, true, (short) 0); //byte []payload = encrypt(buildRelay(lastHop, RELAY_EXTEND, (short)0, extend)); //sock.sendCell(circId, Cell.RELAY_EARLY, payload); setState(STATES.EXTENDING); if (blocking) waitForState(STATES.READY, false); } /** * Handles created cell (also used for extended cell as payload the same) * * @param in Cell payload (e.g. handshake data) */ private void handleCreated(byte in[]) throws TorCircuitException { // other side's public key byte y_bytes[] = Arrays.copyOfRange(in, 0, TorCrypto.DH_LEN); // kh for verification of derivation byte kh[] = Arrays.copyOfRange(in, TorCrypto.DH_LEN, TorCrypto.DH_LEN + TorCrypto.HASH_LEN); //calculate g^xy shared secret BigInteger secret = TorCrypto.byteToBN(y_bytes).modPow(temp_x, TorCrypto.DH_P); // derive key data data byte kdf[] = TorCrypto.torKDF(TorCrypto.BNtoByte(secret), 3 * TorCrypto.HASH_LEN + 2 * TorCrypto.KEY_LEN); // ad hop hops.add(new TorHop(kdf, kh, temp_r)); if (circuitToBuild.isEmpty()) setState(STATES.READY); } public TorStream createDirStream(TorStream.TorStreamListener list) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } //TODO: allocate stream and circuit IDS properly int stid = streamId_counter++; send(null, RELAY_BEGIN_DIR, false, (short) stid); TorStream st = new TorStream(stid, this, list); streams.put(stid, st); return st; } /** * Creates a stream using this circuit and connects to a host * * @param host Hostname/ip * @param port Port * @param list A listener for stream events * @return TorStream object */ public TorStream createStream(String host, int port, TorStream.TorStreamListener list) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } byte b[] = new byte[100]; ByteBuffer buf = ByteBuffer.wrap(b); buf.put((host + ":" + port).getBytes("UTF-8")); buf.put((byte) 0); // null terminator buf.putInt(0); //TODO: allocate stream and circuit IDS properly int stid = streamId_counter++; send(b, RELAY_BEGIN, false, (short) stid); TorStream st = new TorStream(stid, this, list); streams.put(stid, st); return st; } // must be synchronised due to hash calculation - out of sync = bad public synchronized void send(byte[] payload, int relaytype, boolean early, short stream) throws IOException { if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } if (relaytype == RELAY_DATA) sendWindow--; byte relcell[] = buildRelay(hops.get(hops.size() - 1), relaytype, stream, payload); sock.sendCell(circId, early ? Cell.RELAY_EARLY : Cell.RELAY, encrypt(relcell)); sentPackets++; sentBytes += relcell.length; } public void rendezvousSetup() throws IOException { TorCrypto.rnd.nextBytes(rendezvousCookie); rendezvousSetup(rendezvousCookie); } public void rendezvousSetup(byte[] cookie) throws IOException { rendezvousCookie = ArrayUtils.clone(cookie); send(rendezvousCookie, RELAY_COMMAND_ESTABLISH_RENDEZVOUS, false, (short) 0); setState(STATES.RENDEZVOUS_WAIT); if (blocking) waitForState(STATES.RENDEZVOUS_ESTABLISHED, false); } public void destroy() throws IOException { sock.sendCell(circId, Cell.DESTROY, null); } /** * Set the digest to zero for a relay payload - used by hash calculation code in handleReceived() * * @param relayCell * @return cell with digest removed */ public byte[] relayCellRemoveDigest(byte[] relayCell) { byte buf[] = new byte[relayCell.length]; System.arraycopy(relayCell, 0, buf, 0, 5); System.arraycopy(relayCell, 9, buf, 9, relayCell.length - 9); return buf; } public boolean handleCell(Cell c) throws IOException { boolean handled = false; if (receiveWindow < 900) { //System.out.println("sent SENDME (CIRCUIT): " + receiveWindow); send(null, RELAY_SENDME, false, (short) 0); receiveWindow += 100; } if (state == STATES.DESTROYED) { log.error("Trying to use destroyed circuit"); throw new RuntimeException("Trying to use destroyed circuit"); } if (c.cmdId == Cell.CREATED) // create { handleCreated(c.payload); if (!circuitToBuild.isEmpty()) {// more? boolean block = blocking; setBlocking(false); extend(circuitToBuild.removeFirst()); setBlocking(block); } handled = true; } else if (c.cmdId == Cell.RELAY_EARLY || c.cmdId == Cell.PADDING || c.cmdId == Cell.VPADDING) { // these are used in deanon attacks log.error("WARNING**** cell CMD " + c.cmdId + " received in - Possible DEANON attack!!: Route: " + hops.toArray()); } else if (c.cmdId == Cell.RELAY) // relay cell { // cell decrypt logic - decrypt from each hop in turn checking recognised and digest until success // remember, we can receive cells from intermediate hops, so it's an iterative decrypt and check if successful // for each hop. int cellFromHop = -1; for (int di = 0; di < hops.size(); di++) { // loop through circuit hops TorHop hop = hops.get(di); c.payload = hop.decrypt(c.payload); // decrypt for this hop if (c.payload[1] == 0 && c.payload[2] == 0) { // are recognised bytes set to zero? byte nodgrl[] = relayCellRemoveDigest(c.payload); // remove digest from cell byte hash[] = Arrays.copyOfRange(c.payload, 5, 9); // put digest from cell into hash byte[] digest; try { // calculate the digest that we thing it should be // must clone here to stop clobbering original digest = ((MessageDigest) hop.db_md.clone()).digest(nodgrl); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } // compare our calculations with digest in cell - if right, we've decrypted correctly if (Arrays.equals(hash, Arrays.copyOfRange(digest, 0, 4))) { hop.db_md.update(nodgrl); // update digest for hop for future cells cellFromHop = di; // hop number this cell is from break; } } } if (cellFromHop == -1) { log.warn("unrecognised cell - didn't decrypt"); return false; } // successfully decrypted - now let's proceed // decode relay header ByteBuffer buf = ByteBuffer.wrap(c.payload); int cmd = buf.get(); if (buf.getShort() != 0) { log.warn("invalid relay cell"); return false; } int streamid = buf.getShort(); int digest = buf.getInt(); int length = buf.getShort(); byte data[] = Arrays.copyOfRange(c.payload, 1 + 2 + 2 + 4 + 2, 1 + 2 + 2 + 4 + 2 + length); // now pass cell off to handler function below handled = handleRelayCell(cmd, streamid, cellFromHop, data); } else if (c.cmdId == Cell.DESTROY) { log.info("Circuit destroyed " + circId); log.info("Reason: " + DESTROY_ERRORS[c.payload[0]]); for (TorStream s : streams.values()) { s.notifyDisconnect(); } setState(STATES.DESTROYED); handled = true; } return handled; } /** * Handles a decrypted relay cell * * @param cmdId RELAY_* command ID * @param streamId Stream ID * @param fromHop Received from which hop in circuit, e.g., 0,1,2.. * @param payload Decrypted payload * @return whether handled or not * @throws IOException */ public boolean handleRelayCell(int cmdId, int streamId, int fromHop, byte[] payload) throws IOException { TorStream stream = streams.get(new Integer(streamId)); log.trace("Got RELAY cell with streamId{} cmdID {}", streamId, cmdId); if (streamId > 0 && stream == null) { log.info("invalid stream id " + streamId); return false; } if (fromHop != hops.size() - 1) log.info("Cell from intermediate hop " + fromHop); switch (cmdId) { case RELAY_DROP: log.warn("WARNING**** _relay_ cell CMD DROP received - Possible DEANON attack!!: Route: " + hops.toArray()); break; case RELAY_COMMAND_INTRODUCE_ACK: setState(STATES.INTRODUCED); break; case RELAY_COMMAND_RENDEZVOUS2: assert state == STATES.RENDEZVOUS_ESTABLISHED; handleCreated(payload); setState(STATES.RENDEZVOUS_COMPLETE); break; case RELAY_COMMAND_RENDEZVOUS_ESTABLISHED: setState(STATES.RENDEZVOUS_ESTABLISHED); break; case RELAY_TRUNCATED: log.error("TRUNCATED CELL RECEIVED - Cannot handle yet! " + DESTROY_ERRORS[payload[0]]); for (int hi = hops.size() - 1; hi > fromHop; hi--) { log.info("removing hop " + hi + " from circ"); hops.remove(hi); } throw new RuntimeException("see err above"); //break; case RELAY_EXTENDED: // extended handleCreated(payload); if (!circuitToBuild.isEmpty()) { // needs extending further? boolean block = blocking; setBlocking(false); extend(circuitToBuild.removeFirst()); setBlocking(block); } else { log.info("Circuit build complete"); setState(STATES.READY); } break; case RELAY_CONNECTED: if (stream != null) stream.notifyConnect(); break; case RELAY_SENDME: if (streamId == 0) sendWindow += 100; log.trace("RELAY_SENDME circ " + circId + " Stream " + streamId + " cur window " + sendWindow); break; case RELAY_DATA: if (state == STATES.READY) receiveWindow--; if (stream != null) stream._putRecved(payload); break; case RELAY_END: if (payload[0] != 6) log.info("Remote stream closed with error code " + STREAM_ERRORS[payload[0]]); if (stream != null) { stream.notifyDisconnect(); streams.remove(new Integer(streamId)); } break; default: log.warn("unknown relay cell cmd " + cmdId); return false; } return true; } public enum STATES { NONE, CREATING, EXTENDING, READY, DESTROYED, RENDEZVOUS_WAIT, RENDEZVOUS_ESTABLISHED, RENDEZVOUS_COMPLETE, INTRODUCED } }