001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.converter.crypto;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.DataInputStream;
021    import java.io.DataOutputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.OutputStream;
025    import java.security.Key;
026    import java.security.spec.AlgorithmParameterSpec;
027    
028    import javax.crypto.Cipher;
029    import javax.crypto.CipherInputStream;
030    import javax.crypto.CipherOutputStream;
031    import javax.crypto.spec.IvParameterSpec;
032    
033    import static javax.crypto.Cipher.DECRYPT_MODE;
034    import static javax.crypto.Cipher.ENCRYPT_MODE;
035    
036    import org.apache.camel.Exchange;
037    import org.apache.camel.spi.DataFormat;
038    import org.apache.camel.util.ExchangeHelper;
039    import org.apache.camel.util.IOHelper;
040    import org.apache.commons.logging.Log;
041    import org.apache.commons.logging.LogFactory;
042    
043    /**
044     * <code>CryptoDataFormat</code> uses a specified key and algorithm to encrypt,
045     * decrypt and verify exchange payloads. The Data format allows an
046     * initialization vector to be supplied. The use of this initialization vector
047     * or IV is different depending on the algorithm type block or streaming, but it
048     * is desirable to be able to control it. Also in certain cases it may be
049     * necessary to have access to the IV in the decryption phase and as the IV
050     * doens't necessarily need to be kept secret it is ok to inline this in the
051     * stream and read it out on the other side prior to decryption. For more
052     * information on Initialization vectors see
053     * <ul>
054     * <li>http://en.wikipedia.org/wiki/Initialization_vector</li>
055     * <li>http://www.herongyang.com/Cryptography/</li>
056     * <li>http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation</li>
057     * <ul>
058     * <p/>
059     * To avoid attacks against the encrypted data while it is in transit the
060     * {@link CryptoDataFormat} can also calculate a Message Authentication Code for
061     * the encrypted exchange contents based on a configurable MAC algorithm. The
062     * calculated HMAC is appended to the stream after encryption. It is separated
063     * from the stream in the decryption phase. The MAC is recalculated and verified
064     * against the transmitted version to insure nothing was tampered with in
065     * transit.For more information on Message Authentication Codes see
066     * <ul>
067     * <li>http://en.wikipedia.org/wiki/HMAC</li>
068     * </ul>
069     */
070    public class CryptoDataFormat implements DataFormat {
071    
072        public static final String KEY = "CamelCryptoKey";
073    
074        private static final Log LOG = LogFactory.getLog(CryptoDataFormat.class);
075        private static final String INIT_VECTOR = "CamelCryptoInitVector";
076        private String algorithm = "DES/CBC/PKCS5Padding";
077        private String cryptoProvider;
078        private Key configuredkey;
079        private int bufferSize = 4096;
080        private byte[] initializationVector;
081        private boolean inline;
082        private String macAlgorithm = "HmacSHA1";
083        private boolean shouldAppendHMAC;
084        private AlgorithmParameterSpec parameterSpec;
085    
086        public CryptoDataFormat() {
087        }
088    
089        public CryptoDataFormat(String algorithm, Key key) {
090            this(algorithm, key, null);
091        }
092    
093        public CryptoDataFormat(String algorithm, Key key, String cryptoProvider) {
094            this.algorithm = algorithm;
095            this.configuredkey = key;
096            this.cryptoProvider = cryptoProvider;
097        }
098    
099        private Cipher initializeCipher(int mode, Key key, byte[] iv) throws Exception {
100            Cipher cipher = cryptoProvider == null ? Cipher.getInstance(algorithm) : Cipher.getInstance(algorithm, cryptoProvider);
101    
102            if (key == null) {
103                throw new IllegalStateException("A valid encryption key is required. Either configure the CryptoDataFormat "
104                        + "with a key or provide one in a header using the header name 'CamelCryptoKey'");
105            }
106    
107            if (mode == ENCRYPT_MODE || mode == DECRYPT_MODE) {
108                if (iv != null) {
109                    cipher.init(mode, key, new IvParameterSpec(iv));
110                } else if (parameterSpec != null) {
111                    cipher.init(mode, key, parameterSpec);
112                } else {
113                    cipher.init(mode, key);
114                }
115            }
116            return cipher;
117        }
118    
119        public void marshal(Exchange exchange, Object graph, OutputStream outputStream) throws Exception {
120            byte[] iv = getInitializationVector(exchange);
121            Key key = getKey(exchange);
122    
123            CipherOutputStream cipherStream = new CipherOutputStream(outputStream, initializeCipher(ENCRYPT_MODE, key, iv));
124            InputStream plaintextStream = ExchangeHelper.convertToMandatoryType(exchange, InputStream.class, graph);
125            HMACAccumulator hmac = getMessageAuthenticationCode(key);
126            if (plaintextStream != null) {
127                inlineInitVector(outputStream, iv);
128                byte[] buffer = new byte[bufferSize];
129                int read;
130                try {
131                    while ((read = plaintextStream.read(buffer)) > 0) {
132                        cipherStream.write(buffer, 0, read);
133                        cipherStream.flush();
134                        hmac.encryptUpdate(buffer, read);
135                    }
136                    // only write if there is data to write (IBM JDK throws exception if no data)
137                    byte[] mac = hmac.getCalculatedMac();
138                    if (mac != null && mac.length > 0) {
139                        cipherStream.write(mac);
140                    }
141                } finally {
142                    IOHelper.close(cipherStream, "cipher", LOG);
143                }
144            }
145        }
146    
147        public Object unmarshal(Exchange exchange, InputStream encryptedStream) throws Exception {
148            Object unmarshalled = null;
149            if (encryptedStream != null) {
150                byte[] iv = getInlinedInitializationVector(exchange, encryptedStream);
151                Key key = getKey(exchange);
152                CipherInputStream cipherStream = new CipherInputStream(encryptedStream, initializeCipher(DECRYPT_MODE, key, iv));
153    
154                ByteArrayOutputStream plaintextStream = new ByteArrayOutputStream(bufferSize);
155                HMACAccumulator hmac = getMessageAuthenticationCode(key);
156                byte[] buffer = new byte[bufferSize];
157                hmac.attachStream(plaintextStream);
158                int read;
159                while ((read = cipherStream.read(buffer)) >= 0) {
160                    hmac.decryptUpdate(buffer, read);
161                }
162                hmac.validate();
163                unmarshalled = plaintextStream.toByteArray();
164            }
165            return unmarshalled;
166        }
167    
168        private void inlineInitVector(OutputStream outputStream, byte[] iv) throws IOException {
169            if (inline) {
170                DataOutputStream dout = new DataOutputStream(outputStream);
171                dout.writeInt(iv.length);
172                outputStream.write(iv);
173                outputStream.flush();
174            }
175        }
176    
177        private byte[] getInlinedInitializationVector(Exchange exchange, InputStream encryptedStream) throws IOException {
178            byte[] iv = getInitializationVector(exchange);
179            if (inline) {
180                try {
181                    int ivLength = new DataInputStream(encryptedStream).readInt();
182                    iv = new byte[ivLength];
183                    int read = encryptedStream.read(iv);
184                    if (read != ivLength) {
185                        throw new IOException(String.format("Attempted to read a '%d' byte initialization vector from inputStream but only"
186                                + " '%d' bytes were retrieved", ivLength, read));
187                    }
188                } catch (IOException e) {
189                    throw IOHelper.createIOException("Error Reading Initialization vector from encrypted stream", e);
190                }
191            }
192            return iv;
193        }
194    
195        private HMACAccumulator getMessageAuthenticationCode(Key key) throws Exception {
196            // return an actual Hmac Calculator or a 'Null' noop version.
197            return shouldAppendHMAC ? new HMACAccumulator(key, macAlgorithm, cryptoProvider, bufferSize) : new HMACAccumulator() {
198                byte[] empty = new byte[0];
199    
200                public void encryptUpdate(byte[] buffer, int read) {
201                }
202    
203                public void decryptUpdate(byte[] buffer, int read) throws IOException {
204                    outputStream.write(buffer, 0, read);
205                }
206    
207                public void validate() {
208                }
209    
210                public byte[] getCalculatedMac() {
211                    return empty;
212                }
213            };
214        }
215    
216        private byte[] getInitializationVector(Exchange exchange) {
217            byte[] iv = exchange.getIn().getHeader(INIT_VECTOR, byte[].class);
218            if (iv == null) {
219                iv = initializationVector;
220            }
221            return iv;
222        }
223    
224        private Key getKey(Exchange exchange) {
225            Key key = exchange.getIn().getHeader(KEY, Key.class);
226            if (key != null) {
227                exchange.getIn().setHeader(KEY, null);
228            } else {
229                key = configuredkey;
230            }
231            return key;
232        }
233    
234        public void setInitializationVector(byte[] initializationVector) {
235            if (initializationVector != null) {
236                this.initializationVector = initializationVector;
237            }
238        }
239    
240        /**
241         * Meant for use with a Symmetric block Cipher and specifies that the
242         * initialization vector should be written to the cipher stream ahead of the
243         * encrypted ciphertext. When the payload is to be decrypted this
244         * initialization vector will need to be read from the stream. Requires that
245         * the formatter has been configured with an init vector that is valid for
246         * the give algorithm.
247         *
248         * @param inline true if the initialization vector should be inlined in the stream.
249         */
250        public void setShouldInlineInitializationVector(boolean inline) {
251            this.inline = inline;
252        }
253    
254        /**
255         * Sets the JCE name of the Encryption Algorithm that should be used
256         */
257        public void setAlgorithm(String algorithm) {
258            this.algorithm = algorithm;
259        }
260    
261        /**
262         * Sets a custom {@link AlgorithmParameterSpec} that should be used to
263         * configure the Cipher. Note that if an Initalization vector is provided
264         * then the IvParameterSpec will be used and any value set here will be
265         * ignored
266         */
267        public void setAlgorithmParameterSpec(AlgorithmParameterSpec parameterSpec) {
268            this.parameterSpec = parameterSpec;
269        }
270    
271        /**
272         * Sets the name of the JCE provider e.g. SUN or BC for Bouncy
273         */
274        public void setCryptoProvider(String cryptoProvider) {
275            this.cryptoProvider = cryptoProvider;
276        }
277    
278        /**
279         * Sets the algorithm used to create the Hash-based Message Authentication
280         * Code (HMAC) appended to the stream.
281         */
282        public void setMacAlgorithm(String macAlgorithm) {
283            this.macAlgorithm = macAlgorithm;
284        }
285    
286        /**
287         * Whether a Hash-based Message Authentication Code (HMAC) should be
288         * calculated and appended to the stream.
289         */
290        public void setShouldAppendHMAC(boolean shouldAppendHMAC) {
291            this.shouldAppendHMAC = shouldAppendHMAC;
292        }
293    
294        /**
295         * Set the key that should be used to encrypt or decrypt incoming encrypted exchanges.
296         */
297        public void setKey(Key key) {
298            this.configuredkey = key;
299        }
300    
301        /**
302         * Set the size of the buffer used to
303         */
304        public void setBufferSize(int bufferSize) {
305            this.bufferSize = bufferSize;
306        }
307    }