2222import java .security .NoSuchProviderException ;
2323import java .security .SecureRandom ;
2424import java .security .spec .AlgorithmParameterSpec ;
25+ import java .util .ArrayDeque ;
26+ import java .util .HashMap ;
2527import java .util .Locale ;
28+ import java .util .Map ;
29+ import java .util .TreeMap ;
2630import java .util .concurrent .ConcurrentLinkedQueue ;
2731
2832import javax .crypto .Cipher ;
3640import org .apache .catalina .tribes .ChannelInterceptor ;
3741import org .apache .catalina .tribes .ChannelMessage ;
3842import org .apache .catalina .tribes .Member ;
43+ import org .apache .catalina .tribes .UniqueId ;
3944import org .apache .catalina .tribes .group .ChannelInterceptorBase ;
4045import org .apache .catalina .tribes .group .InterceptorPayload ;
4146import 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
0 commit comments