diff --git a/history.txt b/history.txt index d69e305beb..b2ae167078 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,15 @@ +2019-11-02 zzz + * Router: NSR/ES fixes for proposal 144 + +2019-10-31 zzz + * Router: Updates for proposal 144 + +2019-10-27 zzz + * NetDB: Don't send encrypted lookup reply to ratchet dest + * OCMOSJ: + - Bundle unwrapped ack with LS for ratchet dest + - Keep bundling LS until acked + 2019-10-25 zzz * Router (proposal 144): - Set client SKM based on configured encryption diff --git a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java index e8bd9b97c0..f33baae9ce 100644 --- a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java +++ b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java @@ -44,6 +44,7 @@ public class HandshakeState implements Destroyable, Cloneable { private int action; private final int requirements; private int patternIndex; + private boolean wasCloned; /** * Enumerated value that indicates that the handshake object @@ -230,13 +231,32 @@ public class HandshakeState implements Destroyable, Cloneable { * @since 0.9.44 */ protected HandshakeState(HandshakeState o) throws CloneNotSupportedException { - // everything is shallow copied except for symmetric state + // everything is shallow copied except for symmetric state and keys + // so destroy() doesn't zero them out later symmetric = o.symmetric.clone(); isInitiator = o.isInitiator; - localKeyPair = o.localKeyPair; - localEphemeral = o.localEphemeral; - remotePublicKey = o.remotePublicKey; - remoteEphemeral = o.remoteEphemeral; + if (o.localKeyPair != null) + localKeyPair = o.localKeyPair.clone(); + if (o.localEphemeral != null) { + if (isInitiator) { + // always save Alice's local keys + localEphemeral = o.localEphemeral.clone(); + } else { + if (o.wasCloned) { + // new keys after first time for Bob + localEphemeral = o.localEphemeral.clone(); + localEphemeral.generateKeyPair(); + } else { + // first time for Bob, use the eph. keys previously generated + localEphemeral = o.localEphemeral; + o.wasCloned = true; + } + } + } + if (o.remotePublicKey != null) + remotePublicKey = o.remotePublicKey.clone(); + if (o.remoteEphemeral != null) + remoteEphemeral = o.remoteEphemeral.clone(); action = o.action; if (action == SPLIT || action == COMPLETE) throw new CloneNotSupportedException("clone after NSR"); diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 282c18b422..0725033fa6 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 2; + public final static long BUILD = 3; /** for example "-test" */ public final static String EXTRA = ""; 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 481acd7245..955d5a0252 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -145,21 +145,14 @@ public final class ECIESAEADEngine { CloveSet decrypted; final boolean shouldDebug = _log.shouldDebug(); if (key != null) { - //if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st); - if (shouldDebug) - _log.debug("Decrypting existing session encrypted with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes " /* + Base64.encode(data, 0, 64) */ ); - HandshakeState state = key.getHandshakeState(); if (state == null) { + if (shouldDebug) + _log.debug("Decrypting ES with tag: " + st + ": key: " + key.toBase64() + ": " + data.length + " bytes"); decrypted = decryptExistingSession(tag, data, key, targetPrivateKey); } else if (data.length >= MIN_NSR_SIZE) { - try { - state = state.clone(); - } catch (CloneNotSupportedException e) { - if (_log.shouldWarn()) - _log.warn("ECIES decrypt fail: clone()", e); - return null; - } + if (shouldDebug) + _log.debug("Decrypting NSR with tag: " + st + ": key: " + key.toBase64() + ": " + data.length + " bytes"); decrypted = decryptNewSessionReply(tag, data, state, keyManager); } else { decrypted = null; @@ -179,7 +172,6 @@ public final class ECIESAEADEngine { if (shouldDebug) _log.debug("IB Tag " + st + " not found, trying NS decrypt"); decrypted = decryptNewSession(data, targetPrivateKey, keyManager); if (decrypted != null) { - if (shouldDebug) _log.debug("NS decrypt success"); _context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession"); } else { _context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed"); @@ -248,14 +240,13 @@ public final class ECIESAEADEngine { byte[] bobPK = new byte[KEYLEN]; state.getRemotePublicKey().getPublicKey(bobPK, 0); + if (_log.shouldDebug()) + _log.debug("NS decrypt success from PK " + Base64.encode(bobPK)); if (Arrays.equals(bobPK, NULLPK)) { // TODO if (_log.shouldWarn()) _log.warn("Zero static key in IB NS"); return null; - } else { - if (_log.shouldDebug()) - _log.debug("Received NS from PK " + Base64.encode(bobPK)); } // payload @@ -308,11 +299,20 @@ public final class ECIESAEADEngine { * * @param tag 8 bytes, same as first 8 bytes of data * @param data 56 bytes minimum - * @param state must have already been cloned + * @param state will be cloned here * @return null if decryption fails */ - private CloveSet decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState state, RatchetSKM keyManager) + private CloveSet decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState oldState, RatchetSKM keyManager) throws DataFormatException { + HandshakeState state; + try { + state = oldState.clone(); + } catch (CloneNotSupportedException e) { + if (_log.shouldWarn()) + _log.warn("ECIES decrypt fail: clone()", e); + return null; + } + // part 1 - handshake byte[] yy = new byte[KEYLEN]; System.arraycopy(data, TAGLEN, yy, 0, KEYLEN); @@ -337,13 +337,8 @@ public final class ECIESAEADEngine { byte[] k_ab = new byte[32]; byte[] k_ba = new byte[32]; _hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0); - SessionKey tk = new SessionKey(ck); - byte[] temp_key = doHMAC(tk, ZEROLEN); -// unused - tk = new SessionKey(temp_key); CipherStatePair ckp = state.split(); CipherState rcvr = ckp.getReceiver(); - CipherState sender = ckp.getSender(); byte[] hash = state.getHandshakeHash(); // part 2 - payload @@ -373,10 +368,22 @@ public final class ECIESAEADEngine { } catch (Exception e) { throw new DataFormatException("NSR payload error", e); } - long now = _context.clock().now(); - RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), now, 0); - RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, keyManager, new SessionKey(ck), new SessionKey(k_ba), now, 0, 5, 5); + + byte[] bobPK = new byte[KEYLEN]; + state.getRemotePublicKey().getPublicKey(bobPK, 0); + if (_log.shouldDebug()) + _log.debug("NSR decrypt success from PK " + Base64.encode(bobPK)); + if (Arrays.equals(bobPK, NULLPK)) { + // TODO + if (_log.shouldWarn()) + _log.warn("NSR reply to zero static key NS"); + return null; + } + // tell the SKM + PublicKey bob = new PublicKey(EncType.ECIES_X25519, bobPK); + keyManager.updateSession(bob, oldState, state); + if (pc.cloveSet.isEmpty()) { if (_log.shouldWarn()) _log.warn("No garlic block in NSR payload"); @@ -531,10 +538,9 @@ public final class ECIESAEADEngine { _log.warn("ECIES encrypt fail: clone()", e); return null; } -// register state with skm if (_log.shouldDebug()) _log.debug("Encrypting as NSR to " + target + " with tag " + re.tag); - return encryptNewSessionReply(cloves, state, re.tag, keyManager); + return encryptNewSessionReply(cloves, target, state, re.tag, keyManager); } if (_log.shouldDebug()) _log.debug("Encrypting as ES to " + target + " with key " + re.key + " and tag " + re.tag); @@ -616,7 +622,7 @@ public final class ECIESAEADEngine { * @param state must have already been cloned * @return encrypted data or null on failure */ - private byte[] encryptNewSessionReply(CloveSet cloves, HandshakeState state, + private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state, RatchetSessionTag currentTag, RatchetSKM keyManager) { byte[] tag = currentTag.getData(); state.mixHash(tag, 0, TAGLEN); @@ -648,12 +654,7 @@ public final class ECIESAEADEngine { byte[] k_ab = new byte[32]; byte[] k_ba = new byte[32]; _hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0); - SessionKey tk = new SessionKey(ck); - byte[] temp_key = doHMAC(tk, ZEROLEN); -// unused - tk = new SessionKey(temp_key); CipherStatePair ckp = state.split(); - CipherState rcvr = ckp.getReceiver(); CipherState sender = ckp.getSender(); byte[] hash = state.getHandshakeHash(); @@ -668,10 +669,8 @@ public final class ECIESAEADEngine { _log.warn("Encrypt fail NSR part 2", gse); return null; } - long now = _context.clock().now(); - RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), now, 0); - RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, keyManager, new SessionKey(ck), new SessionKey(k_ba), now, 0, 5, 5); // tell the SKM + keyManager.updateSession(target, null, state); 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 ba14c6888c..2fe3730e7b 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java @@ -24,6 +24,7 @@ import net.i2p.crypto.EncType; import net.i2p.crypto.HKDF; import net.i2p.crypto.SessionKeyManager; import net.i2p.crypto.TagSetHandle; +import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; @@ -42,6 +43,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener private final Log _log; /** Map allowing us to go from the targeted PublicKey to the OutboundSession used */ private final ConcurrentHashMap _outboundSessions; + private final HashMap> _pendingOutboundSessions; /** Map allowing us to go from a SessionTag to the containing RatchetTagSet */ private final ConcurrentHashMap _inboundTagSets; protected final I2PAppContext _context; @@ -89,6 +91,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener _log = context.logManager().getLog(RatchetSKM.class); _context = context; _outboundSessions = new ConcurrentHashMap(64); + _pendingOutboundSessions = new HashMap>(64); _inboundTagSets = new ConcurrentHashMap(128); _hkdf = new HKDF(context); // start the precalc of Elg2 keys if it wasn't already started @@ -176,12 +179,100 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener } else { // we are Alice, NS sent OutboundSession sess = new OutboundSession(target, null, state); - if (_log.shouldInfo()) - _log.info("New OB session as Alice. Bob: " + toString(target)); + synchronized (_pendingOutboundSessions) { + List pending = _pendingOutboundSessions.get(target); + if (pending != null) { + pending.add(sess); + if (_log.shouldInfo()) + _log.info("Another new OB session as Alice, total now: " + pending.size() + + ". Bob: " + toString(target)); + } else { + pending = new ArrayList(4); + pending.add(sess); + _pendingOutboundSessions.put(target, pending); + if (_log.shouldInfo()) + _log.info("First new OB session as Alice. Bob: " + toString(target)); + } + } return true; } } + /** + * Inbound or outbound. Checks state.getRole() to determine. + * For outbound (NSR rcvd by Alice), sets session to transition to ES mode outbound. + * For inbound (NSR sent by Bob), sets up inbound ES tagset. + * + * @param oldState null for inbound, pre-clone for outbound + * + */ + boolean updateSession(PublicKey target, HandshakeState oldState, HandshakeState state) { + EncType type = target.getType(); + if (type != EncType.ECIES_X25519) + throw new IllegalArgumentException("Bad public key type " + type); + boolean isInbound = state.getRole() == HandshakeState.RESPONDER; + if (isInbound) { + // we are Bob, NSR sent + OutboundSession sess = getSession(target); + if (sess == null) { + if (_log.shouldDebug()) + _log.debug("Update session but no session found for " + target); + // TODO can we recover? + return false; + } + sess.updateSession(state); + if (_log.shouldInfo()) + _log.info("Session update as Bob. Alice: " + toString(target)); + } else { + // we are Alice, NSR received + synchronized (_pendingOutboundSessions) { + List pending = _pendingOutboundSessions.get(target); + if (pending == null) { + if (_log.shouldDebug()) + _log.debug("Update session but no sessions found for " + target); + // TODO can we recover? + return false; + } + boolean found = false; + for (OutboundSession sess : pending) { + for (RatchetTagSet ts : sess.getTagSets()) { + if (ts.getHandshakeState().equals(oldState)) { + if (!found) { + found = true; + sess.updateSession(state); + boolean ok = addSession(sess); + if (_log.shouldDebug()) { + if (ok) + _log.debug("Update session from NSR to ES for " + target); + else + _log.debug("Session already updated from NSR to ES for " + target); + } + } else { + if (_log.shouldDebug()) + _log.debug("Dup tagset " + ts + " for " + target); + } + } else { + // TODO + // remove old tags + if (_log.shouldDebug()) + _log.debug("Remove tagset " + ts + " for " + target); + } + } + } + _pendingOutboundSessions.remove(target); + if (!found) { + if (_log.shouldDebug()) + _log.debug("Update session but no session found (out of " + pending.size() + ") for " + target); + // TODO can we recover? + return false; + } + } + if (_log.shouldInfo()) + _log.info("Session update as Alice. Bob: " + toString(target)); + } + return true; + } + /** * @throws UnsupportedOperationException always */ @@ -665,24 +756,69 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener SessionKey rk = new SessionKey(ck); SessionKey tk = new SessionKey(tagsetkey); if (isInbound) { + // We are Bob // This is an INBOUND NS, we make an OUTBOUND tagset for the NSR RatchetTagSet tagset = new RatchetTagSet(_hkdf, state, rk, tk, - _established, 0); + _established, _sentTagSetID.getAndIncrement()); _tagSets.add(tagset); if (_log.shouldDebug()) _log.debug("New OB Session, rk = " + rk + " tk = " + tk + " 1st tagset: " + tagset); } else { + // We are Alice // This is an OUTBOUND NS, we make an INBOUND tagset for the NSR RatchetTagSet tagset = new RatchetTagSet(_hkdf, RatchetSKM.this, state, rk, tk, - _established, 0, 5, 5); + _established, _rcvTagSetID.getAndIncrement(), 5, 5); _unackedTagSets.add(tagset); if (_log.shouldDebug()) _log.debug("New IB Session, rk = " + rk + " tk = " + tk + " 1st tagset: " + tagset); } } + void updateSession(HandshakeState state) { + 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); + long now = _context.clock().now(); + boolean isInbound = state.getRole() == HandshakeState.RESPONDER; + 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, rk, new SessionKey(k_ab), + now, _rcvTagSetID.getAndIncrement(), 5, 5); + // and a pending outbound one + RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ba), + now, _sentTagSetID.getAndIncrement()); + if (_log.shouldDebug()) { + _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset: " + tagset_ab); + _log.debug("Pending OB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset: " + tagset_ba); + } + synchronized (_tagSets) { + _unackedTagSets.add(tagset_ba); + } + } 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), + now, _sentTagSetID.getAndIncrement()); + // and an inbound one + RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, RatchetSKM.this, rk, new SessionKey(k_ba), + now, _rcvTagSetID.getAndIncrement(), 5, 5); + if (_log.shouldDebug()) { + _log.debug("Update OB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset: " + tagset_ab); + _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset: " + tagset_ba); + } + synchronized (_tagSets) { + _tagSets.add(tagset_ab); + _unackedTagSets.clear(); + } + } + //state.destroy(); + } + /** * @return list of RatchetTagSet objects * This is used only by renderStatusHTML(). 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 aaebf95ab4..105be61e78 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java @@ -153,7 +153,7 @@ class RatchetTagSet implements TagSetHandle { } /** - * For inbound NSR only, else null. + * For inbound/outbound NSR only, else null. * MUST be cloned before processing NSR. */ public HandshakeState getHandshakeState() { @@ -339,12 +339,17 @@ class RatchetTagSet implements TagSetHandle { @Override public String toString() { StringBuilder buf = new StringBuilder(256); + if (_state != null) + buf.append("NSR "); + else + buf.append("ES "); buf.append("TagSet #").append(_id).append(" created: ").append(new Date(_date)); int sz = size(); buf.append(" Size: ").append(sz); buf.append('/').append(getOriginalSize()); buf.append(" Acked? ").append(_acked); if (_sessionTags != null) { + buf.append(" Inbound"); for (int i = 0; i < sz; i++) { int n = _sessionTags.keyAt(i); RatchetSessionTag tag = _sessionTags.valueAt(i); @@ -357,6 +362,8 @@ class RatchetTagSet implements TagSetHandle { buf.append("\tdeferred"); } } + } else { + buf.append(" Outbound"); } return buf.toString(); }