From acf3abb19bb2a2075e874d6852d7ee3a63f6d873 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Fri, 27 Mar 2020 16:55:53 +0000
Subject: [PATCH] Ratchet: Updates - Modify NextKey, start of support (WIP) -
 Don't expect DSM reply to ECIES destinations - Debug setting to always sent
 ack request

---
 .../crypto/ratchet/ECIESAEADEngine.java       | 33 +++++++++++++------
 .../router/crypto/ratchet/NextSessionKey.java | 26 ++++++++++++---
 .../router/crypto/ratchet/RatchetPayload.java | 22 ++++++++-----
 .../i2p/router/crypto/ratchet/RatchetSKM.java |  1 +
 .../router/crypto/ratchet/RatchetTagSet.java  | 26 +++++++++++++++
 .../OutboundClientMessageOneShotJob.java      |  3 +-
 6 files changed, 87 insertions(+), 24 deletions(-)

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 19c74065b2..ab0cdd98d9 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -62,6 +62,8 @@ public final class ECIESAEADEngine {
     private static final int MIN_ENCRYPTED_SIZE = MIN_ES_SIZE;
     private static final byte[] NULLPK = new byte[KEYLEN];
     private static final int MAXPAD = 16;
+    // debug, send ACKREQ in every ES
+    private static final boolean ACKREQ_IN_ES = false;
 
     private static final String INFO_0 = "SessionReplyTags";
     private static final String INFO_6 = "AttachPayloadKDF";
@@ -603,7 +605,7 @@ public final class ECIESAEADEngine {
         }
         if (_log.shouldDebug())
             _log.debug("Encrypting as ES to " + target + " with key " + re.key + " and tag " + re.tag.toBase64());
-        byte rv[] = encryptExistingSession(cloves, target, re.key, re.tag, replyDI);
+        byte rv[] = encryptExistingSession(cloves, target, re, replyDI);
         return rv;
     }
 
