Author: schultz Date: Fri Nov 23 21:18:48 2018 New Revision: 1847318 URL: http://svn.apache.org/viewvc?rev=1847318&view=rev Log: Fix EncryptInterceptor to be thread-safe. Add multi-threaded unit test.
Modified: tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java?rev=1847318&r1=1847317&r2=1847318&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java (original) +++ tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java Fri Nov 23 21:18:48 2018 @@ -17,13 +17,10 @@ package org.apache.catalina.tribes.group.interceptors; import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; import java.security.SecureRandom; +import java.util.concurrent.ConcurrentLinkedQueue; -import javax.crypto.BadPaddingException; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; @@ -61,10 +58,14 @@ public class EncryptInterceptor extends private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM; private byte[] encryptionKeyBytes; private String encryptionKeyString; - private SecretKeySpec secretKey; - private Cipher encryptionCipher; - private Cipher decryptionCipher; + /** + * This is the name of the core encryption algorithm e.g. AES. + */ + private String algorithmName; + private SecretKeySpec secretKey; + private ConcurrentLinkedQueue<Cipher> cipherPool; + private ConcurrentLinkedQueue<SecureRandom> randomPool; public EncryptInterceptor() { } @@ -84,6 +85,17 @@ public class EncryptInterceptor extends } @Override + public void stop(int svc) throws ChannelException { + if(Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) { + // Individual Cipher and SecureRandom objects need no explicit teardown + cipherPool.clear(); + randomPool.clear(); + } + + super.stop(svc); + } + + @Override public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { try { @@ -101,18 +113,9 @@ public class EncryptInterceptor extends super.sendMessage(destination, msg, payload); - } catch (IllegalBlockSizeException ibse) { - log.error(sm.getString("encryptInterceptor.encrypt.failed")); - throw new ChannelException(ibse); - } catch (BadPaddingException bpe) { - log.error(sm.getString("encryptInterceptor.encrypt.failed")); - throw new ChannelException(bpe); - } catch (InvalidKeyException ike) { + } catch (GeneralSecurityException gse) { log.error(sm.getString("encryptInterceptor.encrypt.failed")); - throw new ChannelException(ike); - } catch (InvalidAlgorithmParameterException iape) { - log.error(sm.getString("encryptInterceptor.encrypt.failed")); - throw new ChannelException(iape); + throw new ChannelException(gse); } } @@ -130,14 +133,8 @@ public class EncryptInterceptor extends xbb.append(data, 0, data.length); super.messageReceived(msg); - } catch (IllegalBlockSizeException ibse) { - log.error(sm.getString("encryptInterceptor.decrypt.failed"), ibse); - } catch (BadPaddingException bpe) { - log.error(sm.getString("encryptInterceptor.decrypt.failed"), bpe); - } catch (InvalidKeyException ike) { - log.error(sm.getString("encryptInterceptor.decrypt.failed"), ike); - } catch (InvalidAlgorithmParameterException iape) { - log.error(sm.getString("encryptInterceptor.decrypt.failed"), iape); + } catch (GeneralSecurityException gse) { + log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse); } } @@ -261,6 +258,22 @@ public class EncryptInterceptor extends return providerName; } + private void setSecretKey(SecretKeySpec secretKey) { + this.secretKey = secretKey; + } + + private SecretKeySpec getSecretKey() { + return secretKey; + } + + private void setAlgorithmName(String algorithm) { + algorithmName = algorithm; + } + + private String getAlgorithmName() { + return algorithmName; + } + private void initCiphers() throws GeneralSecurityException { if(null == getEncryptionKey()) throw new IllegalStateException(sm.getString("encryptInterceptor.key.required")); @@ -294,32 +307,45 @@ public class EncryptInterceptor extends || "CFB".equalsIgnoreCase(algorithmMode))) throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported-mode", algorithmMode)); + setAlgorithmName(algorithm); setSecretKey(new SecretKeySpec(getEncryptionKeyInternal(), algorithmName)); - String providerName = getProviderName(); - if(null == providerName) { - encryptionCipher = Cipher.getInstance(algorithm); - decryptionCipher = Cipher.getInstance(algorithm); - } else { - encryptionCipher = Cipher.getInstance(algorithm, getProviderName()); - decryptionCipher = Cipher.getInstance(algorithm, getProviderName()); - } + cipherPool = new ConcurrentLinkedQueue<>(); + randomPool = new ConcurrentLinkedQueue<>(); } - private void setSecretKey(SecretKeySpec secretKey) { - this.secretKey = secretKey; + private Cipher getCipher() throws GeneralSecurityException { + Cipher cipher = cipherPool.poll(); + + if(null == cipher) { + String providerName = getProviderName(); + + if(null == providerName) { + return Cipher.getInstance(getAlgorithmName()); + } else { + return Cipher.getInstance(getAlgorithmName(), providerName); + } + } + + return cipher; } - private SecretKeySpec getSecretKey() { - return secretKey; + private void returnCipher(Cipher cipher) { + cipherPool.offer(cipher); } - private Cipher getEncryptionCipher() { - return encryptionCipher; + private SecureRandom getRandom() throws GeneralSecurityException { + SecureRandom random = randomPool.poll(); + + if(null == random) { + random = new SecureRandom(); + } + + return random; } - private Cipher getDecryptionCipher() { - return decryptionCipher; + private void returnRandom(SecureRandom random) { + randomPool.offer(random); } /** @@ -335,33 +361,39 @@ public class EncryptInterceptor extends * * @return The IV in [0] and the encrypted data in [1]. * - * @throws IllegalBlockSizeException If the input data is not a multiple of - * the block size and no padding has been requested (for block - * ciphers) or if the input data cannot be encrypted - * @throws BadPaddingException Declared but should not occur during encryption - * @throws InvalidAlgorithmParameterException If the algorithm is invalid - * @throws InvalidKeyException If the key is invalid + * @throws GeneralSecurityException If the input data cannot be encrypted. */ - private byte[][] encrypt(byte[] bytes) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { - Cipher cipher = getEncryptionCipher(); - - byte[] iv = new byte[cipher.getBlockSize()]; - - // Always use a random IV For cipher setup. - // The recipient doesn't need the (matching) IV because we will always - // pre-pad messages with the IV as a nonce. - new SecureRandom().nextBytes(iv); + private byte[][] encrypt(byte[] bytes) throws GeneralSecurityException { + Cipher cipher = null; + SecureRandom random = null; - IvParameterSpec IV = new IvParameterSpec(iv); - - cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), IV); + try { + cipher = getCipher(); + random = getRandom(); - // Prepend the IV to the beginning of the encrypted data - byte[][] data = new byte[2][]; - data[0] = iv; - data[1] = cipher.doFinal(bytes); + byte[] iv = new byte[cipher.getBlockSize()]; - return data; + // Always use a random IV For cipher setup. + // The recipient doesn't need the (matching) IV because we will always + // pre-pad messages with the IV as a nonce. + random.nextBytes(iv); + + IvParameterSpec IV = new IvParameterSpec(iv); + + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), IV); + + // Prepend the IV to the beginning of the encrypted data + byte[][] data = new byte[2][]; + data[0] = iv; + data[1] = cipher.doFinal(bytes); + + return data; + } finally { + if(null != cipher) + returnCipher(cipher); + if(null != random) + returnRandom(random); + } } /** @@ -371,23 +403,26 @@ public class EncryptInterceptor extends * * @return The decrypted data. * - * @throws IllegalBlockSizeException If the input data cannot be encrypted - * @throws BadPaddingException If the decrypted data does not include the - * expected number of padding bytes - * @throws InvalidAlgorithmParameterException If the algorithm is invalid - * @throws InvalidKeyException If the key is invalid + * @throws GeneralSecurityException If the input data cannot be decrypted. */ - private byte[] decrypt(byte[] bytes) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { - Cipher cipher = getDecryptionCipher(); + private byte[] decrypt(byte[] bytes) throws GeneralSecurityException { + Cipher cipher = null; - int blockSize = cipher.getBlockSize(); + try { + cipher = getCipher(); - // Use first-block of incoming data as IV - IvParameterSpec IV = new IvParameterSpec(bytes, 0, blockSize); - cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IV); + int blockSize = cipher.getBlockSize(); - // Decrypt remainder of the message. - return cipher.doFinal(bytes, blockSize, bytes.length - blockSize); + // Use first-block of incoming data as IV + IvParameterSpec IV = new IvParameterSpec(bytes, 0, blockSize); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IV); + + // Decrypt remainder of the message. + return cipher.doFinal(bytes, blockSize, bytes.length - blockSize); + } finally { + if(null != cipher) + returnCipher(cipher); + } } Modified: tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java?rev=1847318&r1=1847317&r2=1847318&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java (original) +++ tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java Fri Nov 23 21:18:48 2018 @@ -19,6 +19,8 @@ package org.apache.catalina.tribes.group import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Collection; import org.hamcrest.core.IsEqual; import org.hamcrest.core.IsNot; @@ -349,6 +351,60 @@ public class TestEncryptInterceptor { } /** + * This test isn't guaranteed to catch any multithreaded issues, but it + * gives a good exercise. + */ + @Test + public void testMultithreaded() throws Exception { + String inputValue = "A test string to fight over."; + final byte[] bytes = inputValue.getBytes("UTF-8"); + int numThreads = 100; + final int messagesPerThread = 10; + + dest.setPrevious(new ValuesCaptureInterceptor()); + + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + Runnable job = new Runnable() { + public void run() { + try { + ChannelData msg = new ChannelData(false); + XByteBuffer xbb = new XByteBuffer(1024, false); + xbb.append(bytes, 0, bytes.length); + msg.setMessage(xbb); + + for(int i=0; i<messagesPerThread; ++i) + src.sendMessage(null, msg, null); + } catch (ChannelException e) { + Assert.fail("Encountered exception sending messages: " + e.getMessage()); + } + } + }; + + Thread[] threads = new Thread[numThreads]; + for(int i=0; i<numThreads; ++i) { + threads[i] = new Thread(job); + threads[i].setName("Message-Thread-" + i); + } + + for(int i=0; i<numThreads; ++i) + threads[i].start(); + + for(int i=0; i<numThreads; ++i) + threads[i].join(); + + // Check all received messages to make sure they are not corrupted + Collection<byte[]> messages = ((ValuesCaptureInterceptor)dest.getPrevious()).getValues(); + + Assert.assertEquals("Did not receive all expected messages", + numThreads * messagesPerThread, messages.size()); + + for(byte[] message : messages) + Assert.assertArrayEquals("Message is corrupted", message, bytes); + } + + /** * Interceptor that delivers directly to a destination. */ private static class PipedInterceptor @@ -393,4 +449,33 @@ public class TestEncryptInterceptor { return value; } } + + /** + * Interceptor that simply captures all messages sent to or received by it. + */ + private static class ValuesCaptureInterceptor + extends ChannelInterceptorBase + { + private ArrayList<byte[]> messages = new ArrayList<byte[]>(); + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + synchronized(messages) { + messages.add(msg.getMessage().getBytes()); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + synchronized(messages) { + messages.add(msg.getMessage().getBytes()); + } + } + + @SuppressWarnings("unchecked") + public Collection<byte[]> getValues() { + return (Collection<byte[]>)messages.clone(); + } + } } Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1847318&r1=1847317&r2=1847318&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Fri Nov 23 21:18:48 2018 @@ -159,6 +159,10 @@ executor from the Catalina Service. If running independently, the Channel will provide the executor. (remm) </update> + <fix> + Make EncryptInterceptor thread-safe. This makes this interceptor + actually usable. (schultz/markt) + </fix> </changelog> </subsection> <subsection name="Other"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org