com.ovea.tadjin.util.crypto.JcaCipherService Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2011 Ovea
*
* Licensed 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 com.ovea.tadjin.util.crypto;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
/**
* Abstract {@code CipherService} implementation utilizing Java's JCA APIs.
* Auto-generated Initialization Vectors
* Shiro does something by default for all of its {@code CipherService} implementations that the JCA
* {@link javax.crypto.Cipher Cipher} does not do: by default,
* initialization vectors are automatically randomly
* generated and prepended to encrypted data before returning from the {@code encrypt} methods. That is, the returned
* byte array or {@code OutputStream} is actually a concatenation of an initialization vector byte array plus the actual
* encrypted data byte array. The {@code decrypt} methods in turn know to read this prepended initialization vector
* before decrypting the real data that follows.
*
* This is highly desirable because initialization vectors guarantee that, for a key and any plaintext, the encrypted
* output will always be different even if you call {@code encrypt} multiple times with the exact same arguments.
* This is essential in cryptography to ensure that data patterns cannot be identified across multiple input sources
* that are the same or similar.
*
* You can turn off this behavior by setting the
* {@link #setGenerateInitializationVectors(boolean) generateInitializationVectors} property to {@code false}, but it
* is highly recommended that you do not do this unless you have a very good reason to do so, since you would be losing
* a critical security feature.
* Initialization Vector Size
* This implementation defaults the {@link #setInitializationVectorSize(int) initializationVectorSize} attribute to
* {@code 128} bits, a fairly common size. Initialization vector sizes are very algorithm specific however, so subclass
* implementations will often override this value in their constructor if necessary.
*
* Also note that {@code initializationVectorSize} values are specified in the number of
* bits (not bytes!) to match common references in most cryptography documentation. In practice though, initialization
* vectors are always specified as a byte array, so ensure that if you set this property, that the value is a multiple
* of {@code 8} to ensure that the IV can be correctly represented as a byte array (the
* {@link #setInitializationVectorSize(int) setInitializationVectorSize} mutator method enforces this).
*
* @since 1.0
*/
public abstract class JcaCipherService implements CipherService {
/**
* Default key size (in bits) for generated keys.
*/
private static final int DEFAULT_KEY_SIZE = 128;
/**
* Default size of the internal buffer (in bytes) used to transfer data between streams during stream operations
*/
private static final int DEFAULT_STREAMING_BUFFER_SIZE = 512;
private static final int BITS_PER_BYTE = 8;
/**
* Default SecureRandom algorithm name to use when acquiring the SecureRandom instance.
*/
private static final String RANDOM_NUM_GENERATOR_ALGORITHM_NAME = "SHA1PRNG";
/**
* The name of the cipher algorithm to use for all encryption, decryption, and key operations
*/
private String algorithmName;
/**
* The size in bits (not bytes) of generated cipher keys
*/
private int keySize;
/**
* The size of the internal buffer (in bytes) used to transfer data from one stream to another during stream operations
*/
private int streamingBufferSize;
private boolean generateInitializationVectors;
private int initializationVectorSize;
private SecureRandom secureRandom;
/**
* Creates a new {@code JcaCipherService} instance which will use the specified cipher {@code algorithmName}
* for all encryption, decryption, and key operations. Also, the following defaults are set:
*
* - {@link #setKeySize keySize} = 128 bits
* - {@link #setInitializationVectorSize(int) initializationVectorSize} = 128 bits
* - {@link #setStreamingBufferSize(int) streamingBufferSize} = 512 bytes
*
*
* @param algorithmName the name of the cipher algorithm to use for all encryption, decryption, and key operations
*/
protected JcaCipherService(String algorithmName) {
if (algorithmName == null || algorithmName.length() == 0) {
throw new IllegalArgumentException("algorithmName argument cannot be null or empty.");
}
this.algorithmName = algorithmName;
this.keySize = DEFAULT_KEY_SIZE;
this.initializationVectorSize = DEFAULT_KEY_SIZE; //default to same size as the key size (a common algorithm practice)
this.streamingBufferSize = DEFAULT_STREAMING_BUFFER_SIZE;
this.generateInitializationVectors = true;
}
/**
* Returns the cipher algorithm name that will be used for all encryption, decryption, and key operations (for
* example, 'AES', 'Blowfish', 'RSA', 'DSA', 'TripleDES', etc).
*
* @return the cipher algorithm name that will be used for all encryption, decryption, and key operations
*/
public String getAlgorithmName() {
return algorithmName;
}
/**
* Returns the size in bits (not bytes) of generated cipher keys.
*
* @return the size in bits (not bytes) of generated cipher keys.
*/
public int getKeySize() {
return keySize;
}
/**
* Sets the size in bits (not bytes) of generated cipher keys.
*
* @param keySize the size in bits (not bytes) of generated cipher keys.
*/
public void setKeySize(int keySize) {
this.keySize = keySize;
}
public boolean isGenerateInitializationVectors() {
return generateInitializationVectors;
}
public void setGenerateInitializationVectors(boolean generateInitializationVectors) {
this.generateInitializationVectors = generateInitializationVectors;
}
/**
* Returns the algorithm-specific size in bits of generated initialization vectors.
*
* @return the algorithm-specific size in bits of generated initialization vectors.
*/
public int getInitializationVectorSize() {
return initializationVectorSize;
}
/**
* Sets the algorithm-specific initialization vector size in bits (not bytes!) to be used when generating
* initialization vectors. The value must be a multiple of {@code 8} to ensure that the IV can be represented
* as a byte array.
*
* @param initializationVectorSize the size in bits (not bytes) of generated initialization vectors.
* @throws IllegalArgumentException if the size is not a multiple of {@code 8}.
*/
public void setInitializationVectorSize(int initializationVectorSize) throws IllegalArgumentException {
if (initializationVectorSize % BITS_PER_BYTE != 0) {
String msg = "Initialization vector sizes are specified in bits, but must be a multiple of 8 so they " +
"can be easily represented as a byte array.";
throw new IllegalArgumentException(msg);
}
this.initializationVectorSize = initializationVectorSize;
}
protected boolean isGenerateInitializationVectors(boolean streaming) {
return isGenerateInitializationVectors();
}
/**
* Returns the size in bytes of the internal buffer used to transfer data from one stream to another during stream
* operations ({@link #encrypt(java.io.InputStream, java.io.OutputStream, byte[])} and
* {@link #decrypt(java.io.InputStream, java.io.OutputStream, byte[])}).
*
* Default size is {@code 512} bytes.
*
* @return the size of the internal buffer used to transfer data from one stream to another during stream
* operations
*/
public int getStreamingBufferSize() {
return streamingBufferSize;
}
/**
* Sets the size in bytes of the internal buffer used to transfer data from one stream to another during stream
* operations ({@link #encrypt(java.io.InputStream, java.io.OutputStream, byte[])} and
* {@link #decrypt(java.io.InputStream, java.io.OutputStream, byte[])}).
*
* Default size is {@code 512} bytes.
*
* @param streamingBufferSize the size of the internal buffer used to transfer data from one stream to another
* during stream operations
*/
public void setStreamingBufferSize(int streamingBufferSize) {
this.streamingBufferSize = streamingBufferSize;
}
/**
* Returns a source of randomness for encryption operations. If one is not configured, and the underlying
* algorithm needs one, the JDK {@code SHA1PRNG} instance will be used by default.
*
* @return a source of randomness for encryption operations. If one is not configured, and the underlying
* algorithm needs one, the JDK {@code SHA1PRNG} instance will be used by default.
*/
public SecureRandom getSecureRandom() {
return secureRandom;
}
/**
* Sets a source of randomness for encryption operations. If one is not configured, and the underlying
* algorithm needs one, the JDK {@code SHA1PRNG} instance will be used by default.
*
* @param secureRandom a source of randomness for encryption operations. If one is not configured, and the
* underlying algorithm needs one, the JDK {@code SHA1PRNG} instance will be used by default.
*/
public void setSecureRandom(SecureRandom secureRandom) {
this.secureRandom = secureRandom;
}
protected static SecureRandom getDefaultSecureRandom() {
try {
return SecureRandom.getInstance(RANDOM_NUM_GENERATOR_ALGORITHM_NAME);
} catch (java.security.NoSuchAlgorithmException e) {
return new SecureRandom();
}
}
protected SecureRandom ensureSecureRandom() {
SecureRandom random = getSecureRandom();
if (random == null) {
random = getDefaultSecureRandom();
}
return random;
}
/**
* Returns the transformation string to use with the {@link javax.crypto.Cipher#getInstance} invocation when
* creating a new {@code Cipher} instance. This default implementation always returns
* {@link #getAlgorithmName() getAlgorithmName()}. Block cipher implementations will want to override this method
* to support appending cipher operation modes and padding schemes.
*
* @param streaming if the transformation string is going to be used for a Cipher for stream-based encryption or not.
* @return the transformation string to use with the {@link javax.crypto.Cipher#getInstance} invocation when
* creating a new {@code Cipher} instance.
*/
protected String getTransformationString(boolean streaming) {
return getAlgorithmName();
}
protected byte[] generateInitializationVector(boolean streaming) {
int size = getInitializationVectorSize();
if (size <= 0) {
String msg = "initializationVectorSize property must be greater than zero. This number is " +
"typically set in the " + CipherService.class.getSimpleName() + " subclass constructor. " +
"Also check your configuration to ensure that if you are setting a value, it is positive.";
throw new IllegalStateException(msg);
}
if (size % BITS_PER_BYTE != 0) {
String msg = "initializationVectorSize property must be a multiple of 8 to represent as a byte array.";
throw new IllegalStateException(msg);
}
int sizeInBytes = size / BITS_PER_BYTE;
byte[] ivBytes = new byte[sizeInBytes];
SecureRandom random = ensureSecureRandom();
random.nextBytes(ivBytes);
return ivBytes;
}
public byte[] encrypt(byte[] plaintext, byte[] key) {
byte[] ivBytes = null;
boolean generate = isGenerateInitializationVectors(false);
if (generate) {
ivBytes = generateInitializationVector(false);
if (ivBytes == null || ivBytes.length == 0) {
throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
"cannot be null or empty.");
}
}
return encrypt(plaintext, key, ivBytes, generate);
}
private byte[] encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
final int MODE = javax.crypto.Cipher.ENCRYPT_MODE;
byte[] output;
if (prependIv && iv != null && iv.length > 0) {
byte[] encrypted = crypt(plaintext, key, iv, MODE);
output = new byte[iv.length + encrypted.length];
//now copy the iv bytes + encrypted bytes into one output array:
// iv bytes:
System.arraycopy(iv, 0, output, 0, iv.length);
// + encrypted bytes:
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
} else {
output = crypt(plaintext, key, iv, MODE);
}
return output;
}
public byte[] decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
byte[] encrypted = ciphertext;
//No IV, check if we need to read the IV from the stream:
byte[] iv = null;
if (isGenerateInitializationVectors(false)) {
try {
//We are generating IVs, so the ciphertext argument array is not actually 100% cipher text. Instead, it
//is:
// - the first N bytes is the initialization vector, where N equals the value of the
// 'initializationVectorSize' attribute.
// - the remaining bytes in the method argument (arg.length - N) is the real cipher text.
//So we need to chunk the method argument into its constituent parts to find the IV and then use
//the IV to decrypt the real ciphertext:
int ivSize = getInitializationVectorSize();
int ivByteSize = ivSize / BITS_PER_BYTE;
//now we know how large the iv is, so extract the iv bytes:
iv = new byte[ivByteSize];
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);
//remaining data is the actual encrypted ciphertext. Isolate it:
int encryptedSize = ciphertext.length - ivByteSize;
encrypted = new byte[encryptedSize];
System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
} catch (Exception e) {
String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
throw new CryptoException(msg, e);
}
}
return decrypt(encrypted, key, iv);
}
private byte[] decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws CryptoException {
byte[] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE);
return decrypted == null ? null : decrypted;
}
/**
* Returns a new {@link javax.crypto.Cipher Cipher} instance to use for encryption/decryption operations. The
* Cipher's {@code transformationString} for the {@code Cipher}.{@link javax.crypto.Cipher#getInstance getInstance}
* call is obtaind via the {@link #getTransformationString(boolean) getTransformationString} method.
*
* @param streaming {@code true} if the cipher instance will be used as a stream cipher, {@code false} if it will be
* used as a block cipher.
* @return a new JDK {@code Cipher} instance.
* @throws CryptoException if a new Cipher instance cannot be constructed based on the
* {@link #getTransformationString(boolean) getTransformationString} value.
*/
private javax.crypto.Cipher newCipherInstance(boolean streaming) throws CryptoException {
String transformationString = getTransformationString(streaming);
try {
return javax.crypto.Cipher.getInstance(transformationString);
} catch (Exception e) {
String msg = "Unable to acquire a Java JCA Cipher instance using " +
javax.crypto.Cipher.class.getName() + ".getInstance( \"" + transformationString + "\" ). " +
getAlgorithmName() + " under this configuration is required for the " +
getClass().getName() + " instance to function.";
throw new CryptoException(msg, e);
}
}
/**
* Functions as follows:
*
* - Creates a {@link #newCipherInstance(boolean) new JDK cipher instance}
* - Converts the specified key bytes into an {@link #getAlgorithmName() algorithm}-compatible JDK
* {@link java.security.Key key} instance
* - {@link #init(javax.crypto.Cipher, int, java.security.Key, java.security.spec.AlgorithmParameterSpec, java.security.SecureRandom) Initializes}
* the JDK cipher instance with the JDK key
* - Calls the {@link #crypt(javax.crypto.Cipher, byte[]) crypt(cipher,bytes)} method to either encrypt or
* decrypt the data based on the specified Cipher behavior mode
* ({@link javax.crypto.Cipher#ENCRYPT_MODE Cipher.ENCRYPT_MODE} or
* {@link javax.crypto.Cipher#DECRYPT_MODE Cipher.DECRYPT_MODE})
*
*
* @param bytes the bytes to crypt
* @param key the key to use to perform the encryption or decryption.
* @param iv the initialization vector to use for the crypt operation (optional, may be {@code null}).
* @param mode the JDK Cipher behavior mode (Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE).
* @return the resulting crypted byte array
* @throws IllegalArgumentException if {@code bytes} are null or empty.
* @throws CryptoException if Cipher initialization or the crypt operation fails
*/
private byte[] crypt(byte[] bytes, byte[] key, byte[] iv, int mode) throws IllegalArgumentException, CryptoException {
if (key == null || key.length == 0) {
throw new IllegalArgumentException("key argument cannot be null or empty.");
}
javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false);
return crypt(cipher, bytes);
}
/**
* Calls the {@link javax.crypto.Cipher#doFinal(byte[]) doFinal(bytes)} method, propagating any exception that
* might arise in an {@link CryptoException}
*
* @param cipher the JDK Cipher to finalize (perform the actual cryption)
* @param bytes the bytes to crypt
* @return the resulting crypted byte array.
* @throws CryptoException if there is an illegal block size or bad padding
*/
private byte[] crypt(javax.crypto.Cipher cipher, byte[] bytes) throws CryptoException {
try {
return cipher.doFinal(bytes);
} catch (Exception e) {
String msg = "Unable to execute 'doFinal' with cipher instance [" + cipher + "].";
throw new CryptoException(msg, e);
}
}
/**
* Initializes the JDK Cipher with the specified mode and key. This is primarily a utility method to catch any
* potential {@link java.security.InvalidKeyException InvalidKeyException} that might arise.
*
* @param cipher the JDK Cipher to {@link javax.crypto.Cipher#init(int, java.security.Key) init}.
* @param mode the Cipher mode
* @param key the Cipher's Key
* @param spec the JDK AlgorithmParameterSpec for cipher initialization (optional, may be null).
* @param random the SecureRandom to use for cipher initialization (optional, may be null).
* @throws CryptoException if the key is invalid
*/
private void init(javax.crypto.Cipher cipher, int mode, Key key,
AlgorithmParameterSpec spec, SecureRandom random) throws CryptoException {
try {
if (random != null) {
if (spec != null) {
cipher.init(mode, key, spec, random);
} else {
cipher.init(mode, key, random);
}
} else {
if (spec != null) {
cipher.init(mode, key, spec);
} else {
cipher.init(mode, key);
}
}
} catch (Exception e) {
String msg = "Unable to init cipher instance.";
throw new CryptoException(msg, e);
}
}
public void encrypt(InputStream in, OutputStream out, byte[] key) throws CryptoException {
byte[] iv = null;
boolean generate = isGenerateInitializationVectors(true);
if (generate) {
iv = generateInitializationVector(true);
if (iv == null || iv.length == 0) {
throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
"cannot be null or empty.");
}
}
encrypt(in, out, key, iv, generate);
}
private void encrypt(InputStream in, OutputStream out, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
if (prependIv && iv != null && iv.length > 0) {
try {
//first write the IV:
out.write(iv);
} catch (IOException e) {
throw new CryptoException(e);
}
}
crypt(in, out, key, iv, javax.crypto.Cipher.ENCRYPT_MODE);
}
public void decrypt(InputStream in, OutputStream out, byte[] key) throws CryptoException {
decrypt(in, out, key, isGenerateInitializationVectors(true));
}
private void decrypt(InputStream in, OutputStream out, byte[] key, boolean ivPrepended) throws CryptoException {
byte[] iv = null;
//No Initialization Vector provided as a method argument - check if we need to read the IV from the stream:
if (ivPrepended) {
//we are generating IVs, so we need to read the previously-generated IV from the stream before
//we decrypt the rest of the stream (we need the IV to decrypt):
int ivSize = getInitializationVectorSize();
int ivByteSize = ivSize / BITS_PER_BYTE;
iv = new byte[ivByteSize];
int read;
try {
read = in.read(iv);
} catch (IOException e) {
String msg = "Unable to correctly read the Initialization Vector from the input stream.";
throw new CryptoException(msg, e);
}
if (read != ivByteSize) {
throw new CryptoException("Unable to read initialization vector bytes from the InputStream. " +
"This is required when initialization vectors are autogenerated during an encryption " +
"operation.");
}
}
decrypt(in, out, key, iv);
}
private void decrypt(InputStream in, OutputStream out, byte[] decryptionKey, byte[] iv) throws CryptoException {
crypt(in, out, decryptionKey, iv, javax.crypto.Cipher.DECRYPT_MODE);
}
private void crypt(InputStream in, OutputStream out, byte[] keyBytes, byte[] iv, int cryptMode) throws CryptoException {
if (in == null) {
throw new NullPointerException("InputStream argument cannot be null.");
}
if (out == null) {
throw new NullPointerException("OutputStream argument cannot be null.");
}
javax.crypto.Cipher cipher = initNewCipher(cryptMode, keyBytes, iv, true);
CipherInputStream cis = new CipherInputStream(in, cipher);
int bufSize = getStreamingBufferSize();
byte[] buffer = new byte[bufSize];
int bytesRead;
try {
while ((bytesRead = cis.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
throw new CryptoException(e);
}
}
private javax.crypto.Cipher initNewCipher(int jcaCipherMode, byte[] key, byte[] iv, boolean streaming)
throws CryptoException {
javax.crypto.Cipher cipher = newCipherInstance(streaming);
Key jdkKey = new SecretKeySpec(key, getAlgorithmName());
IvParameterSpec ivSpec = null;
if (iv != null && iv.length > 0) {
ivSpec = new IvParameterSpec(iv);
}
init(cipher, jcaCipherMode, jdkKey, ivSpec, getSecureRandom());
return cipher;
}
}