
package com.outerspacecat.util;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import java.security.SecureRandom;
import java.text.Normalizer;

/**
 * Defines utility methods for working with passwords.
 * 
 * @author Caleb Richardson
 */
public final class Passwords
{
  private final static SecureRandom SECURE_RANDOM = new SecureRandom();

  private Passwords ()
  {
  }

  /**
   * Hashes {@code password}. Uses the following algorithm:
   * <p>
   * <code>
   * PBKDF2WithHmacSHA1 ( HmacSHA1 ( password, pepper ), [random 64 byte salt], 8192, 32 )
   * </code>
   * <p>
   * Generated values are suitable for password storage.
   * 
   * @param password the password to hash. Will be normalized to NFC. Must be
   *        non {@code null}.
   * @param pepper the pepper to use. Should not be stored in the same place as
   *        hashed passwords. Must be non {@code null} and must be 64 bytes
   *        long.
   * @return a hashed password. Never {@code null}, 220 ASCII characters long.
   */
  public static String hashPassword ( final CharSequence password,
      final byte[] pepper )
  {
    Preconditions.checkNotNull(password, "password required");
    Preconditions.checkNotNull(pepper, "pepper required");
    Preconditions.checkArgument(pepper.length == 64, "pepper must be 64 bytes");

    String nfcPassword = Normalizer.normalize(password, Normalizer.Form.NFC);

    byte[] hmacPassword =
        Utils.hmacSha1(nfcPassword.getBytes(Charsets.UTF_8), pepper);

    byte[] salt = new byte[64];
    SECURE_RANDOM.nextBytes(salt);

    byte[] computedHash =
        Utils.pbkdf2WithHmacSha1(Utils.toHex(hmacPassword), salt, 8192, 32);

    StringBuilder sb =
        new StringBuilder(28 + salt.length * 2 + computedHash.length * 2);

    sb.append("PBKDF2WithHmacSHA1$8192$32$").append(Utils.toHex(salt)).append(
        '$').append(Utils.toHex(computedHash));

    return sb.toString();
  }

  /**
   * Returns whether or not {@code storedHash} is a hashed representation of
   * {@code password} previously generated by
   * {@link #hashPassword(CharSequence, byte[])}.
   * 
   * @param password the password to check against {@code hashed}. Will be
   *        normalized to NFC. Must be non {@code null}.
   * @param pepper the pepper used to generate {@code hashed}. Must be non
   *        {@code null} and must be 64 bytes long.
   * @param storedHash the hashed representation of {@code password} to check
   *        against. Must be non {@code null}.
   * @return whether or not {@code hash} is a hashed representation of
   *         {@code password} previously generated by
   *         {@link #hashPassword(CharSequence, byte[])}
   */
  public static boolean verifyPassword ( final CharSequence password,
      final byte[] pepper, final CharSequence storedHash )
  {
    Preconditions.checkNotNull(password, "password required");
    Preconditions.checkNotNull(pepper, "pepper required");
    Preconditions.checkArgument(pepper.length == 64, "pepper must be 64 bytes");
    Preconditions.checkNotNull(storedHash, "storedHash required");

    String nfcPassword = Normalizer.normalize(password, Normalizer.Form.NFC);

    byte[] hmacPassword =
        Utils.hmacSha1(nfcPassword.getBytes(Charsets.UTF_8), pepper);

    String[] parts = storedHash.toString().split("\\$");
    if ( parts[0].equals("PBKDF2WithHmacSHA1") ) {
      if ( parts.length != 5 )
        throw new IllegalArgumentException("invalid storedHash: " + storedHash);

      byte[] computedHash =
          Utils.pbkdf2WithHmacSha1(Utils.toHex(hmacPassword),
              Utils.fromHex(parts[3].toCharArray()),
              Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));

      return parts[4].equals(new String(Utils.toHex(computedHash)));
    } else {
      throw new IllegalArgumentException("invalid storedHash: " + storedHash);
    }
  }
}
