Skip to content

Commit 24f501c

Browse files
committed
Second version of replay protection.
Co-authored with GPT
1 parent 66e05c4 commit 24f501c

6 files changed

Lines changed: 345 additions & 18 deletions

File tree

java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java

Lines changed: 182 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
import java.security.NoSuchProviderException;
2323
import java.security.SecureRandom;
2424
import java.security.spec.AlgorithmParameterSpec;
25+
import java.util.ArrayDeque;
26+
import java.util.HashMap;
2527
import java.util.Locale;
28+
import java.util.Map;
29+
import java.util.TreeMap;
2630
import java.util.concurrent.ConcurrentLinkedQueue;
2731

2832
import javax.crypto.Cipher;
@@ -36,6 +40,7 @@
3640
import org.apache.catalina.tribes.ChannelInterceptor;
3741
import org.apache.catalina.tribes.ChannelMessage;
3842
import org.apache.catalina.tribes.Member;
43+
import org.apache.catalina.tribes.UniqueId;
3944
import org.apache.catalina.tribes.group.ChannelInterceptorBase;
4045
import org.apache.catalina.tribes.group.InterceptorPayload;
4146
import org.apache.catalina.tribes.io.XByteBuffer;
@@ -63,6 +68,9 @@ public class EncryptInterceptor extends ChannelInterceptorBase implements Encryp
6368
private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM;
6469
private byte[] encryptionKeyBytes;
6570
private String encryptionKeyString;
71+
// Milliseconds
72+
private long replayWindowTime = 10_000;
73+
private int replayWindowMessageCount = 8192;
6674

6775

