From 7707c4bb944d6480fea865bd2e9f39a6245b0563 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Sun, 15 Mar 2020 18:40:01 +0000
Subject: [PATCH] Ratchet: Stub out ack and ack request blocks

---
 .../crypto/ratchet/ECIESAEADEngine.java       | 52 +++++++++---
 .../router/crypto/ratchet/RatchetPayload.java | 83 +++++++++++++++++++
 .../router/message/GarlicMessageBuilder.java  | 12 ++-
 .../OutboundClientMessageJobHelper.java       | 29 ++++++-
 4 files changed, 158 insertions(+), 18 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 8d24b05180..7438605925 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -26,6 +26,7 @@ import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
+import net.i2p.data.i2np.DeliveryInstructions;
 import net.i2p.data.i2np.GarlicClove;
 import static net.i2p.router.crypto.ratchet.RatchetPayload.*;
 import net.i2p.router.RouterContext;
@@ -554,13 +555,14 @@ public final class ECIESAEADEngine {
      *
      * @param target public key to which the data should be encrypted. 
      * @param priv local private key to encrypt with, from the leaseset
+     * @param replyDI non-null to request an ack, or null
      * @return encrypted data or null on failure
      *
      */
     public byte[] encrypt(CloveSet cloves, PublicKey target, PrivateKey priv,
-                          RatchetSKM keyManager) {
+                          RatchetSKM keyManager, DeliveryInstructions replyDI) {
         try {
-            return x_encrypt(cloves, target, priv, keyManager);
+            return x_encrypt(cloves, target, priv, keyManager, replyDI);
         } catch (Exception e) {
             _log.error("ECIES encrypt error", e);
             return null;
@@ -568,7 +570,7 @@ public final class ECIESAEADEngine {
     }
 
     private byte[] x_encrypt(CloveSet cloves, PublicKey target, PrivateKey priv,
-                             RatchetSKM keyManager) {
+                             RatchetSKM keyManager, DeliveryInstructions replyDI) {
         if (target.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
         if (Arrays.equals(target.getData(), NULLPK)) {
@@ -581,7 +583,7 @@ public final class ECIESAEADEngine {
         if (re == null) {
             if (_log.shouldDebug())
                 _log.debug("Encrypting as NS to " + target);
-            return encryptNewSession(cloves, target, priv, keyManager);
+            return encryptNewSession(cloves, target, priv, keyManager, replyDI);
         }
 
         HandshakeState state = re.key.getHandshakeState();
@@ -595,11 +597,11 @@ public final class ECIESAEADEngine {
             }
             if (_log.shouldDebug())
                 _log.debug("Encrypting as NSR to " + target + " with tag " + re.tag.toBase64());
-            return encryptNewSessionReply(cloves, target, state, re.tag, keyManager);
+            return encryptNewSessionReply(cloves, target, state, re.tag, keyManager, replyDI);
         }
         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);
+        byte rv[] = encryptExistingSession(cloves, target, re.key, re.tag, replyDI);
         return rv;
     }
 
@@ -618,10 +620,11 @@ public final class ECIESAEADEngine {
      *  - 16 byte MAC
      * </pre>
      *
+     * @param replyDI non-null to request an ack, or null
      * @return encrypted data or null on failure
      */
     private byte[] encryptNewSession(CloveSet cloves, PublicKey target, PrivateKey priv,
-                                     RatchetSKM keyManager) {
+                                     RatchetSKM keyManager, DeliveryInstructions replyDI) {
         HandshakeState state;
         try {
             state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.INITIATOR, _edhThread);
@@ -635,7 +638,7 @@ public final class ECIESAEADEngine {
         if (_log.shouldDebug())
             _log.debug("State before encrypt new session: " + state);
 
-        byte[] payload = createPayload(cloves, cloves.getExpiration());
+        byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI);
 
         byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN];
         try {
@@ -681,10 +684,12 @@ public final class ECIESAEADEngine {
      * </pre>
      *
      * @param state must have already been cloned
+     * @param replyDI non-null to request an ack, or null
      * @return encrypted data or null on failure
      */
     private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state,
-                                          RatchetSessionTag currentTag, RatchetSKM keyManager) {
+                                          RatchetSessionTag currentTag, RatchetSKM keyManager,
+                                          DeliveryInstructions replyDI) {
         if (_log.shouldDebug())
             _log.debug("State before encrypt new session reply: " + state);
         byte[] tag = currentTag.getData();
@@ -692,7 +697,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);
+        byte[] payload = createPayload(cloves, 0, replyDI);
 
         // part 1 - tag and empty payload
         byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN];
@@ -752,12 +757,14 @@ public final class ECIESAEADEngine {
      * </pre>
      *
      * @param target unused, this is AEAD encrypt only using the session key and tag
+     * @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) {
+                                          RatchetSessionTag currentTag, 
+                                          DeliveryInstructions replyDI) {
         byte rawTag[] = currentTag.getData();
-        byte[] payload = createPayload(cloves, 0);
+        byte[] payload = createPayload(cloves, 0, replyDI);
         byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce());
         System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
         return encr;
@@ -831,6 +838,16 @@ public final class ECIESAEADEngine {
             nextKey = next;
         }
 
+        public void gotAck(int id, int n) {
+            if (_log.shouldDebug())
+                _log.debug("Got ACK block: " + n);
+        }
+
+        public void gotAckRequest(int id, DeliveryInstructions di) {
+            if (_log.shouldDebug())
+                _log.debug("Got ACK REQUEST block: " + di);
+        }
+
         public void gotTermination(int reason, long count) {
             if (_log.shouldDebug())
                 _log.debug("Got TERMINATION block, reason: " + reason + " count: " + count);
@@ -849,12 +866,15 @@ 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) {
+    private byte[] createPayload(CloveSet cloves, long expiration, DeliveryInstructions replyDI) {
         int count = cloves.getCloveCount();
         int numblocks = count + 1;
         if (expiration > 0)
             numblocks++;
+        if (replyDI != null)
+            numblocks++;
         int len = 0;
         List<Block> blocks = new ArrayList<Block>(numblocks);
         if (expiration > 0) {
@@ -868,6 +888,12 @@ public final class ECIESAEADEngine {
             blocks.add(block);
             len += block.getTotalLength();
         }
+        if (replyDI != null) {
+            // put after the cloves so recipient has any LS garlic
+            Block block = new AckRequestBlock(0, replyDI);
+            blocks.add(block);
+            len += block.getTotalLength();
+        }
         int padlen = 1 + _context.random().nextInt(MAXPAD);
         // random data
         //Block block = new PaddingBlock(_context, padlen);
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 5e272f8f33..0290131c45 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
@@ -8,6 +8,7 @@ import java.util.List;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.data.i2np.DeliveryInstructions;
 import net.i2p.data.i2np.GarlicClove;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -61,6 +62,16 @@ class RatchetPayload {
          */
         public void gotNextKey(NextSessionKey nextKey);
 
+        /**
+         *  @since 0.9.46
+         */
+        public void gotAck(int id, int n);
+
+        /**
+         *  @since 0.9.46
+         */
+        public void gotAckRequest(int id, DeliveryInstructions di);
+
         /**
          *  For stats.
          *  @param paddingLength the number of padding bytes, not including the 3-byte block header
@@ -123,6 +134,7 @@ class RatchetPayload {
                     break;
 
                 case BLOCK_NEXTKEY:
+                  {
                     if (len != 34)
                         throw new IOException("Bad length for NEXTKEY: " + len);
                     int id = (int) DataHelper.fromLong(payload, i, 2);
@@ -130,6 +142,30 @@ class RatchetPayload {
                     System.arraycopy(payload, i + 2, data, 0, 32);
                     NextSessionKey nsk = new NextSessionKey(data, id);
                     cb.gotNextKey(nsk);
+                  }
+                    break;
+
+                case BLOCK_ACKKEY:
+                  {
+                    if (len < 4 || (len % 4) != 0)
+                        throw new IOException("Bad length for REPLYDI: " + len);
+                    for (int j = i; j < i + len; j += 4) {
+                        int id = (int) DataHelper.fromLong(payload, j, 2);
+                        int n = (int) DataHelper.fromLong(payload, j + 2, 2);
+                        cb.gotAck(id, n);
+                    }
+                  }
+                    break;
+
+                case BLOCK_REPLYDI:
+                  {
+                    if (len < 6)
+                        throw new IOException("Bad length for REPLYDI: " + len);
+                    int id = (int) DataHelper.fromLong(payload, i, 4);
+                    DeliveryInstructions di = new DeliveryInstructions();
+                    di.readBytes(payload, i + 5);
+                    cb.gotAckRequest(id, di);
+                  }
                     break;
 
                 case BLOCK_TERMINATION:
@@ -318,6 +354,53 @@ class RatchetPayload {
         }
     }
 
+    /**
+     *  @since 0.9.46
+     */
+    public static class AckBlock extends Block {
+        private final byte[] data;
+
+        public AckBlock(int keyID, int n) {
+            super(BLOCK_ACKKEY);
+            data = new byte[4];
+            DataHelper.toLong(data, 0, 2, keyID);
+            DataHelper.toLong(data, 2, 2, n);
+        }
+
+        public int getDataLength() {
+            return 4;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            System.arraycopy(data, 0, tgt, off, data.length);
+            return off + data.length;
+        }
+    }
+
+    /**
+     *  @since 0.9.46
+     */
+    public static class AckRequestBlock extends Block {
+        private final byte[] data;
+
+        public AckRequestBlock(int sessionID, DeliveryInstructions di) {
+            super(BLOCK_REPLYDI);
+            data = new byte[5 + di.getSize()];
+            DataHelper.toLong(data, 0, 4, sessionID);
+            // flag is zero
+            di.writeBytes(data, 5);
+        }
+
+        public int getDataLength() {
+            return data.length;
+        }
+
+        public int writeData(byte[] tgt, int off) {
+            System.arraycopy(data, 0, tgt, off, data.length);
+            return off + data.length;
+        }
+    }
+
     public static class TerminationBlock extends Block {
         private final byte rsn;
         private final long rcvd;
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
index 424a774c68..dbd612734d 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
@@ -24,6 +24,7 @@ import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
+import net.i2p.data.i2np.DeliveryInstructions;
 import net.i2p.data.i2np.GarlicClove;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -251,12 +252,14 @@ public class GarlicMessageBuilder {
      * @param config how/what to wrap
      * @param target public key of the location being garlic routed to (may be null if we 
      *               know the encryptKey and encryptTag)
+     * @param replyDI non-null to request an ack, or null
      * @return null if expired or on other errors
      * @throws IllegalArgumentException on error
      * @since 0.9.44
      */
     static GarlicMessage buildECIESMessage(RouterContext ctx, GarlicConfig config,
-                                           PublicKey target, Hash from, SessionKeyManager skm) {
+                                           PublicKey target, Hash from, SessionKeyManager skm,
+                                           DeliveryInstructions replyDI) {
         PublicKey key = config.getRecipientPublicKey();
         if (key.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
@@ -286,7 +289,7 @@ public class GarlicMessageBuilder {
                 log.warn("No SKM for " + from.toBase32());
             return null;
         }
-        byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm);
+        byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm, replyDI);
         if (encData == null) {
             if (log.shouldWarn())
                 log.warn("Encrypt fail for " + from.toBase32());
@@ -438,6 +441,11 @@ public class GarlicMessageBuilder {
         return rv;
     }
     
+    /**
+     * Build a single clove
+     *
+     * @since 0.9.44
+     */
     private static GarlicClove buildECIESClove(RouterContext ctx, PayloadGarlicConfig config) {
         GarlicClove clove = new GarlicClove(ctx);
         clove.setData(config.getPayload());
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
index bfe007f6ef..9c906c5e19 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
@@ -118,13 +118,36 @@ class OutboundClientMessageJobHelper {
         SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from);
         if (skm == null)
             return null;
+        boolean isECIES = recipientPK.getType() == EncType.ECIES_X25519;
+        // force ack off if ECIES
+        boolean ackInGarlic = isECIES ? false : requireAck;
         GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove,
-                                                 from, dest, replyTunnel, requireAck, bundledReplyLeaseSet, skm);
+                                                 from, dest, replyTunnel, ackInGarlic, bundledReplyLeaseSet, skm);
         if (config == null)
             return null;
         GarlicMessage msg;
-        if (recipientPK.getType() == EncType.ECIES_X25519) {
-            msg = GarlicMessageBuilder.buildECIESMessage(ctx, config, recipientPK, from, skm);
+        if (isECIES) {
+            DeliveryInstructions di;
+            if (requireAck) {
+                // setup reply DI
+                di = new DeliveryInstructions();
+                if (bundledReplyLeaseSet != null) {
+                    di.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_DESTINATION);
+                    di.setDestination(from);
+                } else if (replyTunnel != null) {
+                    di.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL);
+                    TunnelId replyToTunnelId = replyTunnel.getReceiveTunnelId(0);
+                    Hash replyToTunnelRouter = replyTunnel.getPeer(0);
+                    di.setRouter(replyToTunnelRouter);
+                    di.setTunnelId(replyToTunnelId);
+                } else {
+                    // shouldn't happen
+                    di = null;
+                }
+            } else {
+                di = null;
+            }
+            msg = GarlicMessageBuilder.buildECIESMessage(ctx, config, recipientPK, from, skm, di);
         } else {
             // no use sending tags unless we have a reply token set up already
             int tagsToSend = replyToken >= 0 ? (tagsToSendOverride > 0 ? tagsToSendOverride : skm.getTagsToSend()) : 0;
-- 
GitLab