NetDB: Add support for database lookup ratchet replies (proposal 154)

Add support for ElG-encrypted database lookups and stores from ECIES-only destinations
Add ratchet support to MessageWrapper
Application-specific timeout for MessageWrapper-generated tags
Refactor tunnel TestJob to use MessageWrapper
Add ratchet support to TestJob
TestJob cleanups
This commit is contained in:
zzz
2020-03-31 19:21:35 +00:00
parent 9307cc8a0c
commit 7404bdc4fd
11 changed files with 269 additions and 126 deletions

View File

@ -1,4 +1,25 @@
2020-03-31 zzz
* NetDB:
- Add support for ratchet replies (proposal 154)
- Add support for ElG lookups and stores from ECIES-only destinations
- Variable timeout for MessageWrapper-generated tags
* Ratchet:
- Variable timeout for tagsets
- Expire tags too far behind current one
- Remove ID and DI from ACKREQ block
- Add timeout job in OCMOSJ
- Prep for next key support
- Add support for acks and callbacks
* Tunnels:
- Refactor TestJob to use MessageWrapper
- Add support for ratchet
2020-03-24 zzz 2020-03-24 zzz
* Blockfile: Add b32 to export output
* Graphs: Fix rrd4j deprecation warnings
* Profiles:
- Don't decay during first 90 minutes of uptime
- Change decay from .75 twice a day to .84 four times a day
* Tunnels: Make new tunnel selection round-robin * Tunnels: Make new tunnel selection round-robin
2020-03-20 zzz 2020-03-20 zzz

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */ /** deprecated */
public final static String ID = "Monotone"; public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION; public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 4; public final static long BUILD = 5;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";

View File

