package org.openstack.atlas.util.crypto;
import org.openstack.atlas.util.config.MossoConfig;
import org.openstack.atlas.util.crypto.exception.DecryptException;
import org.openstack.atlas.util.crypto.exception.EncryptException;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* A utility class providing static encryption and decryption methods.
*/
public final class CryptoUtil {
public static final String ENCODING = "UTF-8";
private static final Logger LOGGER = Logger.getLogger(CryptoUtil.class);
private CryptoUtil() {
}
/**
* Decrypts an encrypted String containing sensitive data using an AES/ECB/NoPadding transformation.
*
* @param encrypted The encrypted input string
* @return A plaintext string
* @throws DecryptException
*/
public static String decrypt(String encrypted) throws DecryptException {
String s = null;
if (encrypted != null) {
byte[] b = decryptAES(MossoConfig.getCryptoKeySpec(),
CryptoUtilValues.TRANSFORMATION_MODE_ECB,
CryptoUtilValues.TRANSFORMATION_PADDING_NO_PADDING, CryptoUtil.asBytes(encrypted,
CryptoUtilValues.CRYPTO_PAD_BYTE));
try {
s = new String(b, ENCODING);
} catch (UnsupportedEncodingException ex) {
// This shouldn't happen with UTF-8
s = new String(b);
}
if (s != null) {
s = s.trim();
}
}
return s;
}
/**
* Encrypts a String containing sensitive data using an AES/ECB/NoPadding transformation
*
* @param decrypted The plaintext string to be encrypted
* @return An encrypted string, or null if "decrypted" is null
* @throws EncryptException If there are any errors
*/
public static String encrypt(String decrypted) throws EncryptException {
if (decrypted == null) {
return null;
} else {
return asHex(encryptAES(MossoConfig.getCryptoKeySpec(), CryptoUtilValues.TRANSFORMATION_MODE_ECB,
CryptoUtilValues.TRANSFORMATION_PADDING_NO_PADDING, padString(decrypted)));
}
}
/**
* Transforms a hex string into a byte array, and removes any padding.
*
* @param hexString The hex string to transform
* @param padByte The padding byte to remove
* @return A byte array
*/
protected static byte[] asBytes(String hexString, byte padByte) throws DecryptException {
byte[] number = new byte[hexString.length() / 2];
try {
for (int i = 0; i < hexString.length(); i += 2) {
int j = Integer.parseInt(hexString.substring(i, i + 2), 16);
number[i / 2] = (byte) (j & 0x000000ff);
}
} catch (StringIndexOutOfBoundsException ex) {
ex.printStackTrace();
throw new DecryptException("Invalid hex string");
}
return number;
}
/**
* Transforms a byte array into a hex string.
*
* @param bytes An array of bytes to convert
* @return A hex string
*/
protected static String asHex(byte bytes[]) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
if ((bytes[i] & 0xff) < 0x10) {
sb.append("0");
}
sb.append(Long.toString(bytes[i] & 0xff, 16));
}
return sb.toString();
}
/**
* Decrypts an encrypted byte array using a random key spec with the specified key length.
*
* @param keySize The key size
* @param cipherMode The mode to use for the transformation
* @param cipherPadding The paddint to use for the transformation
* @param encrypted The encrypted bytes
* @return The decrypted bytes
* @throws DecryptException
*/
protected static byte[] decryptAES(int keySize, String cipherMode, String cipherPadding, byte[] encrypted)
throws DecryptException {
try {
// Initialize the key generator
KeyGenerator kgen = KeyGenerator.getInstance(CryptoUtilValues.TRANSFORMATION_ALG_AES);
kgen.init(keySize);
// Generate the secret key specs.
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
SecretKeySpec skeySpec = new SecretKeySpec(raw, CryptoUtilValues.TRANSFORMATION_ALG_AES);
return decryptAES(skeySpec, cipherMode, cipherPadding, encrypted);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Could not instantiate keygen for AES decryption", e);
throw new DecryptException("Could not instantiate keygen", e);
}
}
/**
* Decrypts an encrypted byte array using the given key spec.
*
* @param key The key spec to use
* @param cipherMode The mode to use for the transformation
* @param cipherPadding The paddint to use for the transformation
* @param encrypted
* @return The unencrypted bytes
* @throws DecryptException
*/
protected static byte[] decryptAES(SecretKeySpec key, String cipherMode, String cipherPadding,
byte[] encrypted) throws DecryptException {
try {
// Build the transformation string
StringBuilder transformation = new StringBuilder(CryptoUtilValues.TRANSFORMATION_ALG_AES);
// Add the cipher mode if specified
if (StringUtils.isNotEmpty(cipherMode)) {
transformation.append("/").append(cipherMode);
}
// Add the cipher padding if specified
if (StringUtils.isNotEmpty(cipherPadding)) {
transformation.append("/").append(cipherPadding);
}
// Initialize the cipher
Cipher c = Cipher.getInstance(transformation.toString());
c.init(Cipher.DECRYPT_MODE, key);
return c.doFinal(encrypted);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Could not instantiate cipher for AES decryption", e);
throw new DecryptException("Could not instantiate cipher", e);
} catch (NoSuchPaddingException e) {
LOGGER.error("Could not instantiate cipher for AES decryption", e);
throw new DecryptException("Could not instantiate cipher", e);
} catch (InvalidKeyException e) {
LOGGER.error("Invalid key for AES encryption", e);
throw new DecryptException("Invalid key for AES decryption", e);
} catch (IllegalBlockSizeException e) {
LOGGER.error("Could not perform AES decryption on: " + new String(encrypted), e);
throw new DecryptException("Could not perform AES decryption on: " + new String(encrypted), e);
} catch (BadPaddingException e) {
LOGGER.error("Could not perform AES decryption on: " + new String(encrypted), e);
throw new DecryptException("Could not perform AES decryption on: " + new String(encrypted), e);
}
}
/**
* Performs AES (Rijndael) encryption with a random key spec and specified key size on a string.
*
* @param keySize The key size to use
* @param cipherMode
* @param cipherPadding
* @param decrypted The bytes to encrypt
* @return An encrypted byte[]
* @throws EncryptException If there are any errors
*/
protected static byte[] encryptAES(int keySize, String cipherMode, String cipherPadding, byte[] decrypted)
throws EncryptException {
try {
// set up the key generator for use
KeyGenerator kgen = KeyGenerator.getInstance(CryptoUtilValues.TRANSFORMATION_ALG_AES);
kgen.init(keySize);
// Generate the secret key specs
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
SecretKeySpec skeySpec = new SecretKeySpec(raw, CryptoUtilValues.TRANSFORMATION_ALG_AES);
return encryptAES(skeySpec, cipherMode, cipherPadding, decrypted);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Could not decrypt data", e);
throw new EncryptException("Could not decrypt data", e);
} catch (Exception e) {
LOGGER.error("Could not decrypt data", e);
throw new EncryptException("Could not decrypt data", e);
}
}
/**
* Performs AES (Rijndael) encryption with the specified key spec on a string.
*
* @param key
* @param cipherMode
* @param cipherPadding
* @param decrypted The bytes to encrypt
* @return An encrypted byte[]
* @throws EncryptException Thrown if anything goes wrong during encryption
*/
protected static byte[] encryptAES(SecretKeySpec key, String cipherMode, String cipherPadding,
byte[] decrypted) throws EncryptException {
try {
// Build the transformation string
StringBuilder transformation = new StringBuilder(CryptoUtilValues.TRANSFORMATION_ALG_AES);
// Add the cipher mode if specified
if (StringUtils.isNotEmpty(cipherMode)) {
transformation.append("/").append(cipherMode);
}
// Add the cipher padding if specified
if (StringUtils.isNotEmpty(cipherPadding)) {
transformation.append("/").append(cipherPadding);
}
// Initialize the cipher
Cipher c = Cipher.getInstance(transformation.toString());
c.init(Cipher.ENCRYPT_MODE, key);
return c.doFinal(decrypted);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
} catch (NoSuchPaddingException e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
} catch (InvalidKeyException e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
} catch (IllegalBlockSizeException e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
} catch (BadPaddingException e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
} catch (Exception e) {
LOGGER.error("Could not encrypt data", e);
throw new EncryptException("Could not encrypt data", e);
}
}
/**
* Pads a string for AES encryption so that compatibility with PHP is preserved, returning the result to
* the caller as a byte array.
*
* @param in The input string to be padded
* @return A byte array
*/
protected static byte[] padString(String in) {
int inputLength = 0;
try {
inputLength = in.getBytes(ENCODING).length;
} catch (UnsupportedEncodingException ex) {
// This should never happen with UTF-8 encoding, but none the less.
inputLength = in.getBytes().length;
}
int extraBytes = inputLength % CryptoUtilValues.CRYPTO_PAD_BLOCK_SIZE;
byte[] ret = null;
if (extraBytes > 0) {
int bytesNeeded = CryptoUtilValues.CRYPTO_PAD_BLOCK_SIZE - extraBytes;
StringBuilder sb = new StringBuilder(inputLength + bytesNeeded);
sb.append(in);
for (int i = 0; i < bytesNeeded; i++) {
sb.append(" ");
}
try {
ret = sb.toString().getBytes(ENCODING);
} catch (UnsupportedEncodingException ex) {
// This should never happen with UTF-8 encoding, but none the less.
ret = sb.toString().getBytes();
}
} else {
try {
ret = in.getBytes(ENCODING);
} catch (UnsupportedEncodingException ex) {
// This should never happen with UTF-8 encoding, but none the less.
ret = in.getBytes();
}
}
return ret;
}
}