/** * 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.services.factory.DSpaceServicesFactory; 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 = DSpaceServicesFactory.getInstance().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(); } }