From dee92b52903c70fd39fcb80303bce3cf6fa31410 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 30 Mar 2020 16:44:42 +0000
Subject: [PATCH] Ratchet: Updates - Prep for prop. 154 with SingleTagSet -
 Variable timeout for tagsets - Start cleaner sooner - Make key optional in
 next key block - HTML debug output improvement - log tweaks and javadocs

---
 .../crypto/ratchet/ECIESAEADEngine.java       | 29 +++++++-
 .../router/crypto/ratchet/MuxedEngine.java    |  8 +--
 .../router/crypto/ratchet/NextSessionKey.java |  3 +
 .../router/crypto/ratchet/RatchetPayload.java | 31 ++++++---
 .../i2p/router/crypto/ratchet/RatchetSKM.java | 66 +++++++++++--------
 .../router/crypto/ratchet/RatchetTagSet.java  | 60 +++++++++++++++--
 .../router/crypto/ratchet/SingleTagSet.java   | 53 +++++++++++++++
 7 files changed, 199 insertions(+), 51 deletions(-)
 create mode 100644 router/java/src/net/i2p/router/crypto/ratchet/SingleTagSet.java

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 955c8ccfc0..72ba13439b 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -772,14 +772,14 @@ public final class ECIESAEADEngine {
      *  - 16 byte MAC
      * </pre>
      *
-     * @param target unused, this is AEAD encrypt only using the session key and tag
+     * @param target only used if callback is non-null to register it
      * @param replyDI non-null to request an ack, or null
      * @return encrypted data or null on failure
      */
     private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, RatchetEntry re,
                                           DeliveryInstructions replyDI, ReplyCallback callback,
                                           RatchetSKM keyManager) {
-        //
+        // TODO remove DI, just make it a boolean
         if (ACKREQ_IN_ES && replyDI == null)
             replyDI = new DeliveryInstructions();
         byte rawTag[] = re.tag.getData();
@@ -794,6 +794,31 @@ public final class ECIESAEADEngine {
         return encr;
     }
 
+    /**
+     * Create an Existing Session Message to an anonymous target
+     * using the given session key and tag, for netdb DSM/DSRM replies.
+     * Called from MessageWrapper.
+     *
+     * No datetime, no next key, no acks, no ack requests.
+     * n=0, ad=null.
+     *
+     * <pre>
+     *  - 8 byte SessionTag
+     *  - payload
+     *  - 16 byte MAC
+     * </pre>
+     *
+     * @return encrypted data or null on failure
+     * @since 0.9.46
+     */
+    public byte[] encrypt(CloveSet cloves, SessionKey key, RatchetSessionTag tag) {
+        byte rawTag[] = tag.getData();
+        byte[] payload = createPayload(cloves, 0, null, null, null);
+        byte encr[] = encryptAEADBlock(rawTag, payload, key, 0);
+        System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
+        return encr;
+    }
+
     /**
      * No ad
      */
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
index 4fc114e2f5..5d09aadf1a 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
@@ -48,12 +48,12 @@ final class MuxedEngine {
                 try {
                     rv = _context.garlicMessageParser().readCloveSet(dec, 0);
                 } catch (DataFormatException dfe) {
-                    if (_log.shouldWarn())
-                        _log.warn("ElG decrypt failed, trying ECIES", dfe);
+                    if (_log.shouldInfo())
+                        _log.info("ElG decrypt failed, trying ECIES", dfe);
                 }
             } else {
-                if (_log.shouldWarn())
-                    _log.warn("ElG decrypt failed, trying ECIES");
+                //if (_log.shouldDebug())
+                //    _log.debug("ElG decrypt failed, trying ECIES");
             }
         }
         if (rv == null) {
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
index eaa3057d8d..03cd18d35d 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
@@ -12,6 +12,9 @@ class NextSessionKey extends PublicKey {
     private final int _id;
     private final boolean _isReverse, _isRequest;
 
+    /**
+     *  @param data may be null
+     */
     public NextSessionKey(byte[] data, int id, boolean isReverse, boolean isRequest) {
         super(EncType.ECIES_X25519, data);
         _id = id;
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
index 4d89e4229e..8619b3ba36 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
@@ -136,13 +136,19 @@ class RatchetPayload {
 
                 case BLOCK_NEXTKEY:
                   {
-                    if (len != 35)
+                    if (len != 3 && len != 35)
                         throw new IOException("Bad length for NEXTKEY: " + len);
-                    boolean isReverse = (payload[i] & 0x01) != 0;
-                    boolean isRequest = (payload[i] & 0x02) != 0;
+                    boolean hasKey = (payload[i] & 0x01) != 0;
+                    boolean isReverse = (payload[i] & 0x02) != 0;
+                    boolean isRequest = (payload[i] & 0x04) != 0;
                     int id = (int) DataHelper.fromLong(payload, i + 1, 2);
-                    byte[] data = new byte[32];
-                    System.arraycopy(payload, i + 3, data, 0, 32);
+                    byte[] data;
+                    if (hasKey) {
+                        data = new byte[32];
+                        System.arraycopy(payload, i + 3, data, 0, 32);
+                    } else {
+                        data = null;
+                    }
                     NextSessionKey nsk = new NextSessionKey(data, id, isReverse, isRequest);
                     cb.gotNextKey(nsk);
                   }
@@ -352,17 +358,22 @@ class RatchetPayload {
         }
 
         public int getDataLength() {
-            return 35;
+            return next.getData() != null ? 35 : 3;
         }
 
         public int writeData(byte[] tgt, int off) {
-            if (next.isReverse())
+            if (next.getData() != null)
                 tgt[off] = 0x01;
-            if (next.isRequest())
+            if (next.isReverse())
                 tgt[off] |= 0x02;
+            if (next.isRequest())
+                tgt[off] |= 0x04;
             DataHelper.toLong(tgt, off + 1, 2, next.getID());
-            System.arraycopy(next.getData(), 0, tgt, off + 3, 32);
-            return off + 35;
+            if (next.getData() != null) {
+                System.arraycopy(next.getData(), 0, tgt, off + 3, 32);
+                return off + 35;
+            }
+            return off + 3;
         }
     }
 
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 2267f6c626..4c08ec57c2 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
@@ -54,7 +54,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
      * Let outbound session tags sit around for this long before expiring them.
      * Inbound tag expiration is set by SESSION_LIFETIME_MAX_MS
      */
-    private final static long SESSION_TAG_DURATION_MS = 12 * 60 * 1000;
+    final static long SESSION_TAG_DURATION_MS = 12 * 60 * 1000;
 
     /**
      * Keep unused inbound session tags around for this long (a few minutes longer than
@@ -63,9 +63,9 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
      *
      * This is also the max idle time for an outbound session.
      */
-    private final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 3 * 60 * 1000;
+    final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 3 * 60 * 1000;
 
-    private final static long SESSION_PENDING_DURATION_MS = 5 * 60 * 1000;
+    final static long SESSION_PENDING_DURATION_MS = 5 * 60 * 1000;
 
     /**
      * Time to send more if we are this close to expiration
@@ -110,8 +110,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
 
     private class CleanupEvent extends SimpleTimer2.TimedEvent {
         public CleanupEvent() {
-            // wait until outbound expiration time to start
-            super(_context.simpleTimer2(), SESSION_TAG_DURATION_MS);
+            // wait until first expiration time to start
+            super(_context.simpleTimer2(), SESSION_PENDING_DURATION_MS);
         }
 
         public void timeReached() {
@@ -491,6 +491,14 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
         throw new UnsupportedOperationException();
     }
 
+    /**
+     * One time session
+     * @param expire time from now
+     */
+    public void tagsReceived(SessionKey key, RatchetSessionTag tag, long expire) {
+        new SingleTagSet(this, key, tag, _context.clock().now(),  expire);
+    }
+
     /**
      * remove a bunch of arbitrarily selected tags, then drop all of
      * the associated tag sets.  this is very time consuming - iterating
@@ -543,13 +551,15 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 if (state == null) {
                     // TODO this should really be after decrypt...
                     PublicKey pk = tagSet.getRemoteKey();
-                    OutboundSession sess = getSession(pk);
-                    if (sess != null) {
-                        sess.firstTagConsumed(tagSet);
-                    } else {
-                        if (_log.shouldDebug())
-                            _log.debug("First tag consumed but session is gone");
-                    }
+                    if (pk != null) {
+                        OutboundSession sess = getSession(pk);
+                        if (sess != null) {
+                            sess.firstTagConsumed(tagSet);
+                        } else {
+                            if (_log.shouldDebug())
+                                _log.debug("First tag consumed but session is gone");
+                        }
+                    } // else null for SingleTagSets
                 }
             }
             if (_log.shouldDebug()) {
@@ -611,13 +621,12 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
      */
     private int aggressiveExpire() {
         long now = _context.clock().now();
-        long exp = now - SESSION_LIFETIME_MAX_MS;
 
         // inbound
         int removed = 0;
         for (Iterator<RatchetTagSet> iter = _inboundTagSets.values().iterator(); iter.hasNext();) {
             RatchetTagSet ts = iter.next();
-            if (ts.getDate() < exp) {
+            if (ts.getExpiration() < now) {
                 iter.remove();
                 removed++;
             }
@@ -626,7 +635,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
         // outbound
         int oremoved = 0;
         int cremoved = 0;
-        exp = now - (SESSION_LIFETIME_MAX_MS / 2);
+        long exp = now - (SESSION_LIFETIME_MAX_MS / 2);
         for (Iterator<OutboundSession> iter = _outboundSessions.values().iterator(); iter.hasNext();) {
             OutboundSession sess = iter.next();
             oremoved += sess.expireTags(now);
@@ -753,7 +762,6 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
         int total = 0;
         int totalSets = 0;
         long now = _context.clock().now();
-        long exp = now - SESSION_LIFETIME_MAX_MS;
         Set<RatchetTagSet> sets = new TreeSet<RatchetTagSet>(new RatchetTagSetComparator());
         for (Map.Entry<SessionKey, Set<RatchetTagSet>> e : inboundSets.entrySet()) {
             SessionKey skey = e.getKey();
@@ -767,8 +775,9 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 int size = ts.size();
                 total += size;
                 buf.append("<li><b>ID: ").append(ts.getID());
-                buf.append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()));
-                long expires = ts.getDate() - exp;
+                buf.append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()))
+                   .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate()));
+                long expires = ts.getExpiration() - now;
                 if (expires > 0)
                     buf.append(" <b>expires in:</b> ").append(DataHelper.formatDuration2(expires)).append(" with ");
                 else
@@ -789,7 +798,6 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
 
         // outbound
         totalSets = 0;
-        exp = now - SESSION_TAG_DURATION_MS;
         Set<OutboundSession> outbound = getOutboundSessions();
         for (OutboundSession sess : outbound) {
             sets.clear();
@@ -808,8 +816,9 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             for (RatchetTagSet ts : sets) {
                 int size = ts.remaining();
                 buf.append("<li><b>ID: ").append(ts.getID())
-                   .append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()));
-                long expires = ts.getDate() - exp;
+                   .append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()))
+                   .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate()));
+                long expires = ts.getExpiration() - now;
                 if (expires > 0)
                     buf.append(" <b>expires in:</b> ").append(DataHelper.formatDuration2(expires)).append(" with ");
                 else
