This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 24f501c698 Second version of replay protection.
24f501c698 is described below

commit 24f501c698499ea5d64c406fd610fc5dcbb2904c
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Jun 17 18:11:56 2026 +0100

    Second version of replay protection.
    
    Co-authored with GPT
---
 .../group/interceptors/EncryptInterceptor.java     | 197 +++++++++++++++++++--
 .../interceptors/EncryptInterceptorMBean.java      |  28 +++
 .../group/interceptors/LocalStrings.properties     |   3 +
 .../group/interceptors/TestEncryptInterceptor.java | 100 +++++++++++
 webapps/docs/changelog.xml                         |   4 +
 webapps/docs/config/cluster-interceptor.xml        |  31 +++-
 6 files changed, 345 insertions(+), 18 deletions(-)

diff --git 
a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java 
b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
index 5cc9359a5d..0616ab03c0 100644
--- a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
+++ b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
@@ -22,7 +22,11 @@ import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.security.SecureRandom;
 import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayDeque;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 import javax.crypto.Cipher;
@@ -36,6 +40,7 @@ import org.apache.catalina.tribes.ChannelException;
 import org.apache.catalina.tribes.ChannelInterceptor;
 import org.apache.catalina.tribes.ChannelMessage;
 import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.UniqueId;
 import org.apache.catalina.tribes.group.ChannelInterceptorBase;
 import org.apache.catalina.tribes.group.InterceptorPayload;
 import org.apache.catalina.tribes.io.XByteBuffer;
@@ -63,6 +68,9 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
     private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM;
     private byte[] encryptionKeyBytes;
     private String encryptionKeyString;
+    // Milliseconds
+    private long replayWindowTime = 10_000;
+    private int replayWindowMessageCount = 8192;
 
 
     private BaseEncryptionManager encryptionManager;
@@ -80,7 +88,7 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
         if (Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) {
             try {
                 encryptionManager = 
createEncryptionManager(getEncryptionAlgorithm(), getEncryptionKeyInternal(),
-                        getProviderName());
+                        getProviderName(), getReplayWindowTime(), 
getReplayWindowMessageCount());
             } catch (GeneralSecurityException gse) {
                 throw new 
ChannelException(sm.getString("encryptInterceptor.init.failed"), gse);
             }