@ -232,7 +232,7 @@ class OutboundClientMessageJobHelper {
LeaseSetKeys lsk = ctx.keyManager().getKeys(from); LeaseSetKeys lsk = ctx.keyManager().getKeys(from);
I2NPMessage msg; I2NPMessage msg;
if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) {
msg = wrapDSM(ctx, skm, dsm); msg = wrapDSM(ctx, skm, dsm, expiration);
if (msg == null) { if (msg == null) {
if (log.shouldLog(Log.WARN)) if (log.shouldLog(Log.WARN))
log.warn("Failed to wrap ack clove"); log.warn("Failed to wrap ack clove");
@ -279,12 +279,14 @@ class OutboundClientMessageJobHelper {
* @return null on error * @return null on error
* @since 0.9.12 * @since 0.9.12
*/ */
private static GarlicMessage wrapDSM(RouterContext ctx, SessionKeyManager skm, DeliveryStatusMessage dsm) { private static GarlicMessage wrapDSM(RouterContext ctx, SessionKeyManager skm,
DeliveryStatusMessage dsm, long expiration) {
// garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways // garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways
// can't tell its a test. to simplify this, we encrypt it with a random key and tag, // can't tell its a test. to simplify this, we encrypt it with a random key and tag,
// remembering that key+tag so that we can decrypt it later. this means we can do the // remembering that key+tag so that we can decrypt it later. this means we can do the
// garlic encryption without any ElGamal (yay) // garlic encryption without any ElGamal (yay)
MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(ctx, skm); long fromNow = expiration - ctx.clock().now();
MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(ctx, skm, fromNow, true);
GarlicMessage msg = MessageWrapper.wrap(ctx, dsm, sess); GarlicMessage msg = MessageWrapper.wrap(ctx, dsm, sess);
return msg; return msg;
} }

View File

@ -18,6 +18,7 @@ import net.i2p.data.LeaseSet;
import net.i2p.data.router.RouterIdentity; import net.i2p.data.router.RouterIdentity;
import net.i2p.data.router.RouterInfo; import net.i2p.data.router.RouterInfo;
import net.i2p.data.SessionKey; import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.TunnelId; import net.i2p.data.TunnelId;
import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseLookupMessage;
import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage;
@ -29,6 +30,7 @@ import net.i2p.router.JobImpl;
import net.i2p.router.OutNetMessage; import net.i2p.router.OutNetMessage;
import net.i2p.router.Router; import net.i2p.router.Router;
import net.i2p.router.RouterContext; 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;
import net.i2p.router.message.SendMessageDirectJob; import net.i2p.router.message.SendMessageDirectJob;
import net.i2p.util.Log; import net.i2p.util.Log;
@ -313,11 +315,19 @@ public class HandleDatabaseLookupMessageJob extends JobImpl {
SessionKey replyKey = _message.getReplyKey(); SessionKey replyKey = _message.getReplyKey();
if (replyKey != null) { if (replyKey != null) {
// encrypt the reply // encrypt the reply
if (_log.shouldLog(Log.INFO)) SessionTag tag = _message.getReplyTag();
_log.info("Sending encrypted reply to " + toPeer + ' ' + replyKey + ' ' + _message.getReplyTag()); if (tag != null) {
message = MessageWrapper.wrap(getContext(), message, replyKey, _message.getReplyTag()); if (_log.shouldLog(Log.INFO))
_log.info("Sending AES reply to " + toPeer + ' ' + replyKey + ' ' + tag);
message = MessageWrapper.wrap(getContext(), message, replyKey, tag);
} else {
RatchetSessionTag rtag = _message.getRatchetReplyTag();
if (_log.shouldLog(Log.INFO))
_log.info("Sending AEAD reply to " + toPeer + ' ' + replyKey + ' ' + rtag);
message = MessageWrapper.wrap(getContext(), message, replyKey, rtag);
}
if (message == null) { if (message == null) {
_log.error("Encryption error"); _log.error("DLM reply encryption error");
return; return;
} }
_replyKeyConsumed = true; _replyKeyConsumed = true;

View File

@ -141,7 +141,7 @@ class ExploreJob extends SearchJob {
// request encrypted reply? // request encrypted reply?
if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) {
MessageWrapper.OneTimeSession sess; MessageWrapper.OneTimeSession sess;
sess = MessageWrapper.generateSession(getContext()); sess = MessageWrapper.generateSession(getContext(), MAX_EXPLORE_TIME);
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() + _log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() +
' ' + sess.key + ' ' + sess.tag); ' ' + sess.key + ' ' + sess.tag);

View File

@ -146,16 +146,23 @@ class FloodfillVerifyStoreJob extends JobImpl {
_facade.verifyFinished(_key); _facade.verifyFinished(_key);
return; return;
} }
boolean supportsElGamal = true;
boolean supportsRatchet = false;
if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) {
// register the session with the right SKM // register the session with the right SKM
MessageWrapper.OneTimeSession sess; MessageWrapper.OneTimeSession sess;
if (isInboundExploratory) { if (isInboundExploratory) {
sess = MessageWrapper.generateSession(getContext()); sess = MessageWrapper.generateSession(getContext(), VERIFY_TIMEOUT);
} else { } else {
LeaseSetKeys lsk = getContext().keyManager().getKeys(_client); LeaseSetKeys lsk = getContext().keyManager().getKeys(_client);
if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { supportsRatchet = lsk != null &&
lsk.isSupported(EncType.ECIES_X25519) &&
DatabaseLookupMessage.supportsRatchetReplies(peer);
supportsElGamal = lsk != null &&
lsk.isSupported(EncType.ELGAMAL_2048);
if (supportsElGamal || supportsRatchet) {
// garlic encrypt // garlic encrypt
sess = MessageWrapper.generateSession(getContext(), _client); sess = MessageWrapper.generateSession(getContext(), _client, VERIFY_TIMEOUT, !supportsRatchet);
if (sess == null) { if (sess == null) {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("No SKM to reply to"); _log.warn("No SKM to reply to");
@ -163,7 +170,7 @@ class FloodfillVerifyStoreJob extends JobImpl {
return; return;
} }
} else { } else {
// We don't yet have any way to request/get a ECIES-tagged reply, // We don't have a compatible way to get a reply,
// skip it for now. // skip it for now.
if (_log.shouldWarn()) if (_log.shouldWarn())
_log.warn("Skipping store verify for ECIES client " + _client.toBase32()); _log.warn("Skipping store verify for ECIES client " + _client.toBase32());
@ -171,23 +178,41 @@ class FloodfillVerifyStoreJob extends JobImpl {
return; return;
} }
} }
if (_log.shouldLog(Log.INFO)) if (sess.tag != null) {
_log.info(getJobId() + ": Requesting encrypted reply from " + _target + ' ' + sess.key + ' ' + sess.tag); if (_log.shouldInfo())
lookup.setReplySession(sess.key, sess.tag); _log.info(getJobId() + ": Requesting AES reply from " + peer + ' ' + sess.key + ' ' + sess.tag);
lookup.setReplySession(sess.key, sess.tag);
} else {
if (_log.shouldInfo())
_log.info(getJobId() + ": Requesting AEAD reply from " + peer + ' ' + sess.key + ' ' + sess.rtag);
lookup.setReplySession(sess.key, sess.rtag);
}
} }
Hash fromKey; Hash fromKey;
if (_isRouterInfo) I2NPMessage sent;
fromKey = null; if (supportsElGamal) {
else if (_isRouterInfo)
fromKey = _client; fromKey = null;
_wrappedMessage = MessageWrapper.wrap(getContext(), lookup, fromKey, peer); else
if (_wrappedMessage == null) { fromKey = _client;
if (_log.shouldLog(Log.WARN)) _wrappedMessage = MessageWrapper.wrap(getContext(), lookup, fromKey, peer);
_log.warn("Fail Garlic encrypting"); if (_wrappedMessage == null) {
_facade.verifyFinished(_key); if (_log.shouldLog(Log.WARN))
return; _log.warn("Fail Garlic encrypting");
_facade.verifyFinished(_key);
return;
}
sent = _wrappedMessage.getMessage();
} else {
// force full ElG for ECIES fromkey
sent = MessageWrapper.wrap(getContext(), lookup, peer);
if (sent == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Fail Garlic encrypting");
_facade.verifyFinished(_key);
return;
}
} }
I2NPMessage sent = _wrappedMessage.getMessage();
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Starting verify (stored " + _key + " to " + _sentTo + "), asking " + _target); _log.info(getJobId() + ": Starting verify (stored " + _key + " to " + _sentTo + "), asking " + _target);

View File

@ -336,19 +336,27 @@ public class IterativeSearchJob extends FloodSearchJob {
TunnelInfo replyTunnel; TunnelInfo replyTunnel;
boolean isClientReplyTunnel; boolean isClientReplyTunnel;
boolean isDirect; boolean isDirect;
boolean supportsRatchet = false;
boolean supportsElGamal = true;
if (_fromLocalDest != null) { if (_fromLocalDest != null) {
outTunnel = tm.selectOutboundTunnel(_fromLocalDest, peer); outTunnel = tm.selectOutboundTunnel(_fromLocalDest, peer);
if (outTunnel == null) if (outTunnel == null)
outTunnel = tm.selectOutboundExploratoryTunnel(peer); outTunnel = tm.selectOutboundExploratoryTunnel(peer);
LeaseSetKeys lsk = getContext().keyManager().getKeys(_fromLocalDest); LeaseSetKeys lsk = getContext().keyManager().getKeys(_fromLocalDest);
if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { supportsRatchet = lsk != null &&
lsk.isSupported(EncType.ECIES_X25519) &&
DatabaseLookupMessage.supportsRatchetReplies(ri);
supportsElGamal = !supportsRatchet &&
lsk != null &&
lsk.isSupported(EncType.ELGAMAL_2048);
if (supportsElGamal || supportsRatchet) {
// garlic encrypt to dest SKM // garlic encrypt to dest SKM
replyTunnel = tm.selectInboundTunnel(_fromLocalDest, peer); replyTunnel = tm.selectInboundTunnel(_fromLocalDest, peer);
isClientReplyTunnel = replyTunnel != null; isClientReplyTunnel = replyTunnel != null;
if (!isClientReplyTunnel) if (!isClientReplyTunnel)
replyTunnel = tm.selectInboundExploratoryTunnel(peer); replyTunnel = tm.selectInboundExploratoryTunnel(peer);
} else { } else {
// We don't yet have any way to request/get a ECIES-tagged reply, // We don't have a way to request/get a ECIES-tagged reply,
// so send it to the router SKM // so send it to the router SKM
isClientReplyTunnel = false; isClientReplyTunnel = false;
replyTunnel = tm.selectInboundExploratoryTunnel(peer); replyTunnel = tm.selectInboundExploratoryTunnel(peer);
@ -443,18 +451,24 @@ public class IterativeSearchJob extends FloodSearchJob {
_log.warn(getJobId() + ": Can't do encrypted lookup to " + peer + " with EncType " + type); _log.warn(getJobId() + ": Can't do encrypted lookup to " + peer + " with EncType " + type);
return; return;
} }
if (true) {
MessageWrapper.OneTimeSession sess; MessageWrapper.OneTimeSession sess;
if (isClientReplyTunnel) if (isClientReplyTunnel)
sess = MessageWrapper.generateSession(getContext(), _fromLocalDest); sess = MessageWrapper.generateSession(getContext(), _fromLocalDest, SINGLE_SEARCH_MSG_TIME, !supportsRatchet);
else else
sess = MessageWrapper.generateSession(getContext()); sess = MessageWrapper.generateSession(getContext(), SINGLE_SEARCH_MSG_TIME);
if (sess != null) { if (sess != null) {
if (_log.shouldLog(Log.INFO)) if (sess.tag != null) {
_log.info(getJobId() + ": Requesting encrypted reply from " + peer + ' ' + sess.key + ' ' + sess.tag); if (_log.shouldInfo())
_log.info(getJobId() + ": Requesting AES reply from " + peer + ' ' + sess.key + ' ' + sess.tag);
dlm.setReplySession(sess.key, sess.tag); dlm.setReplySession(sess.key, sess.tag);
} // else client went away, but send it anyway } else {
} if (_log.shouldInfo())
_log.info(getJobId() + ": Requesting AEAD reply from " + peer + ' ' + sess.key + ' ' + sess.rtag);
dlm.setReplySession(sess.key, sess.rtag);
}
} // else client went away, but send it anyway
outMsg = MessageWrapper.wrap(getContext(), dlm, ri); outMsg = MessageWrapper.wrap(getContext(), dlm, ri);
// ElG can take a while so do a final check before we send it, // ElG can take a while so do a final check before we send it,
// a response may have come in. // a response may have come in.

View File

@ -9,13 +9,17 @@ import net.i2p.crypto.TagSetHandle;
import net.i2p.data.Certificate; import net.i2p.data.Certificate;
import net.i2p.data.Hash; import net.i2p.data.Hash;
import net.i2p.data.PublicKey; import net.i2p.data.PublicKey;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.SessionKey; import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag; import net.i2p.data.SessionTag;
import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.DeliveryInstructions;
import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.GarlicMessage;
import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.crypto.TransientSessionKeyManager;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.crypto.ratchet.RatchetSKM;
import net.i2p.router.crypto.ratchet.RatchetSessionTag;
import net.i2p.router.message.GarlicMessageBuilder; import net.i2p.router.message.GarlicMessageBuilder;
import net.i2p.router.message.PayloadGarlicConfig; import net.i2p.router.message.PayloadGarlicConfig;
import net.i2p.router.util.RemovableSingletonSet; import net.i2p.router.util.RemovableSingletonSet;
@ -63,7 +67,7 @@ public class MessageWrapper {
if (skm == null) if (skm == null)
return null; return null;
SessionKey sentKey = new SessionKey(); SessionKey sentKey = new SessionKey();
Set<SessionTag> sentTags = new HashSet<SessionTag>(); Set<SessionTag> sentTags = new HashSet<SessionTag>(NETDB_TAGS_TO_DELIVER);
GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, sentKey, sentTags, GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, sentKey, sentTags,
NETDB_TAGS_TO_DELIVER, NETDB_LOW_THRESHOLD, skm); NETDB_TAGS_TO_DELIVER, NETDB_LOW_THRESHOLD, skm);
if (msg == null) if (msg == null)
@ -150,11 +154,25 @@ public class MessageWrapper {
* @since 0.9.7 * @since 0.9.7
*/ */
public static class OneTimeSession { public static class OneTimeSession {
/** ElG or ratchet */
public final SessionKey key; public final SessionKey key;
/** non-null for ElG */
public final SessionTag tag; public final SessionTag tag;
/**
* non-null for ratchet
* @since 0.9.46
*/
public final RatchetSessionTag rtag;
public OneTimeSession(SessionKey key, SessionTag tag) { public OneTimeSession(SessionKey key, SessionTag tag) {
this.key = key; this.tag = tag; this.key = key; this.tag = tag;
rtag = null;
}
/** @since 0.9.46 */
public OneTimeSession(SessionKey key, RatchetSessionTag tag) {
this.key = key; rtag = tag;
this.tag = null;
} }
} }
@ -164,10 +182,11 @@ public class MessageWrapper {
* The recipient can then send us an AES-encrypted message, * The recipient can then send us an AES-encrypted message,
* avoiding ElGamal. * avoiding ElGamal.
* *
* @param expiration time from now
* @since 0.9.7 * @since 0.9.7
*/ */
public static OneTimeSession generateSession(RouterContext ctx) { public static OneTimeSession generateSession(RouterContext ctx, long expiration) {
return generateSession(ctx, ctx.sessionKeyManager()); return generateSession(ctx, ctx.sessionKeyManager(), expiration, true);
} }
/** /**
@ -176,14 +195,16 @@ public class MessageWrapper {
* The recipient can then send us an AES-encrypted message, * The recipient can then send us an AES-encrypted message,
* avoiding ElGamal. * avoiding ElGamal.
* *
* @param expiration time from now
* @return null if we can't find the SKM for the localDest * @return null if we can't find the SKM for the localDest
* @since 0.9.9 * @since 0.9.9
*/ */
public static OneTimeSession generateSession(RouterContext ctx, Hash localDest) { public static OneTimeSession generateSession(RouterContext ctx, Hash localDest,
long expiration, boolean forceElG) {
SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(localDest); SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(localDest);
if (skm == null) if (skm == null)
return null; return null;
return generateSession(ctx, skm); return generateSession(ctx, skm, expiration, forceElG);
} }
/** /**
@ -192,29 +213,49 @@ public class MessageWrapper {
* The recipient can then send us an AES-encrypted message, * The recipient can then send us an AES-encrypted message,
* avoiding ElGamal. * avoiding ElGamal.
* *
* @param expiration time from now
* @return non-null * @return non-null
* @since 0.9.9 * @since 0.9.9
*/ */
public static OneTimeSession generateSession(RouterContext ctx, SessionKeyManager skm) { public static OneTimeSession generateSession(RouterContext ctx, SessionKeyManager skm,
long expiration, boolean forceElG) {
SessionKey key = ctx.keyGenerator().generateSessionKey(); SessionKey key = ctx.keyGenerator().generateSessionKey();
SessionTag tag = new SessionTag(true); if (forceElG || (skm instanceof TransientSessionKeyManager)) {
Set<SessionTag> tags = new RemovableSingletonSet<SessionTag>(tag); SessionTag tag = new SessionTag(true);
skm.tagsReceived(key, tags, 2*60*1000); Set<SessionTag> tags = new RemovableSingletonSet<SessionTag>(tag);
skm.tagsReceived(key, tags, expiration);
return new OneTimeSession(key, tag);
}
// ratchet
RatchetSKM rskm;
if (skm instanceof RatchetSKM) {
rskm = (RatchetSKM) skm;
} else if (skm instanceof MuxedSKM) {
rskm = ((MuxedSKM) skm).getECSKM();
} else {
throw new IllegalStateException("skm not a ratchet " + skm);
}
RatchetSessionTag tag = new RatchetSessionTag(ctx.random().nextLong());
rskm.tagsReceived(key, tag, expiration);
return new OneTimeSession(key, tag); return new OneTimeSession(key, tag);
} }
/** /**
* Garlic wrap a message from nobody, destined for an unknown router, * Garlic wrap a message from nobody, destined for an unknown router,
* to hide the contents from the IBGW. * to hide the contents from the IBGW.
* Uses a supplied one-time session key tag for AES encryption, * Uses a supplied one-time session key tag for AES or AEAD encryption,
* avoiding ElGamal. * avoiding ElGamal or X25519.
*
* Used by OCMJH for DSM.
* *
* @param session non-null * @param session non-null
* @return null on encrypt failure * @return null on encrypt failure
* @since 0.9.12 * @since 0.9.12
*/ */
public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, OneTimeSession session) { public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, OneTimeSession session) {
return wrap(ctx, m, session.key, session.tag); if (session.tag != null)
return wrap(ctx, m, session.key, session.tag);
return wrap(ctx, m, session.key, session.rtag);
} }
/** /**
@ -223,6 +264,8 @@ public class MessageWrapper {
* Uses a supplied session key and session tag for AES encryption, * Uses a supplied session key and session tag for AES encryption,
* avoiding ElGamal. * avoiding ElGamal.
* *
* Used by above and for DLM replies in HDLMJ.
*
* @param encryptKey non-null * @param encryptKey non-null
* @param encryptTag non-null * @param encryptTag non-null
* @return null on encrypt failure * @return null on encrypt failure
@ -233,9 +276,30 @@ public class MessageWrapper {
ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE), ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE),
m.getMessageExpiration(), m.getMessageExpiration(),
DeliveryInstructions.LOCAL, m); DeliveryInstructions.LOCAL, m);
GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, null, GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, null,
null, encryptKey, encryptTag); null, encryptKey, encryptTag);
return msg; return msg;
} }
/**
* Garlic wrap a message from nobody, destined for an unknown router,
* to hide the contents from the IBGW.
* Uses a supplied session key and session tag for ratchet encryption,
* avoiding full ECIES.
*
* Used by above and for DLM replies in HDLMJ.
*
* @param encryptKey non-null
* @param encryptTag non-null
* @return null on encrypt failure
* @since 0.9.46
*/
public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, SessionKey encryptKey, RatchetSessionTag encryptTag) {
PayloadGarlicConfig payload = new PayloadGarlicConfig(Certificate.NULL_CERT,
ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE),
m.getMessageExpiration(),
DeliveryInstructions.LOCAL, m);
GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, encryptKey, encryptTag);
return msg;
}
} }

View File

@ -14,6 +14,7 @@ import java.util.Set;
import net.i2p.crypto.EncType; import net.i2p.crypto.EncType;
import net.i2p.crypto.SigType; import net.i2p.crypto.SigType;
import net.i2p.data.Base64;
import net.i2p.data.Certificate; import net.i2p.data.Certificate;
import net.i2p.data.DatabaseEntry; import net.i2p.data.DatabaseEntry;
import net.i2p.data.DataFormatException; import net.i2p.data.DataFormatException;
@ -297,7 +298,8 @@ abstract class StoreJob extends JobImpl {
Hash rkey = getContext().routingKeyGenerator().getRoutingKey(key); Hash rkey = getContext().routingKeyGenerator().getRoutingKey(key);
KBucketSet<Hash> ks = _facade.getKBuckets(); KBucketSet<Hash> ks = _facade.getKBuckets();
if (ks == null) return new ArrayList<Hash>(); if (ks == null) return new ArrayList<Hash>();
return ((FloodfillPeerSelector)_peerSelector).selectFloodfillParticipants(rkey, numClosest, alreadyChecked, ks); List<Hash> rv = ((FloodfillPeerSelector)_peerSelector).selectFloodfillParticipants(rkey, numClosest, alreadyChecked, ks);
return rv;
} }
/** limit expiration for direct sends */ /** limit expiration for direct sends */
@ -496,9 +498,19 @@ abstract class StoreJob extends JobImpl {
} }
sent = wm.getMessage(); sent = wm.getMessage();
_state.addPending(to, wm); _state.addPending(to, wm);
} else if (lsk.isSupported(EncType.ECIES_X25519)) {
// force full ElG for ECIES-only
sent = MessageWrapper.wrap(getContext(), msg, peer);
if (sent == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Fail garlic encrypting from: " + client);
fail();
return;
}
_state.addPending(to);
} else { } else {
// We don't yet have any way to request/get a ECIES-tagged reply, // Above are the only two enc types for now, won't get here.
// so send it unencrypted. // Send it unencrypted.
sent = msg; sent = msg;
_state.addPending(to); _state.addPending(to);
} }

