CryptoUtils.java

package org.flasby.crypto;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import org.mindrot.jbcrypt.BCrypt;

public final class CryptoUtils {

  public static final String DEFAULT_KEY_ALGORITHM = "PBKDF2WithHmacSHA512";
  public static final String DEFAULT_CIPHER = "AES/CBC/PKCS5Padding";
  public static final String ENCODING_CHARSET = "UTF-8";

  private static final byte[] DEFAULT_SALT = {
    (byte) 0xA9,
    (byte) 0x9B,
    (byte) 0xC8,
    (byte) 0x32,
    (byte) 0x56,
    (byte) 0x35,
    (byte) 0xE3,
    (byte) 0x03
  };
  private static final int ITERATION_COUNT = 1024;
  private static final int KEY_LENGTH = 128;

  public static void main(final String[] args) {
    final String x = hashPassword("123");
    System.out.println("Check 1: " + checkPassword("123", x));
    System.out.println("Check 2: " + checkPassword("12x", x));
  }

  public static class CipherPair {
    public final Cipher enCipher;
    public final Cipher deCipher;

    public CipherPair(final Cipher enCipher, final Cipher deCipher) {
      this.enCipher = enCipher;
      this.deCipher = deCipher;
    }
  }

  /**
   * generates a new CipherPair which can be used to encrypt and decrypt stuff. Importantly, the IV
   * is random so you can't decrypt anything using a new pair with the same passPhrase without first
   * initialising the decrypt IV.
   *
   * @param passPhrase
   * @return
   * @throws SecurityException
   */
  public static CipherPair generateCipherPair(char[] passPhrase) {
    return generateCipherPair(passPhrase, null);
  }

  public static final CipherPair generateCipherPair(final char[] passPhrase, byte[] iv)
      throws SecurityException {
    return generateCipherPair(passPhrase, DEFAULT_SALT, DEFAULT_KEY_ALGORITHM, iv);
  }

  public static final CipherPair generateCipherPair(
      final char[] passPhrase, final byte[] salt, final String keyAlgorithm, byte[] iv)
      throws SecurityException {

    KeySpec spec;
    {
      // Copy the salt and pass phrase
      final byte[] mySalt = Arrays.copyOf(salt, salt.length);
      final char[] myPassPhrase = Arrays.copyOf(passPhrase, passPhrase.length);
      // Generate the KeySpec
      spec = new PBEKeySpec(myPassPhrase, mySalt, ITERATION_COUNT, KEY_LENGTH);
      // And now destroy my copy of the salt & passPhrase, it's up to the caller to manage their
      // inputs
      Arrays.fill(mySalt, Byte.MAX_VALUE);
      Arrays.fill(myPassPhrase, 'X');
    }
    try {
      final SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgorithm);
      final SecretKey tmp = factory.generateSecret(spec);
      final SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

      final Cipher ecipher = Cipher.getInstance(CryptoUtils.DEFAULT_CIPHER);
      if (iv == null || iv.length == 0) {
        ecipher.init(Cipher.ENCRYPT_MODE, secret);
      } else {
        ecipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(iv));
      }

      iv = ecipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
      final Cipher dcipher = generateDeCipher(passPhrase, salt, keyAlgorithm, iv);
      return new CipherPair(ecipher, dcipher);
    } catch (NoSuchAlgorithmException
        | InvalidKeySpecException
        | NoSuchPaddingException
        | InvalidKeyException
        | InvalidParameterSpecException
        | InvalidAlgorithmParameterException e) {
      throw new SecurityException(e);
    }
  }

  public static Cipher generateDeCipher(
      final char[] passPhrase, final byte[] salt, final String keyAlgorithm, final byte[] iv)
      throws NoSuchAlgorithmException,
          InvalidKeySpecException,
          NoSuchPaddingException,
          InvalidKeyException,
          InvalidAlgorithmParameterException {
    final char[] myPassPhrase = Arrays.copyOf(passPhrase, passPhrase.length);
    final byte[] mySalt = Arrays.copyOf(salt, salt.length);

    // Generate the KeySpec
    KeySpec spec = new PBEKeySpec(myPassPhrase, mySalt, ITERATION_COUNT, KEY_LENGTH);
    // And now destroy my copy of the salt & passPhrase, it's up to the caller to manage their
    // inputs
    Arrays.fill(mySalt, Byte.MAX_VALUE);
    Arrays.fill(myPassPhrase, 'X');
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgorithm);
    final SecretKey tmp = factory.generateSecret(spec);
    final SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

    final Cipher dcipher = Cipher.getInstance(CryptoUtils.DEFAULT_CIPHER);
    dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
    return dcipher;
  }

  public static final String hashPassword(final String password) {
    final String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12));
    return hashed;
  }

  public static boolean checkPassword(final String candidatePassword, final String hash) {
    return (BCrypt.checkpw(candidatePassword, hash));
  }

  public final byte[] hashPassword(
      final char[] password, final byte[] salt, final int iterations, final int keyLength) {
    try {
      final SecretKeyFactory skf = SecretKeyFactory.getInstance(DEFAULT_KEY_ALGORITHM);
      final PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
      final SecretKey key = skf.generateSecret(spec);
      final byte[] res = key.getEncoded();
      key.destroy();
      spec.clearPassword();
      return res;

    } catch (DestroyFailedException | NoSuchAlgorithmException | InvalidKeySpecException e) {
      throw new RuntimeException(e);
    }
  }

  public static String toBase64(byte[] toEncode) {
    return Base64.getEncoder().encodeToString(toEncode);
  }

  public static byte[] fromBase64(String toDecode) {
    return Base64.getDecoder().decode(toDecode);
  }
}