/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.dspace.services.ConfigurationService;
import org.dspace.utils.DSpace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* For handling digested secrets (such as passwords).
* Use {@link #PasswordHash(String, byte[], byte[])} to package and manipulate
* secrets that have already been hashed, and {@link #PasswordHash(String)} for
* plaintext secrets. Compare a plaintext candidate to a hashed secret with
* {@link #matches(String)}.
*
* @author mwood
*/
public class PasswordHash
{
private static final Logger log = LoggerFactory.getLogger(PasswordHash.class);
private static final ConfigurationService config
= new DSpace().getConfigurationService();
private static final Charset UTF_8 = Charset.forName("UTF-8"); // Should always succeed: UTF-8 is required
private static final String DEFAULT_DIGEST_ALGORITHM = "SHA-512"; // XXX magic
private static final String ALGORITHM_PROPERTY = "authentication-password.digestAlgorithm";
private static final int SALT_BYTES = 128/8; // XXX magic we want 128 bits
private static final int HASH_ROUNDS = 1024; // XXX magic 1024 rounds
private static final int SEED_BYTES = 64; // XXX magic
private static final int RESEED_INTERVAL = 100; // XXX magic
/** A secure random number generator instance. */
private static SecureRandom rng = null;
/** How many times has the RNG been called without re-seeding? */
private static int rngUses;
private String algorithm;
private byte[] salt;
private byte[] hash;
/** Don't allow empty instances. */
private PasswordHash() {}
/**
* Construct a hash structure from existing data, just for passing around.
*
* @param algorithm the digest algorithm used in producing {@code hash}.
* If empty, set to null. Other methods will treat this as unsalted MD5.
* If you want salted multi-round MD5, specify "MD5".
* @param salt the salt hashed with the secret, or null.
* @param hash the hashed secret.
*/
public PasswordHash(String algorithm, byte[] salt, byte[] hash)
{
if ((null != algorithm) && algorithm.isEmpty())
this.algorithm = null;
else
this.algorithm = algorithm;
this.salt = salt;
this.hash = hash;
}
/**
* Convenience: like {@link #PasswordHash(String, byte[], byte[])} but with
* hexadecimal-encoded {@code String}s.
* @param algorithm the digest algorithm used in producing {@code hash}.
* If empty, set to null. Other methods will treat this as unsalted MD5.
* If you want salted multi-round MD5, specify "MD5".
* @param salt hexadecimal digits encoding the bytes of the salt, or null.
* @param hash hexadecimal digits encoding the bytes of the hash.
* @throws DecoderException if salt or hash is not proper hexadecimal.
*/
public PasswordHash(String algorithm, String salt, String hash)
throws DecoderException
{
if ((null != algorithm) && algorithm.isEmpty())
this.algorithm = null;
else
this.algorithm = algorithm;
if (null == salt)
this.salt = null;
else
this.salt = Hex.decodeHex(salt.toCharArray());
if (null == hash)
this.hash = null;
else
this.hash = Hex.decodeHex(hash.toCharArray());
}
/**
* Construct a hash structure from a cleartext password using the configured
* digest algorithm.
*
* @param password the secret to be hashed.
*/
public PasswordHash(String password)
{
// Generate some salt
salt = generateSalt();
// What digest algorithm to use?
algorithm = config.getPropertyAsType(ALGORITHM_PROPERTY, DEFAULT_DIGEST_ALGORITHM);
// Hash it!
try {
hash = digest(salt, algorithm, password);
} catch (NoSuchAlgorithmException e) {
log.error(e.getMessage());
hash = new byte[] { 0 };
}
}
/**
* Is this the string whose hash I hold?
*
* @param secret string to be hashed and compared to this hash.
* @return true if secret hashes to the value held by this instance.
*/
public boolean matches(String secret)
{
byte[] candidate;
try {
candidate = digest(salt, algorithm, secret);
} catch (NoSuchAlgorithmException e) {
log.error(e.getMessage());
return false;
}
return Arrays.equals(candidate, hash);
}
/**
* Get the hash.
*
* @return the value of hash
*/
public byte[] getHash()
{
return hash;
}
/**
* Get the hash, as a String.
*
* @return hash encoded as hexadecimal digits, or null if none.
*/
public String getHashString()
{
if (null != hash)
return new String(Hex.encodeHex(hash));
else
return null;
}
/**
* Get the salt.
*
* @return the value of salt
*/
public byte[] getSalt()
{
return salt;
}
/**
* Get the salt, as a String.
*
* @return salt encoded as hexadecimal digits, or null if none.
*/
public String getSaltString()
{
if (null != salt)
return new String(Hex.encodeHex(salt));
else
return null;
}
/**
* Get the value of algorithm
*
* @return the value of algorithm
*/
public String getAlgorithm()
{
return algorithm;
}
/**
* The digest algorithm used if none is configured.
*
* @return name of the default digest.
*/
static public String getDefaultAlgorithm()
{
return DEFAULT_DIGEST_ALGORITHM;
}
/** Generate an array of random bytes. */
private synchronized byte[] generateSalt()
{
// Initialize a random-number generator
if (null == rng)
{
rng = new SecureRandom();
log.info("Initialized a random number stream using {} provided by {}",
rng.getAlgorithm(), rng.getProvider());
rngUses = 0;
}
if (rngUses++ > RESEED_INTERVAL)
{ // re-seed the generator periodically to break up possible patterns
log.debug("Re-seeding the RNG");
rng.setSeed(rng.generateSeed(SEED_BYTES));
rngUses = 0;
}
salt = new byte[SALT_BYTES];
rng.nextBytes(salt);
return salt;
}
/**
* Generate a salted hash of a string using a given algorithm.
*
* @param salt random bytes to salt the hash.
* @param algorithm name of the digest algorithm to use. Assume unsalted MD5 if null.
* @param secret the string to be hashed. Null is treated as an empty string ("").
* @return hash bytes.
* @throws NoSuchAlgorithmException if algorithm is unknown.
*/
private byte[] digest(byte[] salt, String algorithm, String secret)
throws NoSuchAlgorithmException
{
MessageDigest digester;
if (null == secret)
secret = "";
// Special case: old unsalted one-trip MD5 hash.
if (null == algorithm)
{
digester = MessageDigest.getInstance("MD5");
digester.update(secret.getBytes(UTF_8));
return digester.digest();
}
// Set up a digest
digester = MessageDigest.getInstance(algorithm);
// Grind up the salt with the password, yielding a hash
if (null != salt)
digester.update(salt);
digester.update(secret.getBytes(UTF_8)); // Round 0
for (int round = 1; round < HASH_ROUNDS; round++)
{
byte[] lastRound = digester.digest();
digester.reset();
digester.update(lastRound);
}
return digester.digest();
}
}