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