2006-01-17 jrandom

* First pass of the new tunnel creation crypto, specified in the new
      router/doc/tunnel-alt-creation.html (referenced in the current
      router/doc/tunnel-alt.html).  It isn't actually used anywhere yet, other
      than in the test code, but the code verifies the technical viability, so
      further scrutiny would be warranted.
This commit is contained in:
jrandom
2006-01-17 22:56:15 +00:00
committed by zzz
parent eb3442823e
commit a12ede096a
15 changed files with 1024 additions and 42 deletions

View File

@ -0,0 +1,238 @@
package net.i2p.data.i2np;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
/**
* Hold the tunnel request record, managing its encryption and decryption.
* Cleartext:
* <pre>
* bytes 0-3: tunnel ID to receive messages as
* bytes 4-35: local router identity hash
* bytes 36-39: next tunnel ID
* bytes 40-71: next router identity hash
* bytes 72-103: AES-256 tunnel layer key
* bytes 104-135: AES-256 tunnel IV key
* bytes 136-167: AES-256 reply key
* bytes 168-183: reply IV
* byte 184: flags
* bytes 185-188: request time (in hours since the epoch)
* bytes 189-222: uninterpreted / random padding
* </pre>
*
*/
public class BuildRequestRecord {
private ByteArray _data;
/**
* If set in the flag byte, any peer may send a message into this tunnel, but if
* not set, only the current predecessor may send messages in. This is only set on
* an inbound tunnel gateway.
*/
private static final int FLAG_UNRESTRICTED_PREV = (1 << 7);
/**
* If set in the flag byte, this is an outbound tunnel endpoint, which means that
* there is no 'next hop' and that the next hop fields contain the tunnel to which the
* reply message should be sent.
*/
private static final int FLAG_OUTBOUND_ENDPOINT = (1 << 6);
public static final int IV_SIZE = 16;
/** we show 16 bytes of the peer hash outside the elGamal block */
public static final int PEER_SIZE = 16;
public BuildRequestRecord(ByteArray data) { _data = data; }
public BuildRequestRecord() { }
public ByteArray getData() { return _data; }
public void setData(ByteArray data) { _data = data; }
private static final int OFF_RECV_TUNNEL = 0;
private static final int OFF_OUR_IDENT = OFF_RECV_TUNNEL + 4;
private static final int OFF_SEND_TUNNEL = OFF_OUR_IDENT + Hash.HASH_LENGTH;
private static final int OFF_SEND_IDENT = OFF_SEND_TUNNEL + 4;
private static final int OFF_LAYER_KEY = OFF_SEND_IDENT + Hash.HASH_LENGTH;
private static final int OFF_IV_KEY = OFF_LAYER_KEY + SessionKey.KEYSIZE_BYTES;
private static final int OFF_REPLY_KEY = OFF_IV_KEY + SessionKey.KEYSIZE_BYTES;
private static final int OFF_REPLY_IV = OFF_REPLY_KEY + SessionKey.KEYSIZE_BYTES;
private static final int OFF_FLAG = OFF_REPLY_IV + IV_SIZE;
private static final int OFF_REQ_TIME = OFF_FLAG + 1;
/** what tunnel ID should this receive messages on */
public long readReceiveTunnelId() {
return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_RECV_TUNNEL, 4);
}
/** true if the identity they expect us to be is who we are */
public boolean readOurIdentityMatches(Hash ourIdentity) {
return DataHelper.eq(ourIdentity.getData(), 0, _data.getData(), _data.getOffset() + OFF_OUR_IDENT, Hash.HASH_LENGTH);
}
/**
* What tunnel ID the next hop receives messages on. If this is the outbound tunnel endpoint,
* this specifies the tunnel ID to which the reply should be sent.
*/
public long readNextTunnelId() {
return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_SEND_TUNNEL, 4);
}
/**
* Read the next hop from the record. If this is the outbound tunnel endpoint, this specifies
* the gateway to which the reply should be sent.
*/
public Hash readNextIdentity() {
byte rv[] = new byte[Hash.HASH_LENGTH];
System.arraycopy(_data.getData(), _data.getOffset() + OFF_SEND_IDENT, rv, 0, Hash.HASH_LENGTH);
return new Hash(rv);
}
/**
* Tunnel layer encryption key that the current hop should use
*/
public SessionKey readLayerKey() {
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
System.arraycopy(_data.getData(), _data.getOffset() + OFF_LAYER_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
return new SessionKey(key);
}
/**
* Tunnel IV encryption key that the current hop should use
*/
public SessionKey readIVKey() {
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
System.arraycopy(_data.getData(), _data.getOffset() + OFF_IV_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
return new SessionKey(key);
}
/**
* Session key that should be used to encrypt the reply
*/
public SessionKey readReplyKey() {
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
System.arraycopy(_data.getData(), _data.getOffset() + OFF_REPLY_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
return new SessionKey(key);
}
/**
* IV that should be used to encrypt the reply
*/
public byte[] readReplyIV() {
byte iv[] = new byte[IV_SIZE];
System.arraycopy(_data.getData(), _data.getOffset() + OFF_REPLY_IV, iv, 0, IV_SIZE);
return iv;
}
/**
* The current hop is the inbound gateway. If this is true, it means anyone can send messages to
* this tunnel, but if it is false, only the current predecessor can.
*
*/
public boolean readIsInboundGateway() {
return (_data.getData()[_data.getOffset() + OFF_FLAG] & FLAG_UNRESTRICTED_PREV) != 0;
}
/**
* The current hop is the outbound endpoint. If this is true, the next identity and next tunnel
* fields refer to where the reply should be sent.
*/
public boolean readIsOutboundEndpoint() {
return (_data.getData()[_data.getOffset() + OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0;
}
/**
* Time that the request was sent, truncated to the nearest hour
*/
public long readRequestTime() {
return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * 60l * 60l * 1000l;
}
/**
* Encrypt the record to the specified peer. The result is formatted as: <pre>
* bytes 0-15: SHA-256-128 of the current hop's identity (the toPeer parameter)
* bytes 15-527: ElGamal-2048 encrypted block
* </pre>
*/
public void encryptRecord(I2PAppContext ctx, PublicKey toKey, Hash toPeer, byte out[], int outOffset) {
System.arraycopy(toPeer.getData(), 0, out, outOffset, PEER_SIZE);
byte preEncr[] = new byte[OFF_REQ_TIME + 4 + PADDING_SIZE];
System.arraycopy(_data.getData(), _data.getOffset(), preEncr, 0, preEncr.length);
byte encrypted[] = ctx.elGamalEngine().encrypt(preEncr, toKey);
// the elg engine formats it kind of weird, giving 257 bytes for each part rather than 256, so
// we want to strip out that excess byte and store it in the record
System.arraycopy(encrypted, 1, out, outOffset + PEER_SIZE, 256);
System.arraycopy(encrypted, 258, out, outOffset + 256 + PEER_SIZE, 256);
}
/**
* Decrypt the data from the specified record, writing the decrypted record into this instance's
* buffer (but not overwriting the array contained within the old buffer)
*/
public boolean decryptRecord(I2PAppContext ctx, PrivateKey ourKey, Hash ourIdent, ByteArray encryptedRecord) {
if (DataHelper.eq(ourIdent.getData(), 0, encryptedRecord.getData(), encryptedRecord.getOffset(), PEER_SIZE)) {
byte preDecrypt[] = new byte[514];
System.arraycopy(encryptedRecord.getData(), encryptedRecord.getOffset() + PEER_SIZE, preDecrypt, 1, 256);
System.arraycopy(encryptedRecord.getData(), encryptedRecord.getOffset() + PEER_SIZE + 256, preDecrypt, 258, 256);
byte decrypted[] = ctx.elGamalEngine().decrypt(preDecrypt, ourKey);
if (decrypted != null) {
_data = new ByteArray(decrypted);
_data.setOffset(0);
return true;
} else {
return false;
}
} else {
return false;
}
}
private static final int PADDING_SIZE = 33;
/**
* Populate this instance with data. A new buffer is created to contain the data, with the
* necessary randomized padding.
*
* @param receiveTunnelId tunnel the current hop will receive messages on
* @param peer current hop's identity
* @param nextTunnelId id for the next hop, or where we send the reply (if we are the outbound endpoint)
* @param nextHop next hop's identity, or where we send the reply (if we are the outbound endpoint)
* @param layerKey tunnel layer key to be used by the peer
* @param ivKey tunnel IV key to be used by the peer
* @param replyKey key to be used when encrypting the reply to this build request
* @param iv iv to be used when encrypting the reply to this build request
* @param isInGateway are we the gateway of an inbound tunnel?
* @param isOutEndpoint are we the endpoint of an outbound tunnel?
*/
public void createRecord(I2PAppContext ctx, long receiveTunnelId, Hash peer, long nextTunnelId, Hash nextHop,
SessionKey layerKey, SessionKey ivKey, SessionKey replyKey, byte iv[], boolean isInGateway,
boolean isOutEndpoint) {
if ( (_data == null) || (_data.getData() != null) )
_data = new ByteArray();
byte buf[] = new byte[OFF_REQ_TIME+4+PADDING_SIZE];
_data.setData(buf);
/* bytes 0-3: tunnel ID to receive messages as
* bytes 4-35: local router identity hash
* bytes 36-39: next tunnel ID
* bytes 40-71: next router identity hash
* bytes 72-103: AES-256 tunnel layer key
* bytes 104-135: AES-256 tunnel IV key
* bytes 136-167: AES-256 reply key
* bytes 168-183: reply IV
* byte 184: flags
* bytes 185-188: request time (in hours since the epoch)
* bytes 189-222: uninterpreted / random padding
*/
DataHelper.toLong(buf, OFF_RECV_TUNNEL, 4, receiveTunnelId);
System.arraycopy(peer.getData(), 0, buf, OFF_OUR_IDENT, Hash.HASH_LENGTH);
DataHelper.toLong(buf, OFF_SEND_TUNNEL, 4, nextTunnelId);
System.arraycopy(nextHop.getData(), 0, buf, OFF_SEND_IDENT, Hash.HASH_LENGTH);
System.arraycopy(layerKey.getData(), 0, buf, OFF_LAYER_KEY, SessionKey.KEYSIZE_BYTES);
System.arraycopy(ivKey.getData(), 0, buf, OFF_IV_KEY, SessionKey.KEYSIZE_BYTES);
System.arraycopy(replyKey.getData(), 0, buf, OFF_REPLY_KEY, SessionKey.KEYSIZE_BYTES);
System.arraycopy(iv, 0, buf, OFF_REPLY_IV, IV_SIZE);
if (isInGateway)
buf[OFF_FLAG] |= FLAG_UNRESTRICTED_PREV;
else if (isOutEndpoint)
buf[OFF_FLAG] |= FLAG_OUTBOUND_ENDPOINT;
long truncatedHour = ctx.clock().now();
truncatedHour /= (60l*60l*1000l);
DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour);
byte rnd[] = new byte[PADDING_SIZE];
ctx.random().nextBytes(rnd);
System.arraycopy(rnd, 0, buf, OFF_REQ_TIME+4, rnd.length);
byte wroteIV[] = readReplyIV();
if (!DataHelper.eq(iv, wroteIV))
throw new RuntimeException("foo");
}
}

View File

@ -0,0 +1,23 @@
package net.i2p.data.i2np;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
/**
* Read and write the reply to a tunnel build message record.
*
*/
public class BuildResponseRecord {
/**
* Create a new encrypted response
*/
public byte[] create(I2PAppContext ctx, int status, SessionKey replyKey, byte replyIV[]) {
byte rv[] = new byte[TunnelBuildReplyMessage.RECORD_SIZE];
ctx.random().nextBytes(rv);
DataHelper.toLong(rv, TunnelBuildMessage.RECORD_SIZE-1, 1, status);
// rv = AES(SHA256(padding+status) + padding + status, replyKey, replyIV)
ctx.sha().calculateHash(rv, Hash.HASH_LENGTH, rv.length - Hash.HASH_LENGTH, rv, 0);
ctx.aes().encrypt(rv, 0, rv, 0, replyKey, replyIV, rv.length);
return rv;
}
}

View File

@ -345,6 +345,10 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
return new TunnelCreateMessage(context);
case TunnelCreateStatusMessage.MESSAGE_TYPE:
return new TunnelCreateStatusMessage(context);
case TunnelBuildMessage.MESSAGE_TYPE:
return new TunnelBuildMessage(context);
case TunnelBuildReplyMessage.MESSAGE_TYPE:
return new TunnelBuildReplyMessage(context);
default:
Builder builder = (Builder)_builders.get(new Integer(type));
if (builder == null)

View File

@ -0,0 +1,51 @@
package net.i2p.data.i2np;
import java.io.*;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
/**
*
*/
public class TunnelBuildMessage extends I2NPMessageImpl {
private ByteArray _records[];
public static final int MESSAGE_TYPE = 21;
public static final int RECORD_COUNT = 8;
public TunnelBuildMessage(I2PAppContext context) {
super(context);
_records = new ByteArray[RECORD_COUNT];
}
public void setRecord(int index, ByteArray record) { _records[index] = record; }
public ByteArray getRecord(int index) { return _records[index]; }
public static final int RECORD_SIZE = 512+16;
protected int calculateWrittenLength() { return RECORD_SIZE * RECORD_COUNT; }
public int getType() { return MESSAGE_TYPE; }
public void readMessage(byte[] data, int offset, int dataSize, int type) throws I2NPMessageException, IOException {
if (type != MESSAGE_TYPE)
throw new I2NPMessageException("Message type is incorrect for this message");
if (dataSize != calculateWrittenLength())
throw new I2NPMessageException("Wrong length (expects " + calculateWrittenLength() + ", recv " + dataSize + ")");
for (int i = 0; i < RECORD_COUNT; i++) {
int off = offset + (i * RECORD_SIZE);
int len = RECORD_SIZE;
setRecord(i, new ByteArray(data, off, len));
}
}
protected int writeMessageBody(byte[] out, int curIndex) throws I2NPMessageException {
int remaining = out.length - (curIndex + calculateWrittenLength());
if (remaining <= 0)
throw new I2NPMessageException("Not large enough (too short by " + remaining + ")");
for (int i = 0; i < RECORD_COUNT; i++) {
System.arraycopy(_records[i].getData(), _records[i].getOffset(), out, curIndex, RECORD_SIZE);
curIndex += RECORD_SIZE;
}
return curIndex;
}
}

View File

@ -0,0 +1,52 @@
package net.i2p.data.i2np;
import java.io.*;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
/**
* Transmitted from the new outbound endpoint to the creator through a
* reply tunnel
*/
public class TunnelBuildReplyMessage extends I2NPMessageImpl {
private ByteArray _records[];
public static final int MESSAGE_TYPE = 22;
public static final int RECORD_COUNT = TunnelBuildMessage.RECORD_COUNT;
public TunnelBuildReplyMessage(I2PAppContext context) {
super(context);
_records = new ByteArray[RECORD_COUNT];
}
public void setRecord(int index, ByteArray record) { _records[index] = record; }
public ByteArray getRecord(int index) { return _records[index]; }
public static final int RECORD_SIZE = TunnelBuildMessage.RECORD_SIZE;
protected int calculateWrittenLength() { return RECORD_SIZE * RECORD_COUNT; }
public int getType() { return MESSAGE_TYPE; }
public void readMessage(byte[] data, int offset, int dataSize, int type) throws I2NPMessageException, IOException {
if (type != MESSAGE_TYPE)
throw new I2NPMessageException("Message type is incorrect for this message");
if (dataSize != calculateWrittenLength())
throw new I2NPMessageException("Wrong length (expects " + calculateWrittenLength() + ", recv " + dataSize + ")");
for (int i = 0; i < RECORD_COUNT; i++) {
int off = offset + (i * RECORD_SIZE);
int len = RECORD_SIZE;
setRecord(i, new ByteArray(data, off, len));
}
}
protected int writeMessageBody(byte[] out, int curIndex) throws I2NPMessageException {
int remaining = out.length - (curIndex + calculateWrittenLength());
if (remaining <= 0)
throw new I2NPMessageException("Not large enough (too short by " + remaining + ")");
for (int i = 0; i < RECORD_COUNT; i++) {
System.arraycopy(_records[i].getData(), _records[i].getOffset(), out, curIndex, RECORD_SIZE);
curIndex += RECORD_SIZE;
}
return curIndex;
}
}

View File

@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
*
*/
public class RouterVersion {
public final static String ID = "$Revision: 1.333 $ $Date: 2006/01/13 01:27:44 $";
public final static String ID = "$Revision: 1.334 $ $Date: 2006/01/14 15:14:47 $";
public final static String VERSION = "0.6.1.9";
public final static long BUILD = 2;
public final static long BUILD = 3;
public static void main(String args[]) {
System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
System.out.println("Router ID: " + RouterVersion.ID);

View File

@ -0,0 +1,136 @@
package net.i2p.router.tunnel;
import java.util.*;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
import net.i2p.data.i2np.*;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
/**
*
*/
public class BuildMessageGenerator {
// cached, rather than creating lots of temporary Integer objects whenever we build a tunnel
static final Integer ORDER[] = new Integer[TunnelBuildMessage.RECORD_COUNT];
static { for (int i = 0; i < ORDER.length; i++) ORDER[i] = new Integer(i); }
/** return null if it is unable to find a router's public key (etc) */
public TunnelBuildMessage createInbound(RouterContext ctx, TunnelCreatorConfig cfg) {
return create(ctx, cfg, null, -1);
}
/** return null if it is unable to find a router's public key (etc) */
public TunnelBuildMessage createOutbound(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) {
return create(ctx, cfg, replyRouter, replyTunnel);
}
private TunnelBuildMessage create(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) {
TunnelBuildMessage msg = new TunnelBuildMessage(ctx);
List order = new ArrayList(ORDER.length);
for (int i = 0; i < ORDER.length; i++) order.add(ORDER[i]);
Collections.shuffle(order, ctx.random());
for (int i = 0; i < ORDER.length; i++) {
int hop = ((Integer)order.get(i)).intValue();
Hash peer = cfg.getPeer(hop);
RouterInfo ri = ctx.netDb().lookupRouterInfoLocally(peer);
if (ri == null)
return null;
createRecord(i, hop, msg, cfg, replyRouter, replyTunnel, ctx, ri.getIdentity().getPublicKey());
}
layeredEncrypt(ctx, msg, cfg, order);
return msg;
}
/**
* Place the asymmetrically encrypted record in the specified record slot,
* containing the hop's configuration (as well as the reply info, if it is an outbound endpoint)
*/
public void createRecord(int recordNum, int hop, TunnelBuildMessage msg, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel, I2PAppContext ctx, PublicKey peerKey) {
Log log = ctx.logManager().getLog(getClass());
BuildRequestRecord req = null;
if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint
req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel);
else
req = createUnencryptedRecord(ctx, cfg, hop, null, -1);
byte encrypted[] = new byte[TunnelBuildMessage.RECORD_SIZE];
if (hop < cfg.getLength()) {
Hash peer = cfg.getPeer(hop);
if (peerKey == null)
throw new RuntimeException("hop = " + hop + " recordNum = " + recordNum + " len = " + cfg.getLength());
//if (log.shouldLog(Log.DEBUG))
// log.debug("Record " + recordNum + "/" + hop + ": unencrypted = " + Base64.encode(req.getData().getData()));
req.encryptRecord(ctx, peerKey, peer, encrypted, 0);
//if (log.shouldLog(Log.DEBUG))
// log.debug("Record " + recordNum + "/" + hop + ": encrypted = " + Base64.encode(encrypted));
} else {
ctx.random().nextBytes(encrypted);
}
msg.setRecord(recordNum, new ByteArray(encrypted));
}
private BuildRequestRecord createUnencryptedRecord(I2PAppContext ctx, TunnelCreatorConfig cfg, int hop, Hash replyRouter, long replyTunnel) {
if (hop < cfg.getLength()) {
// ok, now lets fill in some data
HopConfig hopConfig = cfg.getConfig(hop);
Hash peer = cfg.getPeer(hop);
long recvTunnelId = hopConfig.getReceiveTunnel().getTunnelId();
long nextTunnelId = -1;
Hash nextPeer = null;
if (hop + 1 < cfg.getLength()) {
nextTunnelId = cfg.getConfig(hop+1).getReceiveTunnel().getTunnelId();
nextPeer = cfg.getPeer(hop+1);
} else {
if ( (replyTunnel >= 0) && (replyRouter != null) ) {
nextTunnelId = replyTunnel;
nextPeer = replyRouter;
} else {
// inbound endpoint (aka creator)
nextTunnelId = 0;
nextPeer = peer; // self
}
}
SessionKey layerKey = hopConfig.getLayerKey();
SessionKey ivKey = hopConfig.getIVKey();
SessionKey replyKey = hopConfig.getReplyKey();
byte iv[] = hopConfig.getReplyIV().getData();
if ( (iv == null) || (iv.length != BuildRequestRecord.IV_SIZE) ) {
iv = new byte[BuildRequestRecord.IV_SIZE];
ctx.random().nextBytes(iv);
hopConfig.getReplyIV().setData(iv);
}
boolean isInGW = (cfg.isInbound() && (hop == 0));
boolean isOutEnd = (!cfg.isInbound() && (hop + 1 >= cfg.getLength()));
BuildRequestRecord rec= new BuildRequestRecord();
rec.createRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer, layerKey, ivKey, replyKey,
iv, isInGW, isOutEnd);
return rec;
} else {
return null;
}
}
/**
* Encrypt the records so their hop ident is visible at the appropriate times
* @param order list of hop #s as Integers. For instance, if (order.get(1) is 4), it is peer cfg.getPeer(4)
*/
public void layeredEncrypt(I2PAppContext ctx, TunnelBuildMessage msg, TunnelCreatorConfig cfg, List order) {
// encrypt the records so that the right elements will be visible at the right time
for (int i = 0; i < TunnelBuildMessage.RECORD_COUNT; i++) {
ByteArray rec = msg.getRecord(i);
Integer hopNum = (Integer)order.get(i);
int hop = hopNum.intValue();
if (hop >= cfg.getLength())
continue; // no need to encrypt it, as its random
// ok, now decrypt the record with all of the reply keys from cfg.getConfig(0) through hop-1
for (int j = hop-1; j >= 0; j--) {
HopConfig hopConfig = cfg.getConfig(j);
SessionKey key = hopConfig.getReplyKey();
byte iv[] = hopConfig.getReplyIV().getData();
int off = rec.getOffset();
ctx.aes().decrypt(rec.getData(), off, rec.getData(), off, key, iv, TunnelBuildMessage.RECORD_SIZE);
}
}
}
}

View File

@ -0,0 +1,61 @@
package net.i2p.router.tunnel;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
import net.i2p.data.i2np.*;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
/**
* Receive the build message at a certain hop, decrypt its encrypted record,
* read the enclosed tunnel request, decide how to reply, write the reply,
* encrypt the reply record, and return a TunnelBuildMessage to forward on to
* the next hop
*/
class BuildMessageProcessor {
/**
* Decrypt the record targetting us, encrypting all of the other records with the included
* reply key and IV. The original, encrypted record targetting us is removed from the request
* message (so that the reply can be placed in that position after going through the decrypted
* request record).
*
* @return the current hop's decrypted record
*/
public BuildRequestRecord decrypt(I2PAppContext ctx, TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) {
Log log = ctx.logManager().getLog(getClass());
BuildRequestRecord rv = null;
int ourHop = -1;
for (int i = 0; i < TunnelBuildMessage.RECORD_COUNT; i++) {
ByteArray rec = msg.getRecord(i);
int off = rec.getOffset();
int len = BuildRequestRecord.PEER_SIZE;
if (DataHelper.eq(ourHash.getData(), 0, rec.getData(), off, len)) {
BuildRequestRecord req = new BuildRequestRecord();
boolean ok = req.decryptRecord(ctx, privKey, ourHash, rec);
if (ok)
rv = req;
else
return null; // our hop is invalid? b0rkage
ourHop = i;
}
}
if (rv == null) {
// none of the records matched, b0rk
return null;
}
SessionKey replyKey = rv.readReplyKey();
byte iv[] = rv.readReplyIV();
int ivOff = 0;
for (int i = 0; i < TunnelBuildMessage.RECORD_COUNT; i++) {
if (i != ourHop) {
if (log.shouldLog(Log.DEBUG))
log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv, ivOff, 16));
ByteArray data = msg.getRecord(i);
ctx.aes().encrypt(data.getData(), data.getOffset(), data.getData(), data.getOffset(), replyKey,
iv, ivOff, data.getValid());
}
}
msg.setRecord(ourHop, null);
return rv;
}
}

View File

@ -0,0 +1,172 @@
package net.i2p.router.tunnel;
import java.util.*;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
import net.i2p.data.i2np.*;
import net.i2p.util.Log;
/**
* Simple test to create an encrypted TunnelBuildMessage, decrypt its layers (as it would be
* during transmission), inject replies, then handle the TunnelBuildReplyMessage (unwrapping
* the reply encryption and reading the replies).
*/
public class BuildMessageTest {
private Hash _peers[];
private PrivateKey _privKeys[];
private PublicKey _pubKeys[];
private Hash _replyRouter;
private long _replyTunnel;
public static void main(String args[]) {
BuildMessageTest test = new BuildMessageTest();
try {
test.runTest();
} catch (Exception e) { e.printStackTrace(); }
}
private void runTest() {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
Log log = ctx.logManager().getLog(getClass());
List order = pickOrder(ctx);
TunnelCreatorConfig cfg = createConfig(ctx);
_replyRouter = new Hash();
byte h[] = new byte[Hash.HASH_LENGTH];
Arrays.fill(h, (byte)0xFF);
_replyRouter.setData(h);
_replyTunnel = 42;
// populate and encrypt the message
BuildMessageGenerator gen = new BuildMessageGenerator();
TunnelBuildMessage msg = new TunnelBuildMessage(ctx);
for (int i = 0; i < BuildMessageGenerator.ORDER.length; i++) {
int hop = ((Integer)order.get(i)).intValue();
PublicKey key = null;
if (hop < _pubKeys.length)
key = _pubKeys[hop];
gen.createRecord(i, hop, msg, cfg, _replyRouter, _replyTunnel, ctx, key);
}
gen.layeredEncrypt(ctx, msg, cfg, order);
log.debug("\n================================================================" +
"\nMessage fully encrypted" +
"\n================================================================");
// now msg is fully encrypted, so lets go through the hops, decrypting and replying
// as necessary
for (int i = 0; i < cfg.getLength(); i++) {
BuildMessageProcessor proc = new BuildMessageProcessor();
// this not only decrypts the current hop's record, but encrypts the other records
// with the reply key
BuildRequestRecord req = proc.decrypt(ctx, msg, _peers[i], _privKeys[i]);
if (req == null) {
// no records matched the _peers[i], or the decryption failed
throw new RuntimeException("foo @ " + i);
}
long ourId = req.readReceiveTunnelId();
byte replyIV[] = req.readReplyIV();
long nextId = req.readNextTunnelId();
Hash nextPeer = req.readNextIdentity();
boolean isInGW = req.readIsInboundGateway();
boolean isOutEnd = req.readIsOutboundEndpoint();
long time = req.readRequestTime();
long now = (ctx.clock().now() / (60l*60l*1000l)) * (60*60*1000);
int ourSlot = -1;
BuildResponseRecord resp = new BuildResponseRecord();
byte reply[] = resp.create(ctx, 0, req.readReplyKey(), req.readReplyIV());
for (int j = 0; j < TunnelBuildMessage.RECORD_COUNT; j++) {
if (msg.getRecord(j) == null) {
ourSlot = j;
msg.setRecord(j, new ByteArray(reply));
break;
}
}
log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64()
+ " receives on " + ourId
+ " w/ replyIV " + Base64.encode(replyIV) + " sending to " + nextId
+ " on " + nextPeer.toBase64()
+ " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time));
}
log.debug("\n================================================================" +
"\nAll hops traversed and replies gathered" +
"\n================================================================");
// now all of the replies are populated, toss 'em into a reply message and handle it
TunnelBuildReplyMessage reply = new TunnelBuildReplyMessage(ctx);
for (int i = 0; i < TunnelBuildMessage.RECORD_COUNT; i++)
reply.setRecord(i, msg.getRecord(i));
BuildReplyHandler handler = new BuildReplyHandler();
int statuses[] = handler.decrypt(ctx, reply, cfg, order);
if (statuses == null) throw new RuntimeException("bar");
boolean allAgree = true;
for (int i = 0; i < cfg.getLength(); i++) {
Hash peer = cfg.getPeer(i);
int record = ((Integer)order.get(i)).intValue();
if (statuses[record] != 0)
allAgree = false;
//else
// penalize peer according to the rejection cause
}
log.debug("\n================================================================" +
"\nAll peers agree? " + allAgree +
"\n================================================================");
}
private static final List pickOrder(I2PAppContext ctx) {
// pseudorandom, yet consistent (so we can be repeatable)
List rv = new ArrayList(8);
rv.add(new Integer(2));
rv.add(new Integer(4));
rv.add(new Integer(6));
rv.add(new Integer(0));
rv.add(new Integer(1));
rv.add(new Integer(3));
rv.add(new Integer(5));
rv.add(new Integer(7));
return rv;
}
private TunnelCreatorConfig createConfig(I2PAppContext ctx) {
return configOutbound(ctx);
}
private TunnelCreatorConfig configOutbound(I2PAppContext ctx) {
_peers = new Hash[4];
_pubKeys = new PublicKey[_peers.length];
_privKeys = new PrivateKey[_peers.length];
for (int i = 0; i < _peers.length; i++) {
byte buf[] = new byte[Hash.HASH_LENGTH];
Arrays.fill(buf, (byte)i); // consistent for repeatability
Hash h = new Hash(buf);
_peers[i] = h;
Object kp[] = ctx.keyGenerator().generatePKIKeypair();
_pubKeys[i] = (PublicKey)kp[0];
_privKeys[i] = (PrivateKey)kp[1];
}
TunnelCreatorConfig cfg = new TunnelCreatorConfig(null, _peers.length, false);
long now = ctx.clock().now();
// peers[] is ordered endpoint first, but cfg.getPeer() is ordered gateway first
for (int i = 0; i < _peers.length; i++) {
cfg.setPeer(i, _peers[i]);
HopConfig hop = cfg.getConfig(i);
hop.setExpiration(now+10*60*1000);
hop.setIVKey(ctx.keyGenerator().generateSessionKey());
hop.setLayerKey(ctx.keyGenerator().generateSessionKey());
hop.setReplyKey(ctx.keyGenerator().generateSessionKey());
byte iv[] = new byte[BuildRequestRecord.IV_SIZE];
Arrays.fill(iv, (byte)i); // consistent for repeatability
hop.setReplyIV(new ByteArray(iv));
hop.setReceiveTunnelId(new TunnelId(i+1));
}
return cfg;
}
}

View File

@ -0,0 +1,73 @@
package net.i2p.router.tunnel;
import java.util.*;
import net.i2p.I2PAppContext;
import net.i2p.data.*;
import net.i2p.data.i2np.*;
import net.i2p.util.Log;
/**
* Decrypt the layers of a tunnel build reply message, determining whether the individual
* hops agreed to participate in the tunnel, or if not, why not.
*
*/
public class BuildReplyHandler {
public BuildReplyHandler() {}
/**
* Decrypt the tunnel build reply records. This overwrites the contents of the reply
*
* @return status for the records (in record order), or null if the replies were not valid. Fake records
* always have 0 as their value
*/
public int[] decrypt(I2PAppContext ctx, TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, List recordOrder) {
int rv[] = new int[TunnelBuildReplyMessage.RECORD_COUNT];
for (int i = 0; i < rv.length; i++) {
int hop = ((Integer)recordOrder.get(i)).intValue();
int ok = decryptRecord(ctx, reply, cfg, i, hop);
if (ok == -1) return null;
rv[i] = ok;
}
return rv;
}
/**
* Decrypt the record (removing the layers of reply encyption) and read out the status
*
* @return -1 on decrypt failure
*/
private int decryptRecord(I2PAppContext ctx, TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) {
Log log = ctx.logManager().getLog(getClass());
if (hop >= cfg.getLength()) {
if (log.shouldLog(Log.DEBUG))
log.debug("Record " + recordNum + "/" + hop + " is fake, so consider it valid...");
return 0;
}
ByteArray rec = reply.getRecord(recordNum);
int off = rec.getOffset();
for (int j = cfg.getLength() - 1; j >= hop; j--) {
HopConfig hopConfig = cfg.getConfig(j);
SessionKey replyKey = hopConfig.getReplyKey();
byte replyIV[] = hopConfig.getReplyIV().getData();
int replyIVOff = hopConfig.getReplyIV().getOffset();
if (log.shouldLog(Log.DEBUG))
log.debug("Decrypting record " + recordNum + "/" + hop + " with replyKey " + replyKey.toBase64() + "/" + Base64.encode(replyIV, replyIVOff, 16));
ctx.aes().decrypt(rec.getData(), off, rec.getData(), off, replyKey, replyIV, replyIVOff, TunnelBuildReplyMessage.RECORD_SIZE);
}
// ok, all of the layered encryption is stripped, so lets verify it
// (formatted per BuildResponseRecord.create)
Hash h = ctx.sha().calculateHash(rec.getData(), off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH);
if (!DataHelper.eq(h.getData(), 0, rec.getData(), off, Hash.HASH_LENGTH)) {
if (log.shouldLog(Log.DEBUG))
log.debug("Failed verification on " + recordNum + "/" + hop + ": " + h.toBase64() + " calculated, " +
Base64.encode(rec.getData(), off, Hash.HASH_LENGTH) + " expected\n" +
"Record: " + Base64.encode(rec.getData()));
return -1;
} else {
int rv = (int)DataHelper.fromLong(rec.getData(), off + TunnelBuildReplyMessage.RECORD_SIZE - 1, 1);
if (log.shouldLog(Log.DEBUG))
log.debug("Verified: " + rv + " for record " + recordNum + "/" + hop);
return rv;
}
}
}

View File

@ -2,6 +2,7 @@ package net.i2p.router.tunnel;
import java.util.Map;
import net.i2p.data.ByteArray;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.SessionKey;
@ -20,10 +21,15 @@ public class HopConfig {
private Hash _sendTo;
private SessionKey _layerKey;
private SessionKey _ivKey;
private SessionKey _replyKey;
private ByteArray _replyIV;
private long _expiration;
private Map _options;
private long _messagesProcessed;
/** IV length for {@link #getReplyIV} */
public static final int REPLY_IV_LENGTH = 16;
public HopConfig() {
_receiveTunnelId = null;
_receiveFrom = null;
@ -77,6 +83,14 @@ public class HopConfig {
public SessionKey getIVKey() { return _ivKey; }
public void setIVKey(SessionKey key) { _ivKey = key; }
/** key to encrypt the reply sent for the new tunnel creation crypto */
public SessionKey getReplyKey() { return _replyKey; }
public void setReplyKey(SessionKey key) { _replyKey = key; }
/** iv used to encrypt the reply sent for the new tunnel creation crypto */
public ByteArray getReplyIV() { return _replyIV; }
public void setReplyIV(ByteArray iv) { _replyIV = iv; }
/** when does this tunnel expire (in ms since the epoch)? */
public long getExpiration() { return _expiration; }
public void setExpiration(long when) { _expiration = when; }

View File

@ -99,8 +99,9 @@ public class TunnelCreatorConfig implements TunnelInfo {
double normalized = (double)tot * 60d*1000d / (double)timeSince;
_peakThroughputLastCoallesce = now;
_peakThroughputCurrentTotal = 0;
for (int i = 0; i < _peers.length; i++)
_context.profileManager().tunnelDataPushed1m(_peers[i], (int)normalized);
if (_context != null)
for (int i = 0; i < _peers.length; i++)
_context.profileManager().tunnelDataPushed1m(_peers[i], (int)normalized);
}
}
public long getVerifiedBytesTransferred() { return _verifiedBytesTransferred; }