@@ -114,9 +122,18 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
             throws ChannelException {
         try {
             byte[] data = msg.getMessage().getBytes();
+            // Need trusted time stamp on receiving side, so add time stamp to 
encrypted data.
+            long timestamp = msg.getTimestamp();
+            if (timestamp <= 0) {
+                timestamp = System.currentTimeMillis();
+                msg.setTimestamp(timestamp);
+            }
+            byte[] message = new byte[data.length + 8];
+            XByteBuffer.toBytes(timestamp, message, 0);
+            System.arraycopy(data, 0, message, 8, data.length);
 
             // See #encrypt(byte[]) for an explanation of the return value
-            byte[][] bytes = encryptionManager.encrypt(data);
+            byte[][] bytes = encryptionManager.encrypt(message);
 
             XByteBuffer xbb = msg.getMessage();
 
@@ -137,14 +154,32 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
     public void messageReceived(ChannelMessage msg) {
         try {
             byte[] data = msg.getMessage().getBytes();
+            byte[] encryptedData = data;
 
             data = encryptionManager.decrypt(data);
+            if (data.length < 8) {
+                throw new 
GeneralSecurityException(sm.getString("encryptInterceptor.decrypt.error.short-message"));
+            }
+            /*
+             *  This is trusted since it was encrypted.
+             *
+             *  Excessive clock skew will cause problems here. Can't address 
that without creating risks of replay
+             *  attacks.
+             */
+            long trustedTimstamp = XByteBuffer.toLong(data, 0);
+            if (!encryptionManager.checkIncomingMessage(encryptedData, 
trustedTimstamp)) {
+                log.error(sm.getString("encryptInterceptor.decrypt.replay"));
+                return;
+            }
 
             XByteBuffer xbb = msg.getMessage();
 
-            // Completely replace the message with the decrypted one
+            /*
+             * Completely replace the message with the decrypted one. No need 
to replace time stamp. At this point it
+             * will be the same as the trusted time stamp.
+             */
             xbb.clear();
-            xbb.append(data, 0, data.length);
+            xbb.append(data, 8, data.length - 8);
 
             super.messageReceived(msg);
         } catch (GeneralSecurityException gse) {
@@ -274,6 +309,58 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
         return providerName;
     }
 
+    /**
+     * Returns the time-based replay window in milliseconds.
+     *
+     * @return The replay window time
+     */
+    @Override
+    public long getReplayWindowTime() {
+        return replayWindowTime;
+    }
+
+    /**
+     * Sets the time-based replay window in milliseconds.
+     *
+     * @param replayWindowTime The replay window time
+     */
+    @Override
+    public void setReplayWindowTime(long replayWindowTime) {
+        if (replayWindowTime < 1) {
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.replayWindowTime.tooSmall"));
+        }
+        this.replayWindowTime = replayWindowTime;
+        if (encryptionManager != null) {
+            encryptionManager.setReplayWindowTime(replayWindowTime);
+        }
+    }
+
+    /**
+     * Returns the maximum number of replay entries to retain.
+     *
+     * @return The replay window message count
+     */
+    @Override
+    public int getReplayWindowMessageCount() {
+        return replayWindowMessageCount;
+    }
+
+    /**
+     * Sets the maximum number of replay entries to retain.
+     *
+     * @param replayWindowMessageCount The replay window message count
+     */
+    @Override
+    public void setReplayWindowMessageCount(int replayWindowMessageCount) {
+        if (replayWindowMessageCount < 1) {
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.replayWindowMessageCount.tooSmall"));
+        }
+        this.replayWindowMessageCount = replayWindowMessageCount;
+        if (encryptionManager != null) {
+            
encryptionManager.setReplayWindowMessageCount(replayWindowMessageCount);
+        }
+    }
+
     // Copied from org.apache.tomcat.util.buf.HexUtils
     // @formatter:off
     private static final int[] DEC = {
@@ -320,7 +407,8 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
     }
 
     private static BaseEncryptionManager createEncryptionManager(String 
algorithm, byte[] encryptionKey,
-            String providerName) throws NoSuchAlgorithmException, 
NoSuchPaddingException, NoSuchProviderException {
+            String providerName, long replayWindowTime, int 
replayWindowMessageCount)
+            throws NoSuchAlgorithmException, NoSuchPaddingException, 
NoSuchProviderException {
         if (null == encryptionKey) {
             throw new 
IllegalStateException(sm.getString("encryptInterceptor.key.required"));
         }
@@ -359,8 +447,7 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
          */
         if ("NONE".equals(algorithmMode) || "ECB".equals(algorithmMode) || 
"PCBC".equals(algorithmMode) ||
                 "CTS".equals(algorithmMode) || "KW".equals(algorithmMode) || 
"KWP".equals(algorithmMode) ||
-                "CTR".equals(algorithmMode) ||
-                ("CBC".equals(algorithmMode) && 
"NOPADDING".equals(algorithmPadding)) ||
+                "CTR".equals(algorithmMode) || ("CBC".equals(algorithmMode) && 
"NOPADDING".equals(algorithmPadding)) ||
                 ("CFB".equals(algorithmMode) && 
"NOPADDING".equals(algorithmPadding)) ||
                 ("GCM".equals(algorithmMode) && 
"PKCS5PADDING".equals(algorithmPadding)) ||
                 ("OFB".equals(algorithmMode) && 
"NOPADDING".equals(algorithmPadding))) {
@@ -375,17 +462,18 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
 
         } else if (algorithmMode.startsWith("CFB") || 
algorithmMode.startsWith("OFB")) {
             // Using a non-default block size. Not supported as insecure 
and/or inefficient.
-            throw new IllegalArgumentException(
-                    sm.getString("encryptInterceptor.algorithm.unsupported", 
algorithm));
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported",
 algorithm));
 
         } else if ("GCM".equals(algorithmMode) && 
"NOPADDING".equals(algorithmPadding)) {
             // Needs a specialised encryption manager to handle the 
differences between GCM and other modes
-            return new GCMEncryptionManager(algorithm, new 
SecretKeySpec(encryptionKey, algorithmName), providerName);
+            return new GCMEncryptionManager(algorithm, new 
SecretKeySpec(encryptionKey, algorithmName), providerName,
+                    replayWindowTime, replayWindowMessageCount);
         }
 
         // Use the default encryption manager
         try {
-            return new BaseEncryptionManager(algorithm, new 
SecretKeySpec(encryptionKey, algorithmName), providerName);
+            return new BaseEncryptionManager(algorithm, new 
SecretKeySpec(encryptionKey, algorithmName), providerName,
+                    replayWindowTime, replayWindowMessageCount);
         } catch (NoSuchAlgorithmException | NoSuchPaddingException | 
NoSuchProviderException ex) {
             throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported",
 algorithm), ex);
         }
