diff --git a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java index 444e190ba..b66b75691 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -509,17 +509,15 @@ public final class ECIESAEADEngine { _log.debug("State after decrypt new session reply: " + state); // split() - byte[] ck = state.getChainingKey(); - byte[] k_ab = new byte[32]; - byte[] k_ba = new byte[32]; - _hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0); + // Noise does it too but it trashes the keys + SplitKeys split = new SplitKeys(state, _hkdf); CipherStatePair ckp = state.split(); CipherState rcvr = ckp.getReceiver(); byte[] hash = state.getHandshakeHash(); // part 2 - payload byte[] encpayloadkey = new byte[32]; - _hkdf.calculate(k_ba, ZEROLEN, INFO_6, encpayloadkey); + _hkdf.calculate(split.k_ba.getData(), ZEROLEN, INFO_6, encpayloadkey); rcvr.initializeKey(encpayloadkey, 0); byte[] payload = new byte[data.length - (TAGLEN + KEYLEN + MACLEN + MACLEN)]; try { @@ -561,7 +559,7 @@ public final class ECIESAEADEngine { // tell the SKM PublicKey bob = new PublicKey(EncType.ECIES_X25519, bobPK); - keyManager.updateSession(bob, oldState, state, null); + keyManager.updateSession(bob, oldState, state, null, split); if (pc.cloveSet.isEmpty()) { if (_log.shouldWarn()) @@ -842,17 +840,15 @@ public final class ECIESAEADEngine { eph.getEncodedPublicKey(enc, TAGLEN); // split() - byte[] ck = state.getChainingKey(); - byte[] k_ab = new byte[32]; - byte[] k_ba = new byte[32]; - _hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0); + // Noise does it too but it trashes the keys + SplitKeys split = new SplitKeys(state, _hkdf); CipherStatePair ckp = state.split(); CipherState sender = ckp.getSender(); byte[] hash = state.getHandshakeHash(); // part 2 - payload byte[] encpayloadkey = new byte[32]; - _hkdf.calculate(k_ba, ZEROLEN, INFO_6, encpayloadkey); + _hkdf.calculate(split.k_ba.getData(), ZEROLEN, INFO_6, encpayloadkey); sender.initializeKey(encpayloadkey, 0); try { sender.encryptWithAd(hash, payload, 0, enc, TAGLEN + KEYLEN + MACLEN, payload.length); @@ -862,7 +858,7 @@ public final class ECIESAEADEngine { return null; } // tell the SKM - keyManager.updateSession(target, null, state, callback); + keyManager.updateSession(target, null, state, callback, split); return enc; } diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java index c35e46b0a..313be954e 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java @@ -222,7 +222,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener * @param oldState null for inbound, pre-clone for outbound * @return true if this was the first NSR received */ - boolean updateSession(PublicKey target, HandshakeState oldState, HandshakeState state, ReplyCallback callback) { + boolean updateSession(PublicKey target, HandshakeState oldState, HandshakeState state, + ReplyCallback callback, SplitKeys split) { EncType type = target.getType(); if (type != EncType.ECIES_X25519) throw new IllegalArgumentException("Bad public key type " + type); @@ -238,7 +239,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener // TODO can we recover? return false; } - sess.updateSession(state, callback); + sess.updateSession(state, callback, split); } else { // we are Alice, NSR received if (_log.shouldInfo()) @@ -258,7 +259,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener if (oldState.equals(pstate)) { if (!found) { found = true; - sess.updateSession(state, null); + sess.updateSession(state, null, split); boolean ok = addSession(sess, false); if (_log.shouldDebug()) { if (ok) @@ -966,12 +967,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener * @param state current state * @param callback only for inbound (NSR sent by Bob), may be null */ - void updateSession(HandshakeState state, ReplyCallback callback) { - byte[] ck = state.getChainingKey(); - byte[] k_ab = new byte[32]; - byte[] k_ba = new byte[32]; - _hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0); - SessionKey rk = new SessionKey(ck); + void updateSession(HandshakeState state, ReplyCallback callback, SplitKeys split) { + SessionKey rk = split.ck; long now = _context.clock().now(); _lastUsed = now; _lastReceived = now; @@ -979,15 +976,17 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener if (isInbound) { // We are Bob // This is an OUTBOUND NSR, we make an INBOUND tagset for ES - RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, new SessionKey(k_ab), + RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, split.k_ab, now, 0, -1, MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES); // and a pending outbound one - RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ba), + // TODO - We could just save rk and k_ba, and defer + // creation of the OB ES tagset to firstTagConsumed() below + RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, rk, split.k_ba, now, 0, -1); if (_log.shouldDebug()) { - _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset:\n" + tagset_ab); - _log.debug("Pending OB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset:\n" + tagset_ba); + _log.debug("Update IB Session, rk = " + rk + " tk = " + split.k_ab + " ES tagset:\n" + tagset_ab); + _log.debug("Pending OB Session, rk = " + rk + " tk = " + split.k_ba + " ES tagset:\n" + tagset_ba); } synchronized (_unackedTagSets) { _unackedTagSets.add(tagset_ba); @@ -996,15 +995,15 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener } else { // We are Alice // This is an INBOUND NSR, we make an OUTBOUND tagset for ES - RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ab), + RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, rk, split.k_ab, now, 0, -1); // and an inbound one - RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, new SessionKey(k_ba), + RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, split.k_ba, now, 0, -1, MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES); if (_log.shouldDebug()) { - _log.debug("Update OB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset:\n" + tagset_ab); - _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset:\n" + tagset_ba); + _log.debug("Update OB Session, rk = " + rk + " tk = " + split.k_ab + " ES tagset:\n" + tagset_ab); + _log.debug("Update IB Session, rk = " + rk + " tk = " + split.k_ba + " ES tagset:\n" + tagset_ba); } synchronized (_unackedTagSets) { _tagSet = tagset_ab; diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java index 113d7e742..f483dd842 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java @@ -173,6 +173,9 @@ class RatchetTagSet implements TagSetHandle { _sessionTags = null; _sessionKeys = null; } + // prevent adjusted expiration and search for matching OB ts + if (tagsetid > 0 && tagsetid <= 65535) + _acked = true; } /** @@ -200,6 +203,8 @@ class RatchetTagSet implements TagSetHandle { hkdf = null; _sessionTags = null; _sessionKeys = null; + // prevent adjusted expiration and search for matching OB ts + _acked = true; } public void clear() { diff --git a/router/java/src/net/i2p/router/crypto/ratchet/SplitKeys.java b/router/java/src/net/i2p/router/crypto/ratchet/SplitKeys.java new file mode 100644 index 000000000..96531946b --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/SplitKeys.java @@ -0,0 +1,29 @@ +package net.i2p.router.crypto.ratchet; + +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.crypto.HKDF; +import net.i2p.data.SessionKey; + +/** + * Standard Noise split(). + * Passed from the engine to the SKM so we don't have + * to do it twice. + * + * @since 0.9.46 + */ +class SplitKeys { + + private static final byte[] ZEROLEN = new byte[0]; + public final SessionKey ck, k_ab, k_ba; + + public SplitKeys(HandshakeState state, HKDF hkdf) { + byte[] ckd = state.getChainingKey(); + byte[] ab = new byte[32]; + byte[] ba = new byte[32]; + hkdf.calculate(ckd, ZEROLEN, ab, ba, 0); + ck = new SessionKey(ckd); + k_ab = new SessionKey(ab); + k_ba = new SessionKey(ba); + } +} diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 04c9f599d..11a94d40a 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -147,6 +147,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { private final static long LS_LOOKUP_TIMEOUT = 15*1000; private final static long OVERALL_TIMEOUT_NOLS_MIN = OVERALL_TIMEOUT_MS_MIN + LS_LOOKUP_TIMEOUT; private final static long REPLY_TIMEOUT_MS_MIN = OVERALL_TIMEOUT_MS_DEFAULT - 5*1000; + // callback timeout. Longer so we can have success-after-failure + private final static long RATCHET_REPLY_TIMEOUT_MS_MIN = 30*1000; /** * NOTE: Changed as of 0.9.2. @@ -1152,8 +1154,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } public long getExpiration() { - // same as SendTimeoutJob - return Math.max(_overallExpiration, _start + REPLY_TIMEOUT_MS_MIN); + // longer timeout so we can have success-after-failure via ratchet + return Math.max(_overallExpiration, _start + RATCHET_REPLY_TIMEOUT_MS_MIN); } public void onReply() {