From a7d9ca920f35f51840c0ecd599b00ba957720dc8 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Fri, 16 Jul 2021 12:28:04 -0400
Subject: [PATCH] Prop 157 updates

- Don't require AES keys for short records
- Derive keys from noise ck
- Use derived keys to garlic-encrypt reply at OBEP
- Register reply key with SKM
- Only use short message for client tunnels if client supports EC
- Set nonce for chacha/poly reply record
- Add tagsReceived() for single tag to MuxedSKM
- Add extended TunnelCreatorConfig.toStringFull()
- BRR toString() enhancements
- Test enhancements
---
 .../net/i2p/data/i2np/BuildRequestRecord.java | 106 ++++++++++++++++--
 .../i2p/data/i2np/BuildResponseRecord.java    |  72 ++++++++++--
 .../i2p/router/crypto/ratchet/MuxedSKM.java   |  10 ++
 .../router/tunnel/TunnelCreatorConfig.java    |  41 +++++++
 .../i2p/router/tunnel/pool/BuildHandler.java  |  33 +++---
 .../tunnel/pool/BuildMessageGenerator.java    |  23 ++--
 .../router/tunnel/pool/BuildReplyHandler.java |   6 +-
 .../router/tunnel/pool/BuildRequestor.java    |  73 ++++++++++--
 .../pool/BuildMessageTestStandalone.java      |  49 ++++----
 9 files changed, 338 insertions(+), 75 deletions(-)

diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
index 595b140b6a..8a66b86607 100644
--- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
@@ -9,6 +9,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.KeyFactory;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
@@ -20,12 +21,14 @@ import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.util.Log;
 import net.i2p.router.RouterContext;
