* This is an optional field. If not specified, SHA-256 will be assumed.
*
* If passwords are in clear, they will be over-written by their hash if
* hashing is requested by setting com.sun.management.jmxremote.password.hashpasswords
* property to true in management.properties file and if the password file is
* writable and if the system security policy allows writing into the password
* file, if a security manager is configured
*
*
In order to change password for a role, replace hashed password entry
* with clear text password or new hashed password. If new password is in clear,
* it will be replaced with its hash when new login attempt is made.
*
*
A given role should have at most one entry in this file. If a role
* has no entry, it has no access.
* If multiple entries are found for the same role name, then the last one
* is used.
*
*
User generated hashed password file can also be used instead of clear-text
* password file. If generated by user, hashed passwords must follow
* format specified above.
*/
final public class HashedPasswordManager {
private static final class UserCredentials {
final private String userName;
final private String hashAlgorithm;
final private String b64Salt;
final private String b64Hash;
public UserCredentials(String userName, String hashAlgorithm, String b64Salt, String b64Hash) {
this.userName = userName;
this.hashAlgorithm = hashAlgorithm;
this.b64Salt = b64Salt;
this.b64Hash = b64Hash;
}
}
private static final String DefaultHashAlgorithm = "SHA-256";
private static final int DefaultSaltLength = 32;
private final SecureRandom random = new SecureRandom();
private final Map userCredentialsMap = new HashMap<>();
private final String passwordFile;
private final boolean shouldHashPasswords;
private static final ClassLogger logger
= new ClassLogger("javax.management.remote.misc",
"HashedPasswordManager");
/**
* Creates new password manager for input password file
*
* @param filename UTF-8 encoded input file to read passwords from
* @param shouldHashPasswords Request for clear passwords to be hashed
* @throws IOException if unable to access input password file
* @throws SecurityException if read/write file permissions are not granted
*/
public HashedPasswordManager(String filename, boolean shouldHashPasswords)
throws SecurityException, IOException {
this.passwordFile = filename;
this.shouldHashPasswords = shouldHashPasswords;
loadPasswords();
}
/*
* Checks if the Security manager allows writing to the file
* as well as if the file is writable
*/
private boolean canWriteToFile(String fileName) {
File f = new File(fileName);
SecurityManager security = System.getSecurityManager();
if (security != null) {
try {
security.checkWrite(fileName); // // Check we have write permissions
return f.canWrite(); // Check file is not read-only
} catch (SecurityException ex) {
return false;
}
} else {
return f.canWrite();
}
}
private String[] getHash(String algorithm, String password) {
try {
byte[] salt = new byte[DefaultSaltLength];
random.nextBytes(salt);
MessageDigest digest = MessageDigest.getInstance(algorithm);
digest.reset();
digest.update(salt);
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String saltStr = Base64.getEncoder().encodeToString(salt);
String hashStr = Base64.getEncoder().encodeToString(hash);
return new String[]{saltStr, hashStr};
} catch (NoSuchAlgorithmException ex) {
if(logger.warningOn()) {
logger.warning("getHash", "Invalid algorithm : " + algorithm);
}
// We should never reach here as default Hash Algorithm
// must be always present
return new String[]{"", ""};
}
}
/**
* Authenticate supplied credentials against one present in file
*
* @param userName Input username
* @param inputPassword Input password
* @return true if authentication succeeds, false otherwise
*/
public synchronized boolean authenticate(String userName, String inputPassword) {
if (userCredentialsMap.containsKey(userName)) {
try {
UserCredentials us = userCredentialsMap.get(userName);
byte[] salt = Base64.getDecoder().decode(us.b64Salt);
byte[] targetHash = Base64.getDecoder().decode(us.b64Hash);
MessageDigest digest = MessageDigest.getInstance(us.hashAlgorithm);
digest.reset();
digest.update(salt);
byte[] hash = digest.digest(inputPassword.getBytes(StandardCharsets.UTF_8));
return Arrays.equals(hash, targetHash);
} catch (NoSuchAlgorithmException ex) {
if(logger.warningOn()) {
logger.warning("authenticate", "Unrecognized hash algorithm : " +
userCredentialsMap.get(userName).hashAlgorithm
+ " - for user : " + userName);
}
return false;
}
} else {
if(logger.warningOn()) {
logger.warning("authenticate", "Unknown user : " + userName);
}
return false;
}
}
/**
* Load passwords from password file.
*
*
* This method should be called for every login attempt to load new/changed
* credentials, if any.
*
*
* If hashing is requested, clear passwords will be over-written with their
* SHA-256 hash
*
* @throws IOException If unable to access the file
* @throws SecurityException If read/write file permissions are not granted
*/
public synchronized void loadPasswords()
throws IOException, SecurityException {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(passwordFile);
}
Stream lines = Files.lines(Paths.get(passwordFile));
AtomicBoolean hasClearPasswords = new AtomicBoolean(false);
StringBuilder sbuf = new StringBuilder();
String header = "# The passwords in this file are hashed.\n"
+ "# In order to change password for a role, replace hashed "
+ "password entry\n"
+ "# with clear text password or new hashed password. "
+ "If new password is in clear,\n# it will be replaced with its "
+ "hash when new login attempt is made.\n\n";
lines.forEach(line -> {
if (line.trim().startsWith("#")) { // Ignore comments
sbuf.append(line).append("\n");
return;
}
String[] tokens = line.split("\\s+");
switch (tokens.length) {
case 2: {
// Password is in clear
String[] b64str = getHash(DefaultHashAlgorithm, tokens[1]);
UserCredentials us = new UserCredentials(tokens[0],DefaultHashAlgorithm,b64str[0],b64str[1]);
sbuf.append(us.userName).append(" ").append(us.b64Salt).
append(" ").append(us.b64Hash).append(" ").
append(us.hashAlgorithm).append("\n");
userCredentialsMap.put(tokens[0], us);
hasClearPasswords.set(true);
if (logger.debugOn()) {
logger.debug("loadPasswords",
"Found atleast one clear password");
}
break;
}
case 3:
case 4: {
// Passwords are hashed
UserCredentials us = new UserCredentials(tokens[0], (tokens.length == 4 ? tokens[3] : DefaultHashAlgorithm),
tokens[1], tokens[2]);
sbuf.append(line).append("\n");
userCredentialsMap.put(tokens[0], us);
break;
}
default:
sbuf.append(line).append("\n");
break;
}
});
if (!shouldHashPasswords && hasClearPasswords.get()) {
if (logger.warningOn()) {
logger.warning("loadPasswords",
"Passwords in " + passwordFile + " are in clear but are requested "
+ "not to be hashed !!!");
}
}
// Check if header needs to be inserted
if(!sbuf.toString().startsWith("# The passwords in this file are hashed")) {
sbuf.insert(0, header);
}
// Even if we are unable to write hashed passwords to password file,
// passwords will be hashed in memory so that jvm heap dump should not
// give away clear passwords
if (shouldHashPasswords && hasClearPasswords.get()) {
if (canWriteToFile(passwordFile)) {
try (FileWriter fw = new FileWriter(passwordFile)) {
fw.write(sbuf.toString());
}
if(logger.debugOn()) {
logger.debug("loadPasswords",
"Wrote hash passwords to file");
}
} else if (logger.warningOn()) {
logger.warning("loadPasswords",
"Passwords in " + passwordFile + " are in clear and password file is read-only. "
+ "Passwords cannot be hashed !!!!");
}
}
}
}