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 }