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:
jrandom
2005-01-21 07:54:56 +00:00
committed by zzz
parent 3beb0d9c12
commit f6a34055ac
13 changed files with 623 additions and 630 deletions

View File

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

View File

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

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

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

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

View File

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

View File

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

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

View 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.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;
}
}
}

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

View File

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

View File

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

View File

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