-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

All,

Bump.

I have a full patch at this point (still without documentation), but
this one includes resolution of the IV issue and also a set of unit
tests which pass.

I'd appreciate it if someone could install this into their cluster and
see if it works. Just configure the interceptor into your interceptor
chain and set an encryption key (e.g. "cafebabecafebabe").

Thanks,
- -chris

On 10/23/18 16:14, Christopher Schultz wrote:
> All,
> 
> On 10/23/18 10:05, Christopher Schultz wrote:
>> Can I get a technical review for (a) appropriateness and (b) 
>> technical implementation of the attached cluster interceptor?
>> Let's assume for a moment that encryption is something worth
>> adding to clustering and not argue that point.
> 
>> It should be straightforward. Knowing virtually nothing about
>> the way that Tribes works, implementing this as an interceptor
>> seemed like the least invasive (and easiest!) way to add
>> encryption to clustering.
> 
> For the record, I haven't tested this AT ALL. I just made it
> compile :)
> 
> If someone has a test-cluster available, could you please try to
> add this (it just requires an "encryptionKey" attribute on the 
> interceptor... just use "cafebabe" or something simple) and see if
> (a) the messages flow properly and (b) they transmit in
> non-plaintext?
> 
> Thanks, -chris
> 
> ---------------------------------------------------------------------
>
> 
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
> For additional commands, e-mail: dev-h...@tomcat.apache.org
> 
-----BEGIN PGP SIGNATURE-----
Comment: Using GnuPG with Thunderbird - https://www.enigmail.net/

