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