blob: 52f0742a09c5130b42a3c751cec81a5bd1b5eeec [file] [log] [blame]
/**
* 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.camel.converter.crypto;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import static javax.crypto.Cipher.DECRYPT_MODE;
import static javax.crypto.Cipher.ENCRYPT_MODE;
import org.apache.camel.Exchange;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>CryptoDataFormat</code> uses a specified key and algorithm to encrypt,
* decrypt and verify exchange payloads. The Data format allows an
* initialization vector to be supplied. The use of this initialization vector
* or IV is different depending on the algorithm type block or streaming, but it
* is desirable to be able to control it. Also in certain cases it may be
* necessary to have access to the IV in the decryption phase and as the IV
* doens't necessarily need to be kept secret it is ok to inline this in the
* stream and read it out on the other side prior to decryption. For more
* information on Initialization vectors see
* <ul>
* <li>http://en.wikipedia.org/wiki/Initialization_vector</li>
* <li>http://www.herongyang.com/Cryptography/</li>
* <li>http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation</li>
* <ul>
* <p/>
* To avoid attacks against the encrypted data while it is in transit the
* {@link CryptoDataFormat} can also calculate a Message Authentication Code for
* the encrypted exchange contents based on a configurable MAC algorithm. The
* calculated HMAC is appended to the stream after encryption. It is separated
* from the stream in the decryption phase. The MAC is recalculated and verified
* against the transmitted version to insure nothing was tampered with in
* transit.For more information on Message Authentication Codes see
* <ul>
* <li>http://en.wikipedia.org/wiki/HMAC</li>
* </ul>
*/
public class CryptoDataFormat implements DataFormat {
public static final String KEY = "CamelCryptoKey";
private static final Logger LOG = LoggerFactory.getLogger(CryptoDataFormat.class);
private static final String INIT_VECTOR = "CamelCryptoInitVector";
private String algorithm = "DES/CBC/PKCS5Padding";
private String cryptoProvider;
private Key configuredkey;
private int bufferSize = 4096;
private byte[] initializationVector;
private boolean inline;
private String macAlgorithm = "HmacSHA1";
private boolean shouldAppendHMAC;
private AlgorithmParameterSpec parameterSpec;
public CryptoDataFormat() {
}
public CryptoDataFormat(String algorithm, Key key) {
this(algorithm, key, null);
}
public CryptoDataFormat(String algorithm, Key key, String cryptoProvider) {
this.algorithm = algorithm;
this.configuredkey = key;
this.cryptoProvider = cryptoProvider;
}
private Cipher initializeCipher(int mode, Key key, byte[] iv) throws Exception {
Cipher cipher = cryptoProvider == null ? Cipher.getInstance(algorithm) : Cipher.getInstance(algorithm, cryptoProvider);
if (key == null) {
throw new IllegalStateException("A valid encryption key is required. Either configure the CryptoDataFormat "
+ "with a key or provide one in a header using the header name 'CamelCryptoKey'");
}
if (mode == ENCRYPT_MODE || mode == DECRYPT_MODE) {
if (iv != null) {
cipher.init(mode, key, new IvParameterSpec(iv));
} else if (parameterSpec != null) {
cipher.init(mode, key, parameterSpec);
} else {
cipher.init(mode, key);
}
}
return cipher;
}
public void marshal(Exchange exchange, Object graph, OutputStream outputStream) throws Exception {
byte[] iv = getInitializationVector(exchange);
Key key = getKey(exchange);
CipherOutputStream cipherStream = new CipherOutputStream(outputStream, initializeCipher(ENCRYPT_MODE, key, iv));
InputStream plaintextStream = ExchangeHelper.convertToMandatoryType(exchange, InputStream.class, graph);
HMACAccumulator hmac = getMessageAuthenticationCode(key);
if (plaintextStream != null) {
inlineInitVector(outputStream, iv);
byte[] buffer = new byte[bufferSize];
int read;
try {
while ((read = plaintextStream.read(buffer)) > 0) {
cipherStream.write(buffer, 0, read);
cipherStream.flush();
hmac.encryptUpdate(buffer, read);
}
// only write if there is data to write (IBM JDK throws exception if no data)
byte[] mac = hmac.getCalculatedMac();
if (mac != null && mac.length > 0) {
cipherStream.write(mac);
}
} finally {
IOHelper.close(cipherStream, "cipher", LOG);
}
}
}
public Object unmarshal(Exchange exchange, InputStream encryptedStream) throws Exception {
Object unmarshalled = null;
if (encryptedStream != null) {
byte[] iv = getInlinedInitializationVector(exchange, encryptedStream);
Key key = getKey(exchange);
CipherInputStream cipherStream = new CipherInputStream(encryptedStream, initializeCipher(DECRYPT_MODE, key, iv));
ByteArrayOutputStream plaintextStream = new ByteArrayOutputStream(bufferSize);
HMACAccumulator hmac = getMessageAuthenticationCode(key);
byte[] buffer = new byte[bufferSize];
hmac.attachStream(plaintextStream);
int read;
while ((read = cipherStream.read(buffer)) >= 0) {
hmac.decryptUpdate(buffer, read);
}
hmac.validate();
unmarshalled = plaintextStream.toByteArray();
}
return unmarshalled;
}
private void inlineInitVector(OutputStream outputStream, byte[] iv) throws IOException {
if (inline) {
DataOutputStream dout = new DataOutputStream(outputStream);
dout.writeInt(iv.length);
outputStream.write(iv);
outputStream.flush();
}
}
private byte[] getInlinedInitializationVector(Exchange exchange, InputStream encryptedStream) throws IOException {
byte[] iv = getInitializationVector(exchange);
if (inline) {
try {
int ivLength = new DataInputStream(encryptedStream).readInt();
iv = new byte[ivLength];
int read = encryptedStream.read(iv);
if (read != ivLength) {
throw new IOException(String.format("Attempted to read a '%d' byte initialization vector from inputStream but only"
+ " '%d' bytes were retrieved", ivLength, read));
}
} catch (IOException e) {
throw new IOException("Error reading initialization vector from encrypted stream", e);
}
}
return iv;
}
private HMACAccumulator getMessageAuthenticationCode(Key key) throws Exception {
// return an actual Hmac Calculator or a 'Null' noop version.
return shouldAppendHMAC ? new HMACAccumulator(key, macAlgorithm, cryptoProvider, bufferSize) : new HMACAccumulator() {
byte[] empty = new byte[0];
public void encryptUpdate(byte[] buffer, int read) {
}
public void decryptUpdate(byte[] buffer, int read) throws IOException {
outputStream.write(buffer, 0, read);
}
public void validate() {
}
public byte[] getCalculatedMac() {
return empty;
}
};
}
private byte[] getInitializationVector(Exchange exchange) {
byte[] iv = exchange.getIn().getHeader(INIT_VECTOR, byte[].class);
if (iv == null) {
iv = initializationVector;
}
return iv;
}
private Key getKey(Exchange exchange) {
Key key = exchange.getIn().getHeader(KEY, Key.class);
if (key != null) {
exchange.getIn().setHeader(KEY, null);
} else {
key = configuredkey;
}
return key;
}
public void setInitializationVector(byte[] initializationVector) {
if (initializationVector != null) {
this.initializationVector = initializationVector;
}
}
/**
* Meant for use with a Symmetric block Cipher and specifies that the
* initialization vector should be written to the cipher stream ahead of the
* encrypted ciphertext. When the payload is to be decrypted this
* initialization vector will need to be read from the stream. Requires that
* the formatter has been configured with an init vector that is valid for
* the give algorithm.
*
* @param inline true if the initialization vector should be inlined in the stream.
*/
public void setShouldInlineInitializationVector(boolean inline) {
this.inline = inline;
}
/**
* Sets the JCE name of the Encryption Algorithm that should be used
*/
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
/**
* Sets a custom {@link AlgorithmParameterSpec} that should be used to
* configure the Cipher. Note that if an Initalization vector is provided
* then the IvParameterSpec will be used and any value set here will be
* ignored
*/
public void setAlgorithmParameterSpec(AlgorithmParameterSpec parameterSpec) {
this.parameterSpec = parameterSpec;
}
/**
* Sets the name of the JCE provider e.g. SUN or BC for Bouncy
*/
public void setCryptoProvider(String cryptoProvider) {
this.cryptoProvider = cryptoProvider;
}
/**
* Sets the algorithm used to create the Hash-based Message Authentication
* Code (HMAC) appended to the stream.
*/
public void setMacAlgorithm(String macAlgorithm) {
this.macAlgorithm = macAlgorithm;
}
/**
* Whether a Hash-based Message Authentication Code (HMAC) should be
* calculated and appended to the stream.
*/
public void setShouldAppendHMAC(boolean shouldAppendHMAC) {
this.shouldAppendHMAC = shouldAppendHMAC;
}
/**
* Set the key that should be used to encrypt or decrypt incoming encrypted exchanges.
*/
public void setKey(Key key) {
this.configuredkey = key;
}
/**
* Set the size of the buffer used to
*/
public void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
}
}