iQIzBAEBCAAdFiEEMmKgYcQvxMe7tcJcHPApP6U8pFgFAlvR3hcACgkQHPApP6U8
pFiQjQ/+OOneeU7lH84A/qzplSjPkyYFunMhAVhhK3bO+dMEjYT+Hw5kWcvtudAA
x3djNxKBz2BGB5cva2K7022iMfrC5wKJzQV9CP5/VRgObu+klQem7ui8g8hqVizT
emEbkojYjzudNygjzhpT1CQgEWoUBGL3g1ssR6uY8RS72O9Jr9G9y/WFMusFCiXH
ERYSY9Wv/e5+gB8+k1jtJS1idAB3ZrTT3qjfEKl4q3gerdX4NU/fQOtRdISoj54m
qUjRaSas+x53QHpCbqVlQbuDNlS4cIHrmXNj4hVuaiCon8yYaeV6LGdadSPnP7a2
AXg81MQjL667rv8o91Tw+3NgaepljwrXhiVqtUFuhFCsomTUQTjkqSZNmD3Z27D3
jVjZzmRi6oDM6niDNFulkTpnr9mTe3KuE58dG2TvtpqrZVN2JIjc/bssbVVzqhtV
j5SwmMdehDAy96eguE+MzXXEvpMbI4PJ8HaTg80A9dDihd6vCUI7XXHeE70YVqvT
r+kZgqGykLnXpiWB4u5oNAKDFlvV9N2WzK4UaaZV+Kr8rhij0Us7ajrO+619NYY9
KAUtRksXzTo0VOl5k3SdLV37HwKaqH+3/s/19nULHyjSLgGM/r4WxqBTgjvcM8Tn
yokrOxCaFgwZWWhInXHi07Z6Wz0Eqn8GoB13rNxP+z3vGOlVr6g=
=XEXS
-----END PGP SIGNATURE-----
Index: 
java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java
===================================================================
--- java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java  
(nonexistent)
+++ java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java  
(working copy)
@@ -0,0 +1,292 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+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.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;
+import org.apache.tomcat.util.buf.HexUtils;
+
+/**
+ * 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 {
+
+    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 Cipher encryptionCipher;
+    private Cipher decryptionCipher;
+
+    public EncryptInterceptor() {
+    }
+
+    @Override
+    public void start(int svc) throws ChannelException {
+        super.start(svc);
+
+        if(Channel.SND_TX_SEQ == svc) {
+            try {
+                initCiphers();
+            } catch (GeneralSecurityException gse) {
+                log.fatal(sm.getString("encryptInterceptor.init.failed"));
+                throw new 
ChannelException(sm.getString("encryptInterceptor.init.failed"), gse);
+            }
+        }
+    }
+
+    @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 = encrypt(data);
+
+            XByteBuffer xbb = msg.getMessage();
+
+            // Completely replace the message
+            xbb.setLength(0);
+            xbb.append(bytes[0], 0, bytes[0].length);
+            xbb.append(bytes[1], 0, bytes[1].length);
+
+            super.sendMessage(destination, msg, payload);
+
+        } catch (IllegalBlockSizeException ibse) {
+            log.error(sm.getString("encryptInterceptor.encrypt.failed"));
+            throw new ChannelException(ibse);
+        } catch (BadPaddingException bpe) {
+            log.error(sm.getString("encryptInterceptor.encrypt.failed"));
+            throw new ChannelException(bpe);
+        }
+    }
+
+    @Override
+    public void messageReceived(ChannelMessage msg) {
+        try {
+            byte[] data = msg.getMessage().getBytes();
+
+            data = decrypt(data);
+
+            // Remove the decrypted IV/nonce block from the front of the 
message
+            int blockSize = getDecryptionCipher().getBlockSize();
+            int trimmedSize = data.length - blockSize;
+            if(trimmedSize < 0) {
+                
log.error(sm.getString("encryptInterceptor.decrypt.error.short-message"));
+                throw new 
IllegalStateException(sm.getString("encryptInterceptor.decrypt.error.short-message"));
+            }
+
+            XByteBuffer xbb = msg.getMessage();
+
+            // Completely replace the message with the decrypted one
+            xbb.setLength(0);
+            xbb.append(data, blockSize, data.length - blockSize);
+
+            super.messageReceived(msg);
+        } catch (IllegalBlockSizeException ibse) {
+            log.error(sm.getString("encryptInterceptor.decrypt.failed"), ibse);
+        } catch (BadPaddingException bpe) {
+            log.error(sm.getString("encryptInterceptor.decrypt.failed"), bpe);
+        }
+    }
+
+    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;
+    }
+
+    public String getEncryptionAlgorithm() {
+        return encryptionAlgorithm;
+    }
+
+    public void setEncryptionKey(byte[] key) {
+        encryptionKeyBytes = key;
+    }
+
+    public void setEncryptionKey(String keyBytes) {
+        setEncryptionKey(HexUtils.fromHexString(keyBytes));
+    }
+
+    private byte[] getEncryptionKey() {
+        return encryptionKeyBytes;
+    }
+
+    public void setProviderName(String provider) {
+        providerName = provider;
+    }
+
+    public String getProviderName() {
+        return providerName;
+    }
+
+    private void initCiphers() throws GeneralSecurityException {
+        if(null == getEncryptionKey())
+            throw new 
IllegalStateException(sm.getString("encryptInterceptor.key.required"));
+
+        String algorithm = getEncryptionAlgorithm();
+
+        String mode = getAlgorithmMode(algorithm);
+
+        if(!"CBC".equalsIgnoreCase(mode))
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.requires-cbc-mode",
 mode));
+
+        Cipher cipher;
+
+        String providerName = getProviderName();
+        if(null == providerName) {
+            cipher = Cipher.getInstance(algorithm);
+        } else {
+            cipher = Cipher.getInstance(algorithm, getProviderName());
+        }
+
+        byte[] iv = new byte[cipher.getBlockSize()];
+
+        // Always use a random IV For cipher setup.
+        // The recipient doesn't need the (matching) IV because we will always
+        // pre-pad messages with the IV as a nonce.
+        new SecureRandom().nextBytes(iv);
+
+        IvParameterSpec IV = new IvParameterSpec(iv);
+
+        // If this is a cipher transform of the form ALGO/MODE/PAD,
+        // take just the algorithm part.
+        int pos = algorithm.indexOf('/');
+
+        String bareAlgorithm;
+        if(pos >= 0) {
+            bareAlgorithm = algorithm.substring(0, pos);
+        } else {
+            bareAlgorithm = algorithm;
+        }
+
+        SecretKeySpec encryptionKey = new SecretKeySpec(getEncryptionKey(), 
bareAlgorithm);
+
+        cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, IV);
+
+        encryptionCipher = cipher;
+
+        if(null == providerName) {
+            cipher = Cipher.getInstance(algorithm);
+        } else {
+            cipher = Cipher.getInstance(algorithm, getProviderName());
+        }
+
+        cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new 
IvParameterSpec(iv));
+
+        decryptionCipher = cipher;
+    }
+
+    private Cipher getEncryptionCipher() {
+        return encryptionCipher;
+    }
+
+    private Cipher getDecryptionCipher() {
+        return decryptionCipher;
+    }
+
+    private static String getAlgorithmMode(String algorithm) {
+        int start = algorithm.indexOf('/');
+        if(start < 0)
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
+        int end = algorithm.indexOf('/', start + 1);
+        if(start < 0)
+            throw new 
IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required"));
+
+        return algorithm.substring(start + 1, end);
+    }
+
+    /**
+     * Encrypts the input <code>bytes</code> into two separate byte arrays:
+     * one for the initial block (which will be the encrypted random IV)
+     * 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 encrypted IV block in [0] and the encrypted data in [1].
+     *
+     * @throws GeneralSecurityException If there is a problem performing the 
encryption.
+     */
+    private byte[][] encrypt(byte[] bytes) throws IllegalBlockSizeException, 
BadPaddingException {
+        Cipher cipher = getEncryptionCipher();
+
+        // Adding the IV to the beginning of the encrypted data
+        byte[] iv = cipher.getIV();
+
+        byte[][] data = new byte[2][];
+        data[0] = cipher.update(iv, 0, iv.length);
+        data[1] = cipher.doFinal(bytes);
+
+        return data;
+    }
+
+    /**
+     * Decrypts the input <code>bytes</code>.
+     *
+     * @param bytes The data to decrypt.
+     *
+     * @return The decrypted data.
+     *
+     * @throws GeneralSecurityException If there is a problem performing the 
decryption.
+     */
+    private byte[] decrypt(byte[] bytes) throws IllegalBlockSizeException, 
BadPaddingException {
+        return getDecryptionCipher().doFinal(bytes);
+    }
+}
Index: 
java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
===================================================================
--- java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties  
(revision 1844644)
+++ java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties  
(working copy)
@@ -73,4 +73,10 @@
   \n\tRx Speed:{8} MB/sec (since 1st msg)\
   \n\tReceived:{9} MB]\n
 twoPhaseCommitInterceptor.originalMessage.missing=Received a confirmation, but 
