Skip to content

Commit 6218ec8

Browse files
committed
Add replay protection to the EncrpytInterceptor
1 parent e1727c2 commit 6218ec8

8 files changed

Lines changed: 625 additions & 70 deletions

File tree

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

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
import java.security.SecureRandom;
2424
import java.security.spec.AlgorithmParameterSpec;
2525
import java.util.Locale;
26+
import java.util.Map;
27+
import java.util.concurrent.ConcurrentHashMap;
2628
import java.util.concurrent.ConcurrentLinkedQueue;
29+
import java.util.concurrent.atomic.AtomicLong;
2730

2831
import javax.crypto.Cipher;
2932
import javax.crypto.NoSuchPaddingException;
@@ -39,6 +42,7 @@
3942
import org.apache.catalina.tribes.group.ChannelInterceptorBase;
4043
import org.apache.catalina.tribes.group.InterceptorPayload;
4144
import org.apache.catalina.tribes.io.XByteBuffer;
45+
import org.apache.catalina.tribes.util.CyclicTracker;
4246
import org.apache.catalina.tribes.util.StringManager;
4347
import org.apache.juli.logging.Log;
4448
import org.apache.juli.logging.LogFactory;
@@ -63,6 +67,7 @@ public class EncryptInterceptor extends ChannelInterceptorBase implements Encryp
6367
private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM;
6468
private byte[] encryptionKeyBytes;
6569
private String encryptionKeyString;
70+
private int replayWindowSize = 1024;
6671

6772

6873
private BaseEncryptionManager encryptionManager;
@@ -80,7 +85,7 @@ public void start(int svc) throws ChannelException {
8085
if (Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) {
8186
try {
8287
encryptionManager = createEncryptionManager(getEncryptionAlgorithm(), getEncryptionKeyInternal(),
83-
getProviderName());
88+
getProviderName(), getReplayWindowSize());
8489
} catch (GeneralSecurityException gse) {
8590
throw new ChannelException(sm.getString("encryptInterceptor.init.failed"), gse);
8691
}
@@ -114,9 +119,12 @@ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPay
114119
throws ChannelException {
115120
try {
116121
byte[] data = msg.getMessage().getBytes();
122+
byte[] message = new byte[data.length + 8];
123+
XByteBuffer.toBytes(encryptionManager.getAndIncrementMessageNumber(), message, 0);
124+
System.arraycopy(data, 0, message, 8, data.length);
117125

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

121129
XByteBuffer xbb = msg.getMessage();
122130

@@ -139,19 +147,34 @@ public void messageReceived(ChannelMessage msg) {
139147
byte[] data = msg.getMessage().getBytes();
140148

141149
data = encryptionManager.decrypt(data);
150+
if (data.length < 8) {
151+
throw new GeneralSecurityException(sm.getString("encryptInterceptor.decrypt.error.short-message"));
152+
}
153+
if (!encryptionManager.checkIncomingMessageNumber(msg.getAddress(), XByteBuffer.toLong(data, 0))) {
154+
log.error(sm.getString("encryptInterceptor.decrypt.replay"));
155+
return;
156+
}
142157

143158
XByteBuffer xbb = msg.getMessage();
144159

145160
// Completely replace the message with the decrypted one
146161
xbb.clear();
147-
xbb.append(data, 0, data.length);
162+
xbb.append(data, 8, data.length - 8);
148163

149164
super.messageReceived(msg);
150165
} catch (GeneralSecurityException gse) {
151166
log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse);
152167
}
153168
}
154169

170+
@Override
171+
public void memberDisappeared(Member member) {
172+
if (encryptionManager != null) {
173+
encryptionManager.memberDisappeared(member);
174+
}
175+
super.memberDisappeared(member);
176+
}
177+
155178
/**
156179
* Sets the encryption algorithm to be used for encrypting and decrypting channel messages. You must specify the
157180
* <code>algorithm/mode/padding</code>. Information on standard algorithm names may be found in the
@@ -274,6 +297,36 @@ public String getProviderName() {
274297
return providerName;
275298
}
276299

300+
/**
301+
* Returns the number of message sequence numbers remembered for replay detection.
302+
*
303+
* @return The replay window size
304+
*/
305+
@Override
306+
public int getReplayWindowSize() {
307+
return replayWindowSize;
308+
}
309+
310+
/**
311+
* Sets the number of message sequence numbers remembered for replay detection.
312+
*
313+
* @param replayWindowSize The replay window size
314+
*/
315+
@Override
316+
public void setReplayWindowSize(int replayWindowSize) {
317+
if (replayWindowSize < 1) {
318+
throw new IllegalArgumentException("replayWindowSize must be greater than zero");
319+
}
320+
this.replayWindowSize = replayWindowSize;
321+
}
322+
323+
Long getRemovedMemberHeadValue(Member member) {
324+
if (encryptionManager == null) {
325+
return null;
326+
}
327+
return encryptionManager.getRemovedMemberHeadValue(member);
328+
}
329+
277330
// Copied from org.apache.tomcat.util.buf.HexUtils
278331
// @formatter:off
279332
private static final int[] DEC = {
@@ -320,7 +373,8 @@ private static byte[] fromHexString(String input) {
320373
}
321374

322375
private static BaseEncryptionManager createEncryptionManager(String algorithm, byte[] encryptionKey,
323-
String providerName) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
376+
String providerName, int replayWindowSize)
377+
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
324378
if (null == encryptionKey) {
325379
throw new IllegalStateException(sm.getString("encryptInterceptor.key.required"));
326380
}
@@ -359,8 +413,7 @@ private static BaseEncryptionManager createEncryptionManager(String algorithm, b
359413
*/
360414
if ("NONE".equals(algorithmMode) || "ECB".equals(algorithmMode) || "PCBC".equals(algorithmMode) ||
361415
"CTS".equals(algorithmMode) || "KW".equals(algorithmMode) || "KWP".equals(algorithmMode) ||
362-
"CTR".equals(algorithmMode) ||
363-
("CBC".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
416+
"CTR".equals(algorithmMode) || ("CBC".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
364417
("CFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) ||
365418
("GCM".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding)) ||
366419
("OFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding))) {
@@ -375,17 +428,18 @@ private static BaseEncryptionManager createEncryptionManager(String algorithm, b
375428

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

381433
} else if ("GCM".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) {
382434
// Needs a specialised encryption manager to handle the differences between GCM and other modes
383-
return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName);
435+
return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName,
436+
replayWindowSize);
384437
}
385438

386439
// Use the default encryption manager
387440
try {
388-
return new BaseEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName);
441+
return new BaseEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName,
442+
replayWindowSize);
389443
} catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException ex) {
390444
throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported", algorithm), ex);
391445
}
@@ -423,24 +477,77 @@ private static class BaseEncryptionManager {
423477
* SecureRandom is thread-safe, but sharing a single instance will likely be a bottleneck.
424478
*/
425479
private final ConcurrentLinkedQueue<SecureRandom> randomPool;
480+
private final AtomicLong messageNumberGenerator = new AtomicLong();
481+
private final Map<Member,CyclicTracker> receivedMessageNumbersByMember = new ConcurrentHashMap<>();
482+
private final Map<Member,Long> messageNumbersByRemovedMember = new ConcurrentHashMap<>();
483+
private final CyclicTracker receivedMessageNumbersForUnknownSender;
484+
private final int replayWindowSize;
426485

427-
BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
486+
BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName, int replayWindowSize)
428487
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
429488
this.algorithm = algorithm;
430489
this.providerName = providerName;
431490
this.secretKey = secretKey;
491+
this.replayWindowSize = replayWindowSize;
432492

433493
cipherPool = new ConcurrentLinkedQueue<>();
434494
Cipher cipher = createCipher();
435495
blockSize = cipher.getBlockSize();
436496
cipherPool.offer(cipher);
437497
randomPool = new ConcurrentLinkedQueue<>();
498+
receivedMessageNumbersForUnknownSender = new CyclicTracker(replayWindowSize);
438499
}
439500

