org.chililog.server.common.CryptoUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.chililog.server.common.CryptoUtils.java

Source

//
// 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());
        }
    }
}