@@ -423,12 +511,22 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
          * SecureRandom is thread-safe, but sharing a single instance will 
likely be a bottleneck.
          */
         private final ConcurrentLinkedQueue<SecureRandom> randomPool;
-
-        BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, 
String providerName)
+        private final TreeMap<Long,ArrayDeque<UniqueId>> 
receivedTimestampNonces = new TreeMap<>();
+        private final Map<UniqueId,Long> receivedNonceTimestamps = new 
HashMap<>();
+        private long replayWindowTime;
+        private volatile int replayWindowMessageCount;
+        private long lastRemovedTimestamp;
+        private int receivedNonceCount = 0;
+
+        BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, 
String providerName, long replayWindowTime,
+                int replayWindowMessageCount)
                 throws NoSuchAlgorithmException, NoSuchPaddingException, 
NoSuchProviderException {
             this.algorithm = algorithm;
             this.providerName = providerName;
             this.secretKey = secretKey;
+            this.replayWindowTime = replayWindowTime;
+            this.lastRemovedTimestamp = System.currentTimeMillis() - 
replayWindowTime;
+            this.replayWindowMessageCount = replayWindowMessageCount;
 
             cipherPool = new ConcurrentLinkedQueue<>();
             Cipher cipher = createCipher();
@@ -441,6 +539,52 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
             // Individual Cipher and SecureRandom objects need no explicit 
tear down
             cipherPool.clear();
             randomPool.clear();
+            synchronized (this) {
+                receivedTimestampNonces.clear();
+                receivedNonceTimestamps.clear();
+                lastRemovedTimestamp = Long.MIN_VALUE;
+                receivedNonceCount = 0;
+            }
+        }
+
+        public synchronized void setReplayWindowTime(long replayWindowTime) {
+            this.replayWindowTime = replayWindowTime;
+            // Only move the lastRemovedTimestamp forwards. Moving it 
backwards could open a window for replay attacks.
+            if (lastRemovedTimestamp < System.currentTimeMillis() - 
replayWindowTime) {
+                lastRemovedTimestamp = System.currentTimeMillis() - 
replayWindowTime;
+            }
+        }
+
+        public void setReplayWindowMessageCount(int replayWindowMessageCount) {
+            this.replayWindowMessageCount = replayWindowMessageCount;
+            synchronized (this) {
+                while (receivedNonceCount > replayWindowMessageCount) {
+                    removeEldestEntry();
+                }
+            }
+        }
+
+        public synchronized boolean checkIncomingMessage(byte[] bytes, long 
messageTimestamp) {
+            if (messageTimestamp < (System.currentTimeMillis() - 
replayWindowTime)) {
+                return false;
+            }
+            if (messageTimestamp <= lastRemovedTimestamp) {
+                return false;
+            }
+
+            UniqueId nonce = new UniqueId(bytes, 0, getIVSize());
+            if (receivedNonceTimestamps.containsKey(nonce)) {
+                return false;
+            }
+
+            
receivedTimestampNonces.computeIfAbsent(Long.valueOf(messageTimestamp), k -> 
new ArrayDeque<>()).addLast(nonce);
+            receivedNonceTimestamps.put(nonce, Long.valueOf(messageTimestamp));
+            receivedNonceCount++;
+            while (receivedNonceCount > replayWindowMessageCount) {
+                removeEldestEntry();
+            }
+
+            return true;
         }
 
         private String getAlgorithm() {
@@ -593,6 +737,28 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
         protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int 
offset, int length) {
             return new IvParameterSpec(ivBytes, offset, length);
         }
