Prop 157 updates

- Don't require AES keys for short records
- Derive keys from noise ck
- Use derived keys to garlic-encrypt reply at OBEP
- Register reply key with SKM
- Only use short message for client tunnels if client supports EC
- Set nonce for chacha/poly reply record
- Add tagsReceived() for single tag to MuxedSKM
- Add extended TunnelCreatorConfig.toStringFull()
- BRR toString() enhancements
- Test enhancements
This commit is contained in:
zzz
2021-07-16 12:28:04 -04:00
parent f40eff6b7e
commit a7d9ca920f
9 changed files with 338 additions and 75 deletions

View File

@@ -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: <pre>
* 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()));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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