package com.occamlab.te.realm;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.xml.bind.DatatypeConverter;
/**
* Creates and verifies a password digest using the PBKDF2 function. Original
* code by <a href="https://github.com/defuse">Taylor Hornby</a> and other
* contributors is licensed under the terms of a derivative BSD 2-Clause
* License.
*
* <p>
* The hash format consists of five fields separated by the colon (':')
* character: <code>algorithm:iterations:hashSize:salt:hash</code>.
* </p>
* <ul>
* <li><em>algorithm</em> - the name of the cryptographic hash function ("sha1")
* </li>
* <li><em>iterations</em> - the number of PBKDF2 iterations ("64000")</li>
* <li><em>hashSize</em> - the length, in bytes, of the hash field (after
* decoding)</li>
* <li><em>salt</em> - the salt (base64 encoded)</li>
* <li><em>hash</em> - the PBKDF2 output (base64 encoded)</li>
* </ul>
*
* @see <a href="https://github.com/defuse/password-hashing">Secure Password
* Storage v2.0</a>
*/
public class PasswordStorage {
@SuppressWarnings("serial")
static public class InvalidHashException extends Exception {
public InvalidHashException(String message) {
super(message);
}
public InvalidHashException(String message, Throwable source) {
super(message, source);
}
}
@SuppressWarnings("serial")
static public class CannotPerformOperationException extends Exception {
public CannotPerformOperationException(String message) {
super(message);
}
public CannotPerformOperationException(String message, Throwable source) {
super(message, source);
}
}
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
// These constants may be changed without breaking existing hashes.
public static final int SALT_BYTE_SIZE = 24;
public static final int HASH_BYTE_SIZE = 18;
public static final int PBKDF2_ITERATIONS = 64000;
// These constants define the encoding and may not be changed.
public static final int HASH_SECTIONS = 5;
public static final int HASH_ALGORITHM_INDEX = 0;
public static final int ITERATION_INDEX = 1;
public static final int HASH_SIZE_INDEX = 2;
public static final int SALT_INDEX = 3;
public static final int PBKDF2_INDEX = 4;
/**
* Creates a password digest using the PBKDF2 key derivation function
* (64,000 iterations of SHA1 by default) with a cryptographically-random
* salt.
*
* @param password
* The submitted password.
* @return A hash value in the following format:
* algorithm:iterations:hashSize:salt:hash.
* @throws CannotPerformOperationException
* If the hash cannot be created for some reason (e.g. random
* number generator doesn't work).
*/
public static String createHash(String password) throws CannotPerformOperationException {
return createHash(password.toCharArray());
}
public static String createHash(char[] password) throws CannotPerformOperationException {
// Generate a random salt
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_BYTE_SIZE];
random.nextBytes(salt);
// Hash the password
byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
int hashSize = hash.length;
// format: algorithm:iterations:hashSize:salt:hash
String parts = "sha1:" + PBKDF2_ITERATIONS + ":" + hashSize + ":" + toBase64(salt) + ":" + toBase64(hash);
return parts;
}
/**
* Checks a submitted password against the expected hash value.
*
* @param password
* The submitted password.
* @param correctHash
* The expected hash value.
* @return true if the provided password is correct; false otherwise.
*
* @throws CannotPerformOperationException
* If password verification failed for some reason.
* @throws InvalidHashException
* If correctHash was somehow corrupted.
*/
public static boolean verifyPassword(String password, String correctHash)
throws CannotPerformOperationException, InvalidHashException {
return verifyPassword(password.toCharArray(), correctHash);
}
public static boolean verifyPassword(char[] password, String correctHash)
throws CannotPerformOperationException, InvalidHashException {
// Decode the hash into its parameters
String[] params = correctHash.split(":");
if (params.length != HASH_SECTIONS) {
throw new InvalidHashException("Fields are missing from the password hash.");
}
// Currently, Java only supports SHA1.
if (!params[HASH_ALGORITHM_INDEX].equals("sha1")) {
throw new CannotPerformOperationException("Unsupported hash type.");
}
int iterations = 0;
try {
iterations = Integer.parseInt(params[ITERATION_INDEX]);
} catch (NumberFormatException ex) {
throw new InvalidHashException("Could not parse the iteration count as an integer.", ex);
}
if (iterations < 1) {
throw new InvalidHashException("Invalid number of iterations. Must be >= 1.");
}
byte[] salt = null;
try {
salt = fromBase64(params[SALT_INDEX]);
} catch (IllegalArgumentException ex) {
throw new InvalidHashException("Base64 decoding of salt failed.", ex);
}
byte[] hash = null;
try {
hash = fromBase64(params[PBKDF2_INDEX]);
} catch (IllegalArgumentException ex) {
throw new InvalidHashException("Base64 decoding of pbkdf2 output failed.", ex);
}
int storedHashSize = 0;
try {
storedHashSize = Integer.parseInt(params[HASH_SIZE_INDEX]);
} catch (NumberFormatException ex) {
throw new InvalidHashException("Could not parse the hash size as an integer.", ex);
}
if (storedHashSize != hash.length) {
throw new InvalidHashException("Hash length doesn't match stored hash length.");
}
// Compute the hash of the provided password, using the same salt,
// iteration count, and hash length
byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
// Compare the hashes in constant time. The password is correct if
// both hashes match.
return slowEquals(hash, testHash);
}
private static boolean slowEquals(byte[] a, byte[] b) {
int diff = a.length ^ b.length;
for (int i = 0; i < a.length && i < b.length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes)
throws CannotPerformOperationException {
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
return skf.generateSecret(spec).getEncoded();
} catch (NoSuchAlgorithmException ex) {
throw new CannotPerformOperationException("Hash algorithm not supported.", ex);
} catch (InvalidKeySpecException ex) {
throw new CannotPerformOperationException("Invalid key spec.", ex);
}
}
private static byte[] fromBase64(String hex) throws IllegalArgumentException {
return DatatypeConverter.parseBase64Binary(hex);
}
private static String toBase64(byte[] array) {
return DatatypeConverter.printBase64Binary(array);
}
}