mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-18 15:33:09 +00:00
Merge pull request #41 from mwoodiupui/DS-861
[DS-861] Salt PasswordAuthentication
This commit is contained in:
@@ -32,6 +32,7 @@ import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.eperson.PasswordHash;
|
||||
|
||||
import org.jdom.Namespace;
|
||||
|
||||
@@ -70,6 +71,8 @@ public class RoleDisseminator implements PackageDisseminator
|
||||
public static final String LAST_NAME = "LastName";
|
||||
public static final String LANGUAGE = "Language";
|
||||
public static final String PASSWORD_HASH = "PasswordHash";
|
||||
public static final String PASSWORD_DIGEST = "digest";
|
||||
public static final String PASSWORD_SALT = "salt";
|
||||
public static final String CAN_LOGIN = "CanLogin";
|
||||
public static final String REQUIRE_CERTIFICATE = "RequireCertificate";
|
||||
public static final String SELF_REGISTERED = "SelfRegistered";
|
||||
@@ -475,8 +478,19 @@ public class RoleDisseminator implements PackageDisseminator
|
||||
|
||||
if (emitPassword)
|
||||
{
|
||||
PasswordHash password = eperson.getPasswordHash();
|
||||
|
||||
writer.writeStartElement(PASSWORD_HASH);
|
||||
writer.writeCharacters(eperson.getPasswordHash());
|
||||
|
||||
String algorithm = password.getAlgorithm();
|
||||
if (null != algorithm)
|
||||
writer.writeAttribute(PASSWORD_DIGEST, algorithm);
|
||||
|
||||
String salt = password.getSaltString();
|
||||
if (null != salt)
|
||||
writer.writeAttribute(PASSWORD_SALT, salt);
|
||||
|
||||
writer.writeCharacters(password.getHashString());
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import java.util.List;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Collection;
|
||||
@@ -26,11 +27,10 @@ import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.eperson.PasswordHash;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.w3c.dom.*;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
@@ -171,7 +171,30 @@ public class RoleIngester implements PackageIngester
|
||||
data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH);
|
||||
if (data.getLength() > 0)
|
||||
{
|
||||
eperson.setPasswordHash(data.item(0).getTextContent());
|
||||
Node element = data.item(0);
|
||||
NamedNodeMap attributes = element.getAttributes();
|
||||
|
||||
Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST);
|
||||
String algorithmText;
|
||||
if (null != algorithm)
|
||||
algorithmText = algorithm.getNodeValue();
|
||||
else
|
||||
algorithmText = null;
|
||||
|
||||
Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT);
|
||||
String saltText;
|
||||
if (null != salt)
|
||||
saltText = salt.getNodeValue();
|
||||
else
|
||||
saltText = null;
|
||||
|
||||
PasswordHash password;
|
||||
try {
|
||||
password = new PasswordHash(algorithmText, saltText, element.getTextContent());
|
||||
} catch (DecoderException ex) {
|
||||
throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex);
|
||||
}
|
||||
eperson.setPasswordHash(password);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -10,6 +10,7 @@ package org.dspace.eperson;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
@@ -851,43 +852,64 @@ public class EPerson extends DSpaceObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EPerson's password
|
||||
* Set the EPerson's password.
|
||||
*
|
||||
* @param s
|
||||
* the new email
|
||||
* the new password.
|
||||
*/
|
||||
public void setPassword(String s)
|
||||
{
|
||||
// FIXME: encoding
|
||||
String encoded = Utils.getMD5(s);
|
||||
|
||||
myRow.setColumn("password", encoded);
|
||||
PasswordHash hash = new PasswordHash(s);
|
||||
myRow.setColumn("password", Utils.toHex(hash.getHash()));
|
||||
myRow.setColumn("salt", Utils.toHex(hash.getSalt()));
|
||||
myRow.setColumn("digest_algorithm", hash.getAlgorithm());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EPerson's password hash
|
||||
* Set the EPerson's password hash.
|
||||
*
|
||||
* @param s
|
||||
* hash of the password
|
||||
* @param password
|
||||
* hashed password, or null to set row data to NULL.
|
||||
*/
|
||||
public void setPasswordHash(String s)
|
||||
public void setPasswordHash(PasswordHash password)
|
||||
{
|
||||
myRow.setColumn("password", s);
|
||||
if (null == password)
|
||||
{
|
||||
myRow.setColumnNull("digest_algorithm");
|
||||
myRow.setColumnNull("salt");
|
||||
myRow.setColumnNull("password");
|
||||
}
|
||||
else
|
||||
{
|
||||
myRow.setColumn("digest_algorithm", password.getAlgorithm());
|
||||
myRow.setColumn("salt", password.getSaltString());
|
||||
myRow.setColumn("password", password.getHashString());
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the EPerson's password hash
|
||||
* Return the EPerson's password hash.
|
||||
*
|
||||
* @return hash of the password
|
||||
*/
|
||||
public String getPasswordHash()
|
||||
public PasswordHash getPasswordHash()
|
||||
{
|
||||
return myRow.getStringColumn("password");
|
||||
PasswordHash hash = null;
|
||||
try {
|
||||
hash = new PasswordHash(myRow.getStringColumn("digest_algorithm"),
|
||||
myRow.getStringColumn("salt"),
|
||||
myRow.getStringColumn("password"));
|
||||
} catch (DecoderException ex) {
|
||||
log.error("Problem decoding stored salt or hash: " + ex.getMessage());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check EPerson's password
|
||||
* Check EPerson's password. Side effect: original unsalted MD5 hashes are
|
||||
* converted using the current algorithm.
|
||||
*
|
||||
* @param attempt
|
||||
* the password attempt
|
||||
@@ -895,9 +917,38 @@ public class EPerson extends DSpaceObject
|
||||
*/
|
||||
public boolean checkPassword(String attempt)
|
||||
{
|
||||
String encoded = Utils.getMD5(attempt);
|
||||
PasswordHash myHash;
|
||||
try
|
||||
{
|
||||
myHash = new PasswordHash(
|
||||
myRow.getStringColumn("digest_algorithm"),
|
||||
myRow.getStringColumn("salt"),
|
||||
myRow.getStringColumn("password"));
|
||||
} catch (DecoderException ex)
|
||||
{
|
||||
log.error(ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
boolean answer = myHash.matches(attempt);
|
||||
|
||||
return (encoded.equals(myRow.getStringColumn("password")));
|
||||
// If using the old unsalted hash, and this password is correct, update to a new hash
|
||||
if (answer && (null == myRow.getStringColumn("digest_algorithm")))
|
||||
{
|
||||
log.info("Upgrading password hash for EPerson " + getID());
|
||||
setPassword(attempt);
|
||||
try {
|
||||
myContext.turnOffAuthorisationSystem();
|
||||
update();
|
||||
} catch (SQLException ex) {
|
||||
log.error("Could not update password hash", ex);
|
||||
} catch (AuthorizeException ex) {
|
||||
log.error("Could not update password hash", ex);
|
||||
} finally {
|
||||
myContext.restoreAuthSystemState();
|
||||
}
|
||||
}
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
71
dspace-api/src/main/java/org/dspace/eperson/Groomer.java
Normal file
71
dspace-api/src/main/java/org/dspace/eperson/Groomer.java
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.sql.SQLException;
|
||||
import org.apache.commons.cli.*;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.storage.rdbms.DatabaseManager;
|
||||
import org.dspace.storage.rdbms.TableRow;
|
||||
import org.dspace.storage.rdbms.TableRowIterator;
|
||||
|
||||
/**
|
||||
* Tools for manipulating EPersons and Groups.
|
||||
*
|
||||
* @author mwood
|
||||
*/
|
||||
public class Groomer
|
||||
{
|
||||
/**
|
||||
* Command line tool for "grooming" the EPerson collection.
|
||||
*/
|
||||
static public void main(String[] argv)
|
||||
throws SQLException
|
||||
{
|
||||
final String USAGE = "EPerson -verb [option...]";
|
||||
|
||||
OptionGroup verbs = new OptionGroup();
|
||||
verbs.setRequired(true);
|
||||
verbs.addOption(new Option("h", "help", false, "explain this tool"));
|
||||
verbs.addOption(new Option("u", "unsalted", false, "list accounts with unsalted password hashes"));
|
||||
|
||||
Options options = new Options();
|
||||
options.addOptionGroup(verbs);
|
||||
|
||||
PosixParser parser = new PosixParser();
|
||||
CommandLine command = null;
|
||||
try {
|
||||
command = parser.parse(options, argv);
|
||||
} catch (ParseException ex) {
|
||||
System.err.println(ex.getMessage());
|
||||
if (! (ex instanceof MissingOptionException))
|
||||
new HelpFormatter().printHelp(USAGE, options);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Help the user
|
||||
if (command.hasOption('h') || command.hasOption('?'))
|
||||
{
|
||||
new HelpFormatter().printHelp(USAGE, options);
|
||||
}
|
||||
// Scan for unsalted hashes
|
||||
else if (command.hasOption('u'))
|
||||
{
|
||||
Context myContext = new Context();
|
||||
final TableRowIterator tri = DatabaseManager.query(myContext,
|
||||
"SELECT email FROM EPerson WHERE password IS NOT NULL AND digest_algorithm IS NULL");
|
||||
for (TableRow row = tri.next(); tri.hasNext(); row = tri.next())
|
||||
System.out.println(row.getStringColumn("email"));
|
||||
myContext.abort(); // No changes to commit
|
||||
}
|
||||
// Should not happen: verb option defined but no code!
|
||||
else
|
||||
System.err.println("Unimplemented verb: " + verbs.getSelected());
|
||||
}
|
||||
}
|
283
dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java
Normal file
283
dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user