Mark,

Since this is a breaking change, I think we need something in here: https://tomcat.apache.org/migration-11.0.html#Tomcat_11.0.x_noteable_changes

I can draft this if you'd like.

-chris

On 6/17/26 1:12 PM, [email protected] wrote:
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]



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


Reply via email to