From e2cc62a21f358cda202816815b8d2bd5c7a62695 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Tue, 14 Apr 2020 12:13:00 +0000
Subject: [PATCH] Ratchet: Improve muxed decrypt Try tags for both ratchet and
 AES before DH for either Return empty CloveSet for ratchet errors after
 successful decrypt Don't corrupt data in ECIESEngine on NS/NSR failure, for
 subsequent ElG attempt Log tweaks

---
 .../i2p/router/crypto/ElGamalAESEngine.java   | 117 +++++++++--
 .../crypto/ratchet/ECIESAEADEngine.java       | 181 ++++++++++++++----
 .../router/crypto/ratchet/MuxedEngine.java    |  53 +++--
 .../crypto/ratchet/SessionKeyAndNonce.java    |   1 +
 4 files changed, 283 insertions(+), 69 deletions(-)

diff --git a/router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java b/router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java
index f9487fab37..d779585f5d 100644
--- a/router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java
@@ -114,7 +114,7 @@ public final class ElGamalAESEngine {
         if (key != null) {
             //if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st);
             if (shouldDebug)
-                _log.debug("Decrypting existing session encrypted with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes " /* + Base64.encode(data, 0, 64) */ );
+                _log.debug("Decrypting existing session with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes ");
             
             decrypted = decryptExistingSession(data, key, targetPrivateKey, foundTags, usedKey, foundKey);
             if (decrypted != null) {
@@ -128,8 +128,7 @@ public final class ElGamalAESEngine {
                     _log.warn("ElG decrypt fail: known tag [" + st + "], failed decrypt");
                 }
             }
-        } else {
-            if (shouldDebug) _log.debug("Key is NOT known for tag " + st);
+        } else if (data.length >= ELG_ENCRYPTED_LENGTH) {
             decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey);
             if (decrypted != null) {
                 _context.statManager().updateFrequency("crypto.elGamalAES.decryptNewSession");
@@ -140,6 +139,8 @@ public final class ElGamalAESEngine {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("ElG decrypt fail: unknown tag: " + st);
             }
+        } else {
+            return null;
         }
 
         //if ((key == null) && (decrypted == null)) {
@@ -160,6 +161,95 @@ public final class ElGamalAESEngine {
         return decrypted;
     }
 
+    /**
+     * Tags only. For MuxedEngine use only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    public byte[] decryptFast(byte data[], PrivateKey targetPrivateKey,
+                              SessionKeyManager keyManager) throws DataFormatException {
+        if (data == null)
+            return null;
+        if (data.length < MIN_ENCRYPTED_SIZE)
+            return null;
+        byte tag[] = new byte[32];
+        System.arraycopy(data, 0, tag, 0, 32);
+        SessionTag st = new SessionTag(tag);
+        SessionKey key = keyManager.consumeTag(st);
+        if (key == null)
+            return null;
+        SessionKey foundKey = new SessionKey();
+        SessionKey usedKey = new SessionKey();
+        Set<SessionTag> foundTags = new HashSet<SessionTag>();
+        final boolean shouldDebug = _log.shouldDebug();
+        if (shouldDebug)
+            _log.debug("Decrypting existing session with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes");
+        byte[] decrypted = decryptExistingSession(data, key, targetPrivateKey, foundTags, usedKey, foundKey);
+        if (decrypted != null) {
+            _context.statManager().updateFrequency("crypto.elGamalAES.decryptExistingSession");
+            if (!foundTags.isEmpty() && shouldDebug)
+                _log.debug("ElG/AES decrypt success with " + st + ": found tags: " + foundTags);
+            if (!foundTags.isEmpty()) {
+                if (foundKey.getData() != null) {
+                    if (shouldDebug) 
+                        _log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " in existing session");
+                    keyManager.tagsReceived(foundKey, foundTags);
+                } else if (usedKey.getData() != null) {
+                    if (shouldDebug) 
+                        _log.debug("Used key: " + usedKey.toBase64() + " tags: " + foundTags + " in existing session");
+                    keyManager.tagsReceived(usedKey, foundTags);
+                }
+            }
+        } else {
+            _context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed");
+            if (_log.shouldLog(Log.WARN)) {
+                _log.warn("ElG decrypt fail: known tag [" + st + "], failed decrypt");
+            }
+        }
+        return decrypted;
+    }
+
+    /**
+     * Full ElG only. For MuxedEngine use only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    public byte[] decryptSlow(byte data[], PrivateKey targetPrivateKey,
+                              SessionKeyManager keyManager) throws DataFormatException {
+        if (data == null)
+            return null;
+        if (data.length < ELG_ENCRYPTED_LENGTH)
+            return null;
+        SessionKey foundKey = new SessionKey();
+        SessionKey usedKey = new SessionKey();
+        Set<SessionTag> foundTags = new HashSet<SessionTag>();
+        byte[] decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey);
+        final boolean shouldDebug = _log.shouldDebug();
+        if (decrypted != null) {
+            _context.statManager().updateFrequency("crypto.elGamalAES.decryptNewSession");
+        } else {
+            _context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed");
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("ElG decrypt fail as new session");
+        }
+        if (!foundTags.isEmpty()) {
+            if (shouldDebug)
+                _log.debug("ElG decrypt success: found tags: " + foundTags);
+            if (foundKey.getData() != null) {
+                if (shouldDebug) 
+                    _log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " in new session");
+                keyManager.tagsReceived(foundKey, foundTags);
+            } else if (usedKey.getData() != null) {
+                if (shouldDebug) 
+                    _log.debug("Used key: " + usedKey.toBase64() + " tags: " + foundTags + " in new session");
+                keyManager.tagsReceived(usedKey, foundTags);
+            }
+        }
+        return decrypted;
+    }
+
     /**
      * scenario 1: 
      * Begin with 222 bytes, ElG encrypted, containing:
@@ -180,13 +270,6 @@ public final class ElGamalAESEngine {
      */
     private byte[] decryptNewSession(byte data[], PrivateKey targetPrivateKey, Set<SessionTag> foundTags, SessionKey usedKey,
                                     SessionKey foundKey) throws DataFormatException {
-        if (data == null) {
-            //if (_log.shouldLog(Log.WARN)) _log.warn("Data is null, unable to decrypt new session");
-            return null;
-        } else if (data.length < ELG_ENCRYPTED_LENGTH) {
-            //if (_log.shouldLog(Log.WARN)) _log.warn("Data length is too small (" + data.length + ")");
-            return null;
-        }
         byte elgEncr[] = new byte[ELG_ENCRYPTED_LENGTH];
         if (data.length > ELG_ENCRYPTED_LENGTH) {
             System.arraycopy(data, 0, elgEncr, 0, ELG_ENCRYPTED_LENGTH);
@@ -423,8 +506,8 @@ public final class ElGamalAESEngine {
                 throw new IllegalArgumentException("Bad public key type " + type);
         }
         if (currentTag == null) {
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Current tag is null, encrypting as new session");
+            if (_log.shouldDebug())
+                _log.debug("Encrypting as new session");
             _context.statManager().updateFrequency("crypto.elGamalAES.encryptNewSession");
             return encryptNewSession(data, target, key, tagsForDelivery, newKey, paddedSize);
         }
@@ -535,12 +618,12 @@ public final class ElGamalAESEngine {
 
         //_log.debug("Pre IV for encryptNewSession: " + DataHelper.toString(preIV, 32));
         //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32));
-        long before = _context.clock().now();
+        //long before = _context.clock().now();
         byte elgEncr[] = _context.elGamalEngine().encrypt(elgSrcData, target);
-        if (_log.shouldLog(Log.INFO)) {
-            long after = _context.clock().now();
-            _log.info("elgEngine.encrypt of the session key took " + (after - before) + "ms");
-        }
+        //if (_log.shouldDebug()) {
+        //    long after = _context.clock().now();
+        //    _log.debug("elgEngine.encrypt of the session key took " + (after - before) + "ms");
+        //}
         if (elgEncr.length < ELG_ENCRYPTED_LENGTH) {
             // ??? ElGamalEngine.encrypt() always returns 514 bytes
             byte elg[] = new byte[ELG_ENCRYPTED_LENGTH];
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 7acb5400c1..66c709d417 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -65,6 +65,10 @@ public final class ECIESAEADEngine {
     private static final long MAX_NS_FUTURE = 2*60*1000;
     // debug, send ACKREQ in every ES
     private static final boolean ACKREQ_IN_ES = false;
+    // return value for a payload failure after a successful decrypt,
+    // so we don't continue with ElG
+    private static final GarlicClove[] NO_GARLIC = new GarlicClove[] {};
+    private static final CloveSet NO_CLOVES = new CloveSet(NO_GARLIC, Certificate.NULL_CERT, 0, 0);
 
     private static final String INFO_0 = "SessionReplyTags";
     private static final String INFO_6 = "AttachPayloadKDF";
@@ -149,10 +153,10 @@ public final class ECIESAEADEngine {
         } catch (DataFormatException dfe) {
             if (_log.shouldWarn())
                 _log.warn("ECIES decrypt error", dfe);
-            throw dfe;
+            return NO_CLOVES;
         } catch (Exception e) {
             _log.error("ECIES decrypt error", e);
-            return null;
+            return NO_CLOVES;
         }
     }
 
@@ -165,8 +169,8 @@ public final class ECIESAEADEngine {
             return null;
         }
         if (data.length < MIN_ENCRYPTED_SIZE) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
+            if (_log.shouldWarn())
+                _log.warn("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
             return null;
         }
 
@@ -177,31 +181,128 @@ public final class ECIESAEADEngine {
         CloveSet decrypted;
         final boolean shouldDebug = _log.shouldDebug();
         if (key != null) {
-            HandshakeState state = key.getHandshakeState();
-            if (state == null) {
-                if (shouldDebug)
-                    _log.debug("Decrypting ES with tag: " + st.toBase64() + " key: " + key.toBase64() + ": " + data.length + " bytes");
-                decrypted = decryptExistingSession(tag, data, key, targetPrivateKey, keyManager);
-            } else if (data.length >= MIN_NSR_SIZE) {
-                if (shouldDebug)
-                    _log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key.toBase64() + ": " + data.length + " bytes");
-                decrypted = decryptNewSessionReply(tag, data, state, keyManager);
-            } else {
-                decrypted = null;
-                if (_log.shouldWarn())
-                    _log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
-            }
-            if (decrypted != null) {
-///
-                _context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
-            } else {
-                _context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
-                if (_log.shouldWarn()) {
-                    _log.warn("ECIES decrypt fail: known tag [" + st + "], failed decrypt");
-                }
+            decrypted = xx_decryptFast(tag, st, key, data, targetPrivateKey, keyManager);
+            // we do NOT retry as NS
+        } else {
+            decrypted = x_decryptSlow(data, targetPrivateKey, keyManager);
+        }
+        return decrypted;
+    }
+
+    /**
+     * NSR/ES only. For MuxedEngine use only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    CloveSet decryptFast(byte data[], PrivateKey targetPrivateKey,
+                         RatchetSKM keyManager) throws DataFormatException {
+        try {
+            return x_decryptFast(data, targetPrivateKey, keyManager);
+        } catch (DataFormatException dfe) {
+            if (_log.shouldWarn())
+                _log.warn("ECIES decrypt error", dfe);
+            return NO_CLOVES;
+        } catch (Exception e) {
+            _log.error("ECIES decrypt error", e);
+            return NO_CLOVES;
+        }
+    }
+
+    /**
+     * NSR/ES only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    private CloveSet x_decryptFast(byte data[], PrivateKey targetPrivateKey,
+                                   RatchetSKM keyManager) throws DataFormatException {
+        if (data.length < MIN_ENCRYPTED_SIZE) {
+            if (_log.shouldWarn())
+                _log.warn("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
+            return null;
+        }
+        byte tag[] = new byte[TAGLEN];
+        System.arraycopy(data, 0, tag, 0, TAGLEN);
+        RatchetSessionTag st = new RatchetSessionTag(tag);
+        SessionKeyAndNonce key = keyManager.consumeTag(st);
+        CloveSet decrypted;
+        if (key != null) {
+            decrypted = xx_decryptFast(tag, st, key, data, targetPrivateKey, keyManager);
+        } else {
+            decrypted = null;
+        }
+        return decrypted;
+    }
+
+    /**
+     * NSR/ES only.
+     *
+     * @param key non-null
+     * @param data non-null
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    private CloveSet xx_decryptFast(byte[] tag, RatchetSessionTag st, SessionKeyAndNonce key,
+                                    byte data[], PrivateKey targetPrivateKey,
+                                    RatchetSKM keyManager) throws DataFormatException {
+        CloveSet decrypted;
+        final boolean shouldDebug = _log.shouldDebug();
+        HandshakeState state = key.getHandshakeState();
+        if (state == null) {
+            if (shouldDebug)
+                _log.debug("Decrypting ES with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
+            decrypted = decryptExistingSession(tag, data, key, targetPrivateKey, keyManager);
+        } else if (data.length >= MIN_NSR_SIZE) {
+            if (shouldDebug)
+                _log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
+            decrypted = decryptNewSessionReply(tag, data, state, keyManager);
+        } else {
+            decrypted = null;
+            if (_log.shouldWarn())
+                _log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
+        }
+        if (decrypted != null) {
+            _context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
+        } else {
+            _context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
+            if (_log.shouldWarn()) {
+                _log.warn("ECIES decrypt fail: known tag [" + st + "], failed decrypt with key " + key);
             }
-        } else if (data.length >= MIN_NS_SIZE) {
-            if (shouldDebug) _log.debug("IB Tag " + st + " not found, trying NS decrypt");
+        }
+        return decrypted;
+    }
+
+    /**
+     * NS only. For MuxedEngine use only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    CloveSet decryptSlow(byte data[], PrivateKey targetPrivateKey,
+                            RatchetSKM keyManager) throws DataFormatException {
+        try {
+            return x_decryptSlow(data, targetPrivateKey, keyManager);
+        } catch (DataFormatException dfe) {
+            if (_log.shouldWarn())
+                _log.warn("ECIES decrypt error", dfe);
+            return NO_CLOVES;
+        } catch (Exception e) {
+            _log.error("ECIES decrypt error", e);
+            return NO_CLOVES;
+        }
+    }
+
+    /**
+     * NS only.
+     *
+     * @return decrypted data or null on failure
+     * @since 0.9.46
+     */
+    private CloveSet x_decryptSlow(byte data[], PrivateKey targetPrivateKey,
+                                   RatchetSKM keyManager) throws DataFormatException {
+        CloveSet decrypted;
+        if (data.length >= MIN_NS_SIZE) {
             decrypted = decryptNewSession(data, targetPrivateKey, keyManager);
             if (decrypted != null) {
                 _context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession");
@@ -213,9 +314,8 @@ public final class ECIESAEADEngine {
         } else {
             decrypted = null;
             if (_log.shouldWarn())
-                _log.warn("ECIES decrypt fail, tag not found and too small for NS: " + data.length + " bytes");
+                _log.warn("ECIES decrypt fail, too small for NS: " + data.length + " bytes");
         }
-
         return decrypted;
     }
 
@@ -260,6 +360,7 @@ public final class ECIESAEADEngine {
                 _log.warn("Elg2 decode fail NS");
             return null;
         }
+        // rewrite in place, must restore below on failure
         System.arraycopy(pk.getData(), 0, data, 0, KEYLEN);
 
         int payloadlen = data.length - (KEYLEN + KEYLEN + MACLEN + MACLEN);
@@ -272,6 +373,8 @@ public final class ECIESAEADEngine {
                 if (_log.shouldDebug())
                     _log.debug("State at failure: " + state);
             }
+            // restore original data for subsequent ElG attempt
+            System.arraycopy(tmp, 0, data, 0, KEYLEN);
             return null;
         }
         // bloom filter here based on ephemeral key
@@ -281,7 +384,7 @@ public final class ECIESAEADEngine {
         if (keyManager.isDuplicate(pk)) {
             if (_log.shouldWarn())
                 _log.warn("Dup eph. key in IB NS: " + pk);
-            return null;
+            return NO_CLOVES;
         }
 
         byte[] bobPK = new byte[KEYLEN];
@@ -294,14 +397,14 @@ public final class ECIESAEADEngine {
             // TODO
             if (_log.shouldWarn())
                 _log.warn("Zero static key in IB NS");
-            return null;
+            return NO_CLOVES;
         }
 
         // payload
         if (payloadlen == 0) {
             if (_log.shouldWarn())
                 _log.warn("Zero length payload in NS");
-            return null;
+            return NO_CLOVES;
         }
         PLCallback pc = new PLCallback();
         try {
@@ -317,7 +420,7 @@ public final class ECIESAEADEngine {
         if (pc.datetime == 0) {
             if (_log.shouldWarn())
                 _log.warn("No datetime block in IB NS");
-            return null;
+            return NO_CLOVES;
         }
 
         // tell the SKM
@@ -378,6 +481,7 @@ public final class ECIESAEADEngine {
         }
         if (_log.shouldDebug())
             _log.debug("State before decrypt new session reply: " + state);
+        // rewrite in place, must restore below on failure
         System.arraycopy(k.getData(), 0, data, TAGLEN, KEYLEN);
         state.mixHash(tag, 0, TAGLEN);
         if (_log.shouldDebug())
@@ -390,6 +494,9 @@ public final class ECIESAEADEngine {
                 if (_log.shouldDebug())
                     _log.debug("State at failure: " + state);
             }
+            // restore original data for subsequent ElG attempt
+            // unlikely since we already matched the tag
+            System.arraycopy(yy, 0, data, TAGLEN, KEYLEN);
             return null;
         }
         if (_log.shouldDebug())
@@ -417,12 +524,12 @@ public final class ECIESAEADEngine {
                 if (_log.shouldDebug())
                     _log.debug("State at failure: " + state);
             }
-            return null;
+            return NO_CLOVES;
         }
         if (payload.length == 0) {
             if (_log.shouldWarn())
                 _log.warn("Zero length payload in NSR");
-            return null;
+            return NO_CLOVES;
         }
         PLCallback pc = new PLCallback();
         try {
@@ -443,7 +550,7 @@ public final class ECIESAEADEngine {
             // TODO
             if (_log.shouldWarn())
                 _log.warn("NSR reply to zero static key NS");
-            return null;
+            return NO_CLOVES;
         }
 
         // tell the SKM
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
index 5d09aadf1a..f9380f225e 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
@@ -34,30 +34,53 @@ final class MuxedEngine {
             ecKey.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
         CloveSet rv = null;
-        boolean tryElg = false;
-        // See proposal 144
-        if (data.length >= 128) {
-            int mod = data.length % 16;
-            if (mod == 0 || mod == 2)
-                tryElg = true;
-        }
-        // Always try ElG first, for now
-        if (tryElg) {
-            byte[] dec = _context.elGamalAESEngine().decrypt(data, elgKey, keyManager.getElgSKM());
+        // Try in-order from fastest to slowest
+        // Ratchet Tag
+        rv = _context.eciesEngine().decryptFast(data, ecKey, keyManager.getECSKM());
+        if (rv != null)
+            return rv;
+        if (_log.shouldDebug())
+            _log.debug("Ratchet tag not found");
+        // AES Tag
+        if (data.length >= 128 && (data.length & 0x0f) == 0) {
+            byte[] dec = _context.elGamalAESEngine().decryptFast(data, elgKey, keyManager.getElgSKM());
             if (dec != null) {
                 try {
                     rv = _context.garlicMessageParser().readCloveSet(dec, 0);
+                    if (rv == null && _log.shouldInfo())
+                        _log.info("AES cloveset error");
                 } catch (DataFormatException dfe) {
                     if (_log.shouldInfo())
-                        _log.info("ElG decrypt failed, trying ECIES", dfe);
+                        _log.info("AES cloveset error", dfe);
                 }
+                return rv;
             } else {
-                //if (_log.shouldDebug())
-                //    _log.debug("ElG decrypt failed, trying ECIES");
+                if (_log.shouldDebug())
+                    _log.debug("AES tag not found");
             }
         }
-        if (rv == null) {
-            rv = _context.eciesEngine().decrypt(data, ecKey, keyManager.getECSKM());
+        // Ratchet DH
+        rv = _context.eciesEngine().decryptSlow(data, ecKey, keyManager.getECSKM());
+        if (rv != null)
+            return rv;
+        if (_log.shouldDebug())
+            _log.debug("Ratchet NS decrypt failed");
+        // ElG DH
+        if (data.length >= 514 && (data.length & 0x0f) == 2) {
+            byte[] dec = _context.elGamalAESEngine().decryptSlow(data, elgKey, keyManager.getElgSKM());
+            if (dec != null) {
+                try {
+                    rv = _context.garlicMessageParser().readCloveSet(dec, 0);
+                    if (rv == null && _log.shouldInfo())
+                        _log.info("ElG cloveset error");
+                } catch (DataFormatException dfe) {
+                    if (_log.shouldInfo())
+                        _log.info("ElG cloveset error", dfe);
+                }
+            } else {
+                if (_log.shouldInfo())
+                    _log.info("ElG decrypt failed");
+            }
         }
         return rv;
     }
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java b/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java
index 944585f189..769c919257 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java
@@ -86,6 +86,7 @@ class SessionKeyAndNonce extends SessionKey {
         StringBuilder buf = new StringBuilder(64);
         buf.append("[SessionKeyAndNonce: ");
         buf.append(toBase64());
+        buf.append(_state != null ? " NSR" : " ES");
         buf.append(" nonce: ").append(_nonce);
         buf.append(']');
         return buf.toString();
-- 
GitLab