diff --git a/router/java/src/net/i2p/router/tunnel/GatewayMessage.java b/router/java/src/net/i2p/router/tunnel/GatewayMessage.java
deleted file mode 100644
index 8f9ffbfde5a1bc9660c47baff06d72cd71817967..0000000000000000000000000000000000000000
--- a/router/java/src/net/i2p/router/tunnel/GatewayMessage.java
+++ /dev/null
@@ -1,369 +0,0 @@
-package net.i2p.router.tunnel;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Base64;
-import net.i2p.data.DataHelper;
-import net.i2p.data.Hash;
-import net.i2p.data.SessionKey;
-import net.i2p.util.Log;
-
-/**
- * <p>Manage the actual encryption necessary to turn a message into what the 
- * gateway needs to know so that it can send out a tunnel message.  See the doc
- * "tunnel.html" for more details on the algorithm and motivation.</p>
- * 
- */
-public class GatewayMessage {
-    private I2PAppContext _context;
-    private Log _log;
-    /** _iv[i] is the IV used to encrypt the entire message at peer i */
-    private byte _iv[][];
-    /** payload, overwritten with the encrypted data at each peer */
-    private byte _payload[];
-    /** _eIV[i] is the IV used to encrypt the checksum block at peer i */
-    private byte _eIV[][];
-    /** _H[i] is the SHA256 of the decrypted payload as seen at peer i */
-    private byte _H[][];
-    /** _order[i] is the column of the checksum block in which _H[i] will be placed */
-    private int _order[];
-    /** 
-     * _eH[column][row] contains the (encrypted) hash of _H[_order[column]] as seen in the
-     * column at peer 'row' BEFORE decryption.  when _order[column] == row+1, the hash is 
-     * in the cleartext.
-     */
-    private byte _eH[][][];
-    /** _preV is _eH[*][8] */
-    private byte _preV[];
-    /** 
-     * _V is SHA256 of _preV, overwritten with its own encrypted value at each 
-     * layer, using the last IV_SIZE bytes of _eH[7][*] as the IV
-     */
-    private byte _V[];
-    /** if true, the data has been encrypted */
-    private boolean _encrypted;
-    /** if true, someone gave us a payload */
-    private boolean _payloadSet;
-    /** includes the gateway and endpoint */
-    static final int HOPS = 8;
-    /** aes256 */
-    static final int IV_SIZE = 16;
-    private static final byte EMPTY[] = new byte[0];
-    private static final int COLUMNS = HOPS;
-    private static final int HASH_ROWS = HOPS;
-    /** # bytes of the hash to maintain in each column */
-    static final int COLUMN_WIDTH = IV_SIZE;
-    /** # bytes of the verification hash to maintain */
-    static final int VERIFICATION_WIDTH = IV_SIZE;
-    
-    /** used to munge the IV during per-hop translations */
-    static final byte IV_WHITENER[] = new byte[] { (byte)0x31, (byte)0xd6, (byte)0x74, (byte)0x17, 
-                                                   (byte)0xa0, (byte)0xb6, (byte)0x28, (byte)0xed, 
-                                                   (byte)0xdf, (byte)0xee, (byte)0x5b, (byte)0x86, 
-                                                   (byte)0x74, (byte)0x61, (byte)0x50, (byte)0x7d };
-    
-    public GatewayMessage(I2PAppContext ctx) {
-        _context = ctx;
-        _log = ctx.logManager().getLog(GatewayMessage.class);
-        initialize();
-    }
-    
-    private void initialize() {
-        _iv = new byte[HOPS-1][IV_SIZE];
-        _eIV = new byte[HOPS-1][IV_SIZE];
-        _H = new byte[HOPS][Hash.HASH_LENGTH];
-        _eH = new byte[COLUMNS][HASH_ROWS][COLUMN_WIDTH];
-        _preV = new byte[HOPS*COLUMN_WIDTH];
-        _V = new byte[VERIFICATION_WIDTH];
-        _order = new int[HOPS];
-        _encrypted = false;
-        _payloadSet = false;
-    }
-    
-    /**
-     * Provide the data to be encrypted through the tunnel.  This data will
-     * be available only to the tunnel endpoint as it is entered.  Its size
-     * must be a multiple of 16 bytes (0 is a valid size).  The parameter is 
-     * overwritten during encryption.
-     *
-     */
-    public void setPayload(byte payload[]) { 
-        if ( (payload != null) && (payload.length % 16 != 0) )
-            throw new IllegalArgumentException("Payload size must be a multiple of 16");
-        _payload = payload; 
-        _payloadSet = true;
-    }
-    
-    /** 
-     * Actually encrypt the payload, producing the tunnel checksum that is verified 
-     * at each hop as well as the verification block that verifies the checksum at 
-     * the end of the tunnel.  After encrypting, the data can be retrieved by 
-     * exporting it to a byte array.
-     * 
-     */
-    public void encrypt(GatewayTunnelConfig config) {
-        if (!_payloadSet) throw new IllegalStateException("You must set the payload before encrypting");
-        buildOrder();
-        encryptIV(config);
-        encryptPayload(config);
-        encryptChecksumBlocks(config);
-        encryptVerificationHash(config);
-        _encrypted = true;
-    }
-    
-    /**
-     * We want the hashes to be placed randomly throughout the checksum block so
-     * that sometimes the first peer finds their hash in column 3, sometimes in
-     * column 1, etc (and hence, doesn't know whether they are the first peer)
-     *
-     */
-    private final void buildOrder() {
-        ArrayList order = new ArrayList(HOPS);
-        for (int i = 0; i < HOPS; i++)
-            order.add(new Integer(i));
-        Collections.shuffle(order, _context.random());
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("_order = " + order);
-        
-        for (int i = 0; i < HOPS; i++) {
-            _order[i] = ((Integer)order.get(i)).intValue();
-        }
-    }
-    
-    /**
-     * The IV is a random value, encrypted backwards and hashed along the way to
-     * destroy any keying material from colluding attackers.
-     *
-     */
-    private final void encryptIV(GatewayTunnelConfig cfg) {
-        _context.random().nextBytes(_iv[0]);
-        
-        for (int i = 1; i < HOPS - 1; i++) {
-            SessionKey key = cfg.getSessionKey(i);
-            
-            // decrypt, since we're simulating what the participants do
-            _context.aes().decryptBlock(_iv[i-1], 0, key, _iv[i], 0);
-            DataHelper.xor(_iv[i], 0, IV_WHITENER, 0, _iv[i], 0, IV_SIZE);
-            Hash h = _context.sha().calculateHash(_iv[i]);
-            System.arraycopy(h.getData(), 0, _iv[i], 0, IV_SIZE);
-        }
-        
-        if (_log.shouldLog(Log.DEBUG)) {
-            for (int i = 0; i < HOPS-1; i++)
-                _log.debug("_iv[" + i + "] = " + Base64.encode(_iv[i]));
-        }
-    }
-    
-    /** 
-     * Encrypt the payload and IV blocks, overwriting the _payload, 
-     * populating _H[] with the SHA256(payload) along the way and placing
-     * the last 16 bytes of the encrypted payload into eIV[] at each step.
-     *
-     */
-    private final void encryptPayload(GatewayTunnelConfig cfg) {
-        int numBlocks = (_payload != null ? _payload.length / 16 : 0);
-        
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("# payload blocks: " + numBlocks);
-        
-        for (int i = HOPS - 1; i > 0; i--) {
-            SessionKey key = cfg.getSessionKey(i);
-            
-            if ( (_payload != null) && (_payload.length > 0) ) {
-                // set _H[i] = SHA256 of the payload seen at the i'th peer after decryption
-                Hash h = _context.sha().calculateHash(_payload); 
-                System.arraycopy(h.getData(), 0, _H[i], 0, Hash.HASH_LENGTH);
-            
-                // first block, use the IV
-                DataHelper.xor(_iv[i-1], 0, _payload, 0, _payload, 0, IV_SIZE);
-                _context.aes().encryptBlock(_payload, 0, key, _payload, 0);
-                
-                for (int j = 1; j < numBlocks; j++) {
-                    // subsequent blocks, use the prev block as IV (aka CTR mode)
-                    DataHelper.xor(_payload, (j-1)*IV_SIZE, _payload, j*IV_SIZE, _payload, j*IV_SIZE, IV_SIZE);
-                    _context.aes().encryptBlock(_payload, j*IV_SIZE, key, _payload, j*IV_SIZE);
-                }
-                
-                System.arraycopy(_payload, _payload.length - IV_SIZE, _eIV[i-1], 0, IV_SIZE);
-            } else {
-                Hash h = _context.sha().calculateHash(EMPTY); 
-                System.arraycopy(h.getData(), 0, _H[i], 0, Hash.HASH_LENGTH);
-                
-                // nothing to encrypt... pass on the IV to the checksum blocks
-                System.arraycopy(_iv, 0, _eIV[i-1], 0, IV_SIZE);
-            }
-        }
-        
-        // we need to know what the gateway would "decrypt" to, even though they
-        // aren't actually doing any decrypting (so the first peer can't look 
-        // back and say "hey, the previous peer had no match, they must be the
-        // gateway")
-        Hash h0 = null;
-        if ( (_payload != null) && (_payload.length > 0) )
-            h0 = _context.sha().calculateHash(_payload);
-        else
-            h0 = _context.sha().calculateHash(EMPTY);
-        System.arraycopy(h0.getData(), 0, _H[0], 0, Hash.HASH_LENGTH);
-
-        if (_log.shouldLog(Log.DEBUG)) {
-            for (int i = 0; i < HOPS-1; i++)
-                _log.debug("_eIV["+ i + "] = " + Base64.encode(_eIV[i]));
-            for (int i = 0; i < HOPS; i++)
-                _log.debug("_H["+ i + "] = " + Base64.encode(_H[i]));
-        }
-    }
-    
-    /**
-     * Fill in the _eH[column][step] matrix with the encrypted _H values so
-     * that at each step, exactly one column will contain the _H value for
-     * that step in the clear.  _eH[column][0] will contain what is sent from
-     * the gateway (peer0) to the first hop (peer1).  The encryption uses the _eIV for each 
-     * step so that a plain AES/CTR decrypt of the entire message will expose
-     * the layer.  The columns are ordered according to _order
-     */
-    private final void encryptChecksumBlocks(GatewayTunnelConfig cfg) {
-        for (int column = 0; column < COLUMNS; column++) {
-            // which _H[hash] value are we rendering in this column?
-            int hash = _order[column];
-            // fill in the cleartext version for this column
-            System.arraycopy(_H[hash], 0, _eH[column][hash], 0, COLUMN_WIDTH);
-            
-            // now fill in the "earlier" _eH[column][row-1] values for earlier hops 
-            // by encrypting _eH[column][row] with the peer's key, using the 
-            // previous column (or _eIV[row-1]) as the IV
-            for (int row = hash; row > 0; row--) {
-                SessionKey key = cfg.getSessionKey(row);
-                if (column == 0) {
-                    DataHelper.xor(_eIV[row-1], 0, _eH[column][row], 0, _eH[column][row-1], 0, IV_SIZE);
-                } else {
-                    DataHelper.xor(_eH[column-1][row-1], 0, _eH[column][row], 0, _eH[column][row-1], 0, IV_SIZE);
-                }
-                _context.aes().encryptBlock(_eH[column][row-1], 0, key, _eH[column][row-1], 0);
-            }
-            
-            // fill in the "later" rows by encrypting the previous rows with the 
-            // appropriate key, using the end of the previous column (or _eIV[row])
-            // as the IV
-            for (int row = hash + 1; row < HASH_ROWS; row++) {
-                // row is the one we are *writing* to
-                SessionKey key = cfg.getSessionKey(row);
-                
-                _context.aes().decryptBlock(_eH[column][row-1], 0, key, _eH[column][row], 0);
-                if (column == 0)
-                    DataHelper.xor(_eIV[row-1], 0, _eH[column][row], 0, _eH[column][row], 0, IV_SIZE);
-                else
-                    DataHelper.xor(_eH[column-1][row-1], 0, _eH[column][row], 0, _eH[column][row], 0, IV_SIZE);
-            }
-        }
-        
-        if (_log.shouldLog(Log.DEBUG)) {
-            _log.debug("_eH[column][peer]");
-            for (int peer = 0; peer < HASH_ROWS; peer++) {
-                for (int column = 0; column < COLUMNS; column++) {
-                    try {
-                        _log.debug("_eH[" + column + "][" + peer + "] = " + Base64.encode(_eH[column][peer]) 
-                                   + (peer == 0 ? "" : DataHelper.eq(_H[peer-1], 0, _eH[column][peer], 0, COLUMN_WIDTH) ? " CLEARTEXT" : ""));
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        System.out.println("column="+column + " peer=" + peer);
-                    }
-                }
-            }
-        }
-    }
-    
-    /**
-     * Build the _V hash as the SHA256 of _eH[*][7], then encrypt it on top of
-     * itself using the last 16 bytes of _eH[7][*] as the IV.
-     *
-     */
-    private final void encryptVerificationHash(GatewayTunnelConfig cfg) {
-        for (int i = 0; i < COLUMNS; i++)
-            System.arraycopy(_eH[i][HASH_ROWS-1], 0, _preV, i * COLUMN_WIDTH, COLUMN_WIDTH);
-        Hash v = _context.sha().calculateHash(_preV);
-        System.arraycopy(v.getData(), 0, _V, 0, VERIFICATION_WIDTH);
-
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("_V final = " + Base64.encode(_V));
-        
-        for (int i = HOPS - 1; i > 0; i--) {
-            SessionKey key = cfg.getSessionKey(i);
-            // xor the last block of the encrypted payload with the first block of _V to
-            // continue the CTR operation
-            DataHelper.xor(_V, 0, _eH[COLUMNS-1][i-1], 0, _V, 0, IV_SIZE);
-            _context.aes().encryptBlock(_V, 0, key, _V, 0);
-            
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("_V at peer " + i + " = " + Base64.encode(_V));
-        }
-        
-        // _V now contains the hash of what the checksum blocks look like on the 
-        // endpoint, encrypted for delivery to the first hop
-    }
-    
-    /**
-     * Calculate the total size of the encrypted tunnel message that needs to be
-     * sent to the first hop.  This includes the IV and checksum/verification hashes.
-     *
-     */
-    public final int getExportedSize() { 
-        return IV_SIZE +
-               _payload.length +
-               COLUMNS * COLUMN_WIDTH +
-               VERIFICATION_WIDTH; // verification hash
-    }
-    
-    /**
-     * Write out the fully encrypted tunnel message to the target
-     * (starting with what peer1 should see)
-     *
-     * @param target array to write to, which must be large enough 
-     * @param offset offset into the array to start writing
-     * @return offset into the array after writing
-     * @throws IllegalStateException if it is not yet encrypted
-     * @throws NullPointerException if the target is null 
-     * @throws IllegalArgumentException if the target is too small
-     */
-    public final int export(byte target[], int offset) {
-        if (!_encrypted) throw new IllegalStateException("Not yet encrypted - please call encrypt");
-        if (target == null) throw new NullPointerException("Target is null");
-        if (target.length - offset < getExportedSize()) throw new IllegalArgumentException("target is too small");
-        
-        int cur = offset;
-        System.arraycopy(_iv[0], 0, target, cur, IV_SIZE);
-        cur += IV_SIZE;
-        System.arraycopy(_payload, 0, target, cur, _payload.length);
-        cur += _payload.length;
-        for (int column = 0; column < COLUMNS; column++) {
-            System.arraycopy(_eH[column][0], 0, target, cur, COLUMN_WIDTH);
-            cur += COLUMN_WIDTH;
-        }
-        System.arraycopy(_V, 0, target, cur, VERIFICATION_WIDTH);
-        cur += VERIFICATION_WIDTH;
-        return cur;
-    }
-    
-    /**
-     * Verify that the checksum block is as it should be (per _eH[*][peer+1]) 
-     * when presented with what the peer has decrypted.  Useful for debugging
-     * only.
-     */
-    public final boolean compareChecksumBlock(I2PAppContext ctx, byte message[], int peer) {
-        Log log = ctx.logManager().getLog(GatewayMessage.class);
-        boolean match = true;
-        
-        int off = message.length - (COLUMNS + 1) * COLUMN_WIDTH;
-        for (int column = 0; column < COLUMNS; column++) {
-            boolean ok = DataHelper.eq(_eH[column][peer], 0, message, off, COLUMN_WIDTH);
-            if (log.shouldLog(Log.DEBUG))
-                log.debug("checksum[" + column + "][" + (peer) + "] matches?  " + ok);
-            
-            off += COLUMN_WIDTH;
-            match = match && ok;
-        }
-        
-        return match;
-    }
-}
diff --git a/router/java/src/net/i2p/router/tunnel/GatewayTunnelConfig.java b/router/java/src/net/i2p/router/tunnel/GatewayTunnelConfig.java
deleted file mode 100644
index b9b671844d5891face553ebebdc5b564d55349e7..0000000000000000000000000000000000000000
--- a/router/java/src/net/i2p/router/tunnel/GatewayTunnelConfig.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.i2p.router.tunnel;
-
-import net.i2p.data.SessionKey;
-
-/**
- * Coordinate the data that the gateway to a tunnel needs to know
- *
- */
-public class GatewayTunnelConfig {
-    /** the key for the first hop after the gateway is in _keys[0] */
-    private SessionKey _keys[];
-    
-    /** Creates a new instance of TunnelConfig */
-    public GatewayTunnelConfig() {
-        _keys = new SessionKey[GatewayMessage.HOPS];
-    }
-    
-    /** What is the session key for the given hop? */
-    public SessionKey getSessionKey(int layer) { return _keys[layer]; }
-    public void setSessionKey(int layer, SessionKey key) { _keys[layer] = key; }
-}
diff --git a/router/java/src/net/i2p/router/tunnel/HopConfig.java b/router/java/src/net/i2p/router/tunnel/HopConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ad2e48ce401d5789383158fd1bba02f7615b617
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/HopConfig.java
@@ -0,0 +1,70 @@
+package net.i2p.router.tunnel;
+
+import java.util.Map;
+
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+
+/**
+ * Defines the general configuration for a hop in a tunnel.
+ *
+ */
+public class HopConfig {
+    private byte _receiveTunnelId[];
+    private Hash _receiveFrom;
+    private byte _sendTunnelId[];
+    private Hash _sendTo;
+    private SessionKey _layerKey;
+    private SessionKey _ivKey;
+    private long _expiration;
+    private Map _options;
+    
+    public HopConfig() {
+        _receiveTunnelId = null;
+        _receiveFrom = null;
+        _sendTunnelId = null;
+        _sendTo = null;
+        _layerKey = null;
+        _ivKey = null;
+        _expiration = -1;
+        _options = null;
+    }
+    
+    /** what tunnel ID are we receiving on? */
+    public byte[] getReceiveTunnelId() { return _receiveTunnelId; }
+    public void setReceiveTunnelId(byte id[]) { _receiveTunnelId = id; }
+    
+    /** what is the previous peer in the tunnel (if any)? */
+    public Hash getReceiveFrom() { return _receiveFrom; }
+    public void setReceiveFrom(Hash from) { _receiveFrom = from; }
+    
+    /** what is the next tunnel ID we are sending to? */
+    public byte[] getSendTunnelId() { return _sendTunnelId; }
+    public void setSendTunnelId(byte id[]) { _sendTunnelId = id; }
+    
+    /** what is the next peer in the tunnel (if any)? */
+    public Hash getSendTo() { return _sendTo; }
+    public void setSendTo(Hash to) { _sendTo = to; }
+    
+    /** what key should we use to encrypt the layer before passing it on? */
+    public SessionKey getLayerKey() { return _layerKey; }
+    public void setLayerKey(SessionKey key) { _layerKey = key; }
+    
+    /** what key should we use to encrypt the preIV before passing it on? */
+    public SessionKey getIVKey() { return _ivKey; }
+    public void setIVKey(SessionKey key) { _ivKey = key; }
+    
+    /** when does this tunnel expire (in ms since the epoch)? */
+    public long getExpiration() { return _expiration; }
+    public void setExpiration(long when) { _expiration = when; }
+    
+    /** 
+     * what are the configuration options for this tunnel (if any)?  keys to
+     * this map should be strings and values should be Objects of an 
+     * option-specific type (e.g. "maxMessages" would be an Integer, "shouldPad"
+     * would be a Boolean, etc).
+     *
+     */
+    public Map getOptions() { return _options; }
+    public void setOptions(Map options) { _options = options; }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/HopProcessor.java b/router/java/src/net/i2p/router/tunnel/HopProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..35140e20f8ca737e8c35413fb0db121774d5c42e
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/HopProcessor.java
@@ -0,0 +1,93 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.util.Log;
+
+/**
+ * Take a received tunnel message, verify that it isn't a 
+ * duplicate, and translate it into what the next hop will 
+ * want.  The hop processor works the same on all peers -
+ * inbound and outbound participants, outbound endpoints,
+ * and inbound gateways (with a small modification per 
+ * InbuondGatewayProcessor).  
+ *
+ */
+public class HopProcessor {
+    protected I2PAppContext _context;
+    private Log _log;
+    protected HopConfig _config;
+    private IVValidator _validator;
+    
+    static final int IV_LENGTH = 16;
+    
+    public HopProcessor(I2PAppContext ctx, HopConfig config) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(HopProcessor.class);
+        _config = config;
+        _validator = createValidator();
+    }
+    
+    protected IVValidator createValidator() { 
+        return new HashSetIVValidator();
+    }
+    
+    /**
+     * Process the data for the current hop, overwriting the original data with
+     * what should be sent to the next peer.  This also validates the previous 
+     * peer and the IV, making sure its not a repeat and not a loop.
+     *
+     * @param orig IV+data of the message
+     * @param offset index into orig where the IV begins
+     * @param length how long after the offset does the message go for?
+     * @param prev previous hop in the tunnel, or null if we are the gateway
+     * @return true if the message was updated and valid, false if it was not.
+     */
+    public boolean process(byte orig[], int offset, int length, Hash prev) {
+        // prev is null on gateways
+        if (prev != null) {
+            if (_config.getReceiveFrom() == null)
+                _config.setReceiveFrom(prev);
+            if (!_config.getReceiveFrom().equals(prev)) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Invalid previous peer - attempted hostile loop?  from " + prev 
+                               + ", expected " + _config.getReceiveFrom());
+                return false;
+            }
+        }
+        
+        byte iv[] = new byte[IV_LENGTH];
+        System.arraycopy(orig, offset, iv, 0, IV_LENGTH);
+        boolean okIV = _validator.receiveIV(iv);
+        if (!okIV) {
+            if (_log.shouldLog(Log.WARN)) 
+                _log.warn("Invalid IV received on tunnel " + _config.getReceiveTunnelId());
+            return false;
+        }
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            //_log.debug("IV received: " + Base64.encode(iv));
+            //_log.debug("Before:" + Base64.encode(orig, IV_LENGTH, orig.length - IV_LENGTH));
+        }
+        encrypt(orig, offset, length);
+        updateIV(orig, offset);
+        if (_log.shouldLog(Log.DEBUG)) {
+            //_log.debug("Data after processing: " + Base64.encode(orig, IV_LENGTH, orig.length - IV_LENGTH));
+            //_log.debug("IV sent: " + Base64.encode(orig, 0, IV_LENGTH));
+        }
+        return true;
+    }
+    
+    private final void encrypt(byte data[], int offset, int length) {
+        for (int off = offset + IV_LENGTH; off < length; off += IV_LENGTH) {
+            DataHelper.xor(data, off - IV_LENGTH, data, off, data, off, IV_LENGTH);
+            _context.aes().encryptBlock(data, off, _config.getLayerKey(), data, off);
+        }
+    }
+    
+    private final void updateIV(byte orig[], int offset) {
+        _context.aes().encryptBlock(orig, offset, _config.getIVKey(), orig, offset);
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/IVValidator.java b/router/java/src/net/i2p/router/tunnel/IVValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebf3a54179bb877a8cc58bf28d5ea948b95c6032
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/IVValidator.java
@@ -0,0 +1,45 @@
+package net.i2p.router.tunnel;
+
+import java.util.HashSet;
+import net.i2p.data.ByteArray;
+
+/**
+ * Provide a generic interface for IV validation which may be implemented
+ * through something as simple as a hashtable or more a complicated 
+ * bloom filter.
+ *
+ */
+public interface IVValidator {
+    /** 
+     * receive the IV for the tunnel, returning true if it is valid,
+     * or false if it has already been used (or is otherwise invalid).
+     *
+     */
+    public boolean receiveIV(byte iv[]);
+}
+
+/** accept everything */
+class DummyValidator implements IVValidator {
+    private static final DummyValidator _instance = new DummyValidator();
+    public static DummyValidator getInstance() { return _instance; }
+    private DummyValidator() {}
+    
+    public boolean receiveIV(byte[] iv) { return true; }
+}
+
+/** waste lots of RAM */
+class HashSetIVValidator implements IVValidator {
+    private HashSet _received;
+    
+    public HashSetIVValidator() {
+        _received = new HashSet();
+    }
+    public boolean receiveIV(byte[] iv) {
+        ByteArray ba = new ByteArray(iv);
+        boolean isNew = false;
+        synchronized (_received) {
+            isNew = _received.add(ba);
+        }
+        return isNew;
+    }
+}
\ No newline at end of file
diff --git a/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java b/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb3dcbf3bb3b674392b362ee1444277398a9ea82
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java
@@ -0,0 +1,58 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.util.Log;
+
+/**
+ * Receive the inbound tunnel message, removing all of the layers
+ * added by earlier hops to recover the preprocessed data sent
+ * by the gateway.  This delegates the crypto to the 
+ * OutboundGatewayProcessor, since the tunnel creator does the 
+ * same thing in both instances.
+ *
+ */
+public class InboundEndpointProcessor {
+    private I2PAppContext _context;
+    private Log _log;
+    private TunnelCreatorConfig _config;
+    private IVValidator _validator;
+    
+    public InboundEndpointProcessor(I2PAppContext ctx, TunnelCreatorConfig cfg) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(InboundEndpointProcessor.class);
+        _config = cfg;
+        _validator = DummyValidator.getInstance();
+    }
+    
+    /**
+     * Undo all of the encryption done by the peers in the tunnel, recovering the
+     * preprocessed data sent by the gateway.  
+     *
+     * @return true if the data was recovered (and written in place to orig), false
+     *         if it was a duplicate or from the wrong peer.
+     */
+    public boolean retrievePreprocessedData(byte orig[], int offset, int length, Hash prev) {
+        Hash last = _config.getPeer(_config.getLength()-1);
+        if (!last.equals(prev)) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Invalid previous peer - attempted hostile loop?  from " + prev 
+                           + ", expected " + last);
+            return false;
+        }
+        
+        byte iv[] = new byte[HopProcessor.IV_LENGTH];
+        System.arraycopy(orig, offset, iv, 0, iv.length);
+        boolean ok = _validator.receiveIV(iv);
+        if (!ok) {
+            if (_log.shouldLog(Log.WARN)) 
+                _log.warn("Invalid IV received");
+            return false;
+        }
+        
+        // inbound endpoints and outbound gateways have to undo the crypto in the same way
+        OutboundGatewayProcessor.decrypt(_context, _config, iv, orig, offset, length);
+        return true;
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/InboundGatewayProcessor.java b/router/java/src/net/i2p/router/tunnel/InboundGatewayProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..cba42f2229216a91bc278404c345c58531525497
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/InboundGatewayProcessor.java
@@ -0,0 +1,33 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Hash;
+import net.i2p.util.Log;
+
+/**
+ * Override the hop processor to seed the message with a random
+ * IV.
+ */
+public class InboundGatewayProcessor extends HopProcessor {
+    public InboundGatewayProcessor(I2PAppContext ctx, HopConfig config) {
+        super(ctx, config);
+    }
+
+    /** we are the gateway, no need to validate the IV */
+    protected IVValidator createValidator() { 
+        return DummyValidator.getInstance();
+    }
+
+    /**
+     * Since we are the inbound gateway, pick a random IV, ignore the 'prev'
+     * hop, and encrypt the message like every other participant.
+     *
+     */
+    public boolean process(byte orig[], int offset, int length, Hash prev) {
+        byte iv[] = new byte[IV_LENGTH];
+        _context.random().nextBytes(iv);
+        System.arraycopy(iv, 0, orig, offset, IV_LENGTH);
+        
+        return super.process(orig, offset, length, null);
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/InboundTest.java b/router/java/src/net/i2p/router/tunnel/InboundTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fe69b7408e5fab73320a3726263511eb85af392b
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/InboundTest.java
@@ -0,0 +1,99 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.Log;
+
+/**
+ * Quick unit test for base functionality of inbound tunnel 
+ * operation
+ */
+public class InboundTest {
+    private I2PAppContext _context;
+    private Log _log;
+    
+    public InboundTest() {
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(InboundTest.class);
+    }
+    
+    public void runTest() {
+        int numHops = 8;
+        TunnelCreatorConfig config = prepareConfig(numHops);
+        long start = _context.clock().now();
+        for (int i = 0; i < 1000; i++) 
+            runTest(numHops, config);
+        long time = _context.clock().now() - start;
+        _log.debug("Time for 1000 messages: " + time);
+    }
+    
+    private void runTest(int numHops, TunnelCreatorConfig config) {
+        byte orig[] = new byte[1024];
+        byte message[] = new byte[1024];
+        _context.random().nextBytes(orig); // might as well fill the IV
+        System.arraycopy(orig, 0, message, 0, message.length);
+        
+        InboundGatewayProcessor p = new InboundGatewayProcessor(_context, config.getConfig(0));
+        p.process(message, 0, message.length, null);
+        
+        for (int i = 1; i < numHops; i++) {
+            HopProcessor hop = new HopProcessor(_context, config.getConfig(i));
+            Hash prev = config.getConfig(i).getReceiveFrom();
+            boolean ok = hop.process(message, 0, message.length, prev);
+            if (!ok)
+                _log.error("Error processing at hop " + i);
+            //else
+            //    _log.info("Processing OK at hop " + i);
+        }
+        
+        InboundEndpointProcessor end = new InboundEndpointProcessor(_context, config);
+        boolean ok = end.retrievePreprocessedData(message, 0, message.length, config.getPeer(numHops-1));
+        if (!ok)
+            _log.error("Error retrieving cleartext at the endpoint");
+        
+        //_log.debug("After: " + Base64.encode(message, 16, orig.length-16));
+        boolean eq = DataHelper.eq(orig, 16, message, 16, orig.length - 16);
+        _log.info("equal? " + eq);
+    }
+    
+    private TunnelCreatorConfig prepareConfig(int numHops) {
+        Hash peers[] = new Hash[numHops];
+        byte tunnelIds[][] = new byte[numHops][4];
+        for (int i = 0; i < numHops; i++) {
+            peers[i] = new Hash();
+            peers[i].setData(new byte[Hash.HASH_LENGTH]);
+            _context.random().nextBytes(peers[i].getData());
+            _context.random().nextBytes(tunnelIds[i]);
+        }
+        
+        TunnelCreatorConfig config = new TunnelCreatorConfig(numHops, false);
+        for (int i = 0; i < numHops; i++) {
+            config.setPeer(i, peers[i]);
+            HopConfig cfg = config.getConfig(i);
+            cfg.setExpiration(_context.clock().now() + 60000);
+            cfg.setIVKey(_context.keyGenerator().generateSessionKey());
+            cfg.setLayerKey(_context.keyGenerator().generateSessionKey());
+            if (i > 0)
+                cfg.setReceiveFrom(peers[i-1]);
+            else
+                cfg.setReceiveFrom(null);
+            cfg.setReceiveTunnelId(tunnelIds[i]);
+            if (i < numHops - 1) {
+                cfg.setSendTo(peers[i+1]);
+                cfg.setSendTunnelId(tunnelIds[i+1]);
+            } else {
+                cfg.setSendTo(null);
+                cfg.setSendTunnelId(null);
+            }
+        }
+        return config;
+    }
+    
+    public static void main(String args[]) {
+        InboundTest test = new InboundTest();
+        test.runTest();
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java b/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..d756321536dd91e73a0e1147940109b372c96ab3
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java
@@ -0,0 +1,87 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.util.Log;
+
+/**
+ * Turn the preprocessed tunnel data into something that can be delivered to the
+ * first hop in the tunnel.  The crypto used in this class is also used by the
+ * InboundEndpointProcessor, as its the same 'undo' function of the tunnel crypto.
+ *
+ */
+public class OutboundGatewayProcessor {
+    private I2PAppContext _context;
+    private Log _log;
+    private TunnelCreatorConfig _config;
+    
+    public OutboundGatewayProcessor(I2PAppContext ctx, TunnelCreatorConfig cfg) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(OutboundGatewayProcessor.class);
+        _config = cfg;
+    }
+    
+    /**
+     * Since we are the outbound gateway, pick a random IV and wrap the preprocessed 
+     * data so that it will be exposed at the endpoint.
+     *
+     * @param orig original data with an extra 16 bytes prepended.
+     * @param offset index into the array where the extra 16 bytes (IV) begins
+     * @param length how much of orig can we write to (must be a multiple of 16).
+     */
+    public void process(byte orig[], int offset, int length) {
+        byte iv[] = new byte[HopProcessor.IV_LENGTH];
+        _context.random().nextBytes(iv);
+        System.arraycopy(iv, 0, orig, offset, HopProcessor.IV_LENGTH);
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            _log.debug("Original random IV: " + Base64.encode(iv));
+            _log.debug("data:  " + Base64.encode(orig, iv.length, length - iv.length));
+        }
+        decrypt(_context, _config, iv, orig, offset, length);
+    }
+    
+    /**
+     * Undo the crypto that the various layers in the tunnel added.  This is used
+     * by both the outbound gateway (preemptively undoing the crypto peers will add)
+     * and by the inbound endpoint.
+     *
+     */
+    static void decrypt(I2PAppContext ctx, TunnelCreatorConfig cfg, byte iv[], byte orig[], int offset, int length) {
+        Log log = ctx.logManager().getLog(OutboundGatewayProcessor.class);
+        byte cur[] = new byte[HopProcessor.IV_LENGTH]; // so we dont malloc
+        for (int i = cfg.getLength()-1; i >= 0; i--) {
+            decrypt(ctx, iv, orig, offset, length, cur, cfg.getConfig(i));
+            if (log.shouldLog(Log.DEBUG)) {
+                //_log.debug("IV at hop " + i + ": " + Base64.encode(orig, offset, iv.length));
+                //log.debug("hop " + i + ": " + Base64.encode(orig, offset + iv.length, length - iv.length));
+            }
+        }
+    }
+    
+    private static void decrypt(I2PAppContext ctx, byte iv[], byte orig[], int offset, int length, byte cur[], HopConfig config) {
+        // update the IV for the previous (next?) hop
+        ctx.aes().decryptBlock(orig, offset, config.getIVKey(), orig, offset);
+        
+        int numBlocks = (length - HopProcessor.IV_LENGTH) / HopProcessor.IV_LENGTH;
+        
+        // prev == previous encrypted block (or IV for the first block)
+        byte prev[] = iv;
+        System.arraycopy(orig, offset, prev, 0, HopProcessor.IV_LENGTH);
+        //_log.debug("IV at curHop: " + Base64.encode(iv));
+        
+        //decrypt the whole row
+        for (int i = 0; i < numBlocks; i++) {
+            int off = (i + 1) * HopProcessor.IV_LENGTH + offset;
+        
+            System.arraycopy(orig, off, cur, 0, HopProcessor.IV_LENGTH);
+            ctx.aes().decryptBlock(orig, off, config.getLayerKey(), orig, off);
+            DataHelper.xor(prev, 0, orig, off, orig, off, HopProcessor.IV_LENGTH);
+            byte xf[] = prev;
+            prev = cur;
+            cur = xf;
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundTest.java b/router/java/src/net/i2p/router/tunnel/OutboundTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bed83a9ca82107935707908f479165ae1db50993
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/OutboundTest.java
@@ -0,0 +1,87 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.Log;
+
+/**
+ * Quick unit test for base functionality of outbound tunnel 
+ * operation
+ *
+ */
+public class OutboundTest {
+    private I2PAppContext _context;
+    private Log _log;
+    
+    public OutboundTest() {
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(OutboundTest.class);
+    }
+    
+    public void runTest() {
+        int numHops = 8;
+        TunnelCreatorConfig config = prepareConfig(numHops);
+        
+        byte orig[] = new byte[1024];
+        byte message[] = new byte[1024];
+        _context.random().nextBytes(orig); // might as well fill the IV
+        System.arraycopy(orig, 0, message, 0, message.length);
+        
+        OutboundGatewayProcessor p = new OutboundGatewayProcessor(_context, config);
+        p.process(message, 0, message.length);
+        
+        for (int i = 0; i < numHops; i++) {
+            HopProcessor hop = new HopProcessor(_context, config.getConfig(i));
+            Hash prev = config.getConfig(i).getReceiveFrom();
+            boolean ok = hop.process(message, 0, message.length, prev);
+            if (!ok)
+                _log.error("Error processing at hop " + i);
+            //else
+            //    _log.info("Processing OK at hop " + i);
+        }
+        
+        _log.debug("After: " + Base64.encode(message, 16, orig.length-16));
+        boolean eq = DataHelper.eq(orig, 16, message, 16, orig.length - 16);
+        _log.info("equal? " + eq);
+    }
+    
+    private TunnelCreatorConfig prepareConfig(int numHops) {
+        Hash peers[] = new Hash[numHops];
+        byte tunnelIds[][] = new byte[numHops][4];
+        for (int i = 0; i < numHops; i++) {
+            peers[i] = new Hash();
+            peers[i].setData(new byte[Hash.HASH_LENGTH]);
+            _context.random().nextBytes(peers[i].getData());
+            _context.random().nextBytes(tunnelIds[i]);
+        }
+        
+        TunnelCreatorConfig config = new TunnelCreatorConfig(numHops, false);
+        for (int i = 0; i < numHops; i++) {
+            HopConfig cfg = config.getConfig(i);
+            cfg.setExpiration(_context.clock().now() + 60000);
+            cfg.setIVKey(_context.keyGenerator().generateSessionKey());
+            cfg.setLayerKey(_context.keyGenerator().generateSessionKey());
+            if (i > 0)
+                cfg.setReceiveFrom(peers[i-1]);
+            else
+                cfg.setReceiveFrom(null);
+            cfg.setReceiveTunnelId(tunnelIds[i]);
+            if (i < numHops - 1) {
+                cfg.setSendTo(peers[i+1]);
+                cfg.setSendTunnelId(tunnelIds[i+1]);
+            } else {
+                cfg.setSendTo(null);
+                cfg.setSendTunnelId(null);
+            }
+        }
+        return config;
+    }
+    
+    public static void main(String args[]) {
+        OutboundTest test = new OutboundTest();
+        test.runTest();
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..23f4b2de0aaa2752470ac381eb7b5b6f7ff5fd70
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
@@ -0,0 +1,51 @@
+package net.i2p.router.tunnel;
+
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+
+/**
+ * Coordinate the info that the tunnel creator keeps track of, including what 
+ * peers are in the tunnel and what their configuration is
+ *
+ */
+public class TunnelCreatorConfig {
+    /** only necessary for client tunnels */
+    private Destination _destination;
+    /** gateway first */
+    private HopConfig _config[];
+    /** gateway first */
+    private Hash _peers[];
+    private boolean _isInbound;
+    
+    public TunnelCreatorConfig(int length, boolean isInbound) {
+        this(length, isInbound, null);
+    }
+    public TunnelCreatorConfig(int length, boolean isInbound, Destination destination) {
+        _config = new HopConfig[length];
+        _peers = new Hash[length];
+        for (int i = 0; i < length; i++) {
+            _config[i] = new HopConfig();
+        }
+        _isInbound = isInbound;
+        _destination = destination;
+    }
+    
+    /** how many hops are there in the tunnel? */
+    public int getLength() { return _config.length; }
+    
+    /** 
+     * retrieve the config for the given hop.  the gateway is
+     * hop 0.
+     */
+    public HopConfig getConfig(int hop) { return _config[hop]; }
+    
+    /** retrieve the peer at the given hop.  the gateway is hop 0 */
+    public Hash getPeer(int hop) { return _peers[hop]; }
+    public void setPeer(int hop, Hash peer) { _peers[hop] = peer; }
+    
+    /** is this an inbound tunnel? */
+    public boolean isInbound() { return _isInbound; }
+
+    /** if this is a client tunnel, what destination is it for? */
+    public Destination getDestination() { return _destination; }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java b/router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java
deleted file mode 100644
index 1c17f66c44274d78e578ef5629f587c313874201..0000000000000000000000000000000000000000
--- a/router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package net.i2p.router.tunnel;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.DataHelper;
-import net.i2p.data.Base64;
-import net.i2p.data.Hash;
-import net.i2p.data.SessionKey;
-import net.i2p.util.Log;
-
-/**
- * Decrypt a step in the tunnel, verifying the message in the process.
- *
- */
-public class TunnelMessageProcessor {
-    private static final int IV_SIZE = GatewayMessage.IV_SIZE;
-    private static final int HOPS = GatewayMessage.HOPS;
-    private static final int COLUMN_WIDTH = GatewayMessage.COLUMN_WIDTH;
-    private static final int VERIFICATION_WIDTH = GatewayMessage.VERIFICATION_WIDTH;
-    
-    /**
-     * Unwrap the tunnel message, overwriting it with the decrypted version.
-     *
-     * @param data full message received, written to while decrypted
-     * @param send decrypted tunnel message, ready to send
-     * @param layerKey session key to be used at the current layer
-     * @return true if the message was valid, false if it was not.
-     */
-    public boolean unwrapMessage(I2PAppContext ctx, byte data[], SessionKey layerKey) {
-        Log log = getLog(ctx);
-        
-        int payloadLength = data.length 
-                            - IV_SIZE // IV
-                            - HOPS * COLUMN_WIDTH // checksum blocks 
-                            - VERIFICATION_WIDTH; // verification of the checksum blocks
-        
-        Hash recvPayloadHash = ctx.sha().calculateHash(data, IV_SIZE, payloadLength);
-        if (log.shouldLog(Log.DEBUG))
-            log.debug("H(recvPayload) = " + recvPayloadHash.toBase64());
-        
-        decryptMessage(ctx, data, layerKey);
-        
-        Hash payloadHash = ctx.sha().calculateHash(data, IV_SIZE, payloadLength);
-        if (log.shouldLog(Log.DEBUG))
-            log.debug("H(payload) = " + payloadHash.toBase64());
-        
-        boolean ok = verifyMessage(ctx, data, payloadHash);
-        
-        if (ok) {
-            return true;
-        } else {
-            // no hashes were found that match the seen hash
-            if (log.shouldLog(Log.DEBUG))
-                log.debug("No hashes match");
-            return false;
-        }
-    }
-    
-    private void decryptMessage(I2PAppContext ctx, byte data[], SessionKey layerKey) {
-        Log log = getLog(ctx);
-        if (log.shouldLog(Log.DEBUG))
-            log.debug("IV[recv] = " + Base64.encode(data, 0, IV_SIZE));
-        
-        int numBlocks = (data.length - IV_SIZE) / IV_SIZE;
-        // for debugging, so we can compare eIV
-        int numPayloadBlocks = (data.length - IV_SIZE - COLUMN_WIDTH * HOPS - VERIFICATION_WIDTH) / IV_SIZE;
-        
-        // prev == previous encrypted block (or IV for the first block)
-        byte prev[] = new byte[IV_SIZE];
-        // cur == current encrypted block (so we can overwrite the data in place)
-        byte cur[] = new byte[IV_SIZE];
-        System.arraycopy(data, 0, prev, 0, IV_SIZE);
-        
-        //decrypt the whole row
-        for (int i = 0; i < numBlocks; i++) {
-            int off = (i + 1) * IV_SIZE;
-        
-            if (i == numPayloadBlocks) {
-                // should match the eIV
-                if (log.shouldLog(Log.DEBUG))
-                    log.debug("block[" + i + "].prev=" + Base64.encode(prev));
-            }
-            
-            System.arraycopy(data, off, cur, 0, IV_SIZE);
-            ctx.aes().decryptBlock(data, off, layerKey, data, off);
-            DataHelper.xor(prev, 0, data, off, data, off, IV_SIZE);
-            byte xf[] = prev;
-            prev = cur;
-            cur = xf;
-        }
-        
-        // update the IV for the next layer
-        
-        ctx.aes().decryptBlock(data, 0, layerKey, data, 0);
-        DataHelper.xor(data, 0, GatewayMessage.IV_WHITENER, 0, data, 0, IV_SIZE);
-        Hash h = ctx.sha().calculateHash(data, 0, IV_SIZE);
-        System.arraycopy(h.getData(), 0, data, 0, IV_SIZE);
-        
-        if (log.shouldLog(Log.DEBUG)) {
-            log.debug("IV[send] = " + Base64.encode(data, 0, IV_SIZE));
-            log.debug("key = " + layerKey.toBase64());
-        }
-    }
-    
-    private boolean verifyMessage(I2PAppContext ctx, byte data[], Hash payloadHash) {
-        Log log = getLog(ctx);
-        int matchFound = -1;
-        
-        int off = data.length - HOPS * COLUMN_WIDTH - VERIFICATION_WIDTH;
-        for (int i = 0; i < HOPS; i++) {
-            if (DataHelper.eq(payloadHash.getData(), 0, data, off, COLUMN_WIDTH)) {
-                matchFound = i;
-                break;
-            }
-            
-            off += COLUMN_WIDTH;
-        }
-        
-        if (log.shouldLog(Log.DEBUG)) {
-            off = data.length - HOPS * COLUMN_WIDTH - VERIFICATION_WIDTH;
-            for (int i = 0; i < HOPS; i++)
-                log.debug("checksum[" + i + "] = " + Base64.encode(data, off + i*COLUMN_WIDTH, COLUMN_WIDTH)
-                          + (i == matchFound ? " * MATCH" : ""));
-            
-            log.debug("verification = " + Base64.encode(data, data.length - VERIFICATION_WIDTH, VERIFICATION_WIDTH));
-        }
-        
-        return matchFound != -1;
-    }
-
-    /**
-     * Determine whether the checksum block has been modified by comparing the final
-     * verification hash to the hash of the block.
-     * 
-     * @return true if the checksum is valid, false if it has been modified
-     */
-    public boolean verifyChecksum(I2PAppContext ctx, byte message[]) {
-        int checksumSize = HOPS * COLUMN_WIDTH;
-        int offset = message.length - (checksumSize + VERIFICATION_WIDTH);
-        Hash checksumHash = ctx.sha().calculateHash(message, offset, checksumSize);
-        getLog(ctx).debug("Measured checksum: " + checksumHash.toBase64());
-        byte expected[] = new byte[VERIFICATION_WIDTH];
-        System.arraycopy(message, message.length-VERIFICATION_WIDTH, expected, 0, VERIFICATION_WIDTH);
-        getLog(ctx).debug("Expected checksum: " + Base64.encode(expected));
-        
-        return DataHelper.eq(checksumHash.getData(), 0, message, message.length-VERIFICATION_WIDTH, VERIFICATION_WIDTH);
-    }
-    
-    private static final Log getLog(I2PAppContext ctx) { 
-        return ctx.logManager().getLog(TunnelMessageProcessor.class);
-    }
-}
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelProcessingTest.java b/router/java/src/net/i2p/router/tunnel/TunnelProcessingTest.java
deleted file mode 100644
index cf9dd157d3608a2f5f209b92830171d8e6f91949..0000000000000000000000000000000000000000
--- a/router/java/src/net/i2p/router/tunnel/TunnelProcessingTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.i2p.router.tunnel;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.DataHelper;
-import net.i2p.data.SessionKey;
-import net.i2p.util.Log;
-
-/**
- * Test the tunnel encryption - build a message as a gateway, pass it through
- * the sequence of participants (verifying the message along the way), and 
- * make sure it comes out the other side correctly.
- *
- */
-public class TunnelProcessingTest {
-    public void testTunnel() {
-        I2PAppContext ctx = I2PAppContext.getGlobalContext();
-        Log log = ctx.logManager().getLog(TunnelProcessingTest.class);
-        if (true) {
-            byte orig[] = new byte[16*1024];
-            ctx.random().nextBytes(orig);
-            GatewayTunnelConfig cfg = new GatewayTunnelConfig();
-            for (int i = 0; i < GatewayMessage.HOPS; i++) {
-                cfg.setSessionKey(i, ctx.keyGenerator().generateSessionKey());
-                log.debug("key[" + i + "] = " + cfg.getSessionKey(i).toBase64());
-            }
-            testTunnel(ctx, orig, cfg);
-        }
-        if (false) {
-            GatewayTunnelConfig cfg = new GatewayTunnelConfig();
-            for (int i = 0; i < GatewayMessage.HOPS; i++) {
-                SessionKey key = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
-                cfg.setSessionKey(i, key);
-                log.debug("key[" + i + "] = " + key.toBase64());
-            }
-            
-            testTunnel(ctx, new byte[0], cfg);
-        }
-    }
-    
-    public void testTunnel(I2PAppContext ctx, byte orig[], GatewayTunnelConfig cfg) {
-        Log log = ctx.logManager().getLog(TunnelProcessingTest.class);
-        
-        log.debug("H[orig] = " + ctx.sha().calculateHash(orig).toBase64());
-        
-        log.debug("\n\nEncrypting the payload");
-        
-        byte cur[] = new byte[orig.length];
-        System.arraycopy(orig, 0, cur, 0, cur.length);
-        GatewayMessage msg = new GatewayMessage(ctx);
-        msg.setPayload(cur);
-        msg.encrypt(cfg);
-        int size = msg.getExportedSize();
-        byte message[] = new byte[size];
-        int exp = msg.export(message, 0);
-        if (exp != size) throw new RuntimeException("Foo!");
-        
-        TunnelMessageProcessor proc = new TunnelMessageProcessor();
-        for (int i = 1; i < GatewayMessage.HOPS; i++) {
-            log.debug("\n\nUnwrapping step " + i);
-            boolean ok = proc.unwrapMessage(ctx, message, cfg.getSessionKey(i));
-            if (!ok) 
-                log.error("Unwrap failed at step " + i);
-            else
-                log.info("** Unwrap succeeded at step " + i);
-            boolean match = msg.compareChecksumBlock(ctx, message, i);
-        }
-        
-        log.debug("\n\nVerifying the tunnel processing");
-        
-        for (int i = 0; i < orig.length; i++) {
-            if (orig[i] != message[16 + i]) {
-                log.error("Finished payload does not match at byte " + i + 
-                          ctx.sha().calculateHash(message, 16, orig.length).toBase64());
-                break;
-            }
-        }
-        
-        boolean ok = proc.verifyChecksum(ctx, message);
-        if (!ok)
-            log.error("Checksum could not be verified");
-        else
-            log.error("** Checksum verified");
-    }
-    
-    public static void main(String args[]) {
-        TunnelProcessingTest t = new TunnelProcessingTest();
-        t.testTunnel();
-    }
-}