+
+        private void removeEldestEntry() {
+            Map.Entry<Long,ArrayDeque<UniqueId>> entry = 
receivedTimestampNonces.firstEntry();
+            if (entry != null) {
+                ArrayDeque<UniqueId> nonces = entry.getValue();
+                UniqueId nonce = nonces.pollFirst();
+                if (nonce != null) {
+                    receivedNonceTimestamps.remove(nonce);
+                    updateLastRemovedTimestamp(entry.getKey().longValue());
+                    receivedNonceCount--;
+                }
+                if (nonces.isEmpty()) {
+                    receivedTimestampNonces.pollFirstEntry();
+                }
+            }
+        }
+
+        private void updateLastRemovedTimestamp(long removedTimestamp) {
+            if (removedTimestamp > lastRemovedTimestamp) {
+                lastRemovedTimestamp = removedTimestamp;
+            }
+        }
     }
 
     /**
@@ -611,9 +777,10 @@ public class EncryptInterceptor extends 
ChannelInterceptorBase implements Encryp
      * number of bits supported 128-bit provide the best security.
      */
     private static class GCMEncryptionManager extends BaseEncryptionManager {
-        GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String 
providerName)
+        GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String 
providerName, long replayWindowTime,
+                int replayWindowMessageCount)
                 throws NoSuchAlgorithmException, NoSuchPaddingException, 
NoSuchProviderException {
-            super(algorithm, secretKey, providerName);
+            super(algorithm, secretKey, providerName, replayWindowTime, 
replayWindowMessageCount);
         }
 
         @Override
diff --git 
a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
 
b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
index 7d10a1f4f1..c6362b26c5 100644
--- 
a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
+++ 
b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
@@ -76,4 +76,32 @@ public interface EncryptInterceptorMBean {
      * @return the JCA provider name, or {@code null} for default
      */
     String getProviderName();
+
+    /**
+     * Sets the time-based replay window in milliseconds.
+     *
+     * @param replayWindowTime the replay window time
+     */
+    void setReplayWindowTime(long replayWindowTime);
+
+    /**
+     * Returns the time-based replay window in milliseconds.
+     *
+     * @return the replay window time
+     */
+    long getReplayWindowTime();
+
+    /**
+     * Sets the maximum number of replay cache entries.
+     *
+     * @param replayWindowMessageCount the replay window message count
+     */
+    void setReplayWindowMessageCount(int replayWindowMessageCount);
+
+    /**
+     * Returns the maximum number of replay cache entries.
+     *
+     * @return the replay window message count
+     */
+    int getReplayWindowMessageCount();
 }
diff --git 
a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties 
b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
index a8e3c5d6e5..5c13178485 100644
--- a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
+++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
@@ -21,9 +21,12 @@ encryptInterceptor.algorithm.switch=The EncryptInterceptor 
is using the algorith
 encryptInterceptor.algorithm.unsupported=EncryptInterceptor does not support 
algorithm [{0}]
 encryptInterceptor.decrypt.error.short-message=Failed to decrypt message: 
premature end-of-message
 encryptInterceptor.decrypt.failed=Failed to decrypt message
+encryptInterceptor.decrypt.replay=Failed to decrypt message: replay attack 
detected
 encryptInterceptor.encrypt.failed=Failed to encrypt message
 encryptInterceptor.init.failed=Failed to initialize EncryptInterceptor
 encryptInterceptor.key.required=Encryption key is required
+encryptInterceptor.replayWindowMessageCount.tooSmall=Replay window message 
count must be at least 1
+encryptInterceptor.replayWindowTime.tooSmall=Replay window time must be at 
least 1 millisecond
 encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor must be 
upstream of TcpFailureDetector. Please re-order EncryptInterceptor to be listed 
before TcpFailureDetector in your channel interceptor pipeline.
 
 fragmentationInterceptor.fragments.missing=Fragments are missing.
