package net.i2p.router.tunnel;
import java.util.List;
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.data.i2np.EncryptedBuildRecord;
import net.i2p.data.i2np.TunnelBuildReplyMessage;
import net.i2p.util.Log;
import net.i2p.util.SimpleByteCache;
/**
* 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 {
private final I2PAppContext ctx;
private final Log log;
/**
* @since 0.9.8 (methods were static before)
*/
public BuildReplyHandler(I2PAppContext context) {
ctx = context;
log = ctx.logManager().getLog(BuildReplyHandler.class);
}
/**
* Decrypt the tunnel build reply records. This overwrites the contents of the reply.
* Thread safe (no state).
*
* Note that this layer-decrypts the build records in-place.
* Do not call this more than once for a given message.
*
* @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(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, List<Integer> recordOrder) {
if (reply.getRecordCount() != recordOrder.size()) {
// somebody messed with us
log.error("Corrupted build reply, expected " + recordOrder.size() + " records, got " + reply.getRecordCount());
return null;
}
int rv[] = new int[reply.getRecordCount()];
for (int i = 0; i < rv.length; i++) {
int hop = recordOrder.get(i).intValue();
if (BuildMessageGenerator.isBlank(cfg, hop)) {
// self...
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": no need to decrypt record " + i + "/" + hop + ", as its out of range: " + cfg);
rv[i] = 0;
} else {
int ok = decryptRecord(reply, cfg, i, hop);
if (ok == -1) {
if (log.shouldLog(Log.WARN))
log.warn(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was not ok: " + cfg);
return null;
} else {
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was ok: " + ok + " for " + cfg);
}
rv[i] = ok;
}
}
return rv;
}
/**
* Decrypt the record (removing the layers of reply encyption) and read out the status
*
* Note that this layer-decrypts the build records in-place.
* Do not call this more than once for a given message.
*
* @return the status 0-255, or -1 on decrypt failure
*/
private int decryptRecord(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) {
if (BuildMessageGenerator.isBlank(cfg, hop)) {
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": Record " + recordNum + "/" + hop + " is fake, so consider it valid...");
return 0;
}
EncryptedBuildRecord rec = reply.getRecord(recordNum);
byte[] data = rec.getData();
int start = cfg.getLength() - 1;
if (cfg.isInbound())
start--; // the last hop in an inbound tunnel response doesn't actually encrypt
// do we need to adjust this for the endpoint?
for (int j = start; j >= hop; j--) {
HopConfig hopConfig = cfg.getConfig(j);
SessionKey replyKey = hopConfig.getReplyKey();
byte replyIV[] = hopConfig.getReplyIV();
if (log.shouldLog(Log.DEBUG)) {
log.debug(reply.getUniqueId() + ": Decrypting record " + recordNum + "/" + hop + "/" + j + " with replyKey "
+ replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
log.debug(reply.getUniqueId() + ": before decrypt: " + Base64.encode(data));
log.debug(reply.getUniqueId() + ": Full reply rec: sz=" + data.length + " data=" + Base64.encode(data, 0, TunnelBuildReplyMessage.RECORD_SIZE));
}
ctx.aes().decrypt(data, 0, data, 0, replyKey, replyIV, 0, TunnelBuildReplyMessage.RECORD_SIZE);
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": after decrypt: " + Base64.encode(data));
}
// ok, all of the layered encryption is stripped, so lets verify it
// (formatted per BuildResponseRecord.create)
// don't cache the result
//Hash h = ctx.sha().calculateHash(data, off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH);
byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH);
ctx.sha().calculateHash(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH, h, 0);
boolean ok = DataHelper.eq(h, 0, data, 0, Hash.HASH_LENGTH);
if (!ok) {
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": Failed verification on " + recordNum + "/" + hop + ": " + Base64.encode(h) + " calculated, " +
Base64.encode(data, 0, Hash.HASH_LENGTH) + " expected\n" +
"Record: " + Base64.encode(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH));
SimpleByteCache.release(h);
return -1;
} else {
SimpleByteCache.release(h);
int rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 1] & 0xff;
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": Verified: " + rv + " for record " + recordNum + "/" + hop);
return rv;
}
}
}