forked from I2P_Developers/i2p.i2p
removed the tunnel.html-style tunnel encryption and implemented the new tunnel-alt.html style
still much to be done beyond this, but this stuff turned out quite trivial (w00t)
This commit is contained in:
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
70
router/java/src/net/i2p/router/tunnel/HopConfig.java
Normal file
70
router/java/src/net/i2p/router/tunnel/HopConfig.java
Normal file
@ -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; }
|
||||||
|
}
|
93
router/java/src/net/i2p/router/tunnel/HopProcessor.java
Normal file
93
router/java/src/net/i2p/router/tunnel/HopProcessor.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
45
router/java/src/net/i2p/router/tunnel/IVValidator.java
Normal file
45
router/java/src/net/i2p/router/tunnel/IVValidator.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
99
router/java/src/net/i2p/router/tunnel/InboundTest.java
Normal file
99
router/java/src/net/i2p/router/tunnel/InboundTest.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
router/java/src/net/i2p/router/tunnel/OutboundTest.java
Normal file
87
router/java/src/net/i2p/router/tunnel/OutboundTest.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user