From c77e41c59eb2c84a50cfcef1b38f180965e7da39 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 6 Apr 2020 10:42:44 +0000
Subject: [PATCH] Ratchet: Next Key completion Fix NextSessionKey constructor
 with null data Fix handling of next keys Fix sending OB next keys Fix
 RatchetSessionTag.equals() for use in SKM Delete OB NSR tagset when adding OB
 ES tagset Log tweaks

---
 .../router/crypto/ratchet/NextSessionKey.java |  33 ++-
 .../router/crypto/ratchet/RatchetPayload.java |   8 +-
 .../i2p/router/crypto/ratchet/RatchetSKM.java | 268 ++++++++++++------
 .../router/crypto/ratchet/RatchetTagSet.java  |  49 ++--
 4 files changed, 240 insertions(+), 118 deletions(-)

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 dd1ed582bb..fa23d207d4 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
@@ -13,7 +13,7 @@ class NextSessionKey extends PublicKey {
     private final boolean _isReverse, _isRequest;
 
     /**
-     *  @param data may be null
+     *  @param data non-null
      */
     public NextSessionKey(byte[] data, int id, boolean isReverse, boolean isRequest) {
         super(EncType.ECIES_X25519, data);
@@ -22,6 +22,19 @@ class NextSessionKey extends PublicKey {
         _isRequest = isRequest;
     }
 
+    /**
+     *  Null data, for acks/requests only.
+     *  Type will be ElG but doesn't matter.
+     *  Don't call setData().
+     *  @since 0.9.46
+     */
+    public NextSessionKey(int id, boolean isReverse, boolean isRequest) {
+        super();
+        _id = id;
+        _isReverse = isReverse;
+        _isRequest = isRequest;
+    }
+
     public int getID() {
         return _id;
     }
@@ -41,16 +54,28 @@ class NextSessionKey extends PublicKey {
      */
     @Override
     public int hashCode() {
-        return super.hashCode();
+        int rv = super.hashCode() ^ _id;
+        if (_isReverse)
+            rv ^= 1 << 31;
+        if (_isRequest)
+            rv ^= 1 << 30;
+        return rv;
     }
 
     /**
-     *  Equals if keys are equal
      *  @since 0.9.46
      */
     @Override
     public boolean equals(Object obj) {
-        return super.equals(obj);
+        if (obj == null)
+            return false;
+        if (!(obj instanceof NextSessionKey))
+            return false;
+        NextSessionKey o = (NextSessionKey) obj;
+        return _id == o._id &&
+               _isReverse == o._isReverse &&
+               _isRequest == o._isRequest &&
+               super.equals(o);
     }
 
     @Override
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 f0e4c50592..d290238fdc 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
@@ -141,14 +141,14 @@ class RatchetPayload {
                     boolean isReverse = (payload[i] & 0x02) != 0;
                     boolean isRequest = (payload[i] & 0x04) != 0;
                     int id = (int) DataHelper.fromLong(payload, i + 1, 2);
-                    byte[] data;
+                    NextSessionKey nsk;
                     if (hasKey) {
-                        data = new byte[32];
+                        byte[] data = new byte[32];
                         System.arraycopy(payload, i + 3, data, 0, 32);
+                        nsk = new NextSessionKey(data, id, isReverse, isRequest);
                     } else {
-                        data = null;
+                        nsk = new NextSessionKey(id, isReverse, isRequest);
                     }
-                    NextSessionKey nsk = new NextSessionKey(data, id, isReverse, isRequest);
                     cb.gotNextKey(nsk);
                   }
                     break;
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 a4c9865f64..0a53e9ef40 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
@@ -874,21 +874,24 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
 
         // next key
         private int _myOBKeyID = -1;
-        private int _hisOBKeyID = -1;
         private int _currentOBTagSetID;
         private int _myIBKeyID = -1;
-        private int _hisIBKeyID = -1;
         private int _currentIBTagSetID;
         private int _myIBKeySendCount;
         private KeyPair _myIBKeys;
+        private KeyPair _myOBKeys;
         private NextSessionKey _myIBKey;
+        // last received, may not have data, for dup check
+        private NextSessionKey _hisIBKey;
+        private NextSessionKey _hisOBKey;
+        // last received, with data
+        private NextSessionKey _hisIBKeyWithData;
+        private NextSessionKey _hisOBKeyWithData;
         private SessionKey _nextIBRootKey;
-        private static final String INFO_7 = "XDHRatchetTagSet";
 
+        private static final String INFO_7 = "XDHRatchetTagSet";
         private static final int MAX_FAILS = 2;
         private static final int MAX_SEND_ACKS = 8;
-        private static final int DEBUG_OB_NSR = 0x10001;
-        private static final int DEBUG_IB_NSR = 0x10002;
         private static final int MAX_SEND_REVERSE_KEY = 25;
 
         /**
@@ -917,7 +920,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 // This is an INBOUND NS, we make an OUTBOUND tagset for the NSR
                 RatchetTagSet tagset = new RatchetTagSet(_hkdf, state,
                                                          rk, tk,
-                                                         _established, DEBUG_OB_NSR);
+                                                         _established);
                 _tagSets.add(tagset);
                 _state = null;
                 if (_log.shouldDebug())
@@ -927,7 +930,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 // This is an OUTBOUND NS, we make an INBOUND tagset for the NSR
                 RatchetTagSet tagset = new RatchetTagSet(_hkdf, RatchetSKM.this, state,
                                                          rk, tk,
-                                                         _established, DEBUG_IB_NSR,
+                                                         _established,
                                                          MIN_RCV_WINDOW_NSR, MAX_RCV_WINDOW_NSR);
                 // store the state so we can find the right session when we receive the NSR
                 _state = state;
@@ -956,11 +959,11 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 // We are Bob
                 // This is an OUTBOUND NSR, we make an INBOUND tagset for ES
                 RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, new SessionKey(k_ab),
-                                                            now, 0,
+                                                            now, 0, -1,
                                                             MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES);
                 // and a pending outbound one
                 RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ba),
-                                                            now, 0);
+                                                            now, 0, -1);
                 if (_log.shouldDebug()) {
                     _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset:\n" + tagset_ab);
                     _log.debug("Pending OB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset:\n" + tagset_ba);
@@ -973,16 +976,24 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 // We are Alice
                 // This is an INBOUND NSR, we make an OUTBOUND tagset for ES
                 RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ab),
-                                                            now, 0);
+                                                            now, 0, -1);
                 // and an inbound one
                 RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, rk, new SessionKey(k_ba),
-                                                            now, 0,
+                                                            now, 0, -1,
                                                             MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES);
                 if (_log.shouldDebug()) {
                     _log.debug("Update OB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset:\n" + tagset_ab);
                     _log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset:\n" + tagset_ba);
                 }
                 synchronized (_tagSets) {
+                    for (Iterator<RatchetTagSet> iter = _tagSets.iterator(); iter.hasNext(); ) {
+                        RatchetTagSet set = iter.next();
+                        if (set.getID() == RatchetTagSet.DEBUG_OB_NSR) {
+                            iter.remove();
+                            if (_log.shouldDebug())
+                                _log.debug("Removed OB NSR tagset:\n" + set);
+                        }
+                    }
                     _tagSets.add(tagset_ab);
                     _unackedTagSets.clear();
                 }
@@ -1008,114 +1019,187 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 if (isReverse) {
                     // this is about my outbound tag set,
                     // and is an ack of new key sent
-                    if (_hisIBKeyID != id) {
-                        if (_hisIBKeyID != id - 1) {
+                    if (isRequest) {
+                        if (_log.shouldWarn())
+                            _log.warn("invalid req+rev in nextkey " + key);
+                        return;
+                    }
+                    if (key.equals(_hisIBKey)) {
+                        if (_log.shouldDebug())
+                            _log.debug("Got dup nextkey for OB " + key);
+                        return;
+                    }
+                    int hisLastIBKeyID;
+                    if (_hisIBKey == null)
+                        hisLastIBKeyID = -1;
+                    else
+                        hisLastIBKeyID = _hisIBKey.getID();
+                    _hisIBKey = key;
+                    if (hisLastIBKeyID != id) {
+                        // got a new key, use it
+                        if (hisLastIBKeyID != id - 1) {
                             if (_log.shouldWarn())
-                                _log.warn("Got nextkey id OB: " + id + " expected " + (_hisIBKeyID + 1));
+                                _log.warn("Got nextkey for OB: " + key + " expected " + (hisLastIBKeyID + 1));
                             return;
                         }
                         if (!hasKey) {
-                            // TODO get it from above
                             if (_log.shouldWarn())
-                                _log.warn("Got nextkey OB w/o key but we don't have it " + id);
+                                _log.warn("Got nextkey for OB w/o key but we don't have it " + key);
                             return;
                         }
-                        int oldtsID = 1 + _myIBKeyID + id;
-                        RatchetTagSet oldts = null;
-                        for (RatchetTagSet ts : _tagSets) {
-                            if (ts.getID() == oldtsID) {
-                                oldts = ts;
-                                break;
+                        // save the new key with data
+                        _hisIBKeyWithData = key;
+                    } else {
+                        if (hasKey) {
+                            // got a old key id but new data?
+                            if (_hisIBKeyWithData != null && _log.shouldWarn())
+                                _log.warn("Got nextkey for OB with data, didn't match previous " + key);
+                        } else {
+                            if (_hisIBKeyWithData == null ||
+                                _hisIBKeyWithData.getID() != key.getID()) {
+                                if (_log.shouldWarn())
+                                    _log.warn("Got nextkey for OB w/o key but we don't have it " + key);
+                                return;
                             }
+                            // got a old key, use it
+                            key = _hisIBKeyWithData;
                         }
-                        if (oldts == null) {
-                            if (_log.shouldWarn())
-                                _log.warn("Got nextkey id OB " + id + " but can't find existing OB tagset " + oldtsID);
-                            return;
+                    }
+
+                    int oldtsID;
+                    if (_myOBKeyID == -1 && hisLastIBKeyID == -1)
+                        oldtsID = 0;
+                    else
+                        oldtsID = 1 + _myOBKeyID + hisLastIBKeyID;
+                    RatchetTagSet oldts = null;
+                    for (RatchetTagSet ts : _tagSets) {
+                        if (ts.getID() == oldtsID) {
+                            oldts = ts;
+                            break;
                         }
-                        KeyPair nextKeys = oldts.getNextKeys();
-                        if (nextKeys == null) {
+                    }
+                    if (oldts == null) {
+                        if (_log.shouldWarn())
+                            _log.warn("Got nextkey for OB " + key + " but can't find existing OB tagset " + oldtsID);
+                        return;
+                    }
+                    KeyPair nextKeys = oldts.getNextKeys();
+                    if (nextKeys == null) {
+                        if (oldtsID == 0 || (oldtsID & 0x01) != 0 || _myOBKeys == null) {
                             if (_log.shouldWarn())
-                                _log.warn("Got nextkey id OB " + id + " but didn't send OB keys " + oldtsID);
+                                _log.warn("Got nextkey for OB " + key + " but we didn't send OB keys " + oldtsID);
                             return;
                         }
-                        // create new OB TS, delete old one
-                        int newtsID = oldtsID + 1;
-                        if (_log.shouldWarn())
-                            _log.warn("Got nextkey id, ratchet OB: " + id);
-                        PublicKey pub = nextKeys.getPublic();
-                        PrivateKey priv = nextKeys.getPrivate();
-                        PrivateKey sharedSecret = ECIESAEADEngine.doDH(priv, key);
-                        byte[] sk = new byte[32];
-                        _hkdf.calculate(sharedSecret.getData(), ZEROLEN, INFO_7, sk);
-                        SessionKey ssk = new SessionKey(sk);
-                        RatchetTagSet ts = new RatchetTagSet(_hkdf, oldts.getNextRootKey(), ssk,
-                                                             _context.clock().now(), newtsID);
-                        _tagSets.add(ts);
-                        _tagSets.remove(oldts);
-                        _myOBKeyID++;
-                        _hisIBKeyID = id;
-                        _currentOBTagSetID = newtsID;
-                        if (_log.shouldWarn())
-                            _log.warn("Got nextkey id " + id + " ratchet to new OB ES TS:\n" + ts);
+                        // reuse last keys for tsIDs 2,4,6,...
+                        nextKeys = _myOBKeys;
                     } else {
-                        if (_log.shouldWarn())
-                            _log.warn("Got dup nextkey id for OB " + id);
-                    }
-                    if (isRequest) {
-                        if (_log.shouldWarn())
-                            _log.warn("invalid req+rev in nextkey");
-                        // ignore
+                        // new keys for tsIDs 0,1,3,5...
+                        _myOBKeys = nextKeys;
+                        _myOBKeyID++;
                     }
+                    // create new OB TS, delete old one
+                    PublicKey pub = nextKeys.getPublic();
+                    PrivateKey priv = nextKeys.getPrivate();
+                    PrivateKey sharedSecret = ECIESAEADEngine.doDH(priv, key);
+                    byte[] sk = new byte[32];
+                    _hkdf.calculate(sharedSecret.getData(), ZEROLEN, INFO_7, sk);
+                    SessionKey ssk = new SessionKey(sk);
+                    int newtsID = oldtsID + 1;
+                    RatchetTagSet ts = new RatchetTagSet(_hkdf, oldts.getNextRootKey(), ssk,
+                                                         _context.clock().now(), newtsID, _myOBKeyID);
+                    _tagSets.add(ts);
+                    _tagSets.remove(oldts);
+                    _currentOBTagSetID = newtsID;
+                    if (_log.shouldWarn())
+                        _log.warn("Got nextkey " + key + " ratchet to new OB ES TS:\n" + ts);
                 } else {
                     // this is about my inbound tag set
-                    if (_hisOBKeyID != id) {
-                        if (_hisOBKeyID != id - 1) {
+                    if (key.equals(_hisOBKey)) {
+                        if (_log.shouldDebug())
+                            _log.debug("Got dup nextkey for IB " + key);
+                        return;
+                    }
+                    int hisLastOBKeyID;
+                    if (_hisOBKey == null)
+                        hisLastOBKeyID = -1;
+                    else
+                        hisLastOBKeyID = _hisOBKey.getID();
+                    _hisOBKey = key;
+                    if (hisLastOBKeyID != id) {
+                        // got a new key, use it
+                        if (hisLastOBKeyID != id - 1) {
                             if (_log.shouldWarn())
-                                _log.warn("Got nextkey id IB: " + id + " expected " + (_hisOBKeyID + 1));
+                                _log.warn("Got nextkey for IB: " + key + " expected " + (hisLastOBKeyID + 1));
                             return;
                         }
                         if (!hasKey) {
                             if (_log.shouldWarn())
-                                _log.warn("Got nextkey IB w/o key but we don't have it " + id);
+                                _log.warn("Got nextkey for IB w/o key but we don't have it " + key);
                             return;
                         }
-                        if (_nextIBRootKey == null) {
-                            if (_log.shouldWarn())
-                                _log.warn("Got nextkey IB but we don't have next root key " + id);
-                            return;
+                        // save the new key with data
+                        _hisOBKeyWithData = key;
+                    } else {
+                        if (hasKey) {
+                            // got a old key id but new data?
+                            if (_hisOBKeyWithData != null && _log.shouldWarn())
+                                _log.warn("Got nextkey for IB with data, didn't match previous " + key);
+                        } else {
+                            if (_hisOBKeyWithData == null ||
+                                _hisOBKeyWithData.getID() != key.getID()) {
+                                if (_log.shouldWarn())
+                                    _log.warn("Got nextkey for IB w/o key but we don't have it " + key);
+                                return;
+                            }
+                            // got a old key, use it
+                            key = _hisOBKeyWithData;
                         }
-                        // TODO new key only needed every other time
+                    }
+                    if (_nextIBRootKey == null) {
+                        // first IB ES tagset never used?
+                        if (_log.shouldWarn())
+                            _log.warn("Got nextkey for IB but we don't have next root key " + key);
+                        return;
+                    }
+                    int oldtsID;
+                    if (_myIBKeyID == -1 && hisLastOBKeyID == -1)
+                        oldtsID = 0;
+                    else
+                        oldtsID = 1 + _myIBKeyID + hisLastOBKeyID;
+                    // generate or reuse reverse key
+                    // store next key for sending via getReverseSendKey()
+                    if ((oldtsID & 0x01) == 0) {
+                        // new keys for 0,2,4,...
+                        if (!isRequest && _log.shouldWarn())
+                            _log.warn("Got reverse w/o request, generating new key anyway " + key);
                         _myIBKeys = _context.keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
-                        PrivateKey sharedSecret = ECIESAEADEngine.doDH(_myIBKeys.getPrivate(), key);
-                        // store next key for sending via getReverseSendKey()
                         _myIBKeyID++;
-                        _hisOBKeyID = id;
-                        int newtsID = 1 + _myIBKeyID + id;
-                        _currentOBTagSetID = newtsID;
-                        _myIBKeySendCount = 0;
                         _myIBKey = new NextSessionKey(_myIBKeys.getPublic().getData(), _myIBKeyID, true, false);
-                        // create new IB TS
-                        byte[] sk = new byte[32];
-                        _hkdf.calculate(sharedSecret.getData(), ZEROLEN, INFO_7, sk);
-                        SessionKey ssk = new SessionKey(sk);
-                        RatchetTagSet ts = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, _nextIBRootKey, ssk,
-                                                             _context.clock().now(), newtsID,
-                                                             MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES);
-                        _nextIBRootKey = ts.getNextRootKey();
-                        if (_log.shouldWarn())
-                            _log.warn("Got nextkey id " + id + " ratchet to new IB ES TS:\n" + ts);
                     } else {
-                        if (_log.shouldWarn())
-                            _log.warn("Got dup nextkey id for IB " + id);
-                        // find current OB TS, tell him to send ack if nec.
-                        // create new IB TS if nec.
-                    }
-                    if (!isRequest) {
-                        if (_log.shouldWarn())
-                            _log.warn("invalid fwd w/o req in nextkey");
-                        // ignore
+                        // reuse keys for 1,3,5...
+                        if (_myIBKeys == null) {
+                            if (_log.shouldWarn())
+                                _log.warn("Got nextkey IB but we don't have old keys " + key);
+                            return;
+                        }
+                        if (isRequest && _log.shouldWarn())
+                            _log.warn("Got reverse with request, using old key anyway " + key);
+                        _myIBKey = new NextSessionKey(_myIBKeyID, true, false);
                     }
+                    PrivateKey sharedSecret = ECIESAEADEngine.doDH(_myIBKeys.getPrivate(), key);
+                    int newtsID = oldtsID + 1;
+                    _currentIBTagSetID = newtsID;
+                    _myIBKeySendCount = 0;
+                    // create new IB TS
+                    byte[] sk = new byte[32];
+                    _hkdf.calculate(sharedSecret.getData(), ZEROLEN, INFO_7, sk);
+                    SessionKey ssk = new SessionKey(sk);
+                    RatchetTagSet ts = new RatchetTagSet(_hkdf, RatchetSKM.this, _target, _nextIBRootKey, ssk,
+                                                         _context.clock().now(), newtsID, _myIBKeyID,
+                                                         MIN_RCV_WINDOW_ES, MAX_RCV_WINDOW_ES);
+                    _nextIBRootKey = ts.getNextRootKey();
+                    if (_log.shouldWarn())
+                        _log.warn("Got nextkey " + key + " ratchet to new IB ES TS:\n" + ts);
                 }
             }
         }
@@ -1148,8 +1232,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 for (RatchetTagSet obSet : _unackedTagSets) {
                     if (obSet.getAssociatedKey().equals(sk)) {
                         if (_log.shouldDebug())
-                            _log.debug("First tag received from IB ES " + set +
-                                       ", promoting OB ES " + obSet);
+                            _log.debug("First tag received from IB ES\n" + set +
+                                       "\npromoting OB ES " + obSet);
                         _unackedTagSets.clear();
                         _tagSets.clear();
                         _tagSets.add(obSet);
@@ -1161,7 +1245,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                     }
                 }
                 if (_log.shouldDebug())
-                    _log.debug("First tag received from IB ES " + set +
+                    _log.debug("First tag received from IB ES\n" + set +
                                " but no corresponding OB ES set found, unacked size: " + _unackedTagSets.size() +
                                " acked size: " + _tagSets.size());
             }
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 d74f1a757c..ec169dc56d 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
@@ -54,6 +54,7 @@ class RatchetTagSet implements TagSetHandle {
     private final long _timeout;
     private long _date;
     private final int _id;
+    private final int _keyid;
     private final int _originalSize;
     private final int _maxSize;
     private boolean _acked;
@@ -79,7 +80,12 @@ 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;
+    private static final boolean TEST_RATCHET = false;
+    // 2 * max streaming window
+    private static final int LOW = TEST_RATCHET ? (MAX - 100) : 256;
+    static final int DEBUG_OB_NSR = 0x10001;
+    static final int DEBUG_IB_NSR = 0x10002;
+    static final int DEBUG_SINGLE_ES = 0x10003;
 
     /**
      *  Outbound NSR Tagset
@@ -87,8 +93,8 @@ class RatchetTagSet implements TagSetHandle {
      *  @param date For outbound: creation time
      */
     public RatchetTagSet(HKDF hkdf, HandshakeState state, SessionKey rootKey, SessionKey data,
-                         long date, int id) {
-        this(hkdf, null, state, null, rootKey, data, date, RatchetSKM.SESSION_PENDING_DURATION_MS, id, false, 0, 0);
+                         long date) {
+        this(hkdf, null, state, null, rootKey, data, date, RatchetSKM.SESSION_PENDING_DURATION_MS, DEBUG_OB_NSR, -2, false, 0, 0);
     }
 
     /**
@@ -97,8 +103,8 @@ class RatchetTagSet implements TagSetHandle {
      *  @param date For outbound: creation time
      */
     public RatchetTagSet(HKDF hkdf, SessionKey rootKey, SessionKey data,
-                         long date, int id) {
-        this(hkdf, null, null, null, rootKey, data, date, RatchetSKM.SESSION_TAG_DURATION_MS, id, false, 0, 0);
+                         long date, int tagsetid, int keyid) {
+        this(hkdf, null, null, null, rootKey, data, date, RatchetSKM.SESSION_TAG_DURATION_MS, tagsetid, keyid, false, 0, 0);
     }
 
     /**
@@ -107,8 +113,8 @@ class RatchetTagSet implements TagSetHandle {
      *  @param date For inbound: creation time
      */
     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, RatchetSKM.SESSION_PENDING_DURATION_MS, id, true, minSize, maxSize);
+                         long date, int minSize, int maxSize) {
+        this(hkdf, lsnr, state, null, rootKey, data, date, RatchetSKM.SESSION_PENDING_DURATION_MS, DEBUG_IB_NSR, -2, true, minSize, maxSize);
     }
 
     /**
@@ -118,8 +124,8 @@ 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, RatchetSKM.SESSION_LIFETIME_MAX_MS, id, true, minSize, maxSize);
+                         long date, int tagsetid, int keyid, int minSize, int maxSize) {
+        this(hkdf, lsnr, null, remoteKey, rootKey, data, date, RatchetSKM.SESSION_LIFETIME_MAX_MS, tagsetid, keyid, true, minSize, maxSize);
     }
 
 
@@ -128,7 +134,7 @@ class RatchetTagSet implements TagSetHandle {
      */
     private RatchetTagSet(HKDF hkdf, SessionTagListener lsnr, HandshakeState state,
                           PublicKey remoteKey, SessionKey rootKey, SessionKey data,
-                          long date, long timeout, int id, boolean isInbound, int minSize, int maxSize) {
+                          long date, long timeout, int tagsetid, int keyid, boolean isInbound, int minSize, int maxSize) {
         _lsnr = lsnr;
         _state = state;
         _remoteKey = remoteKey;
@@ -137,7 +143,8 @@ class RatchetTagSet implements TagSetHandle {
         _created = date;
         _timeout = timeout;
         _date = date;
-        _id = id;
+        _id = tagsetid;
+        _keyid = keyid;
         _originalSize = minSize;
         _maxSize = maxSize;
         _nextRootKey = new byte[32];
@@ -178,7 +185,8 @@ class RatchetTagSet implements TagSetHandle {
         _created = date;
         _timeout = timeout;
         _date = date;
-        _id = 0x10003;
+        _id = DEBUG_SINGLE_ES;
+        _keyid = -3;
         _originalSize = 1;
         _maxSize = 1;
         _nextRootKey = null;
@@ -315,11 +323,16 @@ class RatchetTagSet implements TagSetHandle {
     public NextSessionKey getNextKey() {
         if (_sessionTags != null || _state != null || remaining() > LOW)
             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);
+        if (_nextKey == null) {
+            boolean isFirst = _id == 0;
+            if (isFirst || (_id & 0x01) != 0) {
+                // new keys only needed first time and odd times
+                _nextKeys = I2PAppContext.getGlobalContext().keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
+                _nextKey = new NextSessionKey(_nextKeys.getPublic().getData(), _keyid + 1, false, isFirst);
+            } else {
+                // even times, just send old ID
+                _nextKey = new NextSessionKey(_keyid, false, true);
+            }
         }
         return _nextKey;
     }
@@ -553,7 +566,7 @@ class RatchetTagSet implements TagSetHandle {
         else
             buf.append("Outbound ");
         if (_state != null)
-            buf.append("NSR ").append(_state.hashCode()).append(' ');
+            buf.append("NSR ");
         else
             buf.append("ES ");
         buf.append("TagSet #").append(_tagSetID)
-- 
GitLab