original message is missing. Id:[{0}]
-twoPhaseCommitInterceptor.heartbeat.failed=Unable to perform heartbeat on the 
TwoPhaseCommit interceptor.
\ No newline at end of file
+twoPhaseCommitInterceptor.heartbeat.failed=Unable to perform heartbeat on the 
TwoPhaseCommit interceptor.
+encryptInterceptor.init.failed=Failed to initialize EncryptInterceptor
+encryptInterceptor.encrypt.failed=Failed to encrypt message
+encryptInterceptor.decrypt.failed=Failed to decrypt message
+encryptInterceptor.algorithm.required=Encryption algorithm is required, 
fully-specified e.g. AES/CBC/PKCS5Padding
+encryptInterceptor.key.required=Encryption key is required
+encryptInterceptor.algorithm.requires-cbc-mode=EncryptInterceptor only 
supports CBC cipher modes, not [{0}]
\ No newline at end of file
Index: 
test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java
===================================================================
--- 
test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java  
    (nonexistent)
+++ 
test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java  
    (working copy)
@@ -0,0 +1,191 @@
+package org.apache.catalina.tribes.group.interceptors;
+
+import static org.junit.Assert.*;
+
+import java.nio.charset.StandardCharsets;
+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;
+import org.apache.tomcat.util.buf.HexUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 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.
+ */
+public class TestEncryptInterceptor {
+    private static final String encryptionKey128 = 
HexUtils.toHexString("cafebabedeadbeef".getBytes(StandardCharsets.UTF_8));
+    private static final String encryptionKey192 = 
HexUtils.toHexString("cafebabedeadbeefbeefcafe".getBytes(StandardCharsets.UTF_8));
+    private static final String encryptionKey256 = 
HexUtils.toHexString("cafebabedeadbeefcafebabedeadbeef".getBytes(StandardCharsets.UTF_8));
+
+    EncryptInterceptor src;
+    EncryptInterceptor dest;
+
+    @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.";
+
+        assertEquals("Basic 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";
+
+        assertEquals("Tiny payload roundtrip failed",
+                     testInput,
+                     roundTrip(testInput, src, dest));
+    }
+
+    @Test
+    public void testHugePayload() throws Exception {
+        src.start(Channel.SND_TX_SEQ);
+        dest.start(Channel.SND_TX_SEQ);
+
+        byte[] bytes = new byte[1073741824]; // 1MiB, all zeros
+
+        assertArrayEquals("Tiny 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.";
+
+        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.";
+
+        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.";
+
+        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(((ValueCaptureInterceptor)dest.getPrevious()).getValue(), "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();
+    }
+
+    /**
+     * 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;
+        }
+    }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to