forked from I2P_Developers/i2p.i2p
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:
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user