@@ -640,7 +642,7 @@ public final class ECIESAEADEngine {
         if (_log.shouldDebug())
             _log.debug("State before encrypt new session: " + state);
 
-        byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI);
+        byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI, null);
 
         byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN];
         try {
@@ -699,7 +701,7 @@ public final class ECIESAEADEngine {
         if (_log.shouldDebug())
             _log.debug("State after mixhash tag before encrypt new session reply: " + state);
 
-        byte[] payload = createPayload(cloves, 0, replyDI);
+        byte[] payload = createPayload(cloves, 0, replyDI, null);
 
         // part 1 - tag and empty payload
         byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN];
@@ -762,11 +764,14 @@ public final class ECIESAEADEngine {
      * @param replyDI non-null to request an ack, or null
      * @return encrypted data or null on failure
      */
-    private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, SessionKeyAndNonce key,
-                                          RatchetSessionTag currentTag, 
+    private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, RatchetEntry re,
                                           DeliveryInstructions replyDI) {
-        byte rawTag[] = currentTag.getData();
-        byte[] payload = createPayload(cloves, 0, replyDI);
+        //
+        if (ACKREQ_IN_ES && replyDI == null)
+            replyDI = new DeliveryInstructions();
+        byte rawTag[] = re.tag.getData();
+        byte[] payload = createPayload(cloves, 0, replyDI, re.nextKey);
+        SessionKeyAndNonce key = re.key;
         byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce());
         System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
         return encr;
@@ -843,12 +848,12 @@ public final class ECIESAEADEngine {
 
         public void gotAck(int id, int n) {
             if (_log.shouldDebug())
-                _log.debug("Got ACK block: " + n);
+                _log.debug("Got ACK block: " + id + " / " + n);
         }
 
         public void gotAckRequest(int id, DeliveryInstructions di) {
             if (_log.shouldDebug())
-                _log.debug("Got ACK REQUEST block: " + di);
+                _log.debug("Got ACK REQUEST block: " + id + " / " + di);
             ackRequested = true;
         }
 
@@ -872,13 +877,16 @@ public final class ECIESAEADEngine {
      *  @param expiration if greater than zero, add a DateTime block
      *  @param replyDI non-null to request an ack, or null
      */
-    private byte[] createPayload(CloveSet cloves, long expiration, DeliveryInstructions replyDI) {
+    private byte[] createPayload(CloveSet cloves, long expiration,
+                                 DeliveryInstructions replyDI, NextSessionKey nextKey) {
         int count = cloves.getCloveCount();
         int numblocks = count + 1;
         if (expiration > 0)
             numblocks++;
         if (replyDI != null)
             numblocks++;
+        if (nextKey != null)
+            numblocks++;
         int len = 0;
         List<Block> blocks = new ArrayList<Block>(numblocks);
         if (expiration > 0) {
@@ -886,6 +894,11 @@ public final class ECIESAEADEngine {
             blocks.add(block);
             len += block.getTotalLength();
         }
+        if (nextKey != null) {
+            Block block = new NextKeyBlock(nextKey);
+            blocks.add(block);
+            len += block.getTotalLength();
+        }
         for (int i = 0; i < count; i++) {
             GarlicClove clove = cloves.getClove(i);
             Block block = new GarlicBlock(clove);
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 e7e208d218..eaa3057d8d 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
@@ -1,30 +1,46 @@
 package net.i2p.router.crypto.ratchet;
 
-import net.i2p.data.SessionKey;
+import net.i2p.crypto.EncType;
+import net.i2p.data.PublicKey;
 
 /**
- * A session key and key ID.
+ * A X25519 key and key ID.
  *
  * @since 0.9.44
  */
-class NextSessionKey extends SessionKey {
+class NextSessionKey extends PublicKey {
     private final int _id;
+    private final boolean _isReverse, _isRequest;
 
-    public NextSessionKey(byte[] data, int id) {
-        super(data);
+    public NextSessionKey(byte[] data, int id, boolean isReverse, boolean isRequest) {
+        super(EncType.ECIES_X25519, data);
         _id = id;
+        _isReverse = isReverse;
+        _isRequest = isRequest;
     }
 
     public int getID() {
         return _id;
     }
 
+    /** @since 0.9.46 */
+    public boolean isReverse() {
+        return _isReverse;
+    }
+
+    /** @since 0.9.46 */
+    public boolean isRequest() {
+        return _isRequest;
+    }
+
     @Override
     public String toString() {
         StringBuilder buf = new StringBuilder(64);
         buf.append("[NextSessionKey: ");
         buf.append(toBase64());
         buf.append(" ID: ").append(_id);
+        buf.append(" reverse? ").append(_isReverse);
+        buf.append(" request? ").append(_isRequest);
         buf.append(']');
         return buf.toString();
     }
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 836d1db443..c6b065a6fc 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
@@ -136,12 +136,14 @@ class RatchetPayload {
 
                 case BLOCK_NEXTKEY:
                   {
-                    if (len != 34)
+                    if (len != 35)
                         throw new IOException("Bad length for NEXTKEY: " + len);
-                    int id = (int) DataHelper.fromLong(payload, i, 2);
+                    boolean isReverse = (payload[i] & 0x01) != 0;
+                    boolean isRequest = (payload[i] & 0x02) != 0;
+                    int id = (int) DataHelper.fromLong(payload, i + 1, 2);
                     byte[] data = new byte[32];
-                    System.arraycopy(payload, i + 2, data, 0, 32);
-                    NextSessionKey nsk = new NextSessionKey(data, id);
+                    System.arraycopy(payload, i + 3, data, 0, 32);
+                    NextSessionKey nsk = new NextSessionKey(data, id, isReverse, isRequest);
                     cb.gotNextKey(nsk);
                   }
                     break;
@@ -350,13 +352,17 @@ class RatchetPayload {
         }
 
         public int getDataLength() {
-            return 34;
+            return 35;
         }
 
         public int writeData(byte[] tgt, int off) {
-            DataHelper.toLong(tgt, off, 2, next.getID());
-            System.arraycopy(next.getData(), 0, tgt, off + 2, 32);
-            return off + 34;
+            if (next.isReverse())
+                tgt[off] = 0x01;
+            if (next.isRequest())
+                tgt[off] |= 0x02;
+            DataHelper.toLong(tgt, off + 1, 2, next.getID());
+            System.arraycopy(next.getData(), 0, tgt, off + 3, 32);
+            return off + 35;
         }
     }
 
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 0c699bba4b..4c0b2e8162 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
@@ -1100,6 +1100,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                                 set.setDate(now);
                                 SessionKeyAndNonce skn = set.consumeNextKey();
                                 // TODO key ID and PN
+                                // TODO next key
                                 return new RatchetEntry(tag, skn, 0, 0);
                             } else if (_log.shouldInfo()) {
                                 _log.info("Removing empty " + set);
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 f72e52f12a..8107ca478c 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
@@ -13,6 +13,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.KeyPair;
 import net.i2p.crypto.TagSetHandle;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
@@ -56,6 +57,9 @@ class RatchetTagSet implements TagSetHandle {
     private final byte[] _symmkey_constant;
     private int _lastTag = -1;
     private int _lastKey = -1;
+    private KeyPair _nextKeys;
+    private NextSessionKey _nextKey;
+    private boolean _nextKeyAcked;
 
     private static final String INFO_1 = "KDFDHRatchetStep";
     private static final String INFO_2 = "TagAndKeyGenKeys";
@@ -65,6 +69,7 @@ class RatchetTagSet implements TagSetHandle {
     private static final byte[] ZEROLEN = new byte[0];
     private static final int TAGLEN = RatchetSessionTag.LENGTH;
     private static final int MAX = 65535;
+    private static final int LOW = 50;
 
     /**
      *  Outbound NSR Tagset
@@ -241,6 +246,27 @@ class RatchetTagSet implements TagSetHandle {
         return MAX - nextKey;
     }
 
+    /**
+     *  Next Key if applicable
+     *  null if remaining is sufficient
+     *
+     *  @return key or null
+     *  @since 0.9.46
+     */
+    public NextSessionKey getNextKey() {
+        if (remaining() > LOW)
+            return null;
+        if (_nextKeyAcked)  // maybe not needed, keep sending until unused
+            return null;
+        if (_nextKeys == null) {
+            _nextKeys = I2PAppContext.getGlobalContext().keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
+            boolean isIB = _sessionTags != null;
+            // TODO request only needed every other time
+            _nextKey = new NextSessionKey(_nextKeys.getPublic().getData(), 0, isIB, !isIB);
+        }
+        return _nextKey;
+    }
+
     /**
      *  tags still available
      *  inbound only
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 689339b5db..05f43b7c0a 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -678,7 +678,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         SendSuccessJob onReply = null;
         SendTimeoutJob onFail = null;
         ReplySelector selector = null;
-        if (wantACK) {
+
+        if (wantACK && _encryptionKey.getType() == EncType.ELGAMAL_2048) {
             TagSetHandle tsh = null;
             if (!tags.isEmpty()) {
                     SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_from.calculateHash());
-- 
GitLab