@@ -1130,7 +1139,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             synchronized (_tagSets) {
                 for (Iterator<RatchetTagSet> iter = _tagSets.iterator(); iter.hasNext(); ) {
                     RatchetTagSet set = iter.next();
-                    if (set.getDate() + SESSION_TAG_DURATION_MS <= now) {
+                    if (set.getExpiration() <= now) {
                         iter.remove();
                         removed++;
                     }
@@ -1139,7 +1148,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 if ((now & 0x0f) == 0) {
                     for (Iterator<RatchetTagSet> iter = _unackedTagSets.iterator(); iter.hasNext(); ) {
                         RatchetTagSet set = iter.next();
-                        if (set.getDate() + SESSION_TAG_DURATION_MS <= now) {
+                        if (set.getExpiration() <= now) {
                             iter.remove();
                             removed++;
                         }
@@ -1156,7 +1165,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 while (!_tagSets.isEmpty()) {
                     RatchetTagSet set = _tagSets.get(0);
                     synchronized(set) {
-                        if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
+                        if (set.getExpiration() > now) {
                             RatchetSessionTag tag = set.consumeNext();
                             if (tag != null) {
                                 set.setDate(now);
@@ -1186,7 +1195,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                     RatchetTagSet set = _tagSets.get(i);
                     if (!set.getAcked())
                         continue;
-                    if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
+                    if (set.getExpiration() > now) {
                         // or just add fixed number?
                         int sz = set.remaining();
                         tags += sz;
@@ -1205,12 +1214,13 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             long last = 0;
             synchronized (_tagSets) {
                 for (RatchetTagSet set : _tagSets) {
-                    if (set.getDate() > last && set.remaining() > 0) 
-                        last = set.getDate();
+                    long exp = set.getExpiration();
+                    if (exp > last && set.remaining() > 0) 
+                        last = exp;
                 }
             }
             if (last > 0)
-                return last + SESSION_TAG_DURATION_MS;
+                return last;
             return -1;
         }
 
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 e3be95308f..bf2716404d 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
@@ -38,7 +38,7 @@ import net.i2p.util.Log;
 class RatchetTagSet implements TagSetHandle {
     private final SessionTagListener _lsnr;
     private final PublicKey _remoteKey;
-    private final SessionKey _key;
+    protected final SessionKey _key;
     private final HandshakeState _state;
     // inbound only, else null
     // We use object for tags because we must do indexOfValueByValue()
@@ -48,6 +48,7 @@ class RatchetTagSet implements TagSetHandle {
     private final SparseArray<byte[]> _sessionKeys;
     private final HKDF hkdf;
     private final long _created;
+    private final long _timeout;
     private long _date;
     private final int _id;
     private final int _originalSize;
@@ -84,7 +85,7 @@ class RatchetTagSet implements TagSetHandle {
      */
     public RatchetTagSet(HKDF hkdf, HandshakeState state, SessionKey rootKey, SessionKey data,
                          long date, int id) {
-        this(hkdf, null, state, null, rootKey, data, date, id, false, 0, 0);
+        this(hkdf, null, state, null, rootKey, data, date, RatchetSKM.SESSION_PENDING_DURATION_MS, id, false, 0, 0);
     }
 
     /**
@@ -94,7 +95,7 @@ class RatchetTagSet implements TagSetHandle {
      */
     public RatchetTagSet(HKDF hkdf, SessionKey rootKey, SessionKey data,
                          long date, int id) {
-        this(hkdf, null, null, null, rootKey, data, date, id, false, 0, 0);
+        this(hkdf, null, null, null, rootKey, data, date, RatchetSKM.SESSION_TAG_DURATION_MS, id, false, 0, 0);
     }
 
     /**
@@ -104,7 +105,7 @@ class RatchetTagSet implements TagSetHandle {
      */
     public RatchetTagSet(HKDF hkdf, SessionTagListener lsnr, HandshakeState state, SessionKey rootKey, SessionKey data,
                          long date, int id, int minSize, int maxSize) {
-        this(hkdf, lsnr, state, null, rootKey, data, date, id, true, minSize, maxSize);
+        this(hkdf, lsnr, state, null, rootKey, data, date, RatchetSKM.SESSION_PENDING_DURATION_MS, id, true, minSize, maxSize);
     }
 
     /**
@@ -115,7 +116,7 @@ class RatchetTagSet implements TagSetHandle {
     public RatchetTagSet(HKDF hkdf, SessionTagListener lsnr,
                          PublicKey remoteKey, SessionKey rootKey, SessionKey data,
                          long date, int id, int minSize, int maxSize) {
-        this(hkdf, lsnr, null, remoteKey, rootKey, data, date, id, true, minSize, maxSize);
+        this(hkdf, lsnr, null, remoteKey, rootKey, data, date, RatchetSKM.SESSION_LIFETIME_MAX_MS, id, true, minSize, maxSize);
     }
 
 
@@ -124,12 +125,13 @@ class RatchetTagSet implements TagSetHandle {
      */
     private RatchetTagSet(HKDF hkdf, SessionTagListener lsnr, HandshakeState state,
                           PublicKey remoteKey, SessionKey rootKey, SessionKey data,
-                          long date, int id, boolean isInbound, int minSize, int maxSize) {
+                          long date, long timeout, int id, boolean isInbound, int minSize, int maxSize) {
         _lsnr = lsnr;
         _state = state;
         _remoteKey = remoteKey;
         _key = rootKey;
         _created = date;
+        _timeout = timeout;
         _date = date;
         _id = id;
         _originalSize = minSize;
@@ -159,6 +161,31 @@ class RatchetTagSet implements TagSetHandle {
         }
     }
 
+    /**
+     *  For SingleTagSet
+     *  @since 0.9.46
+     */
+    protected RatchetTagSet(SessionTagListener lsnr, SessionKey rootKey, long date, long timeout) {
+        _lsnr = lsnr;
+        _state = null;
+        _remoteKey = null;
+        _key = rootKey;
+        _created = date;
+        _timeout = timeout;
+        _date = date;
+        _id = 0x10003;
+        _originalSize = 1;
+        _maxSize = 1;
+        _nextRootKey = null;
+        _sesstag_ck = null;
+        _sesstag_constant = null;
+        _symmkey_ck = null;
+        _symmkey_constant = null;
+        hkdf = null;
+        _sessionTags = null;
+        _sessionKeys = null;
+    }
+
     public void clear() {
         if (_sessionTags != null)
             _sessionTags.clear();
@@ -201,6 +228,7 @@ class RatchetTagSet implements TagSetHandle {
 
     /**
      *  For inbound and outbound: last used time
+     *  Expiration is getDate() + getTimeout().
      */
     public long getDate() {
         return _date;
@@ -214,12 +242,30 @@ class RatchetTagSet implements TagSetHandle {
     }
 
     /**
-     *  For inbound and outbound: creation time
+     *  For inbound and outbound: creation time, for debugging only
      */
     public long getCreated() {
         return _created;
     }
 
+    /**
+     *  For inbound and outbound: Idle timeout interval.
+     *  Expiration is getDate() + getTimeout().
+     *  @since 0.9.46
+     */
+    public long getTimeout() {
+        return _timeout;
+    }
+
+    /**
+     *  For inbound and outbound: Expiration.
+     *  Expiration is getDate() + getTimeout().
+     *  @since 0.9.46
+     */
+    public synchronized long getExpiration() {
+        return _date + _timeout;
+    }
+
     /** for debugging */
     public int getOriginalSize() {
         return _originalSize;
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/SingleTagSet.java b/router/java/src/net/i2p/router/crypto/ratchet/SingleTagSet.java
new file mode 100644
index 0000000000..970f2180c6
--- /dev/null
+++ b/router/java/src/net/i2p/router/crypto/ratchet/SingleTagSet.java
@@ -0,0 +1,53 @@
+package net.i2p.router.crypto.ratchet;
+
+import net.i2p.data.SessionKey;
+
+/**
+ * Inbound ES tagset with a single tag and key.
+ * Nonce is 0.
+ * For receiving DSM/DSRM replies.
+ *
+ * @since 0.9.46
+ */
+class SingleTagSet extends RatchetTagSet {
+
+    private final RatchetSessionTag _tag;
+    private boolean _isUsed;
+
+    /**
+     *  For outbound Existing Session
+     */
+    public SingleTagSet(SessionTagListener lsnr, SessionKey key, RatchetSessionTag tag, long date, long timeout) {
+        super(lsnr, key, date, timeout);
+        _tag = tag;
+        lsnr.addTag(tag, this);
+    }
+
+    @Override
+    public int size() {
+        return _isUsed ? 0 : 1;
+    }
+
+    @Override
+    public int remaining() {
+        return _isUsed ? 0 : 1;
+    }
+
+    @Override
+    public SessionKeyAndNonce consume(RatchetSessionTag tag) {
+        if (_isUsed || !tag.equals(_tag))
+            return null;
+        _isUsed = true;
+        return new SessionKeyAndNonce(_key.getData(), 0);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder(64);
+        buf.append("[SingleTagSet: 0 ");
+        buf.append(_tag.toBase64());
+        buf.append(' ').append(_key.toBase64());
+        buf.append(']');
+        return buf.toString();
+    }
+}
-- 
GitLab