View File

@ -1,12 +1,9 @@
package net.i2p.router.tunnel.pool; package net.i2p.router.tunnel.pool;
import java.util.Set; import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.crypto.SessionKeyManager; import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.Certificate;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag; import net.i2p.data.SessionTag;
import net.i2p.data.i2np.DeliveryInstructions;
import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.DeliveryStatusMessage;
import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.GarlicMessage;
import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.I2NPMessage;
@ -16,9 +13,7 @@ import net.i2p.router.OutNetMessage;
import net.i2p.router.ReplyJob; import net.i2p.router.ReplyJob;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo; import net.i2p.router.TunnelInfo;
import net.i2p.router.message.GarlicMessageBuilder; import net.i2p.router.networkdb.kademlia.MessageWrapper;
import net.i2p.router.message.PayloadGarlicConfig;
import net.i2p.router.util.RemovableSingletonSet;
import net.i2p.stat.Rate; import net.i2p.stat.Rate;
import net.i2p.stat.RateStat; import net.i2p.stat.RateStat;
import net.i2p.util.Log; import net.i2p.util.Log;
@ -26,6 +21,9 @@ import net.i2p.util.Log;
/** /**
* Repeatedly test a single tunnel for its entire lifetime, * Repeatedly test a single tunnel for its entire lifetime,
* or until the pool is shut down or removed from the client manager. * or until the pool is shut down or removed from the client manager.
*
* Tunnel testing is disabled by default now, except for hidden mode,
* see TunnelPoolManager.buildComplete()
*/ */
class TestJob extends JobImpl { class TestJob extends JobImpl {
private final Log _log; private final Log _log;
@ -37,6 +35,8 @@ class TestJob extends JobImpl {
private PooledTunnelCreatorConfig _otherTunnel; private PooledTunnelCreatorConfig _otherTunnel;
/** save this so we can tell the SKM to kill it if the test fails */ /** save this so we can tell the SKM to kill it if the test fails */
private SessionTag _encryptTag; private SessionTag _encryptTag;
private static final AtomicInteger __id = new AtomicInteger();
private int _id;
/** base to randomize the test delay on */ /** base to randomize the test delay on */
private static final int TEST_DELAY = 40*1000; private static final int TEST_DELAY = 40*1000;
@ -60,15 +60,16 @@ class TestJob extends JobImpl {
public void runJob() { public void runJob() {
if (_pool == null || !_pool.isAlive()) if (_pool == null || !_pool.isAlive())
return; return;
long lag = getContext().jobQueue().getMaxLag(); final RouterContext ctx = getContext();
long lag = ctx.jobQueue().getMaxLag();
if (lag > 3000) { if (lag > 3000) {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Deferring test of " + _cfg + " due to job lag = " + lag); _log.warn("Deferring test of " + _cfg + " due to job lag = " + lag);
getContext().statManager().addRateData("tunnel.testAborted", _cfg.getLength(), 0); ctx.statManager().addRateData("tunnel.testAborted", _cfg.getLength());
scheduleRetest(); scheduleRetest();
return; return;
} }
if (getContext().router().gracefulShutdownInProgress()) if (ctx.router().gracefulShutdownInProgress())
return; // don't reschedule return; // don't reschedule
_found = false; _found = false;
// note: testing with exploratory tunnels always, even if the tested tunnel // note: testing with exploratory tunnels always, even if the tested tunnel
@ -80,11 +81,11 @@ class TestJob extends JobImpl {
if (_cfg.isInbound()) { if (_cfg.isInbound()) {
_replyTunnel = _cfg; _replyTunnel = _cfg;
// TODO if testing is re-enabled, pick closest to far end // TODO if testing is re-enabled, pick closest to far end
_outTunnel = getContext().tunnelManager().selectOutboundTunnel(); _outTunnel = ctx.tunnelManager().selectOutboundTunnel();
_otherTunnel = (PooledTunnelCreatorConfig) _outTunnel; _otherTunnel = (PooledTunnelCreatorConfig) _outTunnel;
} else { } else {
// TODO if testing is re-enabled, pick closest to far end // TODO if testing is re-enabled, pick closest to far end
_replyTunnel = getContext().tunnelManager().selectInboundTunnel(); _replyTunnel = ctx.tunnelManager().selectInboundTunnel();
_outTunnel = _cfg; _outTunnel = _cfg;
_otherTunnel = (PooledTunnelCreatorConfig) _replyTunnel; _otherTunnel = (PooledTunnelCreatorConfig) _replyTunnel;
} }
@ -92,61 +93,59 @@ class TestJob extends JobImpl {
if ( (_replyTunnel == null) || (_outTunnel == null) ) { if ( (_replyTunnel == null) || (_outTunnel == null) ) {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Insufficient tunnels to test " + _cfg + " with: " + _replyTunnel + " / " + _outTunnel); _log.warn("Insufficient tunnels to test " + _cfg + " with: " + _replyTunnel + " / " + _outTunnel);
getContext().statManager().addRateData("tunnel.testAborted", _cfg.getLength(), 0); ctx.statManager().addRateData("tunnel.testAborted", _cfg.getLength());
scheduleRetest(); scheduleRetest();
} else { } else {
int testPeriod = getTestPeriod(); int testPeriod = getTestPeriod();
long testExpiration = getContext().clock().now() + testPeriod; long now = ctx.clock().now();
DeliveryStatusMessage m = new DeliveryStatusMessage(getContext()); long testExpiration = now + testPeriod;
m.setArrival(getContext().clock().now()); DeliveryStatusMessage m = new DeliveryStatusMessage(ctx);
m.setArrival(now);
m.setMessageExpiration(testExpiration); m.setMessageExpiration(testExpiration);
m.setMessageId(getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE)); m.setMessageId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE));
ReplySelector sel = new ReplySelector(getContext(), m.getMessageId(), testExpiration); ReplySelector sel = new ReplySelector(m.getMessageId(), testExpiration);
OnTestReply onReply = new OnTestReply(getContext()); OnTestReply onReply = new OnTestReply();
OnTestTimeout onTimeout = new OnTestTimeout(getContext()); OnTestTimeout onTimeout = new OnTestTimeout();
OutNetMessage msg = getContext().messageRegistry().registerPending(sel, onReply, onTimeout); OutNetMessage msg = ctx.messageRegistry().registerPending(sel, onReply, onTimeout);
onReply.setSentMessage(msg); onReply.setSentMessage(msg);
sendTest(m); sendTest(m, testPeriod);
} }
} }
private void sendTest(I2NPMessage m) { private void sendTest(I2NPMessage m, int testPeriod) {
// garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways // garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways
// can't tell its a test. to simplify this, we encrypt it with a random key and tag, // can't tell its a test. to simplify this, we encrypt it with a random key and tag,
// remembering that key+tag so that we can decrypt it later. this means we can do the // remembering that key+tag so that we can decrypt it later. this means we can do the
// garlic encryption without any ElGamal (yay) // garlic encryption without any ElGamal (yay)
PayloadGarlicConfig payload = new PayloadGarlicConfig(Certificate.NULL_CERT, final RouterContext ctx = getContext();
getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE), MessageWrapper.OneTimeSession sess;
m.getMessageExpiration(), if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
DeliveryInstructions.LOCAL, m); // to client. false means don't force AES
payload.setRecipient(getContext().router().getRouterInfo()); sess = MessageWrapper.generateSession(ctx, _pool.getSettings().getDestination(), testPeriod, false);
} else {
SessionKey encryptKey = getContext().keyGenerator().generateSessionKey(); // to router. AES.
SessionTag encryptTag = new SessionTag(true); sess = MessageWrapper.generateSession(ctx, testPeriod);
_encryptTag = encryptTag; }
Set<SessionTag> sentTags = null; if (sess == null) {
GarlicMessage msg = GarlicMessageBuilder.buildMessage(getContext(), payload, sentTags, scheduleRetest();
getContext().keyManager().getPublicKey(), return;
encryptKey, encryptTag); }
// null for ratchet
_encryptTag = sess.tag;
GarlicMessage msg;
if (sess.tag != null) // AES
msg = MessageWrapper.wrap(ctx, m, sess.key, sess.tag);
else // ratchet
msg = MessageWrapper.wrap(ctx, m, sess.key, sess.rtag);
if (msg == null) { if (msg == null) {
// overloaded / unknown peers / etc // overloaded / unknown peers / etc
scheduleRetest(); scheduleRetest();
return; return;
} }
Set<SessionTag> encryptTags = new RemovableSingletonSet<SessionTag>(encryptTag); _id = __id.getAndIncrement();
// Register the single tag with the appropriate SKM
if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination());
if (skm != null)
skm.tagsReceived(encryptKey, encryptTags);
} else {
getContext().sessionKeyManager().tagsReceived(encryptKey, encryptTags);
}
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Sending garlic test of " + _outTunnel + " / " + _replyTunnel); _log.debug("Sending garlic test #" + _id + " of " + _outTunnel + " / " + _replyTunnel);
getContext().tunnelDispatcher().dispatchOutbound(msg, _outTunnel.getSendTunnelId(0), ctx.tunnelDispatcher().dispatchOutbound(msg, _outTunnel.getSendTunnelId(0),
_replyTunnel.getReceiveTunnelId(0), _replyTunnel.getReceiveTunnelId(0),
_replyTunnel.getPeer(0)); _replyTunnel.getPeer(0));
} }
@ -154,8 +153,8 @@ class TestJob extends JobImpl {
public void testSuccessful(int ms) { public void testSuccessful(int ms) {
if (_pool == null || !_pool.isAlive()) if (_pool == null || !_pool.isAlive())
return; return;
getContext().statManager().addRateData("tunnel.testSuccessLength", _cfg.getLength(), 0); getContext().statManager().addRateData("tunnel.testSuccessLength", _cfg.getLength());
getContext().statManager().addRateData("tunnel.testSuccessTime", ms, 0); getContext().statManager().addRateData("tunnel.testSuccessTime", ms);
_outTunnel.incrementVerifiedBytesTransferred(1024); _outTunnel.incrementVerifiedBytesTransferred(1024);
// reply tunnel is marked in the inboundEndpointProcessor // reply tunnel is marked in the inboundEndpointProcessor
@ -170,7 +169,7 @@ class TestJob extends JobImpl {
_otherTunnel.testJobSuccessful(ms); _otherTunnel.testJobSuccessful(ms);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Tunnel test successful in " + ms + "ms: " + _cfg); _log.debug("Tunnel test #" + _id + " successful in " + ms + "ms: " + _cfg);
scheduleRetest(); scheduleRetest();
} }
@ -193,7 +192,7 @@ class TestJob extends JobImpl {
else else
getContext().statManager().addRateData("tunnel.testFailedTime", timeToFail, timeToFail); getContext().statManager().addRateData("tunnel.testFailedTime", timeToFail, timeToFail);
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Tunnel test failed in " + timeToFail + "ms: " + _cfg); _log.warn("Tunnel test #" + _id + " failed in " + timeToFail + "ms: " + _cfg);
boolean keepGoing = _cfg.tunnelFailed(); boolean keepGoing = _cfg.tunnelFailed();
// blame the expl. tunnel too // blame the expl. tunnel too
if (_otherTunnel.getLength() > 1) if (_otherTunnel.getLength() > 1)
@ -234,6 +233,7 @@ class TestJob extends JobImpl {
} }
private void scheduleRetest() { scheduleRetest(false); } private void scheduleRetest() { scheduleRetest(false); }
private void scheduleRetest(boolean asap) { private void scheduleRetest(boolean asap) {
if (_pool == null || !_pool.isAlive()) if (_pool == null || !_pool.isAlive())
return; return;
@ -248,18 +248,16 @@ class TestJob extends JobImpl {
} }
private class ReplySelector implements MessageSelector { private class ReplySelector implements MessageSelector {
private final RouterContext _context;
private final long _id; private final long _id;
private final long _expiration; private final long _expiration;
public ReplySelector(RouterContext ctx, long id, long expiration) { public ReplySelector(long id, long expiration) {
_context = ctx;
_id = id; _id = id;
_expiration = expiration; _expiration = expiration;
_found = false; _found = false;
} }
public boolean continueMatching() { return !_found && _context.clock().now() < _expiration; } public boolean continueMatching() { return !_found && getContext().clock().now() < _expiration; }
public long getExpiration() { return _expiration; } public long getExpiration() { return _expiration; }
@ -286,7 +284,7 @@ class TestJob extends JobImpl {
private long _successTime; private long _successTime;
private OutNetMessage _sentMessage; private OutNetMessage _sentMessage;
public OnTestReply(RouterContext ctx) { super(ctx); } public OnTestReply() { super(TestJob.this.getContext()); }
public String getName() { return "Tunnel test success"; } public String getName() { return "Tunnel test success"; }
@ -322,22 +320,23 @@ class TestJob extends JobImpl {
private class OnTestTimeout extends JobImpl { private class OnTestTimeout extends JobImpl {
private final long _started; private final long _started;
public OnTestTimeout(RouterContext ctx) { public OnTestTimeout() {
super(ctx); super(TestJob.this.getContext());
_started = ctx.clock().now(); _started = getContext().clock().now();
} }
public String getName() { return "Tunnel test timeout"; } public String getName() { return "Tunnel test timeout"; }
public void runJob() { public void runJob() {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Timeout: found? " + _found); _log.warn("Tunnel test #" + _id + " timeout: found? " + _found);
if (!_found) { if (!_found) {
// don't clog up the SKM with old one-tag tagsets // don't clog up the SKM with old one-tag tagsets
if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) { if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination()); SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination());
if (skm != null) if (skm != null && _encryptTag != null)
skm.consumeTag(_encryptTag); skm.consumeTag(_encryptTag);
// else null tag for ratchet, let it expire
} else { } else {
getContext().sessionKeyManager().consumeTag(_encryptTag); getContext().sessionKeyManager().consumeTag(_encryptTag);
} }

View File

@ -563,12 +563,8 @@ public class TunnelPoolManager implements TunnelManagerFacade {
_context.router().isHidden() || _context.router().isHidden() ||
_context.router().getRouterInfo().getAddressCount() <= 0)) { _context.router().getRouterInfo().getAddressCount() <= 0)) {
Hash client = cfg.getDestination(); Hash client = cfg.getDestination();
LeaseSetKeys lsk = client != null ? _context.keyManager().getKeys(client) : null; TunnelPool pool = cfg.getTunnelPool();
if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { _context.jobQueue().addJob(new TestJob(_context, cfg, pool));
TunnelPool pool = cfg.getTunnelPool();
_context.jobQueue().addJob(new TestJob(_context, cfg, pool));
}
// else we don't yet have any way to request/get a ECIES-tagged reply,
} }
} }