From cf84f453d342bec21fdb82718f61b87951fe3e67 Mon Sep 17 00:00:00 2001
From: jrandom <jrandom>
Date: Fri, 7 Jan 2005 22:55:30 +0000
Subject: [PATCH] Initial implementation of the new tunnel encryption code. 
 Still much more work to be done (e.g. *what* gets encrypted, modifying the
 tunnelCreate messages, the tunnel building process, and the new tunnel
 pooling).  I seem to have lost much of the typed up docs describing this too,
 so I'll be hitting that next.

---
 .../net/i2p/router/tunnel/GatewayMessage.java | 477 ++++++++++++++++++
 .../router/tunnel/GatewayTunnelConfig.java    |  21 +
 .../router/tunnel/TunnelMessageProcessor.java | 147 ++++++
 .../router/tunnel/TunnelProcessingTest.java   |  89 ++++
 4 files changed, 734 insertions(+)
 create mode 100644 router/java/src/net/i2p/router/tunnel/GatewayMessage.java
 create mode 100644 router/java/src/net/i2p/router/tunnel/GatewayTunnelConfig.java
 create mode 100644 router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java
 create mode 100644 router/java/src/net/i2p/router/tunnel/TunnelProcessingTest.java

diff --git a/router/java/src/net/i2p/router/tunnel/GatewayMessage.java b/router/java/src/net/i2p/router/tunnel/GatewayMessage.java
new file mode 100644
index 0000000000..ca3d28f7c9
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/GatewayMessage.java
@@ -0,0 +1,477 @@
+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>Turn some raw data into something we can pass down the tunnel, decrypting
+ * and verifying along the way.  The encryption used is such that decryption
+ * merely requires running over the data with AES in CTR mode, calculating the
+ * SHA256 of a certain fixed portion of the message (bytes 16 through $size-288),
+ * and searching for that hash in the checksum block.  There is a fixed number 
+ * of hops defined (8 peers after the gateway) so that we can verify the message
+ * without either leaking the position in the tunnel or having the message 
+ * continually "shrink" as layers are peeled off.  For tunnels shorter than 9
+ * hops, the tunnel creator will take the place of the excess hops, decrypting 
+ * with their keys (for outbound tunnels, this is done at the beginning, and for
+ * inbound tunnels, the end).</p>
+ *
+ * <p>The hard part in the encryption is building that entangled checksum block, 
+ * which requires essentially finding out what the hash of the payload will look 
+ * like at each step, randomly ordering those hashes, then building a matrix of 
+ * what each of those randomly ordered hashes will look like at each step.  
+ * To visualize this a bit:</p>
+ *
+ * <table border="1">
+ *  <tr><td colspan="2"></td>
+ *      <td><b>IV</b></td><td><b>Payload</b></td>
+ *      <td><b>eH[0]</b></td><td><b>eH[1]</b></td>
+ *      <td><b>eH[2]</b></td><td><b>eH[3]</b></td>
+ *      <td><b>eH[4]</b></td><td><b>eH[5]</b></td>
+ *      <td><b>eH[6]</b></td><td><b>eH[7]</b></td>
+ *      <td><b>V</b></td>
+ *      <td><b>Key</b></td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer0</b></td><td><b>recv</b></td>
+ *      <td>IV[0]</td><td>P[0]</td>
+ *      <td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>
+ *      <td>V[0]</td>
+ *      <td rowspan="2">K[0]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[1]</td><td rowspan="2">P[1]</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2">H(P[1])</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2">V[1]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer1</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[1]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[2]</td><td rowspan="2">P[2]</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2">H(P[2])</td><td rowspan="2"></td>
+ *      <td rowspan="2">V[2]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer2</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[2]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[3]</td><td rowspan="2">P[3]</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2">H(P[3])</td>
+ *      <td rowspan="2">V[3]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer3</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[3]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[4]</td><td rowspan="2">P[4]</td>
+ *      <td rowspan="2">H(P[4])</td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2">V[4]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer4</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[4]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[5]</td><td rowspan="2">P[5]</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2">H(P[5])</td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2">V[5]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer5</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[5]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[6]</td><td rowspan="2">P[6]</td>
+ *      <td rowspan="2"></td><td rowspan="2">H(P[6])</td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2">V[6]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer6</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[6]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td rowspan="2">IV[7]</td><td rowspan="2">P[7]</td>
+ *      <td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2"></td><td rowspan="2">H(P[7])</td><td rowspan="2"></td><td rowspan="2"></td>
+ *      <td rowspan="2">V[7]</td>
+ *  </tr>
+ *  <tr><td rowspan="2"><b>peer7</b></td><td><b>recv</b></td>
+ *      <td rowspan="2">K[7]</td>
+ *  </tr>
+ *  <tr><td><b>send</b></td>
+ *      <td>IV[8]</td><td>P[8]</td>
+ *      <td></td><td></td><td></td><td></td><td>H(P[8])</td><td></td><td></td><td></td>
+ *      <td>V[8]</td>
+ *  </tr>
+ * </table>
+ *
+ * <p>In the above, P[8] is the same as the original data being passed through the
+ * tunnel, and V[8] is the SHA256 of eH[0-8] as seen on peer7 after decryption.  For
+ * cells in the matrix "higher up" than the hash, their value is derived by encrypting
+ * the cell below it with the key for the peer below it, using the end of the column 
+ * to the left of it as the IV.  For cells in the matrix "lower down" than the hash, 
+ * they're equal to the cell above them, decrypted by the current peer's key, using 
+ * the end of the previous encrypted block on that row.</p>
+ * 
+ * <p>With this randomized matrix of checksum blocks, each peer will be able to find
+ * the hash of the payload, or if it is not there, know that the message is corrupt.
+ * The entanglement by using CTR mode increases the difficulty in tagging the 
+ * checksum blocks themselves, but it is still possible for that tagging to go 
+ * briefly undetected if the columns after the tagged data have already been used
+ * to check the payload at a peer.  In any case, the tunnel endpoint (peer 7) knows
+ * for certain whether any of the checksum blocks have been tagged, as that would
+ * corrupt the verification block (V[8]).</p>
+ *
+ * <p>The IV[0] is a random 16 byte value, and IV[i] is the first 16 bytes of 
+ * H(D(IV[i-1], K[i-1])).  We don't use the same IV along the path, as that would
+ * allow trivial collusion, and we use the hash of the decrypted value to propogate 
+ * the IV so as to hamper key leakage.</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;
+    
+    static final int HOPS = 8;
+    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 + 1;
+    
+    public GatewayMessage(I2PAppContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(GatewayMessage.class);
+        initialize();
+    }
+    
+    private void initialize() {
+        _iv = new byte[HOPS][IV_SIZE];
+        _eIV = new byte[HOPS][IV_SIZE];
+        _H = new byte[HOPS][Hash.HASH_LENGTH];
+        _eH = new byte[COLUMNS][HASH_ROWS][Hash.HASH_LENGTH];
+        _preV = new byte[HOPS*Hash.HASH_LENGTH];
+        _V = new byte[Hash.HASH_LENGTH];
+        _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 = 0; i < HOPS - 1; i++) {
+            SessionKey key = cfg.getSessionKey(i);
+            
+            // decrypt, since we're simulating what the participants do
+            _context.aes().decryptBlock(_iv[i], 0, key, _iv[i+1], 0);
+            Hash h = _context.sha().calculateHash(_iv[i+1]);
+            System.arraycopy(h.getData(), 0, _iv[i+1], 0, IV_SIZE);
+        }
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            for (int i = 0; i < HOPS; i++)
+                _log.debug("_iv[" + i + "] = " + Base64.encode(_iv[i]));
+        }
+    }
+    
+    /** 
+     * Encrypt the payload and IV blocks, overwriting _iv and _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], 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], 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], 0, IV_SIZE);
+            }
+        }
+
+        if (_log.shouldLog(Log.DEBUG)) {
+            for (int i = 0; i < HOPS; 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+1] 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 to the first hop.  The encryption uses the _eIV for each 
+     * step so that a plain AES/CTR decrypt of the entire message will expose
+     * the layer.  _eH[column][_order[i]+1] == _H[_order[i]]
+     */
+    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+1], 0, Hash.HASH_LENGTH);
+            
+            // now fill in the "earlier" _eH[column][row] values for earlier hops 
+            // by encrypting _eH[column][row+1] with the peer's key, using the end 
+            // of the previous column (or _eIV[row]) as the IV
+            for (int row = hash; row >= 0; row--) {
+                SessionKey key = cfg.getSessionKey(row);
+                // first half
+                if (column == 0) {
+                    DataHelper.xor(_eIV[row], 0, _eH[column][row+1], 0, _eH[column][row], 0, IV_SIZE);
+                } else {
+                    DataHelper.xor(_eH[column-1][row], IV_SIZE, _eH[column][row+1], 0, _eH[column][row], 0, IV_SIZE);
+                }
+                _context.aes().encryptBlock(_eH[column][row], 0, key, _eH[column][row], 0);
+                
+                // second half
+                DataHelper.xor(_eH[column][row], 0, _eH[column][row+1], IV_SIZE, _eH[column][row], IV_SIZE, IV_SIZE);
+                _context.aes().encryptBlock(_eH[column][row], IV_SIZE, key, _eH[column][row], IV_SIZE);
+            }
+            
+            // 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-1);
+                
+                _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], IV_SIZE, _eH[column][row], 0, _eH[column][row], 0, IV_SIZE);
+                
+                _context.aes().decryptBlock(_eH[column][row-1], IV_SIZE, key, _eH[column][row], IV_SIZE);
+                DataHelper.xor(_eH[column][row-1], 0, _eH[column][row], IV_SIZE, _eH[column][row], IV_SIZE, 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], _eH[column][peer]) ? " CLEARTEXT" : ""));
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                        System.out.println("column="+column + " peer=" + peer);
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Build the _V hash as the SHA256 of _eH[*][8], 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 * Hash.HASH_LENGTH, Hash.HASH_LENGTH);
+        Hash v = _context.sha().calculateHash(_preV);
+        System.arraycopy(v.getData(), 0, _V, 0, Hash.HASH_LENGTH);
+
+        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[HOPS-1][i], IV_SIZE, _V, 0, IV_SIZE);
+            _context.aes().encryptBlock(_V, 0, key, _V, 0);
+            DataHelper.xor(_V, 0, _V, IV_SIZE, _V, IV_SIZE, IV_SIZE);
+            _context.aes().encryptBlock(_V, IV_SIZE, key, _V, IV_SIZE);
+            
+            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 * Hash.HASH_LENGTH +
+               Hash.HASH_LENGTH; // verification hash
+    }
+    
+    /**
+     * Write out the fully encrypted tunnel message to the target
+     *
+     * @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, Hash.HASH_LENGTH);
+            cur += Hash.HASH_LENGTH;
+        }
+        System.arraycopy(_V, 0, target, cur, Hash.HASH_LENGTH);
+        cur += Hash.HASH_LENGTH;
+        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) * Hash.HASH_LENGTH;
+        for (int column = 0; column < COLUMNS; column++) {
+            boolean ok = DataHelper.eq(_eH[column][peer+1], 0, message, off, Hash.HASH_LENGTH);
+            if (log.shouldLog(Log.DEBUG))
+                log.debug("checksum[" + column + "][" + (peer+1) + "] matches?  " + ok);
+            
+            off += Hash.HASH_LENGTH;
+            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
new file mode 100644
index 0000000000..b9b671844d
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/GatewayTunnelConfig.java
@@ -0,0 +1,21 @@
+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/TunnelMessageProcessor.java b/router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java
new file mode 100644
index 0000000000..e6153c2b6c
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/TunnelMessageProcessor.java
@@ -0,0 +1,147 @@
+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;
+    
+    /**
+     * 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 * Hash.HASH_LENGTH // checksum blocks 
+                            - Hash.HASH_LENGTH; // 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 - 2 * IV_SIZE * (GatewayMessage.HOPS + 1)) / 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);
+        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 - (GatewayMessage.HOPS + 1) * Hash.HASH_LENGTH;
+        for (int i = 0; i < GatewayMessage.HOPS; i++) {
+            if (DataHelper.eq(payloadHash.getData(), 0, data, off, Hash.HASH_LENGTH)) {
+                matchFound = i;
+                break;
+            }
+            
+            off += Hash.HASH_LENGTH;
+        }
+        
+        if (log.shouldLog(Log.DEBUG)) {
+            off = data.length - (GatewayMessage.HOPS + 1) * Hash.HASH_LENGTH;
+            for (int i = 0; i < HOPS; i++)
+                log.debug("checksum[" + i + "] = " + Base64.encode(data, off + i*Hash.HASH_LENGTH, Hash.HASH_LENGTH)
+                          + (i == matchFound ? " * MATCH" : ""));
+            
+            log.debug("verification = " + Base64.encode(data, data.length - Hash.HASH_LENGTH, Hash.HASH_LENGTH));
+        }
+        
+        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 = GatewayMessage.HOPS * Hash.HASH_LENGTH;
+        int offset = message.length - (checksumSize + Hash.HASH_LENGTH);
+        Hash checksumHash = ctx.sha().calculateHash(message, offset, checksumSize);
+        getLog(ctx).debug("Measured checksum: " + checksumHash.toBase64());
+        byte expected[] = new byte[Hash.HASH_LENGTH];
+        System.arraycopy(message, message.length-Hash.HASH_LENGTH, expected, 0, Hash.HASH_LENGTH);
+        getLog(ctx).debug("Expected checksum: " + Base64.encode(expected));
+        
+        return DataHelper.eq(checksumHash.getData(), 0, message, message.length-Hash.HASH_LENGTH, Hash.HASH_LENGTH);
+    }
+    
+    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
new file mode 100644
index 0000000000..60a10ea9b0
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/TunnelProcessingTest.java
@@ -0,0 +1,89 @@
+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 = 0; 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();
+    }
+}
-- 
GitLab