+import net.i2p.router.crypto.ratchet.RatchetSessionTag;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
 
 /**
  * As of 0.9.48, supports two formats.
  * As of 0.9.51, supports three formats.
  * The original 222-byte ElGamal format, the new 464-byte ECIES format,
- * and the newest 172-byte ECIES format.
+ * and the newest 154-byte ECIES format.
  * See proposal 152 and 157 for details on the new formats.
  *
  * None of the readXXX() calls are cached. For efficiency,
@@ -128,6 +131,10 @@ public class BuildRequestRecord {
     private final boolean _isEC;
     private SessionKey _chachaReplyKey;
     private byte[] _chachaReplyAD;
+    // derived keys for short records
+    private SessionKey _derivedLayerKey;
+    private SessionKey _derivedIVKey;
+    private OneTimeSession _derivedGarlicKeys;
     
     /** 
      * If set in the flag byte, any peer may send a message into this tunnel, but if
@@ -195,6 +202,12 @@ public class BuildRequestRecord {
     // 16 byte trunc. hash, 32 byte eph. key, 16 byte MAC
     private static final int LENGTH_EC_SHORT = ShortTunnelBuildMessage.SHORT_RECORD_SIZE - (16 + 32 + 16);
     private static final int MAX_OPTIONS_LENGTH_SHORT = LENGTH_EC_SHORT - OFF_OPTIONS_SHORT; // includes options length
+    // short record HKDF
+    private static final byte[] ZEROLEN = new byte[0];
+    private static final String INFO_1 = "SMTunnelReplyKey";
+    private static final String INFO_2 = "SMTunnelLayerKey";
+    private static final String INFO_3 = "TunnelLayerIVKey";
+    private static final String INFO_4 = "RGarlicKeyAndTag";
     
     private static final boolean TEST = false;
     private static KeyFactory TESTKF;
@@ -226,6 +239,8 @@ public class BuildRequestRecord {
      * Tunnel layer encryption key that the current hop should use
      */
     public SessionKey readLayerKey() {
+        if (_data.length == LENGTH_EC_SHORT)
+            return _derivedLayerKey;
         byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
         int off = _isEC ? OFF_LAYER_KEY_EC : OFF_LAYER_KEY;
         System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
@@ -236,6 +251,8 @@ public class BuildRequestRecord {
      * Tunnel IV encryption key that the current hop should use
      */
     public SessionKey readIVKey() {
+        if (_data.length == LENGTH_EC_SHORT)
+            return _derivedIVKey;
         byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
         int off = _isEC ? OFF_IV_KEY_EC : OFF_IV_KEY;
         System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
@@ -345,7 +362,7 @@ public class BuildRequestRecord {
             return null;
         }
     }
-    
+
     /**
      * ECIES short record only.
      * @return 0 for ElGamal or ECIES long record
@@ -357,6 +374,15 @@ public class BuildRequestRecord {
         return 0;
     }
 
+    /**
+     * ECIES short OBEP record only.
+     * @return null for ElGamal or ECIES long record or non-OBEP
+     * @since 0.9.51
+     */
+    public OneTimeSession readGarlicKeys() {
+        return _derivedGarlicKeys;
+    }
+
     /**
      * Encrypt the record to the specified peer.  The result is formatted as: <pre>
      *   bytes 0-15: truncated SHA-256 of the current hop's identity (the toPeer parameter)
@@ -385,8 +411,10 @@ public class BuildRequestRecord {
      * Encrypt the record to the specified peer. ECIES only.
      * The ChaCha reply key and IV will be available via the getters
      * after this call.
+     * For short records, derived keys will be available via
+     * readLayerKey(), readIVKey(), and readGarlicKeys() after this call.
      * See class javadocs for format.
-     * See proposal 152.
+     * See proposals 152 and 157.
      *
      * @return non-null
      * @since 0.9.48
@@ -406,9 +434,37 @@ public class BuildRequestRecord {
             state.start();
             state.writeMessage(out, PEER_SIZE, _data, 0, _data.length);
             EncryptedBuildRecord rv = isShort ? new ShortEncryptedBuildRecord(out) : new EncryptedBuildRecord(out);
-            _chachaReplyKey = new SessionKey(state.getChainingKey());
             _chachaReplyAD = new byte[32];
             System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
+            byte[] ck = state.getChainingKey();
+            if (isShort) {
+                byte[] crk = new byte[32];
+                byte[] newck = new byte[32];
+                HKDF hkdf = new HKDF(ctx);
+                hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0);
+                _chachaReplyKey = new SessionKey(crk);
+                System.arraycopy(newck, 0, ck, 0, 32);
+                byte[] dlk = new byte[32];
+                hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0);
+                _derivedLayerKey = new SessionKey(dlk);
+                boolean isOBEP = readIsOutboundEndpoint();
+                if (isOBEP) {
+                    System.arraycopy(newck, 0, ck, 0, 32);
+                    byte[] divk = new byte[32];
+                    hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0);
+                    _derivedIVKey = new SessionKey(divk);
+                    System.arraycopy(newck, 0, ck, 0, 32);
+                    byte[] dgk = new byte[32];
+                    hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0);
+                    SessionKey sdgk = new SessionKey(dgk);
+                    RatchetSessionTag rst = new RatchetSessionTag(newck);
+                    _derivedGarlicKeys = new OneTimeSession(sdgk, rst);
+                } else {
+                    _derivedIVKey = new SessionKey(newck);
+                }
+            } else {
+                _chachaReplyKey = new SessionKey(ck);
+            }
             return rv;
         } catch (GeneralSecurityException gse) {
             throw new IllegalStateException("failed", gse);
@@ -491,9 +547,37 @@ public class BuildRequestRecord {
                 decrypted = new byte[isShort ? LENGTH_EC_SHORT : LENGTH_EC];
                 state.readMessage(encrypted, PEER_SIZE, len - PEER_SIZE,
                                   decrypted, 0);
-                _chachaReplyKey = new SessionKey(state.getChainingKey());
                 _chachaReplyAD = new byte[32];
                 System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
+                byte[] ck = state.getChainingKey();
+                if (isShort) {
+                    byte[] crk = new byte[32];
+                    byte[] newck = new byte[32];
+                    HKDF hkdf = new HKDF(ctx);
+                    hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0);
+                    _chachaReplyKey = new SessionKey(crk);
+                    System.arraycopy(newck, 0, ck, 0, 32);
+                    byte[] dlk = new byte[32];
+                    hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0);
+                    _derivedLayerKey = new SessionKey(dlk);
+                    boolean isOBEP = (decrypted[OFF_FLAG_EC_SHORT] & FLAG_OUTBOUND_ENDPOINT) != 0;
+                    if (isOBEP) {
+                        System.arraycopy(newck, 0, ck, 0, 32);
+                        byte[] divk = new byte[32];
+                        hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0);
+                        _derivedIVKey = new SessionKey(divk);
+                        System.arraycopy(newck, 0, ck, 0, 32);
+                        byte[] dgk = new byte[32];
+                        hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0);
+                        SessionKey sdgk = new SessionKey(dgk);
+                        RatchetSessionTag rst = new RatchetSessionTag(newck);
+                        _derivedGarlicKeys = new OneTimeSession(sdgk, rst);
+                    } else {
+                        _derivedIVKey = new SessionKey(newck);
+                    }
+                } else {
+                    _chachaReplyKey = new SessionKey(ck);
+                }
             } catch (GeneralSecurityException gse) {
                 if (state != null) {
                     Log log = ctx.logManager().getLog(BuildRequestRecord.class);
@@ -690,7 +774,7 @@ public class BuildRequestRecord {
         StringBuilder buf = new StringBuilder(256);
         buf.append(_isEC ? "ECIES" : "ElGamal");
         if (_data.length == LENGTH_EC_SHORT)
-            buf.append(" short ");
+            buf.append(" short");
         buf.append(" BRR ");
         boolean isIBGW = readIsInboundGateway();
         boolean isOBEP = readIsOutboundEndpoint();
@@ -704,10 +788,10 @@ public class BuildRequestRecord {
                .append(" out: ").append(readNextTunnelId());
         }
         buf.append(" to: ").append(readNextIdentity());
+        buf.append(" layer key: ").append(readLayerKey())
+           .append(" IV key: ").append(readIVKey());
         if (_data.length != LENGTH_EC_SHORT) {
-            buf.append(" layer key: ").append(readLayerKey())
-               .append(" IV key: ").append(readIVKey())
-               .append(" reply key: ").append(readReplyKey())
+            buf.append(" reply key: ").append(readReplyKey())
                .append(" reply IV: ").append(Base64.encode(readReplyIV()));
         }
         buf.append(" time: ").append(DataHelper.formatTime(readRequestTime()))
@@ -719,6 +803,10 @@ public class BuildRequestRecord {
                 buf.append(" chacha reply key: ").append(_chachaReplyKey)
                    .append(" chacha reply IV: ").append(Base64.encode(_chachaReplyAD));
             }
+            if (_derivedGarlicKeys != null) {
+                buf.append(" garlic reply key: ").append(_derivedGarlicKeys.key)
+                   .append(" garlic reply tag: ").append(_derivedGarlicKeys.rtag);
+            }
         }
         // to chase i2pd bug
         //buf.append('\n').append(net.i2p.util.HexDump.dump(readReplyKey().getData()));
diff --git a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
index 9d6a6a3224..18cf50a057 100644
--- a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
@@ -84,12 +84,13 @@ public class BuildResponseRecord {
      * @param status the response 0-255
      * @param replyAD 32 bytes
      * @param options 116 bytes max when serialized
+     * @param slot the slot number, 0-7
      * @return a 218-byte response record
      * @throws IllegalArgumentException if options too big or on encryption failure
      * @since 0.9.51
      */
     public static ShortEncryptedBuildRecord createShort(I2PAppContext ctx, int status, SessionKey replyKey,
-                                                        byte replyAD[], Properties options) {
+                                                        byte replyAD[], Properties options, int slot) {
         byte rv[] = new byte[ShortTunnelBuildMessage.SHORT_RECORD_SIZE];
         int off;
         try {
@@ -103,7 +104,7 @@ public class BuildResponseRecord {
         else if (sz < 0)
             throw new IllegalArgumentException("options");
         rv[ShortTunnelBuildMessage.SHORT_RECORD_SIZE - 17] = (byte) status;
-        boolean ok = encryptAEADBlock(replyAD, rv, replyKey);
+        boolean ok = encryptAEADBlock(replyAD, rv, replyKey, slot);
         if (!ok)
             throw new IllegalArgumentException("encrypt fail");
         return new ShortEncryptedBuildRecord(rv);
@@ -111,14 +112,16 @@ public class BuildResponseRecord {
 
     /**
      * Encrypts in place.
-     * Handles both standard (528) and short (218) byte records as of 0.9.51.
+     * Handles standard (528) byte records only.
      *
      * @param ad non-null
-     * @param data 528 or 218 bytes, data will be encrypted in place.
+     * @param data 528 bytes, data will be encrypted in place.
      * @return success
      * @since 0.9.48
      */
     private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key) {
+        if (data.length != EncryptedBuildRecord.LENGTH)
+            throw new IllegalArgumentException();
         ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
         chacha.initializeKey(key.getData(), 0);
         try {
@@ -131,19 +134,74 @@ public class BuildResponseRecord {
 
     /*
      * ChaCha/Poly only for ECIES routers.
-     * Handles both standard (528) and short (218) byte records as of 0.9.51.
+     * Handles standard (528) byte records only.
      * Decrypts in place.
-     * Status will be rec.getData()[511 or 219].
+     * Status will be rec.getData()[511].
      * Properties will be at rec.getData()[0].
      *
-     * @param rec 528 or 218 bytes, data will be decrypted in place.
+     * @param rec 528 bytes, data will be decrypted in place.
      * @param ad non-null
      * @return success
      * @since 0.9.48
      */
     public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad) {
+        if (rec.length() != EncryptedBuildRecord.LENGTH)
+            throw new IllegalArgumentException();
+        ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+        chacha.initializeKey(key.getData(), 0);
+        try {
+            // this is safe to do in-place, it checks the mac before starting decryption
+            byte[] data = rec.getData();
+            chacha.decryptWithAd(ad, data, 0, data, 0, rec.length());
+        } catch (GeneralSecurityException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Encrypts in place.
+     * Handles short (218) byte records only.
+     *
+     * @param ad non-null
+     * @param data 218 bytes, data will be encrypted in place.
+     * @param nonce the slot number, 0-7
+     * @return success
+     * @since 0.9.51
+     */
+    private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key, int nonce) {
+        if (data.length != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7)
+            throw new IllegalArgumentException();
+        ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
+        chacha.initializeKey(key.getData(), 0);
+        chacha.setNonce(nonce);
+        try {
+            chacha.encryptWithAd(ad, data, 0, data, 0, data.length - 16);
+        } catch (GeneralSecurityException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * ChaCha/Poly only for ECIES routers.
+     * Handles short (218) byte records only.
+     * Decrypts in place.
+     * Status will be rec.getData()[201].
+     * Properties will be at rec.getData()[0].
+     *
+     * @param rec 218 bytes, data will be decrypted in place.
+     * @param ad non-null
+     * @param nonce the slot number, 0-7
+     * @return success
+     * @since 0.9.51
+     */
+    public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad, int nonce) {
+        if (rec.length() != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7)
+            throw new IllegalArgumentException();
         ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
         chacha.initializeKey(key.getData(), 0);
+        chacha.setNonce(nonce);
         try {
             // this is safe to do in-place, it checks the mac before starting decryption
             byte[] data = rec.getData();
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
index 903392b24c..26277b58ce 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
@@ -215,6 +215,16 @@ public class MuxedSKM extends SessionKeyManager {
         _elg.tagsReceived(key, sessionTags, expire);
     }
 
+    /**
+     * EC only
+     * One time session
+     * @param expire time from now
+     * @since 0.9.51
+     */
+    public void tagsReceived(SessionKey key, RatchetSessionTag tag, long expire) {
+        _ec.tagsReceived(key, tag, expire);
+    }
+
     @Override
     public SessionKey consumeTag(SessionTag tag) {
         SessionKey rv = _elg.consumeTag(tag);
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
index a6716f04e4..47364a8563 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java
@@ -10,6 +10,7 @@ import net.i2p.data.SessionKey;
 import net.i2p.data.TunnelId;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
 
 /**
  * Coordinate the info that the tunnel creator keeps track of, including what 
@@ -45,6 +46,8 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
     private byte[][] _ChaReplyADs;
     private final SessionKey[] _AESReplyKeys;
     private final byte[][] _AESReplyIVs;
+    // short record OBEP only
+    private OneTimeSession _garlicReplyKeys;
     
     /**
      *  IV length for {@link #getAESReplyIV}
@@ -270,6 +273,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
     
     /**
      *  Key to encrypt the reply sent for the tunnel creation crypto.
+     *  Null for short build record.
      *
      *  @return key or null
      *  @throws IllegalArgumentException if iv not 16 bytes
@@ -279,6 +283,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
 
     /**
      *  IV used to encrypt the reply sent for the tunnel creation crypto.
+     *  Null for short build record.
      *
      *  @return 16 bytes or null
      *  @since 0.9.48 moved from HopConfig
@@ -340,6 +345,24 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
         return _ChaReplyADs[hop];
     }
 
+    /**
+     * ECIES short OBEP record only.
+     * @return null for ElGamal or ECIES long record or non-OBEP
+     * @since 0.9.51
+     */
+    public void setGarlicReplyKeys(OneTimeSession keys) {
+        _garlicReplyKeys = keys;
+    }
+
+    /**
+     * ECIES short OBEP record only.
+     * @return null for ElGamal or ECIES long record or non-OBEP
+     * @since 0.9.51
+     */
+    public OneTimeSession getGarlicReplyKeys() {
+        return _garlicReplyKeys;
+    }
+
     @Override
     public String toString() {
         // H0:1235-->H1:2345-->H2:2345
@@ -382,4 +405,22 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
             buf.append(" with ").append(_failures).append(" failures");
         return buf.toString();
     }
+
+    /**
+     * @since 0.9.51
+     */
+    public String toStringFull() {
+        StringBuilder buf = new StringBuilder(1024);
+        buf.append(toString());
+        for (int i = 0; i < _peers.length; i++) {
+             if (i == 0)
+                 buf.append("\nGW   ");
+             else if (i == _peers.length - 1)
+                 buf.append("\nEP   ");
+             else
+                 buf.append("\nHop ").append(i);
+             buf.append(": ").append(_config[i]);
+        }
+        return buf.toString();
+    }
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index 90ae30fa58..9cf128ae5e 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -34,6 +34,7 @@ import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.crypto.ratchet.RatchetSessionTag;
 import net.i2p.router.networkdb.kademlia.MessageWrapper;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
 import net.i2p.router.peermanager.TunnelHistory;
 import net.i2p.router.tunnel.HopConfig;
 import net.i2p.router.tunnel.TunnelDispatcher;
@@ -956,6 +957,14 @@ class BuildHandler implements Runnable {
                        + " after " + recvDelay + " with " + response 
                        + " from " + (from != null ? from : "tunnel") + ": " + req);
 
+        int records = state.msg.getRecordCount();
+        int ourSlot = -1;
+        for (int j = 0; j < records; j++) {
+            if (state.msg.getRecord(j) == null) {
+                ourSlot = j;
+                break;
+            }
+        }
         EncryptedBuildRecord reply;
         if (isEC) {
             // TODO options
@@ -966,29 +975,14 @@ class BuildHandler implements Runnable {
                         _log.warn("Unsupported STBM");
                     return;
                 }
-                if (isOutEnd) {
-                    // reply will be sent in plaintext in a OTBRM, see below
-                    reply = null;
-                } else {
-                    reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props);
-                }
+                reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props, ourSlot);
             } else {
                 reply = BuildResponseRecord.create(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props);
             }
         } else {
             reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
         }
-        int records = state.msg.getRecordCount();
-        int ourSlot = -1;
-        for (int j = 0; j < records; j++) {
-            if (state.msg.getRecord(j) == null) {
-                ourSlot = j;
-                if (!(isOutEnd && state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE))
-                    state.msg.setRecord(j, reply);
-                // else reply will be sent in plaintext
-                break;
-            }
-        }
+        state.msg.setRecord(ourSlot, reply);
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Read slot " + ourSlot + " containing: " + req
@@ -1026,8 +1020,9 @@ class BuildHandler implements Runnable {
             I2NPMessage outMessage;
             if (state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE) {
                 // garlic encrypt
-                SessionKey sk = null; // TODO
-                RatchetSessionTag st = null; // TODO
+                OneTimeSession ots = req.readGarlicKeys();
+                SessionKey sk = ots.key;
+                RatchetSessionTag st = ots.rtag;
                 outMessage = MessageWrapper.wrap(_context, replyMsg, sk, st);
                 if (outMessage == null) {
                     if (_log.shouldWarn())
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
index 1ab9117db9..b000cd73f4 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java
@@ -46,7 +46,6 @@ abstract class BuildMessageGenerator {
             boolean isEC = peerKey.getType() == EncType.ECIES_X25519;
             BuildRequestRecord req;
             if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint
-                /// TODO if isEC && isShort
                 req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel, isEC, isShort);
             else
                 req = createUnencryptedRecord(ctx, cfg, hop, null, -1, isEC, isShort);
@@ -55,8 +54,15 @@ abstract class BuildMessageGenerator {
             Hash peer = cfg.getPeer(hop);
             if (isEC) {
                 erec = req.encryptECIESRecord(ctx, peerKey, peer);
-                // TODO if isShort, set derived keys in coonfig
                 cfg.setChaChaReplyKeys(hop, req.getChaChaReplyKey(), req.getChaChaReplyAD());
+                if (isShort) {
+                    // save derived keys
+                    HopConfig hopConfig = cfg.getConfig(hop);
+                    hopConfig.setLayerKey(req.readLayerKey());
+                    hopConfig.setIVKey(req.readIVKey());
+                    if (!cfg.isInbound() && hop + 1 == cfg.getLength()) //outbound endpoint
+                        cfg.setGarlicReplyKeys(req.readGarlicKeys());
+                }
             } else {
                 erec = req.encryptRecord(ctx, peerKey, peer);
             }
@@ -114,11 +120,6 @@ abstract class BuildMessageGenerator {
             }
             SessionKey layerKey = hopConfig.getLayerKey();
             SessionKey ivKey = hopConfig.getIVKey();
-            SessionKey replyKey = cfg.getAESReplyKey(hop);
-            byte iv[] = cfg.getAESReplyIV(hop);
-            if (iv == null) {
-                throw new IllegalStateException();
-            }
             boolean isInGW = (cfg.isInbound() && (hop == 0));
             boolean isOutEnd = (!cfg.isInbound() && (hop + 1 >= cfg.getLength()));
             
@@ -137,11 +138,19 @@ abstract class BuildMessageGenerator {
                                                  nextMsgId,
                                                  isInGW, isOutEnd, EmptyProperties.INSTANCE);
                 } else {
+                    SessionKey replyKey = cfg.getAESReplyKey(hop);
+                    byte iv[] = cfg.getAESReplyIV(hop);
+                    if (iv == null)
+                        throw new IllegalStateException();
                     rec = new BuildRequestRecord(ctx, recvTunnelId, nextTunnelId, nextPeer,
                                                  nextMsgId, layerKey, ivKey, replyKey, 
                                                  iv, isInGW, isOutEnd, EmptyProperties.INSTANCE);
                 }
             } else {
+                SessionKey replyKey = cfg.getAESReplyKey(hop);
+                byte iv[] = cfg.getAESReplyIV(hop);
+                if (iv == null)
+                    throw new IllegalStateException();
                 rec = new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer,
                                              nextMsgId, layerKey, ivKey, replyKey, 
                                              iv, isInGW, isOutEnd);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
index c689ca5006..fba4cb490f 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
@@ -159,7 +159,11 @@ class BuildReplyHandler {
             if (log.shouldDebug())
                 log.debug(reply.getUniqueId() + ": Decrypting chacha/poly record " + recordNum + "/" + hop + " with replyKey " 
                           + replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
-            boolean ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV);
+            boolean ok;
+            if (isShort)
+                ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV, recordNum);
+            else
+                ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV);
             if (!ok) {
                 if (log.shouldWarn())
                     log.debug(reply.getUniqueId() + ": chacha reply decrypt fail on " + recordNum + "/" + hop);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index 360bb5e5fa..9297993843 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -5,22 +5,27 @@ import java.util.Collections;
 import java.util.List;
 
 import net.i2p.crypto.EncType;
+import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.PublicKey;
-import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.ShortTunnelBuildMessage;
 import net.i2p.data.i2np.TunnelBuildMessage;
 import net.i2p.data.i2np.VariableTunnelBuildMessage;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
+import net.i2p.router.LeaseSetKeys;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
 import net.i2p.router.TunnelManagerFacade;
 import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.crypto.ratchet.RatchetSKM;
+import net.i2p.router.crypto.ratchet.MuxedSKM;
 import net.i2p.router.networkdb.kademlia.MessageWrapper;
+import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
 import net.i2p.router.tunnel.HopConfig;
 import net.i2p.router.tunnel.TunnelCreatorConfig;
 import net.i2p.util.Log;
@@ -142,16 +147,51 @@ abstract class BuildRequestor {
         TunnelManagerFacade mgr = ctx.tunnelManager();
         boolean isInbound = settings.isInbound();
         if (settings.isExploratory() || !usePairedTunnels(ctx)) {
-            if (isInbound)
+            if (isInbound) {
                 pairedTunnel = mgr.selectOutboundExploratoryTunnel(farEnd);
-            else
+            } else {
                 pairedTunnel = mgr.selectInboundExploratoryTunnel(farEnd);
+                if (pairedTunnel != null) {
+                    OneTimeSession ots = cfg.getGarlicReplyKeys();
+                    if (ots != null) {
+                        SessionKeyManager skm = ctx.sessionKeyManager();
+                        RatchetSKM rskm = (RatchetSKM) skm;
+                        rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+                        cfg.setGarlicReplyKeys(null);
+                    }
+                }
+            }
         } else {
             // building a client tunnel
-            if (isInbound)
-                pairedTunnel = mgr.selectOutboundTunnel(settings.getDestination(), farEnd);
-            else
-                pairedTunnel = mgr.selectInboundTunnel(settings.getDestination(), farEnd);
+            Hash from = settings.getDestination();
+            if (isInbound) {
+                pairedTunnel = mgr.selectOutboundTunnel(from, farEnd);
+            } else {
+                pairedTunnel = mgr.selectInboundTunnel(from, farEnd);
+                if (pairedTunnel != null) {
+                    OneTimeSession ots = cfg.getGarlicReplyKeys();
+                    if (ots != null) {
+                        SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from);
+                        if (skm != null) {
+                            if (skm instanceof RatchetSKM) {
+                                RatchetSKM rskm = (RatchetSKM) skm;
+                                rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+                                cfg.setGarlicReplyKeys(null);
+                            } else if (skm instanceof MuxedSKM) {
+                                MuxedSKM mskm = (MuxedSKM) skm;
+                                mskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+                                cfg.setGarlicReplyKeys(null);
+                            } else {
+                                // ElG-only won't work, fall back to expl.
+                                pairedTunnel = null;
+                            }
+                        } else {
+                            // no client SKM, fall back to expl.
+                            pairedTunnel = null;
+                        }
+                    }
+                }
+            }
             if (pairedTunnel == null) {   
                 if (isInbound) {
                     // random more reliable than closest ??
@@ -177,6 +217,15 @@ abstract class BuildRequestor {
                         // ditto
                         pairedTunnel = null;
                     }
+                    if (pairedTunnel != null) {
+                        OneTimeSession ots = cfg.getGarlicReplyKeys();
+                        if (ots != null) {
+                            SessionKeyManager skm = ctx.sessionKeyManager();
+                            RatchetSKM rskm = (RatchetSKM) skm;
+                            rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+                            cfg.setGarlicReplyKeys(null);
+                        }
+                    }
                 }
                 if (pairedTunnel != null && log.shouldLog(Log.INFO))
                     log.info("Couldn't find a paired tunnel for " + cfg + ", using exploratory tunnel");
@@ -296,6 +345,16 @@ abstract class BuildRequestor {
         Hash replyRouter;
         boolean useVariable = SEND_VARIABLE && cfg.getLength() <= MEDIUM_RECORDS;
         boolean useShortTBM = SEND_SHORT && ctx.keyManager().getPublicKey().getType() == EncType.ECIES_X25519;
+        if (useShortTBM && !pool.getSettings().isExploratory()) {
+            // pool must be EC also
+            LeaseSetKeys lsk = ctx.keyManager().getKeys(pool.getSettings().getDestination());
+            if (lsk != null) {
+                if (!lsk.isSupported(EncType.ECIES_X25519))
+                    useShortTBM = false;
+            } else {
+                useShortTBM = false;
+            }
+        }
 
         if (cfg.isInbound()) {
             //replyTunnel = 0; // as above
diff --git a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
index f375543028..2e92bb6eee 100644
--- a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
+++ b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
@@ -64,6 +64,9 @@ public class BuildMessageTestStandalone extends TestCase {
      */
     private void x_testBuildMessage(RouterContext ctx, int testType) {
         Log log = ctx.logManager().getLog(getClass());
+        log.debug("\n================================================================" +
+                  "\nTest " + testType +
+                  "\n================================================================");
         // set our keys to avoid NPE
         KeyPair kpr = ctx.keyGenerator().generatePKIKeys((testType == 1 || testType == 4) ? EncType.ELGAMAL_2048 : EncType.ECIES_X25519);
         PublicKey k1 = kpr.getPublic();
@@ -102,7 +105,7 @@ public class BuildMessageTestStandalone extends TestCase {
         
         log.debug("\n================================================================" +
                   "\nMessage fully encrypted" + 
-                  "\n" + cfg +
+                  "\n" + cfg.toStringFull() +
                   "\n================================================================");
         
         if (testType == 3 || testType == 6) {
@@ -137,6 +140,12 @@ public class BuildMessageTestStandalone extends TestCase {
             long time = req.readRequestTime();
             long now = (ctx.clock().now() / (60l*60l*1000l)) * (60*60*1000);
             int ourSlot = -1;
+            for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
+                if (msg.getRecord(j) == null) {
+                    ourSlot = j;
+                    break;
+                }
+            }
 
             EncryptedBuildRecord reply;
             if (testType == 1 || testType == 4) {
@@ -144,25 +153,9 @@ public class BuildMessageTestStandalone extends TestCase {
             } else if (testType == 2 || testType == 5) {
                 reply = BuildResponseRecord.create(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
             } else {
-                reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
-            }
-            if (testType != 3 || i != cfg.getLength() - 1) {
-                for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
-                    if (msg.getRecord(j) == null) {
-                        ourSlot = j;
-                        msg.setRecord(j, reply);
-                        break;
-                    }
-                }
-            } else {
-                for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
-                    if (msg.getRecord(j) == null) {
-                        ourSlot = j;
-                        msg.setRecord(j, reply);
-                        break;
-                    }
-                }
+                reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE, ourSlot);
             }
+            msg.setRecord(ourSlot, reply);
 
             if (testType == 1 || testType == 4) {
                 log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() 
@@ -233,7 +226,7 @@ public class BuildMessageTestStandalone extends TestCase {
         }
         
         log.debug("\n================================================================" +
-                  "\nAll peers agree? " + allAgree + 
+                  "\nTest " + testType + " complete, all peers agree? " + allAgree + 
                   "\n================================================================");
         assertTrue("All peers agree", allAgree);
     }
@@ -289,12 +282,18 @@ public class BuildMessageTestStandalone extends TestCase {
             HopConfig hop = cfg.getConfig(i);
             hop.setCreation(now);
             hop.setExpiration(now+10*60*1000);
-            hop.setIVKey(ctx.keyGenerator().generateSessionKey());
-            hop.setLayerKey(ctx.keyGenerator().generateSessionKey());
-            byte iv[] = new byte[BuildRequestRecord.IV_SIZE];
-            Arrays.fill(iv, (byte)i); // consistent for repeatability
-            cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv);
+            if (testType != 3 && testType != 6) {
+                hop.setIVKey(ctx.keyGenerator().generateSessionKey());
+                hop.setLayerKey(ctx.keyGenerator().generateSessionKey());
+                byte iv[] = new byte[BuildRequestRecord.IV_SIZE];
+                Arrays.fill(iv, (byte)i); // consistent for repeatability
+                cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv);
+            }
             hop.setReceiveTunnelId(new TunnelId(i+1));
+            if (i != _peers.length - 1) {
+                hop.setSendTo(_peers[i + 1]);
+                hop.setSendTunnelId(new TunnelId(i+2));
+            }
         }
         return cfg;
     }
-- 
GitLab