2323import java .security .SecureRandom ;
2424import java .security .spec .AlgorithmParameterSpec ;
2525import java .util .Locale ;
26+ import java .util .Map ;
27+ import java .util .concurrent .ConcurrentHashMap ;
2628import java .util .concurrent .ConcurrentLinkedQueue ;
29+ import java .util .concurrent .atomic .AtomicLong ;
2730
2831import javax .crypto .Cipher ;
2932import javax .crypto .NoSuchPaddingException ;
3942import org .apache .catalina .tribes .group .ChannelInterceptorBase ;
4043import org .apache .catalina .tribes .group .InterceptorPayload ;
4144import org .apache .catalina .tribes .io .XByteBuffer ;
45+ import org .apache .catalina .tribes .util .CyclicTracker ;
4246import org .apache .catalina .tribes .util .StringManager ;
4347import org .apache .juli .logging .Log ;
4448import 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
0 commit comments