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]