On Wed, Jun 17, 2026 at 7: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
The review is happy about this patch, except for the trustedTimstamp
type on line 169.
But then it was happy about the previous iteration too, so ...
Rémy
> ---
> .../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]