6876
private BaseEncryptionManager encryptionManager;
@@ -80,7 +88,7 @@ public void start(int svc) throws ChannelException {
8088
if (Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) {
8189
try {
8290
encryptionManager = createEncryptionManager(getEncryptionAlgorithm(), getEncryptionKeyInternal(),
83-
getProviderName());
91+
getProviderName(), getReplayWindowTime(), getReplayWindowMessageCount());
8492
} catch (GeneralSecurityException gse) {
8593
throw new ChannelException(sm.getString("encryptInterceptor.init.failed"), gse);
8694
}
@@ -114,9 +122,18 @@ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPay
114122
throws ChannelException {
115123
try {
116124
byte[] data = msg.getMessage().getBytes();
125+
// Need trusted time stamp on receiving side, so add time stamp to encrypted data.
126+
long timestamp = msg.getTimestamp();
127+
if (timestamp <= 0) {
128+
timestamp = System.currentTimeMillis();
129+
msg.setTimestamp(timestamp);
130+
}
131+
byte[] message = new byte[data.length + 8];
132+
XByteBuffer.toBytes(timestamp, message, 0);
133+
System.arraycopy(data, 0, message, 8, data.length);
117134

118135
// See #encrypt(byte[]) for an explanation of the return value
119-
byte[][] bytes = encryptionManager.encrypt(data);
136+
byte[][] bytes = encryptionManager.encrypt(message);
120137

121138
XByteBuffer xbb = msg.getMessage();
122139

@@ -137,14 +154,32 @@ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPay
137154
public void messageReceived(ChannelMessage msg) {
138155
try {
139156
byte[] data = msg.getMessage().getBytes();
157+
byte[] encryptedData = data;
140158

141159
data = encryptionManager.decrypt(data);
160+
if (data.length < 8) {
161+
throw new GeneralSecurityException(sm.getString("encryptInterceptor.decrypt.error.short-message"));
162+
}
163+
/*
164+
* This is trusted since it was encrypted.
165+
*
166+
* Excessive clock skew will cause problems here. Can't address that without creating risks of replay
167+
* attacks.
168+
*/
169+
long trustedTimstamp = XByteBuffer.toLong(data, 0);
170+
if (!encryptionManager.checkIncomingMessage(encryptedData, trustedTimstamp)) {
171+
log.error(sm.getString("encryptInterceptor.decrypt.replay"));
172+
return;
173+
}
142174

143175
XByteBuffer xbb = msg.getMessage();
144176

145-
// Completely replace the message with the decrypted one
177+
/*
178+
* Completely replace the message with the decrypted one. No need to replace time stamp. At this point it
179+
* will be the same as the trusted time stamp.
180+
*/
146181
xbb.clear();
147-
xbb.append(data, 0, data.length);
182+
xbb.append(data, 8, data.length - 8);
148183

149184
super.messageReceived(msg);
150185
} catch (GeneralSecurityException gse) {
@@ -274,6 +309,58 @@ public String getProviderName() {
274309
return providerName;
275310
}
276311

312+
/**
313+
* Returns the time-based replay window in milliseconds.
314+
*
315+
* @return The replay window time
316+
*/
317+
@Override
318+
public long getReplayWindowTime() {
319+
return replayWindowTime;
320+
}
321+
322+
/**
323+
* Sets the time-based replay window in milliseconds.
324+
*
325+
* @param replayWindowTime The replay window time
326+
*/
327+
@Override
328+
public void setReplayWindowTime(long replayWindowTime) {
329+
if (replayWindowTime < 1) {
330+
throw new IllegalArgumentException(sm.getString("encryptInterceptor.replayWindowTime.tooSmall"));
331+
}
332+
this.replayWindowTime = replayWindowTime;
333+
if (encryptionManager != null) {
334+
encryptionManager.setReplayWindowTime(replayWindowTime);
335+
}
336+
}
337+
338+
/**
339+
* Returns the maximum number of replay entries to retain.
340+
*
341+
* @return The replay window message count
342+
*/
343+
@Override
344+
public int getReplayWindowMessageCount() {
345+
return replayWindowMessageCount;
346+
}
347+
348+
/**
349+
* Sets the maximum number of replay entries to retain.
350+
*
351+
* @param replayWindowMessageCount The replay window message count
352+
*/
353+
@Override
354+
public void setReplayWindowMessageCount(int replayWindowMessageCount) {
355+
if (replayWindowMessageCount < 1) {
356+
throw new IllegalArgumentException(sm.getString("encryptInterceptor.replayWindowMessageCount.tooSmall"));
357+
}
358+
this.replayWindowMessageCount = replayWindowMessageCount;
359+
if (encryptionManager != null) {
360+
encryptionManager.setReplayWindowMessageCount(replayWindowMessageCount);
361+
}
362+
}
363+
277364
// Copied from org.apache.tomcat.util.buf.HexUtils
278365
// @formatter:off
279366
private static final int[] DEC = {
@@ -320,7 +407,8 @@ private static byte[] fromHexString(String input) {
320407
}
321408

322409
private static BaseEncryptionManager createEncryptionManager(String algorithm, byte[] encryptionKey,
323-
String providerName) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
410+
String providerName, long replayWindowTime, int replayWindowMessageCount)
411+
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
324412
if (null == encryptionKey) {
325413
throw new IllegalStateException(sm.getString("encryptInterceptor.key.required"));
326414
}
@@ -359,8 +447,7 @@ private static BaseEncryptionManager createEncryptionManager(String algorithm, b
359447
*/
360448
if ("NONE".equals(algorithmMode) || "ECB".equals(algorithmMode) || "PCBC".equals(algorithmMode) ||
361449
"CTS".equals(algorithmMode) || "KW".equals(algorithmMode) || "KWP".equals(algorithmMode) ||
362-
"CTR".equals(algorithmMode) ||
363-
("CBC".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
450+
"CTR".equals(algorithmMode) || ("CBC".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
364451
("CFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
365452
("GCM".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding)) ||
366453
("OFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding))) {
@@ -375,17 +462,18 @@ private static BaseEncryptionManager createEncryptionManager(String algorithm, b
375462

376463
} else if (algorithmMode.startsWith("CFB") || algorithmMode.startsWith("OFB")) {
377464
// Using a non-default block size. Not supported as insecure and/or inefficient.
378-
throw new IllegalArgumentException(
379-
sm.getString("encryptInterceptor.algorithm.unsupported", algorithm));
465+
throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported", algorithm));
380466

381467
} else if ("GCM".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) {
382468
// Needs a specialised encryption manager to handle the differences between GCM and other modes
383-
return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName);
469+
return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName,
470+
replayWindowTime, replayWindowMessageCount);
384471
}
385472

386473
// Use the default encryption manager
387474
try {
388-
return new BaseEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName);
475+
return new BaseEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName,
476+
replayWindowTime, replayWindowMessageCount);
389477
} catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException ex) {
390478
throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported", algorithm), ex);
391479
}
@@ -423,12 +511,22 @@ private static class BaseEncryptionManager {
423511
* SecureRandom is thread-safe, but sharing a single instance will likely be a bottleneck.
424512
*/
425513
private final ConcurrentLinkedQueue<SecureRandom> randomPool;
426-
427-
BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
514+
private final TreeMap<Long,ArrayDeque<UniqueId>> receivedTimestampNonces = new TreeMap<>();
515+
private final Map<UniqueId,Long> receivedNonceTimestamps = new HashMap<>();
516+
private long replayWindowTime;
517+
private volatile int replayWindowMessageCount;
518+
private long lastRemovedTimestamp;
519+
private int receivedNonceCount = 0;
520+
521+
BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName, long replayWindowTime,
522+
int replayWindowMessageCount)
428523
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
429524
this.algorithm = algorithm;
430525
this.providerName = providerName;
431526
this.secretKey = secretKey;
527+
this.replayWindowTime = replayWindowTime;
528+
this.lastRemovedTimestamp = System.currentTimeMillis() - replayWindowTime;
529+
this.replayWindowMessageCount = replayWindowMessageCount;
432530

433531
cipherPool = new ConcurrentLinkedQueue<>();
434532
Cipher cipher = createCipher();
@@ -441,6 +539,52 @@ public void shutdown() {
441539
// Individual Cipher and SecureRandom objects need no explicit tear down
442540
cipherPool.clear();
443541
randomPool.clear();
542+
synchronized (this) {
543+
receivedTimestampNonces.clear();
544+
receivedNonceTimestamps.clear();
545+
lastRemovedTimestamp = Long.MIN_VALUE;
546+
receivedNonceCount = 0;
547+
}
548+
}
549+
550+
public synchronized void setReplayWindowTime(long replayWindowTime) {
551+
this.replayWindowTime = replayWindowTime;
552+
// Only move the lastRemovedTimestamp forwards. Moving it backwards could open a window for replay attacks.
553+
if (lastRemovedTimestamp < System.currentTimeMillis() - replayWindowTime) {
554+
lastRemovedTimestamp = System.currentTimeMillis() - replayWindowTime;
555+
}
556+
}
557+
558+
public void setReplayWindowMessageCount(int replayWindowMessageCount) {
559+
this.replayWindowMessageCount = replayWindowMessageCount;
560+
synchronized (this) {
561+
while (receivedNonceCount > replayWindowMessageCount) {
562+
removeEldestEntry();
563+
}
564+
}
565+
}
566+
567+
public synchronized boolean checkIncomingMessage(byte[] bytes, long messageTimestamp) {
568+
if (messageTimestamp < (System.currentTimeMillis() - replayWindowTime)) {
569+
return false;
570+
}
571+
if (messageTimestamp <= lastRemovedTimestamp) {
572+
return false;
573+
}
574+
575+
UniqueId nonce = new UniqueId(bytes, 0, getIVSize());
576+
if (receivedNonceTimestamps.containsKey(nonce)) {
577+
return false;
578+
}
579+
580+
receivedTimestampNonces.computeIfAbsent(Long.valueOf(messageTimestamp), k -> new ArrayDeque<>()).addLast(nonce);
581+
receivedNonceTimestamps.put(nonce, Long.valueOf(messageTimestamp));
582+
receivedNonceCount++;
583+
while (receivedNonceCount > replayWindowMessageCount) {
584+
removeEldestEntry();
585+
}
586+
587+
return true;
444588
}
445589

446590
private String getAlgorithm() {
@@ -593,6 +737,28 @@ protected byte[] generateIVBytes() {
593737
protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int offset, int length) {
594738
return new IvParameterSpec(ivBytes, offset, length);
595739
}
740+
741+
private void removeEldestEntry() {
742+
Map.Entry<Long,ArrayDeque<UniqueId>> entry = receivedTimestampNonces.firstEntry();
743+
if (entry != null) {
744+
ArrayDeque<UniqueId> nonces = entry.getValue();
745+
UniqueId nonce = nonces.pollFirst();
746+
if (nonce != null) {
747+
receivedNonceTimestamps.remove(nonce);
748+
updateLastRemovedTimestamp(entry.getKey().longValue());
749+
receivedNonceCount--;
750+
}
751+
if (nonces.isEmpty()) {
752+
receivedTimestampNonces.pollFirstEntry();
753+
}
754+
}
755+
}
756+
757+
private void updateLastRemovedTimestamp(long removedTimestamp) {
758+
if (removedTimestamp > lastRemovedTimestamp) {
759+
lastRemovedTimestamp = removedTimestamp;
760+
}
761+
}
596762
}
597763

598764
/**
@@ -611,9 +777,10 @@ protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int offset, int leng
611777
* number of bits supported 128-bit provide the best security.
612778
*/
613779
private static class GCMEncryptionManager extends BaseEncryptionManager {
614-
GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
780+
GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName, long replayWindowTime,
781+
int replayWindowMessageCount)
615782
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
616-
super(algorithm, secretKey, providerName);
783+
super(algorithm, secretKey, providerName, replayWindowTime, replayWindowMessageCount);
617784
}
618785

619786
@Override

java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,32 @@ public interface EncryptInterceptorMBean {
7676
* @return the JCA provider name, or {@code null} for default
7777
*/
7878
String getProviderName();
79+
80+
/**
81+
* Sets the time-based replay window in milliseconds.
82+
*
83+
* @param replayWindowTime the replay window time
84+
*/
85+
void setReplayWindowTime(long replayWindowTime);
86+
87+
/**
88+
* Returns the time-based replay window in milliseconds.
89+
*
90+
* @return the replay window time
91+
*/
92+
long getReplayWindowTime();
93+
94+
/**
95+
* Sets the maximum number of replay cache entries.
96+
*
97+
* @param replayWindowMessageCount the replay window message count
98+
*/
99+
void setReplayWindowMessageCount(int replayWindowMessageCount);
100+
101+
/**
102+
* Returns the maximum number of replay cache entries.
103+
*
104+
* @return the replay window message count
105+
*/
106+
int getReplayWindowMessageCount();
79107
}

java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ encryptInterceptor.algorithm.switch=The EncryptInterceptor is using the algorith
2121
encryptInterceptor.algorithm.unsupported=EncryptInterceptor does not support algorithm [{0}]
2222
encryptInterceptor.decrypt.error.short-message=Failed to decrypt message: premature end-of-message
2323
encryptInterceptor.decrypt.failed=Failed to decrypt message
24+
encryptInterceptor.decrypt.replay=Failed to decrypt message: replay attack detected
2425
encryptInterceptor.encrypt.failed=Failed to encrypt message
2526
encryptInterceptor.init.failed=Failed to initialize EncryptInterceptor
2627
encryptInterceptor.key.required=Encryption key is required
28+
encryptInterceptor.replayWindowMessageCount.tooSmall=Replay window message count must be at least 1
29+
encryptInterceptor.replayWindowTime.tooSmall=Replay window time must be at least 1 millisecond
2730
encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor must be upstream of TcpFailureDetector. Please re-order EncryptInterceptor to be listed before TcpFailureDetector in your channel interceptor pipeline.
2831

2932
fragmentationInterceptor.fragments.missing=Fragments are missing.

0 commit comments

Comments
 (0)