/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.picketlink.json.jose.crypto;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.picketlink.json.util.JOSEUtil;
/**
* AES/CBC/PKCS5Padding and AES/CBC/PKCS5Padding/HMAC-SHA2 encryption and decryption methods.
*
* <p>
* Also supports the deprecated AES/CBC/HMAC encryption using a custom concat KDF (JOSE draft suite 08).
*
* <p>
* See draft-ietf-jose-json-web-algorithms-26, section 5.2.
*
* @author Giriraj Sharma
*/
public class AESCBC {
/**
* The standard Initialization Vector (IV) length (128 bits).
*/
public static final int IV_BIT_LENGTH = 128;
/**
* Generates a random 128 bit (16 byte) Initialization Vector(IV) for use in AES-CBC encryption.
*
* @param randomGen The secure random generator to use. Must be correctly initialized and not {@code null}.
*
* @return The random 128 bit IV, as 16 byte array.
*/
public static byte[] generateIV(final SecureRandom randomGen) {
byte[] bytes = new byte[IV_BIT_LENGTH / 8];
randomGen.nextBytes(bytes);
return bytes;
}
/**
* Creates a new AES/CBC/PKCS5Padding cipher.
*
* @param secretKey The AES key. Must not be {@code null}.
* @param forEncryption If {@code true} creates an encryption cipher, else creates a decryption cipher.
* @param iv The initialization vector (IV). Must not be {@code null}.
*
* @return The AES/CBC/PKCS5Padding cipher.
*/
private static Cipher createAESCBCCipher(final SecretKey secretKey,
final boolean forEncryption,
final byte[] iv) {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", new BouncyCastleProvider());
SecretKeySpec keyspec = new SecretKeySpec(secretKey.getEncoded(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
if (forEncryption) {
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivSpec);
} else {
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivSpec);
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
return cipher;
}
/**
* Encrypts the specified plain text using AES/CBC/PKCS5Padding.
*
* @param secretKey The AES key. Must not be {@code null}.
* @param iv The initialization vector (IV). Must not be {@code null}.
* @param plainText The plain text. Must not be {@code null}.
*
* @return The cipher text.
*
* @throws RuntimeException If encryption failed.
*/
public static byte[] encrypt(final SecretKey secretKey,
final byte[] iv,
final byte[] plainText) {
Cipher cipher = createAESCBCCipher(secretKey, true, iv);
try {
return cipher.doFinal(plainText);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Computes the bit length of the specified Additional Authenticated Data (AAD). Used in AES/CBC/PKCS5Padding/HMAC-SHA2
* encryption.
*
* @param aad The Additional Authenticated Data (AAD). Must not be {@code null}.
*
* @return The computed AAD bit length, as a 64 bit big-ending representation (8 byte array).
*/
public static byte[] computeAADLength(final byte[] aad) {
final int bitLength = aad.length * 8;
return ByteBuffer.allocate(8).putLong(bitLength).array();
}
/**
* Encrypts the specified plain text using AES/CBC/PKCS5Padding/ HMAC-SHA2.
*
* <p>
* See draft-ietf-jose-json-web-algorithms-26, section 5.2.
*
* <p>
* See draft-mcgrew-aead-aes-cbc-hmac-sha2-01
*
* @param secretKey The secret key. Must be 256 or 512 bits long. Must not be {@code null}.
* @param iv The initialisation vector (IV). Must not be {@code null}.
* @param plainText The plain text. Must not be {@code null}.
* @param aad The additional authenticated data. Must not be {@code null}.
*
* @return The authenticated cipher text.
*
* @throws RuntimeException If encryption failed.
*/
public static AuthenticatedCipherText encryptAuthenticated(final SecretKey secretKey,
final byte[] iv,
final byte[] plainText,
final byte[] aad) {
// Extract MAC + AES/CBC keys from input secret key
CompositeKey compositeKey = new CompositeKey(secretKey);
// Encrypt plain text
byte[] cipherText = encrypt(compositeKey.getAESKey(), iv, plainText);
// AAD length to 8 byte array
byte[] al = computeAADLength(aad);
// Do MAC
int hmacInputLength = aad.length + iv.length + cipherText.length + al.length;
byte[] hmacInput = ByteBuffer.allocate(hmacInputLength).put(aad).put(iv).put(cipherText).put(al).array();
byte[] hmac = HMAC.compute(compositeKey.getMACKey(), hmacInput);
byte[] authTag = Arrays.copyOf(hmac, compositeKey.getTruncatedMACByteLength());
return new AuthenticatedCipherText(cipherText, authTag);
}
/**
* Decrypts the specified cipher text using AES/CBC/PKCS5Padding.
*
* @param secretKey The AES key. Must not be {@code null}.
* @param iv The initialization vector (IV). Must not be {@code null}.
* @param cipherText The cipher text. Must not be {@code null}.
*
* @return The decrypted plain text.
*
* @throws RuntimeException If decryption failed.
*/
public static byte[] decrypt(final SecretKey secretKey,
final byte[] iv,
final byte[] cipherText) {
Cipher cipher = createAESCBCCipher(secretKey, false, iv);
try {
return cipher.doFinal(cipherText);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Decrypts the specified cipher text using AES/CBC/PKCS5Padding/ HMAC-SHA2.
*
* <p>
* See draft-ietf-jose-json-web-algorithms-26, section 5.2.
*
* @param secretKey The secret key. Must be 256 or 512 bits long. Must not be {@code null}.
* @param iv The initialization vector (IV). Must not be {@code null}.
* @param cipherText The cipher text. Must not be {@code null}.
* @param aad The additional authenticated data. Must not be {@code null}.
* @param authTag The authentication tag. Must not be {@code null}.
*
* @return The decrypted plain text.
*
* @throws RuntimeException If decryption failed.
*/
public static byte[] decryptAuthenticated(final SecretKey secretKey,
final byte[] iv,
final byte[] cipherText,
final byte[] aad,
final byte[] authTag) {
// Extract MAC + AES/CBC keys from input secret key
CompositeKey compositeKey = new CompositeKey(secretKey);
// AAD length to 8 byte array
byte[] al = computeAADLength(aad);
// Check MAC
int hmacInputLength = aad.length + iv.length + cipherText.length + al.length;
byte[] hmacInput = ByteBuffer.allocate(hmacInputLength).put(aad).put(iv).put(cipherText).put(al).array();
byte[] hmac = HMAC.compute(compositeKey.getMACKey(), hmacInput);
byte[] expectedAuthTag = Arrays.copyOf(hmac, compositeKey.getTruncatedMACByteLength());
boolean macCheckPassed = true;
if (!JOSEUtil.constantTimeAreEqual(expectedAuthTag, authTag)) {
// Thwart timing attacks by delaying exception until after decryption
macCheckPassed = false;
}
byte[] plainText = decrypt(compositeKey.getAESKey(), iv, cipherText);
if (!macCheckPassed) {
throw new RuntimeException("MAC check failed");
}
return plainText;
}
/**
* Prevents public instantiation.
*/
private AESCBC() {
}
}