440501
public void shutdown() {
441502
// Individual Cipher and SecureRandom objects need no explicit tear down
442503
cipherPool.clear();
443504
randomPool.clear();
505+
receivedMessageNumbersByMember.clear();
506+
messageNumbersByRemovedMember.clear();
507+
}
508+
509+
public long getAndIncrementMessageNumber() {
510+
return messageNumberGenerator.getAndIncrement();
511+
}
512+
513+
public boolean checkIncomingMessageNumber(Member sender, long messageNumber) {
514+
if (sender == null) {
515+
return receivedMessageNumbersForUnknownSender.track(messageNumber);
516+
}
517+
return receivedMessageNumbersByMember.computeIfAbsent(sender, this::createTrackerForMember)
518+
.track(messageNumber);
519+
}
520+
521+
public void memberDisappeared(Member member) {
522+
CyclicTracker tracker = receivedMessageNumbersByMember.remove(member);
523+
if (tracker != null) {
524+
/*
525+
* There is a security trade off here.
526+
*
527+
* Entries are only removed from this Map if the Member reappears. That means there is a potential DoS
528+
* risks due to the growth of this Map. That is considered unlikely as only Members with the encryption
529+
* key will be added to this Map and the size of the Map.Entry is minimal.
530+
*
531+
* If entries are removed from this Map based either on Map size or time, that exposes the risk of a
532+
* replay attack using any message the Member may have previously sent.
533+
*
534+
* The replay attack is viewed as the higher risk, hence there are no limits on the size of this Map.
535+
*/
536+
messageNumbersByRemovedMember.put(member, Long.valueOf(tracker.getHeadValue()));
537+
}
538+
}
539+
540+
public Long getRemovedMemberHeadValue(Member member) {
541+
return messageNumbersByRemovedMember.get(member);
542+
}
543+
544+
private CyclicTracker createTrackerForMember(Member member) {
545+
CyclicTracker tracker = new CyclicTracker(replayWindowSize);
546+
Long headValue = messageNumbersByRemovedMember.remove(member);
547+
if (headValue != null) {
548+
tracker.track(headValue.longValue());
549+
}
550+
return tracker;
444551
}
445552

446553
private String getAlgorithm() {
@@ -611,9 +718,9 @@ protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int offset, int leng
611718
* number of bits supported 128-bit provide the best security.
612719
*/
613720
private static class GCMEncryptionManager extends BaseEncryptionManager {
614-
GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
721+
GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName, int replayWindowSize)
615722
throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
616-
super(algorithm, secretKey, providerName);
723+
super(algorithm, secretKey, providerName, replayWindowSize);
617724
}
618725

619726
@Override

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,18 @@ public interface EncryptInterceptorMBean {
7676
* @return the JCA provider name, or {@code null} for default
7777
*/
7878
String getProviderName();
79+
80+
/**
81+
* Returns the number of message sequence numbers remembered for replay detection.
82+
*
83+
* @return the replay window size
84+
*/
85+
int getReplayWindowSize();
86+
87+
/**
88+
* Sets the number of message sequence numbers remembered for replay detection.
89+
*
90+
* @param replayWindowSize the replay window size
91+
*/
92+
void setReplayWindowSize(int replayWindowSize);
7993
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ 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

0 commit comments

Comments
 (0)