//
// Copyright 2010 Cinch Logic Pty Ltd.
//
// http://www.chililog.com
//
// 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 org.chililog.server.common;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.NullArgumentException;
/**
* <p>
* Utilities methods for hashing and encrypting.
* </p>
* <p>
* NOTE: see here for list of Sun jdk security providers.
* http://download.oracle.com/javase/6/docs/technotes/guides/security/SunProviders.html
* </p>
*
* @author vibul
*
*/
public class CryptoUtils {
private static final byte[] AES_ENCRYPTION_STRING_SALT = new byte[] { 3, 56, 23, 120, 34, 92 };
private static final byte[] AES_ENCRYPTION_INTIALIZATION_VECTOR = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };
/**
* MD5 hash
*
* @param s
* string to hash
* @return MD5 hash as a hex string
* @throws ChiliLogException
*/
public static String createMD5Hash(String s) throws ChiliLogException {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(s.getBytes("CP1252"));
StringBuffer sb = new StringBuffer();
for (int i = 0; i < array.length; ++i) {
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to MD5 hash: " + ex.getMessage());
}
}
/**
* <p>
* From a password, a number of iterations and a salt, returns the corresponding hash. For convenience, the salt is
* stored within the hash.
* </p>
*
* <p>
* This convention is used: <code>base64(hash(plainTextValue + salt)+salt)</code>
* </p>
*
* @param plainTextValue
* String The password to encrypt
* @param salt
* byte[] The salt. If null, one will be created on your behalf.
* @return String The hash password
* @throws ChiliLogException
* if SHA-512 is not supported or UTF-8 is not a supported encoding algorithm
*/
public static String createSHA512Hash(String plainTextValue, byte[] salt) throws ChiliLogException {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
// Salt generation 64 bits long
salt = new byte[8];
random.nextBytes(salt);
return createSHA512Hash(plainTextValue, salt, true);
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to hash passwords. " + ex.getMessage());
}
}
/**
* <p>
* From a password, a number of iterations and a salt, returns the corresponding hash.
* </p>
* <p>
* If the salt is to be appended, this convention is used: <code>base64(hash(plainTextValue + salt)+salt)</code>
* </p>
* <p>
* If the salt is NOT to be appended, this convention is used: <code>base64(hash(plainTextValue + salt))</code>
* </p>
*
* @param plainTextValue
* String The password to encrypt
* @param salt
* byte[] The salt. If null, one will be created on your behalf.
* @param appendSalt
* True if the salt is to be appended to hashed value. In this way, for convenience, the salt can be kept
* with the hash. Use this only if the hash is to be kept internal to this app. If the hash is to be sent
* to external systems, set this to false and store the hash internally.
* @return String The hash password
* @throws ChiliLogException
* if SHA-512 is not supported or UTF-8 is not a supported encoding algorithm
*/
public static String createSHA512Hash(String plainTextValue, byte[] salt, boolean appendSalt)
throws ChiliLogException {
try {
if (plainTextValue == null) {
throw new NullArgumentException("plainTextValue");
}
if (salt == null) {
throw new NullArgumentException("salt");
}
// Convert plain text into a byte array.
byte[] plainTextBytes = plainTextValue.getBytes("UTF-8");
// Allocate array, which will hold plain text and salt.
byte[] plainTextWithSaltBytes = new byte[plainTextBytes.length + salt.length];
// Copy plain text bytes into resulting array.
for (int i = 0; i < plainTextBytes.length; i++) {
plainTextWithSaltBytes[i] = plainTextBytes[i];
}
// Append salt bytes to the resulting array.
if (appendSalt) {
for (int i = 0; i < salt.length; i++) {
plainTextWithSaltBytes[plainTextBytes.length + i] = salt[i];
}
}
// Create hash
MessageDigest digest = MessageDigest.getInstance("SHA-512");
digest.reset();
byte[] hashBytes = digest.digest(plainTextWithSaltBytes);
// Create array which will hold hash and original salt bytes.
byte[] hashWithSaltBytes = new byte[hashBytes.length + salt.length];
// Copy hash bytes into resulting array.
for (int i = 0; i < hashBytes.length; i++) {
hashWithSaltBytes[i] = hashBytes[i];
}
// Append salt bytes to the result.
for (int i = 0; i < salt.length; i++) {
hashWithSaltBytes[hashBytes.length + i] = salt[i];
}
// Convert hash to string
Base64 encoder = new Base64(1000, new byte[] {}, false);
return encoder.encodeToString(hashWithSaltBytes);
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to hash passwords. " + ex.getMessage());
}
}
/**
* Verifies if a plain text value (like a password) is valid and has not changed. This method assumes that the salt
* is stored in the hash.
*
* @param plainTextValue
* plain text value to check against the hash value
* @param hashValue
* expected has value as returned by <code>createHash</code>.
* @return true if the plain text value has not been changed, false if not
* @throws ChiliLogException
* if SHA-512 is not supported or UTF-8 is not a supported encoding algorithm
*/
public static boolean verifyHash(String plainTextValue, String hashValue) throws ChiliLogException {
return verifyHash(plainTextValue, null, hashValue);
}
/**
* Verifies if a plain text value (like a password) is valid and has not changed.
*
* @param plainTextValue
* plain text value to check against the hash value
* @param salt
* salt to add to the hash. If null, this assumes the the salt is stored within the hash.
* @param hashValue
* expected has value as returned by <code>createHash</code>.
* @return true if the plain text value has not been changed, false if not
* @throws ChiliLogException
* if SHA-512 is not supported or UTF-8 is not a supported encoding algorithm
*/
public static boolean verifyHash(String plainTextValue, byte[] salt, String hashValue) throws ChiliLogException {
try {
if (plainTextValue == null) {
throw new NullArgumentException("plainTextValue");
}
// Convert base64-encoded hash value into a byte array.
Base64 decoder = new Base64(1000, new byte[] {}, false);
byte[] hashWithSaltBytes = decoder.decode(hashValue);
// We must know size of hash (without salt).
int hashSizeInBits, hashSizeInBytes;
// Size of hash is based on the specified algorithm - i.e. 512 for SHA-512.
hashSizeInBits = 512;
// Convert size of hash from bits to bytes.
hashSizeInBytes = hashSizeInBits / 8;
// Make sure that the specified hash value is long enough.
if (hashWithSaltBytes.length < hashSizeInBytes) {
return false;
}
// Get the salt. If not passed in, then assume salt is stored with the hash
boolean saltAppended = (salt == null);
byte[] saltBytes = salt;
if (saltAppended) {
// Allocate array to hold original salt bytes retrieved from hash.
saltBytes = new byte[hashWithSaltBytes.length - hashSizeInBytes];
// Copy salt from the end of the hash to the new array.
for (int i = 0; i < saltBytes.length; i++) {
saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];
}
}
// Compute a new hash string.
String expectedHashString = createSHA512Hash(plainTextValue, saltBytes, saltAppended);
// If the computed hash matches the specified hash,
// the plain text value must be correct.
return (hashValue.equals(expectedHashString));
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to verify passwords. " + ex.getMessage());
}
}
/**
* <p>
* Encrypt a plain text string using AES. The output is an encrypted plain text string. See
* http://stackoverflow.com/questions/992019/java-256bit-aes-encryption/992413#992413
* </p>
* <p>
* The algorithm used is <code>base64(aes(plainText))</code>
* </p>
*
*
* @param plainText
* text to encrypt
* @param password
* password to use for encryption
* @return encrypted text
* @throws ChiliLogException
*/
public static String encryptAES(String plainText, String password) throws ChiliLogException {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password.toCharArray(), AES_ENCRYPTION_STRING_SALT, 1024, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
byte[] plainTextBytes = plainText.getBytes("UTF-8");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(AES_ENCRYPTION_INTIALIZATION_VECTOR);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret, paramSpec);
byte[] cipherText = cipher.doFinal(plainTextBytes);
// Convert hash to string
Base64 encoder = new Base64(1000, new byte[] {}, false);
return encoder.encodeToString(cipherText);
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to encrypt. " + ex.getMessage());
}
}
/**
* <p>
* Decrypt an encrypted text string using AES. The output is the plain text string.
* </p>
*
* @param encryptedText
* encrypted text returned by <code>encrypt</code>
* @param password
* password used at the time of encryption
* @return decrypted plain text string
* @throws ChiliLogException
*/
public static String decryptAES(String encryptedText, String password) throws ChiliLogException {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password.toCharArray(), AES_ENCRYPTION_STRING_SALT, 1024, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
Base64 decoder = new Base64(1000, new byte[] {}, false);
byte[] encryptedTextBytes = decoder.decode(encryptedText);
AlgorithmParameterSpec paramSpec = new IvParameterSpec(AES_ENCRYPTION_INTIALIZATION_VECTOR);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, paramSpec);
byte[] plainTextBytes = cipher.doFinal(encryptedTextBytes);
return new String(plainTextBytes, "UTF-8");
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to decrpt. " + ex.getMessage());
}
}
/**
* <p>
* Encrypt a plain text string using TripleDES. The output is an encrypted plain text string. See
* http://stackoverflow.com/questions/20227/how-do-i-use-3des-encryption-decryption-in-java
* </p>
* <p>
* The algorithm used is <code>base64(tripleDES(plainText))</code>
* </p>
* <p>
* TripleDES is a lot quicker than AES.
* </p>
*
* @param plainText
* text to encrypt
* @param password
* password to use for encryption
* @return encrypted text
* @throws ChiliLogException
*/
public static String encryptTripleDES(String plainText, String password) throws ChiliLogException {
try {
return encryptTripleDES(plainText, password.getBytes("UTF-8"));
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to encrypt. " + ex.getMessage());
}
}
/**
* <p>
* Encrypt a plain text string using TripleDES. The output is an encrypted plain text string. See
* http://stackoverflow.com/questions/20227/how-do-i-use-3des-encryption-decryption-in-java
* </p>
* <p>
* The algorithm used is <code>base64(tripleDES(plainText))</code>
* </p>
* <p>
* TripleDES is a lot quicker than AES.
* </p>
*
* @param plainText
* text to encrypt
* @param password
* password to use for encryption
* @return encrypted text
* @throws ChiliLogException
*/
public static String encryptTripleDES(String plainText, byte[] password) throws ChiliLogException {
try {
final MessageDigest md = MessageDigest.getInstance("md5");
final byte[] digestOfPassword = md.digest(password);
final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
for (int j = 0, k = 16; j < 8;) {
keyBytes[k++] = keyBytes[j++];
}
final SecretKey key = new SecretKeySpec(keyBytes, "DESede");
final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
final Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
final byte[] plainTextBytes = plainText.getBytes("UTF-8");
final byte[] cipherText = cipher.doFinal(plainTextBytes);
// Convert hash to string
Base64 encoder = new Base64(1000, new byte[] {}, false);
return encoder.encodeToString(cipherText);
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to encrypt. " + ex.getMessage());
}
}
/**
* <p>
* Decrypt an encrypted text string using TripleDES. The output is the plain text string.
* </p>
*
* @param encryptedText
* encrypted text returned by <code>encrypt</code>
* @param password
* password used at the time of encryption
* @return decrypted plain text string
* @throws ChiliLogException
*/
public static String decryptTripleDES(String encryptedText, String password) throws ChiliLogException {
try {
return decryptTripleDES(encryptedText, password.getBytes("UTF-8"));
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to decrpt. " + ex.getMessage());
}
}
/**
* <p>
* Decrypt an encrypted text string using TripleDES. The output is the plain text string.
* </p>
*
* @param encryptedText
* encrypted text returned by <code>encrypt</code>
* @param password
* password used at the time of encryption
* @return decrypted plain text string
* @throws ChiliLogException
*/
public static String decryptTripleDES(String encryptedText, byte[] password) throws ChiliLogException {
try {
final MessageDigest md = MessageDigest.getInstance("md5");
final byte[] digestOfPassword = md.digest(password);
final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
for (int j = 0, k = 16; j < 8;) {
keyBytes[k++] = keyBytes[j++];
}
Base64 decoder = new Base64(1000, new byte[] {}, false);
byte[] encryptedTextBytes = decoder.decode(encryptedText);
final SecretKey key = new SecretKeySpec(keyBytes, "DESede");
final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
final Cipher decipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
decipher.init(Cipher.DECRYPT_MODE, key, iv);
final byte[] plainTextBytes = decipher.doFinal(encryptedTextBytes);
return new String(plainTextBytes, "UTF-8");
} catch (Exception ex) {
throw new ChiliLogException(ex, "Error attempting to decrpt. " + ex.getMessage());
}
}
}