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:
238
router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
Normal file
238
router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
Normal 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");
|
||||
}
|
||||
}
|
23
router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
Normal file
23
router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
51
router/java/src/net/i2p/data/i2np/TunnelBuildMessage.java
Normal file
51
router/java/src/net/i2p/data/i2np/TunnelBuildMessage.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
136
router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java
Normal file
136
router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
172
router/java/src/net/i2p/router/tunnel/BuildMessageTest.java
Normal file
172
router/java/src/net/i2p/router/tunnel/BuildMessageTest.java
Normal 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;
|
||||
}
|
||||
}
|
73
router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java
Normal file
73
router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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; }
|
||||
|
Reference in New Issue
Block a user