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