diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java index 595b140b6..8a66b8660 100644 --- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java @@ -9,6 +9,7 @@ import com.southernstorm.noise.protocol.HandshakeState; import net.i2p.I2PAppContext; import net.i2p.crypto.EncType; +import net.i2p.crypto.HKDF; import net.i2p.crypto.KeyFactory; import net.i2p.data.Base64; import net.i2p.data.ByteArray; @@ -20,12 +21,14 @@ import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.util.Log; import net.i2p.router.RouterContext; +import net.i2p.router.crypto.ratchet.RatchetSessionTag; +import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession; /** * As of 0.9.48, supports two formats. * As of 0.9.51, supports three formats. * The original 222-byte ElGamal format, the new 464-byte ECIES format, - * and the newest 172-byte ECIES format. + * and the newest 154-byte ECIES format. * See proposal 152 and 157 for details on the new formats. * * None of the readXXX() calls are cached. For efficiency, @@ -128,6 +131,10 @@ public class BuildRequestRecord { private final boolean _isEC; private SessionKey _chachaReplyKey; private byte[] _chachaReplyAD; + // derived keys for short records + private SessionKey _derivedLayerKey; + private SessionKey _derivedIVKey; + private OneTimeSession _derivedGarlicKeys; /** * If set in the flag byte, any peer may send a message into this tunnel, but if @@ -195,6 +202,12 @@ public class BuildRequestRecord { // 16 byte trunc. hash, 32 byte eph. key, 16 byte MAC private static final int LENGTH_EC_SHORT = ShortTunnelBuildMessage.SHORT_RECORD_SIZE - (16 + 32 + 16); private static final int MAX_OPTIONS_LENGTH_SHORT = LENGTH_EC_SHORT - OFF_OPTIONS_SHORT; // includes options length + // short record HKDF + private static final byte[] ZEROLEN = new byte[0]; + private static final String INFO_1 = "SMTunnelReplyKey"; + private static final String INFO_2 = "SMTunnelLayerKey"; + private static final String INFO_3 = "TunnelLayerIVKey"; + private static final String INFO_4 = "RGarlicKeyAndTag"; private static final boolean TEST = false; private static KeyFactory TESTKF; @@ -226,6 +239,8 @@ public class BuildRequestRecord { * Tunnel layer encryption key that the current hop should use */ public SessionKey readLayerKey() { + if (_data.length == LENGTH_EC_SHORT) + return _derivedLayerKey; byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; int off = _isEC ? OFF_LAYER_KEY_EC : OFF_LAYER_KEY; System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); @@ -236,6 +251,8 @@ public class BuildRequestRecord { * Tunnel IV encryption key that the current hop should use */ public SessionKey readIVKey() { + if (_data.length == LENGTH_EC_SHORT) + return _derivedIVKey; byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; int off = _isEC ? OFF_IV_KEY_EC : OFF_IV_KEY; System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); @@ -345,7 +362,7 @@ public class BuildRequestRecord { return null; } } - + /** * ECIES short record only. * @return 0 for ElGamal or ECIES long record @@ -357,6 +374,15 @@ public class BuildRequestRecord { return 0; } + /** + * ECIES short OBEP record only. + * @return null for ElGamal or ECIES long record or non-OBEP + * @since 0.9.51 + */ + public OneTimeSession readGarlicKeys() { + return _derivedGarlicKeys; + } + /** * Encrypt the record to the specified peer. The result is formatted as:
* bytes 0-15: truncated SHA-256 of the current hop's identity (the toPeer parameter)
@@ -385,8 +411,10 @@ public class BuildRequestRecord {
* Encrypt the record to the specified peer. ECIES only.
* The ChaCha reply key and IV will be available via the getters
* after this call.
+ * For short records, derived keys will be available via
+ * readLayerKey(), readIVKey(), and readGarlicKeys() after this call.
* See class javadocs for format.
- * See proposal 152.
+ * See proposals 152 and 157.
*
* @return non-null
* @since 0.9.48
@@ -406,9 +434,37 @@ public class BuildRequestRecord {
state.start();
state.writeMessage(out, PEER_SIZE, _data, 0, _data.length);
EncryptedBuildRecord rv = isShort ? new ShortEncryptedBuildRecord(out) : new EncryptedBuildRecord(out);
- _chachaReplyKey = new SessionKey(state.getChainingKey());
_chachaReplyAD = new byte[32];
System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
+ byte[] ck = state.getChainingKey();
+ if (isShort) {
+ byte[] crk = new byte[32];
+ byte[] newck = new byte[32];
+ HKDF hkdf = new HKDF(ctx);
+ hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0);
+ _chachaReplyKey = new SessionKey(crk);
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] dlk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0);
+ _derivedLayerKey = new SessionKey(dlk);
+ boolean isOBEP = readIsOutboundEndpoint();
+ if (isOBEP) {
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] divk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0);
+ _derivedIVKey = new SessionKey(divk);
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] dgk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0);
+ SessionKey sdgk = new SessionKey(dgk);
+ RatchetSessionTag rst = new RatchetSessionTag(newck);
+ _derivedGarlicKeys = new OneTimeSession(sdgk, rst);
+ } else {
+ _derivedIVKey = new SessionKey(newck);
+ }
+ } else {
+ _chachaReplyKey = new SessionKey(ck);
+ }
return rv;
} catch (GeneralSecurityException gse) {
throw new IllegalStateException("failed", gse);
@@ -491,9 +547,37 @@ public class BuildRequestRecord {
decrypted = new byte[isShort ? LENGTH_EC_SHORT : LENGTH_EC];
state.readMessage(encrypted, PEER_SIZE, len - PEER_SIZE,
decrypted, 0);
- _chachaReplyKey = new SessionKey(state.getChainingKey());
_chachaReplyAD = new byte[32];
System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
+ byte[] ck = state.getChainingKey();
+ if (isShort) {
+ byte[] crk = new byte[32];
+ byte[] newck = new byte[32];
+ HKDF hkdf = new HKDF(ctx);
+ hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0);
+ _chachaReplyKey = new SessionKey(crk);
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] dlk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0);
+ _derivedLayerKey = new SessionKey(dlk);
+ boolean isOBEP = (decrypted[OFF_FLAG_EC_SHORT] & FLAG_OUTBOUND_ENDPOINT) != 0;
+ if (isOBEP) {
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] divk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0);
+ _derivedIVKey = new SessionKey(divk);
+ System.arraycopy(newck, 0, ck, 0, 32);
+ byte[] dgk = new byte[32];
+ hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0);
+ SessionKey sdgk = new SessionKey(dgk);
+ RatchetSessionTag rst = new RatchetSessionTag(newck);
+ _derivedGarlicKeys = new OneTimeSession(sdgk, rst);
+ } else {
+ _derivedIVKey = new SessionKey(newck);
+ }
+ } else {
+ _chachaReplyKey = new SessionKey(ck);
+ }
} catch (GeneralSecurityException gse) {
if (state != null) {
Log log = ctx.logManager().getLog(BuildRequestRecord.class);
@@ -690,7 +774,7 @@ public class BuildRequestRecord {
StringBuilder buf = new StringBuilder(256);
buf.append(_isEC ? "ECIES" : "ElGamal");
if (_data.length == LENGTH_EC_SHORT)
- buf.append(" short ");
+ buf.append(" short");
buf.append(" BRR ");
boolean isIBGW = readIsInboundGateway();
boolean isOBEP = readIsOutboundEndpoint();
@@ -704,10 +788,10 @@ public class BuildRequestRecord {
.append(" out: ").append(readNextTunnelId());
}
buf.append(" to: ").append(readNextIdentity());
+ buf.append(" layer key: ").append(readLayerKey())
+ .append(" IV key: ").append(readIVKey());
if (_data.length != LENGTH_EC_SHORT) {
- buf.append(" layer key: ").append(readLayerKey())
- .append(" IV key: ").append(readIVKey())
- .append(" reply key: ").append(readReplyKey())
+ buf.append(" reply key: ").append(readReplyKey())
.append(" reply IV: ").append(Base64.encode(readReplyIV()));
}
buf.append(" time: ").append(DataHelper.formatTime(readRequestTime()))
@@ -719,6 +803,10 @@ public class BuildRequestRecord {
buf.append(" chacha reply key: ").append(_chachaReplyKey)
.append(" chacha reply IV: ").append(Base64.encode(_chachaReplyAD));
}
+ if (_derivedGarlicKeys != null) {
+ buf.append(" garlic reply key: ").append(_derivedGarlicKeys.key)
+ .append(" garlic reply tag: ").append(_derivedGarlicKeys.rtag);
+ }
}
// to chase i2pd bug
//buf.append('\n').append(net.i2p.util.HexDump.dump(readReplyKey().getData()));
diff --git a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
index 9d6a6a322..18cf50a05 100644
--- a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
@@ -84,12 +84,13 @@ public class BuildResponseRecord {
* @param status the response 0-255
* @param replyAD 32 bytes
* @param options 116 bytes max when serialized
+ * @param slot the slot number, 0-7
* @return a 218-byte response record
* @throws IllegalArgumentException if options too big or on encryption failure
* @since 0.9.51
*/
public static ShortEncryptedBuildRecord createShort(I2PAppContext ctx, int status, SessionKey replyKey,
- byte replyAD[], Properties options) {
+ byte replyAD[], Properties options, int slot) {
byte rv[] = new byte[ShortTunnelBuildMessage.SHORT_RECORD_SIZE];
int off;
try {
@@ -103,7 +104,7 @@ public class BuildResponseRecord {
else if (sz < 0)
throw new IllegalArgumentException("options");
rv[ShortTunnelBuildMessage.SHORT_RECORD_SIZE - 17] = (byte) status;
- boolean ok = encryptAEADBlock(replyAD, rv, replyKey);
+ boolean ok = encryptAEADBlock(replyAD, rv, replyKey, slot);
if (!ok)
throw new IllegalArgumentException("encrypt fail");
return new ShortEncryptedBuildRecord(rv);
@@ -111,14 +112,16 @@ public class BuildResponseRecord {
/**
* Encrypts in place.
- * Handles both standard (528) and short (218) byte records as of 0.9.51.
+ * Handles standard (528) byte records only.
*
* @param ad non-null
- * @param data 528 or 218 bytes, data will be encrypted in place.
+ * @param data 528 bytes, data will be encrypted in place.
* @return success
* @since 0.9.48
*/
private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key) {
+ if (data.length != EncryptedBuildRecord.LENGTH)
+ throw new IllegalArgumentException();
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
chacha.initializeKey(key.getData(), 0);
try {
@@ -131,17 +134,19 @@ public class BuildResponseRecord {
/*
* ChaCha/Poly only for ECIES routers.
- * Handles both standard (528) and short (218) byte records as of 0.9.51.
+ * Handles standard (528) byte records only.
* Decrypts in place.
- * Status will be rec.getData()[511 or 219].
+ * Status will be rec.getData()[511].
* Properties will be at rec.getData()[0].
*
- * @param rec 528 or 218 bytes, data will be decrypted in place.
+ * @param rec 528 bytes, data will be decrypted in place.
* @param ad non-null
* @return success
* @since 0.9.48
*/
public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad) {
+ if (rec.length() != EncryptedBuildRecord.LENGTH)
+ throw new IllegalArgumentException();
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
chacha.initializeKey(key.getData(), 0);
try {
@@ -153,4 +158,57 @@ public class BuildResponseRecord {
}
return true;
}
+
+ /**
+ * Encrypts in place.
+ * Handles short (218) byte records only.
+ *
+ * @param ad non-null
+ * @param data 218 bytes, data will be encrypted in place.
+ * @param nonce the slot number, 0-7
+ * @return success
+ * @since 0.9.51
+ */
+ private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key, int nonce) {
+ if (data.length != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7)
+ throw new IllegalArgumentException();
+ ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+ chacha.initializeKey(key.getData(), 0);
+ chacha.setNonce(nonce);
+ try {
+ chacha.encryptWithAd(ad, data, 0, data, 0, data.length - 16);
+ } catch (GeneralSecurityException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /*
+ * ChaCha/Poly only for ECIES routers.
+ * Handles short (218) byte records only.
+ * Decrypts in place.
+ * Status will be rec.getData()[201].
+ * Properties will be at rec.getData()[0].
+ *
+ * @param rec 218 bytes, data will be decrypted in place.
+ * @param ad non-null
+ * @param nonce the slot number, 0-7
+ * @return success
+ * @since 0.9.51
+ */
+ public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad, int nonce) {
+ if (rec.length() != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7)
+ throw new IllegalArgumentException();
+ ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+ chacha.initializeKey(key.getData(), 0);
+ chacha.setNonce(nonce);
+ try {
+ // this is safe to do in-place, it checks the mac before starting decryption
+ byte[] data = rec.getData();
+ chacha.decryptWithAd(ad, data, 0, data, 0, rec.length());
+ } catch (GeneralSecurityException e) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
index 903392b24..26277b58c 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
@@ -215,6 +215,16 @@ public class MuxedSKM extends SessionKeyManager {
_elg.tagsReceived(key, sessionTags, expire);
}
+ /**
+ * EC only
+ * One time session
+ * @param expire time from now
+ * @since 0.9.51
+ */
+ public void tagsReceived(SessionKey key, RatchetSessionTag tag, long expire) {
+ _ec.tagsReceived(key, tag, expire);
+ }
+
@Override
public SessionKey consumeTag(SessionTag tag) {
SessionKey rv = _elg.consumeTag(tag);
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
index a6716f04e..47364a856 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
@@ -10,6 +10,7 @@ import net.i2p.data.SessionKey;
import net.i2p.data.TunnelId;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
/**
* Coordinate the info that the tunnel creator keeps track of, including what
@@ -45,6 +46,8 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
private byte[][] _ChaReplyADs;
private final SessionKey[] _AESReplyKeys;
private final byte[][] _AESReplyIVs;
+ // short record OBEP only
+ private OneTimeSession _garlicReplyKeys;
/**
* IV length for {@link #getAESReplyIV}
@@ -270,6 +273,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
/**
* Key to encrypt the reply sent for the tunnel creation crypto.
+ * Null for short build record.
*
* @return key or null
* @throws IllegalArgumentException if iv not 16 bytes
@@ -279,6 +283,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
/**
* IV used to encrypt the reply sent for the tunnel creation crypto.
+ * Null for short build record.
*
* @return 16 bytes or null
* @since 0.9.48 moved from HopConfig
@@ -340,6 +345,24 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
return _ChaReplyADs[hop];
}
+ /**
+ * ECIES short OBEP record only.
+ * @return null for ElGamal or ECIES long record or non-OBEP
+ * @since 0.9.51
+ */
+ public void setGarlicReplyKeys(OneTimeSession keys) {
+ _garlicReplyKeys = keys;
+ }
+
+ /**
+ * ECIES short OBEP record only.
+ * @return null for ElGamal or ECIES long record or non-OBEP
+ * @since 0.9.51
+ */
+ public OneTimeSession getGarlicReplyKeys() {
+ return _garlicReplyKeys;
+ }
+
@Override
public String toString() {
// H0:1235-->H1:2345-->H2:2345
@@ -382,4 +405,22 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
buf.append(" with ").append(_failures).append(" failures");
return buf.toString();
}
+
+ /**
+ * @since 0.9.51
+ */
+ public String toStringFull() {
+ StringBuilder buf = new StringBuilder(1024);
+ buf.append(toString());
+ for (int i = 0; i < _peers.length; i++) {
+ if (i == 0)
+ buf.append("\nGW ");
+ else if (i == _peers.length - 1)
+ buf.append("\nEP ");
+ else
+ buf.append("\nHop ").append(i);
+ buf.append(": ").append(_config[i]);
+ }
+ return buf.toString();
+ }
}
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index 90ae30fa5..9cf128ae5 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -34,6 +34,7 @@ import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.crypto.ratchet.RatchetSessionTag;
import net.i2p.router.networkdb.kademlia.MessageWrapper;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
import net.i2p.router.peermanager.TunnelHistory;
import net.i2p.router.tunnel.HopConfig;
import net.i2p.router.tunnel.TunnelDispatcher;
@@ -956,6 +957,14 @@ class BuildHandler implements Runnable {
+ " after " + recvDelay + " with " + response
+ " from " + (from != null ? from : "tunnel") + ": " + req);
+ int records = state.msg.getRecordCount();
+ int ourSlot = -1;
+ for (int j = 0; j < records; j++) {
+ if (state.msg.getRecord(j) == null) {
+ ourSlot = j;
+ break;
+ }
+ }
EncryptedBuildRecord reply;
if (isEC) {
// TODO options
@@ -966,29 +975,14 @@ class BuildHandler implements Runnable {
_log.warn("Unsupported STBM");
return;
}
- if (isOutEnd) {
- // reply will be sent in plaintext in a OTBRM, see below
- reply = null;
- } else {
- reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props);
- }
+ reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props, ourSlot);
} else {
reply = BuildResponseRecord.create(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props);
}
} else {
reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
}
- int records = state.msg.getRecordCount();
- int ourSlot = -1;
- for (int j = 0; j < records; j++) {
- if (state.msg.getRecord(j) == null) {
- ourSlot = j;
- if (!(isOutEnd && state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE))
- state.msg.setRecord(j, reply);
- // else reply will be sent in plaintext
- break;
- }
- }
+ state.msg.setRecord(ourSlot, reply);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Read slot " + ourSlot + " containing: " + req
@@ -1026,8 +1020,9 @@ class BuildHandler implements Runnable {
I2NPMessage outMessage;
if (state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE) {
// garlic encrypt
- SessionKey sk = null; // TODO
- RatchetSessionTag st = null; // TODO
+ OneTimeSession ots = req.readGarlicKeys();
+ SessionKey sk = ots.key;
+ RatchetSessionTag st = ots.rtag;
outMessage = MessageWrapper.wrap(_context, replyMsg, sk, st);
if (outMessage == null) {
if (_log.shouldWarn())
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
index 1ab9117db..b000cd73f 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
@@ -46,7 +46,6 @@ abstract class BuildMessageGenerator {
boolean isEC = peerKey.getType() == EncType.ECIES_X25519;
BuildRequestRecord req;
if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint
- /// TODO if isEC && isShort
req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel, isEC, isShort);
else
req = createUnencryptedRecord(ctx, cfg, hop, null, -1, isEC, isShort);
@@ -55,8 +54,15 @@ abstract class BuildMessageGenerator {
Hash peer = cfg.getPeer(hop);
if (isEC) {
erec = req.encryptECIESRecord(ctx, peerKey, peer);
- // TODO if isShort, set derived keys in coonfig
cfg.setChaChaReplyKeys(hop, req.getChaChaReplyKey(), req.getChaChaReplyAD());
+ if (isShort) {
+ // save derived keys
+ HopConfig hopConfig = cfg.getConfig(hop);
+ hopConfig.setLayerKey(req.readLayerKey());
+ hopConfig.setIVKey(req.readIVKey());
+ if (!cfg.isInbound() && hop + 1 == cfg.getLength()) //outbound endpoint
+ cfg.setGarlicReplyKeys(req.readGarlicKeys());
+ }
} else {
erec = req.encryptRecord(ctx, peerKey, peer);
}
@@ -114,11 +120,6 @@ abstract class BuildMessageGenerator {
}
SessionKey layerKey = hopConfig.getLayerKey();
SessionKey ivKey = hopConfig.getIVKey();
- SessionKey replyKey = cfg.getAESReplyKey(hop);
- byte iv[] = cfg.getAESReplyIV(hop);
- if (iv == null) {
- throw new IllegalStateException();
- }
boolean isInGW = (cfg.isInbound() && (hop == 0));
boolean isOutEnd = (!cfg.isInbound() && (hop + 1 >= cfg.getLength()));
@@ -137,11 +138,19 @@ abstract class BuildMessageGenerator {
nextMsgId,
isInGW, isOutEnd, EmptyProperties.INSTANCE);
} else {
+ SessionKey replyKey = cfg.getAESReplyKey(hop);
+ byte iv[] = cfg.getAESReplyIV(hop);
+ if (iv == null)
+ throw new IllegalStateException();
rec = new BuildRequestRecord(ctx, recvTunnelId, nextTunnelId, nextPeer,
nextMsgId, layerKey, ivKey, replyKey,
iv, isInGW, isOutEnd, EmptyProperties.INSTANCE);
}
} else {
+ SessionKey replyKey = cfg.getAESReplyKey(hop);
+ byte iv[] = cfg.getAESReplyIV(hop);
+ if (iv == null)
+ throw new IllegalStateException();
rec = new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer,
nextMsgId, layerKey, ivKey, replyKey,
iv, isInGW, isOutEnd);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
index c689ca500..fba4cb490 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
@@ -159,7 +159,11 @@ class BuildReplyHandler {
if (log.shouldDebug())
log.debug(reply.getUniqueId() + ": Decrypting chacha/poly record " + recordNum + "/" + hop + " with replyKey "
+ replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
- boolean ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV);
+ boolean ok;
+ if (isShort)
+ ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV, recordNum);
+ else
+ ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV);
if (!ok) {
if (log.shouldWarn())
log.debug(reply.getUniqueId() + ": chacha reply decrypt fail on " + recordNum + "/" + hop);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index 360bb5e5f..929799384 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -5,22 +5,27 @@ import java.util.Collections;
import java.util.List;
import net.i2p.crypto.EncType;
+import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.PublicKey;
-import net.i2p.data.router.RouterInfo;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.ShortTunnelBuildMessage;
import net.i2p.data.i2np.TunnelBuildMessage;
import net.i2p.data.i2np.VariableTunnelBuildMessage;
+import net.i2p.data.router.RouterInfo;
import net.i2p.router.JobImpl;
+import net.i2p.router.LeaseSetKeys;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.router.TunnelManagerFacade;
import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.crypto.ratchet.RatchetSKM;
+import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.networkdb.kademlia.MessageWrapper;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
import net.i2p.router.tunnel.HopConfig;
import net.i2p.router.tunnel.TunnelCreatorConfig;
import net.i2p.util.Log;
@@ -142,16 +147,51 @@ abstract class BuildRequestor {
TunnelManagerFacade mgr = ctx.tunnelManager();
boolean isInbound = settings.isInbound();
if (settings.isExploratory() || !usePairedTunnels(ctx)) {
- if (isInbound)
+ if (isInbound) {
pairedTunnel = mgr.selectOutboundExploratoryTunnel(farEnd);
- else
+ } else {
pairedTunnel = mgr.selectInboundExploratoryTunnel(farEnd);
+ if (pairedTunnel != null) {
+ OneTimeSession ots = cfg.getGarlicReplyKeys();
+ if (ots != null) {
+ SessionKeyManager skm = ctx.sessionKeyManager();
+ RatchetSKM rskm = (RatchetSKM) skm;
+ rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+ cfg.setGarlicReplyKeys(null);
+ }
+ }
+ }
} else {
// building a client tunnel
- if (isInbound)
- pairedTunnel = mgr.selectOutboundTunnel(settings.getDestination(), farEnd);
- else
- pairedTunnel = mgr.selectInboundTunnel(settings.getDestination(), farEnd);
+ Hash from = settings.getDestination();
+ if (isInbound) {
+ pairedTunnel = mgr.selectOutboundTunnel(from, farEnd);
+ } else {
+ pairedTunnel = mgr.selectInboundTunnel(from, farEnd);
+ if (pairedTunnel != null) {
+ OneTimeSession ots = cfg.getGarlicReplyKeys();
+ if (ots != null) {
+ SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from);
+ if (skm != null) {
+ if (skm instanceof RatchetSKM) {
+ RatchetSKM rskm = (RatchetSKM) skm;
+ rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+ cfg.setGarlicReplyKeys(null);
+ } else if (skm instanceof MuxedSKM) {
+ MuxedSKM mskm = (MuxedSKM) skm;
+ mskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+ cfg.setGarlicReplyKeys(null);
+ } else {
+ // ElG-only won't work, fall back to expl.
+ pairedTunnel = null;
+ }
+ } else {
+ // no client SKM, fall back to expl.
+ pairedTunnel = null;
+ }
+ }
+ }
+ }
if (pairedTunnel == null) {
if (isInbound) {
// random more reliable than closest ??
@@ -177,6 +217,15 @@ abstract class BuildRequestor {
// ditto
pairedTunnel = null;
}
+ if (pairedTunnel != null) {
+ OneTimeSession ots = cfg.getGarlicReplyKeys();
+ if (ots != null) {
+ SessionKeyManager skm = ctx.sessionKeyManager();
+ RatchetSKM rskm = (RatchetSKM) skm;
+ rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+ cfg.setGarlicReplyKeys(null);
+ }
+ }
}
if (pairedTunnel != null && log.shouldLog(Log.INFO))
log.info("Couldn't find a paired tunnel for " + cfg + ", using exploratory tunnel");
@@ -296,6 +345,16 @@ abstract class BuildRequestor {
Hash replyRouter;
boolean useVariable = SEND_VARIABLE && cfg.getLength() <= MEDIUM_RECORDS;
boolean useShortTBM = SEND_SHORT && ctx.keyManager().getPublicKey().getType() == EncType.ECIES_X25519;
+ if (useShortTBM && !pool.getSettings().isExploratory()) {
+ // pool must be EC also
+ LeaseSetKeys lsk = ctx.keyManager().getKeys(pool.getSettings().getDestination());
+ if (lsk != null) {
+ if (!lsk.isSupported(EncType.ECIES_X25519))
+ useShortTBM = false;
+ } else {
+ useShortTBM = false;
+ }
+ }
if (cfg.isInbound()) {
//replyTunnel = 0; // as above
diff --git a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
index f37554302..2e92bb6ee 100644
--- a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
+++ b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
@@ -64,6 +64,9 @@ public class BuildMessageTestStandalone extends TestCase {
*/
private void x_testBuildMessage(RouterContext ctx, int testType) {
Log log = ctx.logManager().getLog(getClass());
+ log.debug("\n================================================================" +
+ "\nTest " + testType +
+ "\n================================================================");
// set our keys to avoid NPE
KeyPair kpr = ctx.keyGenerator().generatePKIKeys((testType == 1 || testType == 4) ? EncType.ELGAMAL_2048 : EncType.ECIES_X25519);
PublicKey k1 = kpr.getPublic();
@@ -102,7 +105,7 @@ public class BuildMessageTestStandalone extends TestCase {
log.debug("\n================================================================" +
"\nMessage fully encrypted" +
- "\n" + cfg +
+ "\n" + cfg.toStringFull() +
"\n================================================================");
if (testType == 3 || testType == 6) {
@@ -137,6 +140,12 @@ public class BuildMessageTestStandalone extends TestCase {
long time = req.readRequestTime();
long now = (ctx.clock().now() / (60l*60l*1000l)) * (60*60*1000);
int ourSlot = -1;
+ for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
+ if (msg.getRecord(j) == null) {
+ ourSlot = j;
+ break;
+ }
+ }
EncryptedBuildRecord reply;
if (testType == 1 || testType == 4) {
@@ -144,25 +153,9 @@ public class BuildMessageTestStandalone extends TestCase {
} else if (testType == 2 || testType == 5) {
reply = BuildResponseRecord.create(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
} else {
- reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
- }
- if (testType != 3 || i != cfg.getLength() - 1) {
- for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
- if (msg.getRecord(j) == null) {
- ourSlot = j;
- msg.setRecord(j, reply);
- break;
- }
- }
- } else {
- for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
- if (msg.getRecord(j) == null) {
- ourSlot = j;
- msg.setRecord(j, reply);
- break;
- }
- }
+ reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE, ourSlot);
}
+ msg.setRecord(ourSlot, reply);
if (testType == 1 || testType == 4) {
log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64()
@@ -233,7 +226,7 @@ public class BuildMessageTestStandalone extends TestCase {
}
log.debug("\n================================================================" +
- "\nAll peers agree? " + allAgree +
+ "\nTest " + testType + " complete, all peers agree? " + allAgree +
"\n================================================================");
assertTrue("All peers agree", allAgree);
}
@@ -289,12 +282,18 @@ public class BuildMessageTestStandalone extends TestCase {
HopConfig hop = cfg.getConfig(i);
hop.setCreation(now);
hop.setExpiration(now+10*60*1000);
- hop.setIVKey(ctx.keyGenerator().generateSessionKey());
- hop.setLayerKey(ctx.keyGenerator().generateSessionKey());
- byte iv[] = new byte[BuildRequestRecord.IV_SIZE];
- Arrays.fill(iv, (byte)i); // consistent for repeatability
- cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv);
+ if (testType != 3 && testType != 6) {
+ hop.setIVKey(ctx.keyGenerator().generateSessionKey());
+ hop.setLayerKey(ctx.keyGenerator().generateSessionKey());
+ byte iv[] = new byte[BuildRequestRecord.IV_SIZE];
+ Arrays.fill(iv, (byte)i); // consistent for repeatability
+ cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv);
+ }
hop.setReceiveTunnelId(new TunnelId(i+1));
+ if (i != _peers.length - 1) {
+ hop.setSendTo(_peers[i + 1]);
+ hop.setSendTunnelId(new TunnelId(i+2));
+ }
}
return cfg;
}