/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.crypto.SecretKey;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.cryptacular.CiphertextHeader;
import org.cryptacular.CryptoException;
import org.cryptacular.EncodingException;
import org.cryptacular.StreamException;
import org.cryptacular.adapter.AEADBlockCipherAdapter;
import org.cryptacular.adapter.BlockCipherAdapter;
import org.cryptacular.adapter.BufferedBlockCipherAdapter;
import org.cryptacular.generator.Nonce;
/**
* Utility class that performs encryption and decryption operations using a block cipher.
*
* @author Middleware Services
*/
public final class CipherUtil
{
/** Mac size in bits. */
private static final int MAC_SIZE_BITS = 128;
/** Private constructor of utility class. */
private CipherUtil() {}
/**
* Encrypts data using an AEAD cipher. A {@link CiphertextHeader} is prepended to the resulting ciphertext and used as
* AAD (Additional Authenticated Data) passed to the AEAD cipher.
*
* @param cipher AEAD cipher.
* @param key Encryption key.
* @param nonce Nonce generator.
* @param data Plaintext data to be encrypted.
*
* @return Concatenation of encoded {@link CiphertextHeader} and encrypted data that completely fills the returned
* byte array.
*
* @throws CryptoException on encryption errors.
*/
public static byte[] encrypt(final AEADBlockCipher cipher, final SecretKey key, final Nonce nonce, final byte[] data)
throws CryptoException
{
final byte[] iv = nonce.generate();
final byte[] header = new CiphertextHeader(iv).encode();
cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header));
return encrypt(new AEADBlockCipherAdapter(cipher), header, data);
}
/**
* Encrypts data using an AEAD cipher. A {@link CiphertextHeader} is prepended to the resulting ciphertext and used as
* AAD (Additional Authenticated Data) passed to the AEAD cipher.
*
* @param cipher AEAD cipher.
* @param key Encryption key.
* @param nonce Nonce generator.
* @param input Input stream containing plaintext data.
* @param output Output stream that receives a {@link CiphertextHeader} followed by ciphertext data produced by the
* AEAD cipher in encryption mode.
*
* @throws CryptoException on encryption errors.
* @throws StreamException on IO errors.
*/
public static void encrypt(
final AEADBlockCipher cipher,
final SecretKey key,
final Nonce nonce,
final InputStream input,
final OutputStream output)
throws CryptoException, StreamException
{
final byte[] iv = nonce.generate();
final byte[] header = new CiphertextHeader(iv).encode();
cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header));
writeHeader(header, output);
process(new AEADBlockCipherAdapter(cipher), input, output);
}
/**
* Decrypts data using an AEAD cipher.
*
* @param cipher AEAD cipher.
* @param key Encryption key.
* @param data Ciphertext data containing a prepended {@link CiphertextHeader}. The header is treated as AAD input
* to the cipher that is verified during decryption.
*
* @return Decrypted data that completely fills the returned byte array.
*
* @throws CryptoException on encryption errors.
* @throws EncodingException on decoding cyphertext header.
*/
public static byte[] decrypt(final AEADBlockCipher cipher, final SecretKey key, final byte[] data)
throws CryptoException, EncodingException
{
final CiphertextHeader header = CiphertextHeader.decode(data);
final byte[] nonce = header.getNonce();
final byte[] hbytes = header.encode();
cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes));
return decrypt(new AEADBlockCipherAdapter(cipher), data, header.getLength());
}
/**
* Decrypts data using an AEAD cipher.
*
* @param cipher AEAD cipher.
* @param key Encryption key.
* @param input Input stream containing a {@link CiphertextHeader} followed by ciphertext data. The header is
* treated as AAD input to the cipher that is verified during decryption.
* @param output Output stream that receives plaintext produced by block cipher in decryption mode.
*
* @throws CryptoException on encryption errors.
* @throws EncodingException on decoding cyphertext header.
* @throws StreamException on IO errors.
*/
public static void decrypt(
final AEADBlockCipher cipher,
final SecretKey key,
final InputStream input,
final OutputStream output)
throws CryptoException, EncodingException, StreamException
{
final CiphertextHeader header = CiphertextHeader.decode(input);
final byte[] nonce = header.getNonce();
final byte[] hbytes = header.encode();
cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes));
process(new AEADBlockCipherAdapter(cipher), input, output);
}
/**
* Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeader} is prepended to the
* resulting ciphertext.
*
* @param cipher Block cipher.
* @param key Encryption key.
* @param nonce IV generator. Callers must take care to ensure that the length of generated IVs is equal to the
* cipher block size.
* @param data Plaintext data to be encrypted.
*
* @return Concatenation of encoded {@link CiphertextHeader} and encrypted data that completely fills the returned
* byte array.
*
* @throws CryptoException on encryption errors.
*/
public static byte[] encrypt(final BlockCipher cipher, final SecretKey key, final Nonce nonce, final byte[] data)
throws CryptoException
{
final byte[] iv = nonce.generate();
final byte[] header = new CiphertextHeader(iv).encode();
final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv));
return encrypt(new BufferedBlockCipherAdapter(padded), header, data);
}
/**
* Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeader} is prepended to the
* resulting ciphertext.
*
* @param cipher Block cipher.
* @param key Encryption key.
* @param nonce IV generator. Callers must take care to ensure that the length of generated IVs is equal to the
* cipher block size.
* @param input Input stream containing plaintext data.
* @param output Output stream that receives ciphertext produced by block cipher in encryption mode.
*
* @throws CryptoException on encryption errors.
* @throws StreamException on IO errors.
*/
public static void encrypt(
final BlockCipher cipher,
final SecretKey key,
final Nonce nonce,
final InputStream input,
final OutputStream output)
throws CryptoException, StreamException
{
final byte[] iv = nonce.generate();
final byte[] header = new CiphertextHeader(iv).encode();
final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv));
writeHeader(header, output);
process(new BufferedBlockCipherAdapter(padded), input, output);
}
/**
* Decrypts data using the given block cipher with PKCS5 padding.
*
* @param cipher Block cipher.
* @param key Encryption key.
* @param data Ciphertext data containing a prepended {@link CiphertextHeader}.
*
* @return Decrypted data that completely fills the returned byte array.
*
* @throws CryptoException on encryption errors.
* @throws EncodingException on decoding cyphertext header.
*/
public static byte[] decrypt(final BlockCipher cipher, final SecretKey key, final byte[] data)
throws CryptoException, EncodingException
{
final CiphertextHeader header = CiphertextHeader.decode(data);
final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce()));
return decrypt(new BufferedBlockCipherAdapter(padded), data, header.getLength());
}
/**
* Decrypts data using the given block cipher with PKCS5 padding.
*
* @param cipher Block cipher.
* @param key Encryption key.
* @param input Input stream containing a {@link CiphertextHeader} followed by ciphertext data.
* @param output Output stream that receives plaintext produced by block cipher in decryption mode.
*
* @throws CryptoException on encryption errors.
* @throws EncodingException on decoding cyphertext header.
* @throws StreamException on IO errors.
*/
public static void decrypt(
final BlockCipher cipher,
final SecretKey key,
final InputStream input,
final OutputStream output)
throws CryptoException, EncodingException, StreamException
{
final CiphertextHeader header = CiphertextHeader.decode(input);
final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce()));
process(new BufferedBlockCipherAdapter(padded), input, output);
}
/**
* Encrypts the given data.
*
* @param cipher Adapter for either a block or AEAD cipher.
* @param header Encoded ciphertext header.
* @param data Plaintext data to encrypt.
*
* @return Concatenation of encoded header and encrypted data that completely fills the returned byte array.
*/
private static byte[] encrypt(final BlockCipherAdapter cipher, final byte[] header, final byte[] data)
{
final int outSize = header.length + cipher.getOutputSize(data.length);
final byte[] output = new byte[outSize];
System.arraycopy(header, 0, output, 0, header.length);
int outOff = header.length;
outOff += cipher.processBytes(data, 0, data.length, output, outOff);
cipher.doFinal(output, outOff);
cipher.reset();
return output;
}
/**
* Decrypts the given data.
*
* @param cipher Adapter for either a block or AEAD cipher.
* @param data Ciphertext data containing prepended header bytes.
* @param inOff Offset into ciphertext at which encrypted data starts (i.e. after header).
*
* @return Decrypted data that completely fills the returned byte array.
*/
private static byte[] decrypt(final BlockCipherAdapter cipher, final byte[] data, final int inOff)
{
final int len = data.length - inOff;
final int outSize = cipher.getOutputSize(len);
final byte[] output = new byte[outSize];
int outOff = cipher.processBytes(data, inOff, len, output, 0);
outOff += cipher.doFinal(output, outOff);
cipher.reset();
if (outOff < output.length) {
final byte[] temp = new byte[outOff];
System.arraycopy(output, 0, temp, 0, outOff);
return temp;
}
return output;
}
/**
* Performs encryption or decryption on the given input stream based on the underlying cipher mode and writes the
* result to the given output stream.
*
* @param cipher Adapter for either a block or AEAD cipher.
* @param input Input stream containing data to be processed by the cipher.
* @param output Output stream that receives the output of the cipher acting on the input.
*/
private static void process(final BlockCipherAdapter cipher, final InputStream input, final OutputStream output)
{
final int inSize = 1024;
final int outSize = cipher.getOutputSize(inSize);
final byte[] inBuf = new byte[inSize];
final byte[] outBuf = new byte[outSize > inSize ? outSize : inSize];
int readLen;
int writeLen;
try {
while ((readLen = input.read(inBuf)) > 0) {
writeLen = cipher.processBytes(inBuf, 0, readLen, outBuf, 0);
output.write(outBuf, 0, writeLen);
}
writeLen = cipher.doFinal(outBuf, 0);
output.write(outBuf, 0, writeLen);
} catch (IOException e) {
throw new StreamException(e);
}
}
/**
* Writes a ciphertext header to the output stream.
*
* @param header Ciphertext header bytes.
* @param output Output stream.
*/
private static void writeHeader(final byte[] header, final OutputStream output)
{
try {
output.write(header, 0, header.length);
} catch (IOException e) {
throw new StreamException(e);
}
}
}