diff --git 
a/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
 
b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
index e30e84321f..8c6785c4a8 100644
--- 
a/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
+++ 
b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
@@ -216,6 +216,90 @@ public class TestEncryptInterceptor extends 
EncryptionInterceptorBaseTest {
                 cipherText1, IsNot.not(IsEqual.equalTo(cipherText2)));
     }
 
+    @Test
+    public void testRejectReplay() throws Exception {
+        src.setNext(new ValueCaptureInterceptor());
+        dest.setPrevious(new ValuesCaptureInterceptor());
+        src.start(Channel.SND_TX_SEQ);
+        dest.start(Channel.SND_TX_SEQ);
+
+        byte[] encrypted = encrypt("msg-1", System.currentTimeMillis());
+
+        deliver(encrypted);
+        deliver(encrypted);
+
+        Collection<byte[]> messages = ((ValuesCaptureInterceptor) 
dest.getPrevious()).getValues();
+        Assert.assertEquals(1, messages.size());
+    }
+
+    @Test
+    public void testReplayWindowRejectsOldMessage() throws Exception {
+        src.setNext(new ValueCaptureInterceptor());
+        dest.setPrevious(new ValuesCaptureInterceptor());
+        dest.setReplayWindowTime(50);
+        src.start(Channel.SND_TX_SEQ);
+        dest.start(Channel.SND_TX_SEQ);
+
+        long now = System.currentTimeMillis();
+        byte[] encryptedRecent = encrypt("msg-recent", now);
+        byte[] encryptedOld = encrypt("msg-old", now - 1000);
+
+        deliver(encryptedRecent);
+        deliver(encryptedOld);
+
+        Collection<byte[]> messages = ((ValuesCaptureInterceptor) 
dest.getPrevious()).getValues();
+        Assert.assertEquals(1, messages.size());
+    }
+
+    @Test
+    public void testReplayWindowRejectsOldMessageAfterCountEviction() throws 
Exception {
+        src.setNext(new ValueCaptureInterceptor());
+        dest.setPrevious(new ValuesCaptureInterceptor());
+        dest.setReplayWindowMessageCount(2);
+        src.start(Channel.SND_TX_SEQ);
+        dest.start(Channel.SND_TX_SEQ);
+
+        long now = System.currentTimeMillis();
+        byte[] encrypted1000 = encrypt("msg-1000", now);
+        byte[] encrypted1001 = encrypt("msg-1001", now + 1);
+        byte[] encrypted1002 = encrypt("msg-1002", now + 2);
+        byte[] encrypted1003 = encrypt("msg-1003", now + 3);
+
+        deliver(encrypted1000);
+        deliver(encrypted1001);
+        deliver(encrypted1002);
+        deliver(encrypted1003);
+        deliver(encrypted1000);
+
+        Collection<byte[]> messages = ((ValuesCaptureInterceptor) 
dest.getPrevious()).getValues();
+        Assert.assertEquals(4, messages.size());
+    }
+
+    @Test
+    public void testReplayWindowEvictsOldestTimestampFirst() throws Exception {
+        src.setNext(new ValueCaptureInterceptor());
+        dest.setPrevious(new ValuesCaptureInterceptor());
+        dest.setReplayWindowMessageCount(2);
+        src.start(Channel.SND_TX_SEQ);
+        dest.start(Channel.SND_TX_SEQ);
+
+        long now = System.currentTimeMillis();
+        byte[] encrypted300 = encrypt("msg-300", now + 300);
+        byte[] encrypted100 = encrypt("msg-100", now + 100);
+        byte[] encrypted200 = encrypt("msg-200", now + 200);
+        byte[] encrypted225 = encrypt("msg-225", now + 225);
+        byte[] encrypted250 = encrypt("msg-250", now + 250);
+
+        deliver(encrypted300);
+        deliver(encrypted100);
+        deliver(encrypted200);
+        deliver(encrypted225);
+        deliver(encrypted250);
+
+        Collection<byte[]> messages = ((ValuesCaptureInterceptor) 
dest.getPrevious()).getValues();
+        Assert.assertEquals(5, messages.size());
+    }
+
     @Test
     public void testPickup() throws Exception {
         File file = new File(MESSAGE_FILE);
@@ -316,4 +400,20 @@ public class TestEncryptInterceptor extends 
EncryptionInterceptorBaseTest {
             Assert.fail("EncryptionInterceptor should throw 
ChannelConfigException, not " + t.getClass().getName());
         }
     }
+
+    private byte[] encrypt(String message, long timestamp) throws Exception {
+        ChannelData msg = new ChannelData(false);
+        msg.setTimestamp(timestamp);
+        msg.setMessage(new XByteBuffer(message.getBytes("UTF-8"), false));
+        src.sendMessage(null, msg, null);
+        return ((ValueCaptureInterceptor) src.getNext()).getValue();
+    }
+
+    private void deliver(byte[] encrypted) {
+        ChannelData incoming = new ChannelData(false);
+        XByteBuffer xbb = new XByteBuffer(encrypted.length, false);
+        xbb.append(encrypted, 0, encrypted.length);
+        incoming.setMessage(xbb);
+        dest.messageReceived(incoming);
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 26d01987ed..e9dd0bdf10 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -602,6 +602,10 @@
         Fix concurrency issues generating MD5 digests in the
         <code>CloudMembershipProvider</code> implementations. (markt)
       </fix>
+      <add>
+        Add replay protection to the <code>EncryptInterceptor</code>. This us a
+        breaking change for the <code>EncryptInterceptor</code>.(markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="WebSocket">
diff --git a/webapps/docs/config/cluster-interceptor.xml 
b/webapps/docs/config/cluster-interceptor.xml
index d0f5f88537..9b26c8ed73 100644
--- a/webapps/docs/config/cluster-interceptor.xml
+++ b/webapps/docs/config/cluster-interceptor.xml
@@ -212,6 +212,12 @@
      <i>before</i> the <code>TcpFailureDetector</code> on the sender and 
<i>after</i>
      it on the receiver, otherwise message corruption will occur.
    </p>
+   <p>
+     The EncryptInterceptor uses the timestamp of the message to provide replay
+     protection. For this to be effective, clock skew between cluster nodes
+     should be minimised. If not, it is likely a high proportion of valid
+     messages will be rejected.
+   </p>
    <attributes>
      <attribute name="encryptionAlgorithm" required="false">
        <p>The encryption algorithm to be used, including the mode and padding.
@@ -231,11 +237,30 @@
        <p>The default algorithm is <code>AES/GCM/NoPadding</code>.</p>
      </attribute>
      <attribute name="encryptionKey" required="true">
-       The key to be used with the encryption algorithm.
+       <p>The key to be used with the encryption algorithm.</p>
 
-       The key should be specified as hex-encoded bytes of the appropriate
+       <p>The key should be specified as hex-encoded bytes of the appropriate
        length for the algorithm (e.g. 16 bytes / 32 characters / 128 bits for
-       AES-128, 32 bytes / 64 characters / 256 bits for AES-256, etc.).
+       AES-128, 32 bytes / 64 characters / 256 bits for AES-256, etc.).</p>
+     </attribute>
+     <attribute name="replayWindowTime" required="false">
+       <p>Messages with a timestamp before the current time less this window
+       will be rejected. This needs to account for clock skew across the 
cluster
+       as well as the expected maximum delay between messages being sent and
+       received. Specified in milliseconds. If not specified, the default value
+       of 10000 (10 seconds) will be used.</p>
+     </attribute>
+     <attribute name="replayWindowMessageCount" required="false">
+       <p>The number of past messages for which the nonces will be tracked to
+       prevent replay attacks. Messages with a nonce that has already been
+       tracked will be rejected. If not specified, the default value of 8192
+       will be used.</p>
+
+       <p>If messages are received at a high rate, it is possible that a nonce
+       will be removed from the list of tracked nonces before
+       <code>replayWindowTime</code> milliseconds have elapsed. If that 
happens,
+       any message with a timestamp older than the last nonce evicted from the
+       list of tracked nonces will also be rejected.</p>
      </attribute>
    </attributes>
   </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to