Author: schultz
Date: Wed Nov 21 15:15:34 2018
New Revision: 1847118
URL: http://svn.apache.org/viewvc?rev=1847118&view=rev
Log:
Use random IVs for encryption.
Support cipher block modes other than CBC.
Expand unit tests.
Modified:
tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
tomcat/trunk/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
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=1847118&r1=1847117&r2=1847118&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
Wed Nov 21 15:15:34 2018
@@ -17,6 +17,8 @@
package org.apache.catalina.tribes.group.interceptors;
import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
@@ -59,6 +61,7 @@ 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;
@@ -92,7 +95,7 @@ public class EncryptInterceptor extends
XByteBuffer xbb = msg.getMessage();
// Completely replace the message
- xbb.setLength(0);
+ xbb.clear();
xbb.append(bytes[0], 0, bytes[0].length);
xbb.append(bytes[1], 0, bytes[1].length);
@@ -104,6 +107,12 @@ public class EncryptInterceptor extends
} catch (BadPaddingException bpe) {
log.error(sm.getString("encryptInterceptor.encrypt.failed"));
throw new ChannelException(bpe);
+ } catch (InvalidKeyException ike) {
+ 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);
}
}
@@ -114,25 +123,21 @@ public class EncryptInterceptor extends
data = decrypt(data);
- // Remove the decrypted IV/nonce block from the front of the
message
- int blockSize = getDecryptionCipher().getBlockSize();
- int trimmedSize = data.length - blockSize;
- if(trimmedSize < 0) {
-
log.error(sm.getString("encryptInterceptor.decrypt.error.short-message"));
- throw new
IllegalStateException(sm.getString("encryptInterceptor.decrypt.error.short-message"));
- }
-
XByteBuffer xbb = msg.getMessage();
// Completely replace the message with the decrypted one
- xbb.setLength(0);
- xbb.append(data, blockSize, data.length - blockSize);
+ xbb.clear();
+ 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);
}
}
@@ -262,55 +267,51 @@ public class EncryptInterceptor extends
String algorithm = getEncryptionAlgorithm();
- String mode = getAlgorithmMode(algorithm);
-
- if(!"CBC".equalsIgnoreCase(mode))
- throw new
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.requires-cbc-mode",
mode));
-
- Cipher cipher;
-
- String providerName = getProviderName();
- if(null == providerName) {
- cipher = Cipher.getInstance(algorithm);
- } else {
- cipher = Cipher.getInstance(algorithm, getProviderName());
- }
-
- byte[] iv = new byte[cipher.getBlockSize()];
+ String algorithmName;
+ String algorithmMode;
- // 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);
-
- IvParameterSpec IV = new IvParameterSpec(iv);
-
- // If this is a cipher transform of the form ALGO/MODE/PAD,
+ // We need to break-apart the algorithm name e.g. AES/CBC/PKCS5Padding
// take just the algorithm part.
int pos = algorithm.indexOf('/');
- String bareAlgorithm;
if(pos >= 0) {
- bareAlgorithm = algorithm.substring(0, pos);
+ algorithmName = algorithm.substring(0, pos);
+ int pos2 = algorithm.indexOf('/', pos+1);
+
+ if(pos2 >= 0) {
+ algorithmMode = algorithm.substring(pos + 1, pos2);
+ } else {
+ algorithmMode = "CBC";
+ }
} else {
- bareAlgorithm = algorithm;
+ algorithmName = algorithm;
+ algorithmMode = "CBC";
}
- SecretKeySpec encryptionKey = new SecretKeySpec(getEncryptionKey(),
bareAlgorithm);
+ // Note: ECB is not an appropriate mode for secure communications.
+ if(!("CBC".equalsIgnoreCase(algorithmMode)
+ || "OFB".equalsIgnoreCase(algorithmMode)
+ || "CFB".equalsIgnoreCase(algorithmMode)))
+ throw new
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported-mode",
algorithmMode));
- cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, IV);
-
- encryptionCipher = cipher;
+ setSecretKey(new SecretKeySpec(getEncryptionKeyInternal(),
algorithmName));
+ String providerName = getProviderName();
if(null == providerName) {
- cipher = Cipher.getInstance(algorithm);
+ encryptionCipher = Cipher.getInstance(algorithm);
+ decryptionCipher = Cipher.getInstance(algorithm);
} else {
- cipher = Cipher.getInstance(algorithm, getProviderName());
+ encryptionCipher = Cipher.getInstance(algorithm,
getProviderName());
+ decryptionCipher = Cipher.getInstance(algorithm,
getProviderName());
}
+ }
- cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new
IvParameterSpec(iv));
+ private void setSecretKey(SecretKeySpec secretKey) {
+ this.secretKey = secretKey;
+ }
- decryptionCipher = cipher;
+ private SecretKeySpec getSecretKey() {
+ return secretKey;
}
private Cipher getEncryptionCipher() {
@@ -321,20 +322,9 @@ public class EncryptInterceptor extends
return decryptionCipher;
}
- private static String getAlgorithmMode(String algorithm) {
- int start = algorithm.indexOf('/');
- if(start < 0)
- throw new
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
- int end = algorithm.indexOf('/', start + 1);
- if(end < 0)
- throw new
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
-
- return algorithm.substring(start + 1, end);
- }
-
/**
* Encrypts the input <code>bytes</code> into two separate byte arrays:
- * one for the initial block (which will be the encrypted random IV)
+ * one for the random initialization vector (IV) used for this message,
* and the second one containing the actual encrypted payload.
*
* This method returns a pair of byte arrays instead of a single
@@ -343,21 +333,32 @@ public class EncryptInterceptor extends
*
* @param bytes The data to encrypt.
*
- * @return The encrypted IV block in [0] and the encrypted data in [1].
+ * @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
*/
- private byte[][] encrypt(byte[] bytes) throws IllegalBlockSizeException,
BadPaddingException {
+ private byte[][] encrypt(byte[] bytes) throws IllegalBlockSizeException,
BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = getEncryptionCipher();
- // Adding the IV to the beginning of the encrypted data
- byte[] iv = cipher.getIV();
+ 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);
+
+ 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] = cipher.update(iv, 0, iv.length);
+ data[0] = iv;
data[1] = cipher.doFinal(bytes);
return data;
@@ -373,10 +374,20 @@ public class EncryptInterceptor extends
* @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
*/
- private byte[] decrypt(byte[] bytes) throws IllegalBlockSizeException,
BadPaddingException {
- return getDecryptionCipher().doFinal(bytes);
+ private byte[] decrypt(byte[] bytes) throws IllegalBlockSizeException,
BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
+ Cipher cipher = getDecryptionCipher();
+
+ int blockSize = cipher.getBlockSize();
+
+ // 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);
}
Modified:
tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties?rev=1847118&r1=1847117&r2=1847118&view=diff
==============================================================================
---
tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
[UTF-8] (original)
+++
tomcat/trunk/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
[UTF-8] Wed Nov 21 15:15:34 2018
@@ -17,7 +17,7 @@ domainFilterInterceptor.member.refused=M
domainFilterInterceptor.message.refused=Received message from cluster[{0}] was
refused.
encryptInterceptor.algorithm.required=Encryption algorithm is required,
fully-specified e.g. AES/CBC/PKCS5Padding
-encryptInterceptor.algorithm.requires-cbc-mode=EncryptInterceptor only
supports CBC cipher modes, not [{0}]
+encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptor does not
support block cipher mode [{0}]
encryptInterceptor.decrypt.error.short-message=Failed to decrypt message:
premature end-of-message
encryptInterceptor.decrypt.failed=Failed to decrypt message
encryptInterceptor.encrypt.failed=Failed to encrypt message
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=1847118&r1=1847117&r2=1847118&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
Wed Nov 21 15:15:34 2018
@@ -18,8 +18,14 @@ package org.apache.catalina.tribes.group
import org.junit.Assert;
import org.junit.Before;
+import org.junit.FixMethodOrder;
import org.junit.Ignore;
import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import org.apache.catalina.tribes.Channel;
import org.apache.catalina.tribes.ChannelException;
@@ -38,8 +44,9 @@ import org.apache.catalina.tribes.io.XBy
* though the interceptor actually operates on byte arrays. This is done
* for readability for the tests and their outputs.
*/
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestEncryptInterceptor {
- private static final String encryptionKey128 =
"cafebabedeadbeefcafebabedeadbeef";
+ private static final String encryptionKey128 =
"cafebabedeadbeefbeefcafecafebabe";
private static final String encryptionKey192 =
"cafebabedeadbeefbeefcafecafebabedeadbeefbeefcafe";
private static final String encryptionKey256 =
"cafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeef";
@@ -95,7 +102,7 @@ public class TestEncryptInterceptor {
}
@Test
- @Ignore // Too big for default settings. Breaks Gump, Eclipse, ...
+ @Ignore("Too big for default settings. Breaks Gump, Eclipse, ...")
public void testHugePayload() throws Exception {
src.start(Channel.SND_TX_SEQ);
dest.start(Channel.SND_TX_SEQ);
@@ -157,7 +164,7 @@ public class TestEncryptInterceptor {
bytes = roundTrip(bytes, src, dest);
- return new
String(((ValueCaptureInterceptor)dest.getPrevious()).getValue(), "UTF-8");
+ return new String(bytes, "UTF-8");
}
/**
@@ -171,6 +178,123 @@ public class TestEncryptInterceptor {
return ((ValueCaptureInterceptor)dest.getPrevious()).getValue();
}
+ @Test
+ @Ignore("ECB mode isn't because it's insecure")
+ public void testECB() throws Exception {
+ src.setEncryptionAlgorithm("AES/ECB/PKCS5Padding");
+ src.start(Channel.SND_TX_SEQ);
+ dest.setEncryptionAlgorithm("AES/ECB/PKCS5Padding");
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed in ECB mode",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testOFB() throws Exception {
+ src.setEncryptionAlgorithm("AES/OFB/PKCS5Padding");
+ src.start(Channel.SND_TX_SEQ);
+ dest.setEncryptionAlgorithm("AES/OFB/PKCS5Padding");
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed in OFB mode",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testCFB() throws Exception {
+ src.setEncryptionAlgorithm("AES/CFB/PKCS5Padding");
+ src.start(Channel.SND_TX_SEQ);
+ dest.setEncryptionAlgorithm("AES/CFB/PKCS5Padding");
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed in CFB mode",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ @Ignore("GCM mode is unsupported because it requires a custom
initialization vector")
+ public void testGCM() throws Exception {
+ src.setEncryptionAlgorithm("AES/GCM/PKCS5Padding");
+ src.start(Channel.SND_TX_SEQ);
+ dest.setEncryptionAlgorithm("AES/GCM/PKCS5Padding");
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed in GCM mode",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testViaFile() throws Exception {
+ src.start(Channel.SND_TX_SEQ);
+ src.setNext(new ValueCaptureInterceptor());
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ ChannelData msg = new ChannelData(false);
+ msg.setMessage(new XByteBuffer(testInput.getBytes("UTF-8"), false));
+ src.sendMessage(null, msg, null);
+
+ byte[] bytes = ((ValueCaptureInterceptor)src.getNext()).getValue();
+
+ try (FileOutputStream out = new FileOutputStream("message.bin")) {
+ out.write(bytes);
+ }
+
+ dest.start(Channel.SND_TX_SEQ);
+
+ bytes = new byte[8192];
+ int read;
+
+ try (FileInputStream in = new FileInputStream("message.bin")) {
+ read = in.read(bytes);
+ }
+
+ msg = new ChannelData(false);
+ XByteBuffer xbb = new XByteBuffer(read, false);
+ xbb.append(bytes, 0, read);
+ msg.setMessage(xbb);
+
+ dest.messageReceived(msg);
+ }
+
+ @Test
+ public void testPickup() throws Exception {
+ File file = new File("message.bin");
+ if(!file.exists()) {
+ System.err.println("File message.bin does not exist. Skipping
test.");
+ return;
+ }
+
+ dest.start(Channel.SND_TX_SEQ);
+
+ byte[] bytes = new byte[8192];
+ int read;
+
+ try (FileInputStream in = new FileInputStream("message.bin")) {
+ read = in.read(bytes);
+ }
+
+ ChannelData msg = new ChannelData(false);
+ XByteBuffer xbb = new XByteBuffer(read, false);
+ xbb.append(bytes, 0, read);
+ msg.setMessage(xbb);
+
+ dest.messageReceived(msg);
+ }
+
/**
* Interceptor that delivers directly to a destination.
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]