You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2020/01/20 21:51:46 UTC
[tomcat] branch 7.0.x updated: Back-port EncryptInterceptor from
Tomcat 8.5.x onwards.
This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 7.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/7.0.x by this push:
new 5668ae6 Back-port EncryptInterceptor from Tomcat 8.5.x onwards.
5668ae6 is described below
commit 5668ae6548722c6d78d5fa7d98e19f356454a1eb
Author: Christopher Schultz <sc...@apache.org>
AuthorDate: Sat Jan 5 20:52:28 2019 +0000
Back-port EncryptInterceptor from Tomcat 8.5.x onwards.
---
.../group/interceptors/EncryptInterceptor.java | 644 +++++++++++++++++++++
.../interceptors/EncryptInterceptorMBean.java | 31 +
.../group/interceptors/LocalStrings.properties | 24 +
res/checkstyle/org-import-control.xml | 1 +
.../group/interceptors/TestEncryptInterceptor.java | 542 +++++++++++++++++
webapps/docs/changelog.xml | 10 +
webapps/docs/config/cluster-interceptor.xml | 41 +-
webapps/docs/config/cluster.xml | 1 +
8 files changed, 1293 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
new file mode 100644
index 0000000..827bf78
--- /dev/null
+++ b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
@@ -0,0 +1,644 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.util.StringManager;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+
+/**
+ * Adds encryption using a pre-shared key.
+ *
+ * The length of the key (in bytes) must be acceptable for the encryption
+ * algorithm being used. For example, for AES, you must use a key of either
+ * 16 bytes (128 bits, 24 bytes 192 bits), or 32 bytes (256 bits).
+ *
+ * You can supply the raw key bytes by calling {@link #setEncryptionKey(byte[])}
+ * or the hex-encoded binary bytes by calling
+ * {@link #setEncryptionKey(String)}.
+ */
+public class EncryptInterceptor extends ChannelInterceptorBase implements EncryptInterceptorMBean {
+
+ private static final Log log = LogFactory.getLog(EncryptInterceptor.class);
+ protected static final StringManager sm = StringManager.getManager(EncryptInterceptor.class);
+
+ private static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
+
+ private String providerName;
+ private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM;
+ private byte[] encryptionKeyBytes;
+ private String encryptionKeyString;
+
+
+ private BaseEncryptionManager encryptionManager;
+
+ public EncryptInterceptor() {
+ }
+
+ @Override
+ public void start(int svc) throws ChannelException {
+ validateChannelChain();
+
+ if(Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) {
+ try {
+ encryptionManager = createEncryptionManager(getEncryptionAlgorithm(),
+ getEncryptionKeyInternal(),
+ getProviderName());
+ } catch (GeneralSecurityException gse) {
+ throw new ChannelException(sm.getString("encryptInterceptor.init.failed"), gse);
+ }
+ }
+
+ super.start(svc);
+ }
+
+ private void validateChannelChain() throws ChannelException {
+ ChannelInterceptor interceptor = getPrevious();
+ while(null != interceptor) {
+ if(interceptor instanceof TcpFailureDetector)
+ throw new ChannelConfigException(sm.getString("encryptInterceptor.tcpFailureDetector.ordering"));
+
+ interceptor = interceptor.getPrevious();
+ }
+ }
+
+ @Override
+ public void stop(int svc) throws ChannelException {
+ if(Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) {
+ encryptionManager.shutdown();
+ }
+
+ super.stop(svc);
+ }
+
+ @Override
+ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload)
+ throws ChannelException {
+ try {
+ byte[] data = msg.getMessage().getBytes();
+
+ // See #encrypt(byte[]) for an explanation of the return value
+ byte[][] bytes = encryptionManager.encrypt(data);
+
+ XByteBuffer xbb = msg.getMessage();
+
+ // Completely replace the message
+ xbb.clear();
+ xbb.append(bytes[0], 0, bytes[0].length);
+ xbb.append(bytes[1], 0, bytes[1].length);
+
+ super.sendMessage(destination, msg, payload);
+
+ } catch (GeneralSecurityException gse) {
+ log.error(sm.getString("encryptInterceptor.encrypt.failed"));
+ throw new ChannelException(gse);
+ }
+ }
+
+ @Override
+ public void messageReceived(ChannelMessage msg) {
+ try {
+ byte[] data = msg.getMessage().getBytes();
+
+ data = encryptionManager.decrypt(data);
+
+ XByteBuffer xbb = msg.getMessage();
+
+ // Completely replace the message with the decrypted one
+ xbb.clear();
+ xbb.append(data, 0, data.length);
+
+ super.messageReceived(msg);
+ } catch (GeneralSecurityException gse) {
+ log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse);
+ }
+ }
+
+ /**
+ * Sets the encryption algorithm to be used for encrypting and decrypting
+ * channel messages. You must specify the <code>algorithm/mode/padding</code>.
+ * Information on standard algorithm names may be found in the
+ * <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html">Java
+ * documentation</a>.
+ *
+ * Default is <code>AES/CBC/PKCS5Padding</code>.
+ *
+ * @param algorithm The algorithm to use.
+ */
+ @Override
+ public void setEncryptionAlgorithm(String algorithm) {
+ if(null == getEncryptionAlgorithm())
+ throw new IllegalStateException(sm.getString("encryptInterceptor.algorithm.required"));
+
+ int pos = algorithm.indexOf('/');
+ if(pos < 0)
+ throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
+ pos = algorithm.indexOf('/', pos + 1);
+ if(pos < 0)
+ throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
+
+ encryptionAlgorithm = algorithm;
+ }
+
+ /**
+ * Gets the encryption algorithm being used to encrypt and decrypt channel
+ * messages.
+ *
+ * @return The algorithm being used, including the algorithm mode and padding.
+ */
+ @Override
+ public String getEncryptionAlgorithm() {
+ return encryptionAlgorithm;
+ }
+
+ /**
+ * Sets the encryption key for encryption and decryption. The length of the
+ * key must be appropriate for the algorithm being used.
+ *
+ * @param key The encryption key.
+ */
+ @Override
+ public void setEncryptionKey(byte[] key) {
+ if (null == key) {
+ encryptionKeyBytes = null;
+ } else {
+ encryptionKeyBytes = key.clone();
+ }
+ }
+
+ /**
+ * Gets the encryption key being used for encryption and decryption.
+ * The key is encoded using hex-encoding where e.g. the byte <code>0xab</code>
+ * will be shown as "ab". The length of the string in characters will
+ * be twice the length of the key in bytes.
+ *
+ * @param keyBytes The encryption key.
+ */
+ public void setEncryptionKey(String keyBytes) {
+ this.encryptionKeyString = keyBytes;
+ if (null == keyBytes) {
+ setEncryptionKey((byte[])null);
+ } else {
+ setEncryptionKey(fromHexString(keyBytes.trim()));
+ }
+ }
+
+ /**
+ * Gets the encryption key being used for encryption and decryption.
+ *
+ * @return The encryption key.
+ */
+ @Override
+ public byte[] getEncryptionKey() {
+ byte[] key = getEncryptionKeyInternal();
+
+ if(null != key)
+ key = key.clone();
+
+ return key;
+ }
+
+ private byte[] getEncryptionKeyInternal() {
+ return encryptionKeyBytes;
+ }
+
+ public String getEncryptionKeyString() {
+ return encryptionKeyString;
+ }
+
+ public void setEncryptionKeyString(String encryptionKeyString) {
+ setEncryptionKey(encryptionKeyString);
+ }
+
+ /**
+ * Sets the JCA provider name used for cryptographic activities.
+ *
+ * Default is the JVM platform default.
+ *
+ * @param provider The name of the JCA provider.
+ */
+ @Override
+ public void setProviderName(String provider) {
+ providerName = provider;
+ }
+
+ /**
+ * Gets the JCA provider name used for cryptographic activities.
+ *
+ * Default is the JVM platform default.
+ *
+ * @return The name of the JCA provider.
+ */
+ @Override
+ public String getProviderName() {
+ return providerName;
+ }
+
+ // Copied from org.apache.tomcat.util.buf.HexUtils
+
+ private static final int[] DEC = {
+ 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1,
+ -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 10, 11, 12, 13, 14, 15,
+ };
+
+
+ private static int getDec(int index) {
+ // Fast for correct values, slower for incorrect ones
+ try {
+ return DEC[index - '0'];
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ return -1;
+ }
+ }
+
+
+ private static byte[] fromHexString(String input) {
+ if (input == null) {
+ return null;
+ }
+
+ if ((input.length() & 1) == 1) {
+ // Odd number of characters
+ throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.oddDigits"));
+ }
+
+ char[] inputChars = input.toCharArray();
+ byte[] result = new byte[input.length() >> 1];
+ for (int i = 0; i < result.length; i++) {
+ int upperNibble = getDec(inputChars[2*i]);
+ int lowerNibble = getDec(inputChars[2*i + 1]);
+ if (upperNibble < 0 || lowerNibble < 0) {
+ // Non hex character
+ throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.nonHex"));
+ }
+ result[i] = (byte) ((upperNibble << 4) + lowerNibble);
+ }
+ return result;
+ }
+
+ private static BaseEncryptionManager createEncryptionManager(String algorithm,
+ byte[] encryptionKey, String providerName)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
+ if(null == encryptionKey)
+ throw new IllegalStateException(sm.getString("encryptInterceptor.key.required"));
+
+ String algorithmName;
+ String algorithmMode;
+
+ // We need to break-apart the algorithm name e.g. AES/CBC/PKCS5Padding
+ // take just the algorithm part.
+ int pos = algorithm.indexOf('/');
+
+ if(pos >= 0) {
+ algorithmName = algorithm.substring(0, pos);
+ int pos2 = algorithm.indexOf('/', pos+1);
+
+ if(pos2 >= 0) {
+ algorithmMode = algorithm.substring(pos + 1, pos2);
+ } else {
+ algorithmMode = "CBC";
+ }
+ } else {
+ algorithmName = algorithm;
+ algorithmMode = "CBC";
+ }
+
+ if("GCM".equalsIgnoreCase(algorithmMode))
+ return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName);
+ else if("CBC".equalsIgnoreCase(algorithmMode)
+ || "OFB".equalsIgnoreCase(algorithmMode)
+ || "CFB".equalsIgnoreCase(algorithmMode))
+ return new BaseEncryptionManager(algorithm,
+ new SecretKeySpec(encryptionKey, algorithmName),
+ providerName);
+ else
+ throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported-mode", algorithmMode));
+ }
+
+ private static class BaseEncryptionManager {
+ /**
+ * The fully-specified algorithm e.g. AES/CBC/PKCS5Padding.
+ */
+ private final String algorithm;
+
+ /**
+ * The block size of the cipher.
+ */
+ private final int blockSize;
+
+ /**
+ * The cryptographic provider name.
+ */
+ private final String providerName;
+
+ /**
+ * The secret key to use for encryption and decryption operations.
+ */
+ private final SecretKeySpec secretKey;
+
+ /**
+ * A pool of Cipher objects. Ciphers are expensive to create, but not
+ * to re-initialize, so we use a pool of them which grows as necessary.
+ */
+ private final ConcurrentLinkedQueue<Cipher> cipherPool;
+
+ /**
+ * A pool of SecureRandom objects. Each encrypt operation requires access
+ * to a source of randomness. SecureRandom is thread-safe, but sharing a
+ * single instance will likely be a bottleneck.
+ */
+ private final ConcurrentLinkedQueue<SecureRandom> randomPool;
+
+ public BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
+ this.algorithm = algorithm;
+ this.providerName = providerName;
+ this.secretKey = secretKey;
+
+ cipherPool = new ConcurrentLinkedQueue<Cipher>();
+ Cipher cipher = createCipher();
+ blockSize = cipher.getBlockSize();
+ cipherPool.offer(cipher);
+ randomPool = new ConcurrentLinkedQueue<SecureRandom>();
+ }
+
+ public void shutdown() {
+ // Individual Cipher and SecureRandom objects need no explicit teardown
+ cipherPool.clear();
+ randomPool.clear();
+ }
+
+ private String getAlgorithm() {
+ return algorithm;
+ }
+
+ private SecretKeySpec getSecretKey() {
+ return secretKey;
+ }
+
+ /**
+ * Gets the size, in bytes, of the initialization vector for the
+ * cipher being used. The IV size is often, but not always, the block
+ * size for the cipher.
+ *
+ * @return The size of the initialization vector for this algorithm.
+ */
+ protected int getIVSize() {
+ return blockSize;
+ }
+
+ private String getProviderName() {
+ return providerName;
+ }
+
+ private Cipher createCipher()
+ throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
+ String providerName = getProviderName();
+
+ if(null == providerName) {
+ return Cipher.getInstance(getAlgorithm());
+ } else {
+ return Cipher.getInstance(getAlgorithm(), providerName);
+ }
+ }
+
+ private Cipher getCipher() throws GeneralSecurityException {
+ Cipher cipher = cipherPool.poll();
+
+ if(null == cipher) {
+ cipher = createCipher();
+ }
+
+ return cipher;
+ }
+
+ private void returnCipher(Cipher cipher) {
+ cipherPool.offer(cipher);
+ }
+
+ private SecureRandom getRandom() {
+ SecureRandom random = randomPool.poll();
+
+ if(null == random) {
+ random = new SecureRandom();
+ }
+
+ return random;
+ }
+
+ private void returnRandom(SecureRandom random) {
+ randomPool.offer(random);
+ }
+
+ /**
+ * Encrypts the input <code>bytes</code> into two separate byte arrays:
+ * 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
+ * concatenated one to reduce the number of byte buffers created
+ * and copied during the whole operation -- including message re-building.
+ *
+ * @param bytes The data to encrypt.
+ *
+ * @return The IV in [0] and the encrypted data in [1].
+ *
+ * @throws GeneralSecurityException If the input data cannot be encrypted.
+ */
+ private byte[][] encrypt(byte[] bytes) throws GeneralSecurityException {
+ Cipher cipher = null;
+
+ // 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.
+ byte[] iv = generateIVBytes();
+
+ try {
+ cipher = getCipher();
+ cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), generateIV(iv, 0, getIVSize()));
+
+ // 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);
+ }
+ }
+
+ /**
+ * Decrypts the input <code>bytes</code>.
+ *
+ * @param bytes The data to decrypt.
+ *
+ * @return The decrypted data.
+ *
+ * @throws GeneralSecurityException If the input data cannot be decrypted.
+ */
+ private byte[] decrypt(byte[] bytes) throws GeneralSecurityException {
+ Cipher cipher = null;
+
+ int ivSize = getIVSize();
+ AlgorithmParameterSpec IV = generateIV(bytes, 0, ivSize);
+
+ try {
+ cipher = getCipher();
+
+ cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IV);
+
+ // Decrypt remainder of the message.
+ return cipher.doFinal(bytes, ivSize, bytes.length - ivSize);
+ } finally {
+ if(null != cipher)
+ returnCipher(cipher);
+ }
+ }
+
+ protected byte[] generateIVBytes() {
+ byte[] ivBytes = new byte[getIVSize()];
+
+ SecureRandom random = null;
+
+ try {
+ random = getRandom();
+
+ // 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(ivBytes);
+
+ return ivBytes;
+ } finally {
+ if(null != random)
+ returnRandom(random);
+ }
+ }
+
+ protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int offset, int length) {
+ return new IvParameterSpec(ivBytes, offset, length);
+ }
+ }
+
+ /**
+ * Implements an EncryptionManager for using GCM block cipher modes.
+ *
+ * GCM works a little differently than some of the other block cipher modes
+ * supported by EncryptInterceptor. First of all, it requires a different
+ * kind of AlgorithmParameterSpec object to be used, and second, it
+ * requires a slightly different initialization vector and something called
+ * an "authentication tag".
+ *
+ * The choice of IV length can be somewhat arbitrary, but there is consensus
+ * that 96-bit (12-byte) IVs for GCM are the best trade-off between security
+ * and performance. For other block cipher modes, IV length is the same as
+ * the block size.
+ *
+ * The "authentication tag" is a computed authentication value based upon
+ * the message and the encryption process. GCM defines these tags as the
+ * number of bits to use for the authentication tag, and it's clear that
+ * the highest number of bits supported 128-bit provide the best security.
+ */
+ private static class GCMEncryptionManager extends BaseEncryptionManager
+ {
+ private static Constructor<?> gcmParameterSpecConstructor;
+
+ static {
+ Constructor<?> c1 = null;
+ try {
+ Class<?> clazz = Class.forName("javax.crypto.spec.GCMParameterSpec");
+ c1 = clazz.getConstructor(int.class, byte[].class, int.class, int.class);
+ } catch (ClassNotFoundException e) {
+ // Ignore
+ } catch (SecurityException e) {
+ // Ignore
+ } catch (NoSuchMethodException e) {
+ // Ignore
+ }
+
+ gcmParameterSpecConstructor = c1;
+ }
+
+ public GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
+ super(algorithm, secretKey, providerName);
+ }
+
+ @Override
+ protected int getIVSize() {
+ return 12; // See class javadoc for explanation of this magic number (12)
+ }
+
+ @Override
+ protected AlgorithmParameterSpec generateIV(byte[] bytes, int offset, int length) {
+ // Can't use org.apache.tomcat.util.compat as Tribes only has JULI
+ // as a dependency.
+ if (gcmParameterSpecConstructor == null) {
+ throw new UnsupportedOperationException(sm.getString("encryptInterceptor.noGCM"));
+ } else {
+ try {
+ return (AlgorithmParameterSpec) gcmParameterSpecConstructor.newInstance(
+ Integer.valueOf(128), bytes, Integer.valueOf(offset), Integer.valueOf(length));
+ } catch (IllegalArgumentException e) {
+ throw new UnsupportedOperationException(sm.getString("encryptInterceptor.noGCM"), e);
+ } catch (InstantiationException e) {
+ throw new UnsupportedOperationException(sm.getString("encryptInterceptor.noGCM"), e);
+ } catch (IllegalAccessException e) {
+ throw new UnsupportedOperationException(sm.getString("encryptInterceptor.noGCM"), e);
+ } catch (InvocationTargetException e) {
+ throw new UnsupportedOperationException(sm.getString("encryptInterceptor.noGCM"), e);
+ }
+ }
+ }
+ }
+
+ static class ChannelConfigException
+ extends ChannelException
+ {
+ private static final long serialVersionUID = 1L;
+
+ public ChannelConfigException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
new file mode 100644
index 0000000..dcf0f7b
--- /dev/null
+++ b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+public interface EncryptInterceptorMBean {
+
+ // Config
+ public int getOptionFlag();
+ public void setOptionFlag(int optionFlag);
+
+ public void setEncryptionAlgorithm(String algorithm);
+ public String getEncryptionAlgorithm();
+ public void setEncryptionKey(byte[] key);
+ public byte[] getEncryptionKey();
+ public void setProviderName(String provider);
+ public String getProviderName();
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
new file mode 100644
index 0000000..e216ba5
--- /dev/null
+++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
@@ -0,0 +1,24 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+encryptInterceptor.algorithm.required=Encryption algorithm is required, fully-specified e.g. AES/CBC/PKCS5Padding
+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
+encryptInterceptor.init.failed=Failed to initialize EncryptInterceptor
+encryptInterceptor.key.required=Encryption key is required
+encryptInterceptor.noGCM=The current JRE does not support GCM. You must use Java 7 or later to use this feature.
+encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor must be upstream of TcpFailureDetector. Please re-order EncryptInterceptor to be listed before TcpFailureDetector in your channel interceptor pipeline.
diff --git a/res/checkstyle/org-import-control.xml b/res/checkstyle/org-import-control.xml
index 463077b..c277886 100644
--- a/res/checkstyle/org-import-control.xml
+++ b/res/checkstyle/org-import-control.xml
@@ -23,6 +23,7 @@
<!-- Anything in J2SE is OK but need to list javax by package as not
all javax packages are in J2SE -->
<allow pkg="java"/>
+ <allow pkg="javax.crypto"/>
<allow class="javax.imageio.ImageIO"/>
<allow pkg="javax.management"/>
<allow pkg="javax.naming"/>
diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
new file mode 100644
index 0000000..a69a68b
--- /dev/null
+++ b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
@@ -0,0 +1,542 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+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;
+
+import org.junit.AfterClass;
+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 org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+
+/**
+ * Tests the EncryptInterceptor.
+ *
+ * Many of the tests in this class use strings as input and output, even
+ * 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 MESSAGE_FILE = "message.bin";
+
+ private static final String encryptionKey128 = "cafebabedeadbeefbeefcafecafebabe";
+ private static final String encryptionKey192 = "cafebabedeadbeefbeefcafecafebabedeadbeefbeefcafe";
+ private static final String encryptionKey256 = "cafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeef";
+
+ EncryptInterceptor src;
+ EncryptInterceptor dest;
+
+
+ @AfterClass
+ public static void cleanup() {
+ File f = new File(MESSAGE_FILE);
+ if (f.isFile()) {
+ Assert.assertTrue(f.delete());
+ }
+ }
+
+ @Before
+ public void setup() {
+ src = new EncryptInterceptor();
+ src.setEncryptionKey(encryptionKey128);
+
+ dest = new EncryptInterceptor();
+ dest.setEncryptionKey(encryptionKey128);
+
+ src.setNext(new PipedInterceptor(dest));
+ dest.setPrevious(new ValueCaptureInterceptor());
+ }
+
+ @Test
+ public void testBasic() throws Exception {
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Basic roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testMultipleMessages() throws Exception {
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Basic roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+
+ Assert.assertEquals("Second roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+
+ Assert.assertEquals("Third roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+
+ Assert.assertEquals("Fourth roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+
+ Assert.assertEquals("Fifth roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testTinyPayload() throws Exception {
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "x";
+
+ Assert.assertEquals("Tiny payload roundtrip failed",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void testLargePayload() throws Exception {
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ byte[] bytes = new byte[1024*1024];
+
+ Assert.assertArrayEquals("Huge payload roundtrip failed",
+ bytes,
+ roundTrip(bytes, src, dest));
+ }
+
+ @Test
+ @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);
+
+ byte[] bytes = new byte[1024*1024*1024];
+
+ Assert.assertArrayEquals("Huge payload roundtrip failed",
+ bytes,
+ roundTrip(bytes, src, dest));
+ }
+
+ @Test
+ public void testCustomProvider() throws Exception {
+ src.setProviderName("SunJCE"); // Explicitly set the provider name
+ dest.setProviderName("SunJCE");
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed to set custom provider name",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void test192BitKey() throws Exception {
+ src.setEncryptionKey(encryptionKey192);
+ dest.setEncryptionKey(encryptionKey192);
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed to set custom provider name",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ @Test
+ public void test256BitKey() throws Exception {
+ src.setEncryptionKey(encryptionKey256);
+ dest.setEncryptionKey(encryptionKey256);
+ src.start(Channel.SND_TX_SEQ);
+ dest.start(Channel.SND_TX_SEQ);
+
+ String testInput = "The quick brown fox jumps over the lazy dog.";
+
+ Assert.assertEquals("Failed to set custom provider name",
+ testInput,
+ roundTrip(testInput, src, dest));
+ }
+
+ /**
+ * Actually go through the interceptor's send/receive message methods.
+ */
+ private static String roundTrip(String input, EncryptInterceptor src, EncryptInterceptor dest) throws Exception {
+ byte[] bytes = input.getBytes("UTF-8");
+
+ bytes = roundTrip(bytes, src, dest);
+
+ return new String(bytes, "UTF-8");
+ }
+
+ /**
+ * Actually go through the interceptor's send/receive message methods.
+ */
+ private static byte[] roundTrip(byte[] input, EncryptInterceptor src, EncryptInterceptor dest) throws Exception {
+ ChannelData msg = new ChannelData(false);
+ msg.setMessage(new XByteBuffer(input, false));
+ src.sendMessage(null, msg, null);
+
+ return ((ValueCaptureInterceptor)dest.getPrevious()).getValue();
+ }
+
+ @Test
+ @Ignore("ECB mode isn't implemented 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
+ 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 testIllegalECB() throws Exception {
+ try {
+ src.setEncryptionAlgorithm("AES/ECB/PKCS5Padding");
+ src.start(Channel.SND_TX_SEQ);
+
+ // start() should trigger IllegalArgumentException
+ Assert.fail("ECB mode is not being refused");
+ } catch (IllegalArgumentException iae) {
+ // Expected
+ }
+ }
+
+ @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();
+
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(MESSAGE_FILE);
+ out.write(bytes);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+
+ dest.start(Channel.SND_TX_SEQ);
+
+ bytes = new byte[8192];
+ int read;
+
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(MESSAGE_FILE);
+ read = in.read(bytes);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+
+ msg = new ChannelData(false);
+ XByteBuffer xbb = new XByteBuffer(read, false);
+ xbb.append(bytes, 0, read);
+ msg.setMessage(xbb);
+
+ dest.messageReceived(msg);
+ }
+
+ @Test
+ public void testMessageUniqueness() 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[] cipherText1 = ((ValueCaptureInterceptor)src.getNext()).getValue();
+
+ msg.setMessage(new XByteBuffer(testInput.getBytes("UTF-8"), false));
+ src.sendMessage(null, msg, null);
+
+ byte[] cipherText2 = ((ValueCaptureInterceptor)src.getNext()).getValue();
+
+ Assert.assertThat("Two identical cleartexts encrypt to the same ciphertext",
+ cipherText1, IsNot.not(IsEqual.equalTo(cipherText2)));
+ }
+
+ @Test
+ public void testPickup() throws Exception {
+ File file = new File(MESSAGE_FILE);
+ 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;
+
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(file);
+ read = in.read(bytes);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+
+
+ ChannelData msg = new ChannelData(false);
+ XByteBuffer xbb = new XByteBuffer(read, false);
+ xbb.append(bytes, 0, read);
+ msg.setMessage(xbb);
+
+ dest.messageReceived(msg);
+ }
+
+ /*
+ * 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() {
+ @Override
+ 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);
+ }
+
+ @Test
+ public void testTcpFailureDetectorDetection() {
+ src.setPrevious(new TcpFailureDetector());
+
+ try {
+ src.start(Channel.SND_TX_SEQ);
+ Assert.fail("EncryptInterceptor should detect TcpFailureDetector and throw an error");
+ } catch (EncryptInterceptor.ChannelConfigException cce) {
+ // Expected behavior
+ } catch (AssertionError ae) {
+ // This is the junit assertion being thrown
+ throw ae;
+ } catch (Throwable t) {
+ Assert.fail("EncryptionInterceptor should throw ChannelConfigException, not " + t.getClass().getName());
+ }
+ }
+
+ /**
+ * Interceptor that delivers directly to a destination.
+ */
+ private static class PipedInterceptor
+ extends ChannelInterceptorBase
+ {
+ private ChannelInterceptor dest;
+
+ public PipedInterceptor(ChannelInterceptor dest) {
+ if(null == dest)
+ throw new IllegalArgumentException("Destination must not be null");
+
+ this.dest = dest;
+ }
+
+ @Override
+ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload)
+ throws ChannelException {
+ dest.messageReceived(msg);
+ }
+ }
+
+ /**
+ * Interceptor that simply captures the latest message sent to or received by it.
+ */
+ private static class ValueCaptureInterceptor
+ extends ChannelInterceptorBase
+ {
+ private byte[] value;
+
+ @Override
+ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload)
+ throws ChannelException {
+ value = msg.getMessage().getBytes();
+ }
+
+ @Override
+ public void messageReceived(ChannelMessage msg) {
+ value = msg.getMessage().getBytes();
+ }
+
+ public byte[] getValue() {
+ 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();
+ }
+ }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 74c7bf5..8c304d0 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -129,6 +129,16 @@
</fix>
</changelog>
</subsection>
+ <subsection name="Tribes">
+ <changelog>
+ <add>
+ Add EncryptInterceptor to the portfolio of available clustering
+ interceptors. This adds symmetric encryption of session data
+ to Tomcat clustering regardless of the type of cluster manager
+ or membership being used. (schultz/markt)
+ </add>
+ </changelog>
+ </subsection>
</section>
<section name="Tomcat 7.0.99 (violetagg)" rtext="released 2019-12-17">
<subsection name="Catalina">
diff --git a/webapps/docs/config/cluster-interceptor.xml b/webapps/docs/config/cluster-interceptor.xml
index db19dcc..594c2ef 100644
--- a/webapps/docs/config/cluster-interceptor.xml
+++ b/webapps/docs/config/cluster-interceptor.xml
@@ -36,7 +36,7 @@
<section name="Introduction">
<p>
Apache Tribes supports an interceptor architecture to intercept both messages and membership notifications.
- This architecture allows decoupling of logic and opens the way for some very kewl feature add ons.
+ This architecture allows decoupling of logic and opens the way for some very useful feature add ons.
</p>
</section>
@@ -55,6 +55,7 @@
<li><code>org.apache.catalina.tribes.group.interceptors.FragmentationInterceptor</code></li>
<li><code>org.apache.catalina.tribes.group.interceptors.GzipInterceptor</code></li>
<li><code>org.apache.catalina.tribes.group.interceptors.TcpPingInterceptor</code></li>
+ <li><code>org.apache.catalina.tribes.group.interceptors.EncryptInterceptor</code></li>
</ul>
</section>
@@ -212,6 +213,44 @@
</attribute>
</attributes>
</subsection>
+ <subsection name="org.apache.catalina.tribes.group.interceptors.EncryptInterceptor Attributes">
+ <p>
+ The EncryptInterceptor adds encryption to the channel messages carrying
+ session data between nodes. Added in Tomcat 7.0.100.
+ </p>
+ <p>
+ If using the <code>TcpFailureDetector</code>, the <code>EncryptInterceptor</code>
+ <i>must</i> be inserted into the interceptor chain <i>before</i> the
+ <code>TcpFailureDetector</code>. This is because when validating cluster
+ members, <code>TcpFailureDetector</code> writes channel data directly
+ to the other members without using the remainder of the interceptor chain,
+ but on the receiving side, the message still goes through the chain (in reverse).
+ Because of this asymmetry, the <code>EncryptInterceptor</code> must execute
+ <i>before</i> the <code>TcpFailureDetector</code> on the sender and <i>after</i>
+ it on the receiver, otherwise message corruption will occur.
+ </p>
+ <attributes>
+ <attribute name="encryptionAlgorithm" required="false">
+ The encryption algorithm to be used, including the mode and padding. Please see
+ <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html">https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html</a>
+ for the standard JCA names that can be used.
+
+ The <i>mode</i> is currently required to be <code>CBC</code>.
+
+ The length of the key will specify the flavor of the encryption algorithm
+ to be used, if applicable (e.g. AES-128 versus AES-256).
+
+ The default algorithm is <code>AES/CBC/PKCS5Padding</code>.
+ </attribute>
+ <attribute name="encryptionKey" required="true">
+ The key to be used with the encryption algorithm.
+
+ The key should be specified as hex-encoded bytes of the appropriate
+ length for the algorithm (e.g. 16 bytes / 32 characters / 128 bits for
+ AES-128, 32 bytes / 64 characters / 256 bits for AES-256, etc.).
+ </attribute>
+ </attributes>
+ </subsection>
</section>
<section name="Nested Components">
diff --git a/webapps/docs/config/cluster.xml b/webapps/docs/config/cluster.xml
index cbaa743..252ae2b 100644
--- a/webapps/docs/config/cluster.xml
+++ b/webapps/docs/config/cluster.xml
@@ -52,6 +52,7 @@ to run a cluster on a insecure, untrusted network.</p>
<p>There are many options for providing a secure, trusted network for use by a
Tomcat cluster. These include:</p>
<ul>
+ <li><a href="cluster-interceptor.html#org.apache.catalina.tribes.group.interceptors.EncryptInterceptor_Attributes">EncryptInterceptor</a></li>
<li>private LAN</li>
<li>a Virtual Private Network (VPN)</li>
<li>IPSEC</li>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org