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.
This commit is contained in:
jrandom
2005-01-07 22:55:30 +00:00
committed by zzz
parent daf32a24bc
commit cf84f453d3
4 changed files with 734 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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();
}
}