--- old/src/java.management/share/classes/com/sun/jmx/remote/security/FileLoginModule.java 2017-10-06 00:40:06.327423998 +0530 +++ new/src/java.management/share/classes/com/sun/jmx/remote/security/FileLoginModule.java 2017-10-06 00:40:06.219423667 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,17 +27,13 @@ import com.sun.jmx.mbeanserver.GetPropertyAction; import com.sun.jmx.mbeanserver.Util; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FilePermission; import java.io.IOException; import java.security.AccessControlException; import java.security.AccessController; import java.util.Arrays; -import java.util.Hashtable; import java.util.Map; -import java.util.Properties; import javax.security.auth.*; import javax.security.auth.callback.*; @@ -59,10 +55,7 @@ * the access control file for JMX remote management or in a Java security * policy. * - *

The password file comprises a list of key-value pairs as specified in - * {@link Properties}. The key represents a user's name and the value is its - * associated cleartext password. By default, the following password file is - * used: + * By default, the following password file is used: *

  *     ${java.home}/conf/management/jmxremote.password
  * 
@@ -105,6 +98,11 @@ *
if true, this module clears the username and password * stored in the module's shared state after both phases of authentication * (login and commit) have completed.
+ * + *
hashPasswords
+ *
if true, this module replaces each clear text passwords + * with its hash, if present.
+ * * */ public class FileLoginModule implements LoginModule { @@ -135,6 +133,7 @@ private boolean tryFirstPass = false; private boolean storePass = false; private boolean clearPass = false; + private boolean hashPasswords = false; // Authentication status private boolean succeeded = false; @@ -154,7 +153,7 @@ private String passwordFileDisplayName; private boolean userSuppliedPasswordFile; private boolean hasJavaHomePermission; - private Properties userCredentials; + private HashedPasswordManager hashPwdMgr; /** * Initialize this LoginModule. @@ -186,6 +185,8 @@ "true".equalsIgnoreCase((String)options.get("storePass")); clearPass = "true".equalsIgnoreCase((String)options.get("clearPass")); + hashPasswords = + "true".equalsIgnoreCase((String)options.get("hashPasswords")); passwordFile = (String)options.get("passwordFile"); passwordFileDisplayName = passwordFile; @@ -221,17 +222,27 @@ public boolean login() throws LoginException { try { - loadPasswordFile(); + if(hashPwdMgr == null) { + hashPwdMgr = new HashedPasswordManager(passwordFile, hashPasswords); + } else { + hashPwdMgr.loadPasswords(); + } } catch (IOException ioe) { LoginException le = new LoginException( "Error: unable to load the password file: " + passwordFileDisplayName); throw EnvHelp.initCause(le, ioe); - } - - if (userCredentials == null) { - throw new LoginException - ("Error: unable to locate the users' credentials."); + } catch (SecurityException e) { + if (userSuppliedPasswordFile || hasJavaHomePermission) { + throw e; + } else { + final FilePermission fp = + new FilePermission(passwordFileDisplayName, "read"); + AccessControlException ace = new AccessControlException( + "access denied " + fp.toString()); + ace.setStackTrace(e.getStackTrace()); + throw ace; + } } if (logger.debugOn()) { @@ -437,12 +448,7 @@ // get the username and password getUsernamePassword(usePasswdFromSharedState); - String localPassword; - - // userCredentials is initialized in login() - if (((localPassword = userCredentials.getProperty(username)) == null) || - (! localPassword.equals(new String(password)))) { - + if (!hashPwdMgr.authenticate(username, new String(password))) { // username not found or passwords do not match if (logger.debugOn()) { logger.debug("login", "Invalid username or password"); @@ -468,38 +474,6 @@ } } - /* - * Read the password file. - */ - private void loadPasswordFile() throws IOException { - FileInputStream fis; - try { - fis = new FileInputStream(passwordFile); - } catch (SecurityException e) { - if (userSuppliedPasswordFile || hasJavaHomePermission) { - throw e; - } else { - final FilePermission fp = - new FilePermission(passwordFileDisplayName, "read"); - AccessControlException ace = new AccessControlException( - "access denied " + fp.toString()); - ace.setStackTrace(e.getStackTrace()); - throw ace; - } - } - try { - final BufferedInputStream bis = new BufferedInputStream(fis); - try { - userCredentials = new Properties(); - userCredentials.load(bis); - } finally { - bis.close(); - } - } finally { - fis.close(); - } - } - /** * Get the username and password. * This method does not return any value. --- old/src/java.management/share/classes/com/sun/jmx/remote/security/JMXPluggableAuthenticator.java 2017-10-06 00:40:06.643424966 +0530 +++ new/src/java.management/share/classes/com/sun/jmx/remote/security/JMXPluggableAuthenticator.java 2017-10-06 00:40:06.539424647 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,8 +34,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Properties; -import javax.management.remote.JMXPrincipal; import javax.management.remote.JMXAuthenticator; import javax.security.auth.AuthPermission; import javax.security.auth.Subject; @@ -91,10 +89,12 @@ String loginConfigName = null; String passwordFile = null; + String hashPasswords = null; if (env != null) { loginConfigName = (String) env.get(LOGIN_CONFIG_PROP); passwordFile = (String) env.get(PASSWORD_FILE_PROP); + hashPasswords = (String) env.get(HASH_PASSWORDS); } try { @@ -114,6 +114,7 @@ } final String pf = passwordFile; + final String hashPass = hashPasswords; try { loginContext = AccessController.doPrivileged( new PrivilegedExceptionAction() { @@ -122,7 +123,7 @@ LOGIN_CONFIG_NAME, null, new JMXCallbackHandler(), - new FileLoginConfig(pf)); + new FileLoginConfig(pf, hashPass)); } }); } catch (PrivilegedActionException pae) { @@ -250,6 +251,8 @@ private static final String LOGIN_CONFIG_NAME = "JMXPluggableAuthenticator"; private static final String PASSWORD_FILE_PROP = "jmx.remote.x.password.file"; + private static final String HASH_PASSWORDS = + "jmx.remote.x.password.hashpasswords"; private static final ClassLogger logger = new ClassLogger("javax.management.remote.misc", LOGIN_CONFIG_NAME); @@ -303,19 +306,22 @@ // The option that identifies the password file to use private static final String PASSWORD_FILE_OPTION = "passwordFile"; + private static final String HASH_PASSWORDS = "hashPasswords"; /** * Creates an instance of FileLoginConfig * * @param passwordFile A filepath that identifies the password file to use. * If null then the default password file is used. + * @param hashPasswords Flag to indicate if password file needs to be hashed */ - public FileLoginConfig(String passwordFile) { + public FileLoginConfig(String passwordFile, String hashPasswords) { Map options; if (passwordFile != null) { options = new HashMap(1); options.put(PASSWORD_FILE_OPTION, passwordFile); + options.put(HASH_PASSWORDS, hashPasswords); } else { options = Collections.emptyMap(); } --- old/src/jdk.management.agent/share/classes/sun/management/jmxremote/ConnectorBootstrap.java 2017-10-06 00:40:06.943425886 +0530 +++ new/src/jdk.management.agent/share/classes/sun/management/jmxremote/ConnectorBootstrap.java 2017-10-06 00:40:06.843425579 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -72,10 +72,9 @@ import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory; import javax.security.auth.Subject; - import com.sun.jmx.remote.internal.rmi.RMIExporter; +import com.sun.jmx.remote.security.HashedPasswordManager; import com.sun.jmx.remote.security.JMXPluggableAuthenticator; - import jdk.internal.agent.Agent; import jdk.internal.agent.AgentConfigurationError; import static jdk.internal.agent.AgentConfigurationError.*; @@ -103,6 +102,7 @@ public static final String USE_REGISTRY_SSL = "false"; public static final String USE_AUTHENTICATION = "true"; public static final String PASSWORD_FILE_NAME = "jmxremote.password"; + public static final String HASH_PASSWORDS = "true"; public static final String ACCESS_FILE_NAME = "jmxremote.access"; public static final String SSL_NEED_CLIENT_AUTH = "false"; } @@ -130,6 +130,8 @@ "com.sun.management.jmxremote.authenticate"; public static final String PASSWORD_FILE_NAME = "com.sun.management.jmxremote.password.file"; + public static final String HASH_PASSWORDS + = "com.sun.management.jmxremote.password.hashpasswords"; public static final String ACCESS_FILE_NAME = "com.sun.management.jmxremote.access.file"; public static final String LOGIN_CONFIG_NAME = @@ -410,6 +412,7 @@ String loginConfigName = null; String passwordFileName = null; + boolean shouldHashPasswords = true; String accessFileName = null; // Initialize settings when authentication is active @@ -424,6 +427,11 @@ passwordFileName = props.getProperty(PropertyNames.PASSWORD_FILE_NAME, getDefaultFileName(DefaultValues.PASSWORD_FILE_NAME)); + String hashPasswords + = props.getProperty(PropertyNames.HASH_PASSWORDS, + DefaultValues.HASH_PASSWORDS); + shouldHashPasswords = Boolean.parseBoolean(hashPasswords); + checkPasswordFile(passwordFileName); } @@ -471,7 +479,7 @@ sslConfigFileName, enabledCipherSuitesList, enabledProtocolsList, sslNeedClientAuth, useAuthentication, loginConfigName, - passwordFileName, accessFileName, bindAddress); + passwordFileName, shouldHashPasswords, accessFileName, bindAddress); cs = data.jmxConnectorServer; url = data.jmxRemoteURL; config("startRemoteConnectorServer", @@ -728,6 +736,7 @@ boolean useAuthentication, String loginConfigName, String passwordFileName, + boolean shouldHashPasswords, String accessFileName, String bindAddress) throws IOException, MalformedURLException { @@ -757,6 +766,9 @@ if (passwordFileName != null) { env.put("jmx.remote.x.password.file", passwordFileName); } + if (shouldHashPasswords) { + env.put("jmx.remote.x.password.hashpasswords", "true"); + } env.put("jmx.remote.x.access.file", accessFileName); --- old/src/jdk.management.agent/share/conf/jmxremote.password.template 2017-10-06 00:40:07.251426830 +0530 +++ new/src/jdk.management.agent/share/conf/jmxremote.password.template 2017-10-06 00:40:07.151426523 +0530 @@ -3,11 +3,12 @@ # # o Copy this template to jmxremote.password # o Set the user/password entries in jmxremote.password -# o Change the permission of jmxremote.password to read-only -# by the owner. +# o Change the permission of jmxremote.password to be accessible +# only by the owner. +# o The jmxremote.passwords file will be re-written by the server + to replace all plain text passwords with hashed passwords when + the file is read by the server. # -# See below for the location of jmxremote.password file. -# ---------------------------------------------------------------------- ############################################################## # Password File for Remote JMX Monitoring @@ -24,41 +25,80 @@ # the management config file $JRE/conf/management/management.properties # or by specifying a system property (See that file for details). - ############################################################## -# File permissions of the jmxremote.password file +# File format of the jmxremote.password file ############################################################## -# Since there are cleartext passwords stored in this file, -# this file must be readable by ONLY the owner, -# otherwise the program will exit with an error. -# -# The file format for password and access files is syntactically the same -# as the Properties file format. The syntax is described in the Javadoc -# for java.util.Properties.load. -# Typical password file has multiple lines, where each line is blank, +# +# The file contains multiple lines where each line is blank, # a comment (like this one), or a password entry. # +# password entry follows below syntax +# role_name W [clearPassword|hashedPassword] # -# A password entry consists of a role name and an associated -# password. The role name is any string that does not itself contain -# spaces or tabs. The password is again any string that does not -# contain spaces or tabs. Note that passwords appear in the clear in -# this file, so it is a good idea not to use valuable passwords. +# role_name is any string that does not itself contain spaces or tabs. +# W = spaces or tabs +# +# Passwords can be specified using clear text or via a hash. Clear text password +# is any string that does not contain spaces or tabs. Hashed passwords must +# follow the below format. +# hashedPassword = base64_encoded_salt W base64_encoded_hash W hash_algorithm +# where, +# base64_encoded_hash = Hash_algorithm(password + salt) +# W = spaces or tabs +# hash_algorithm = Algorithm string specified using format below +# https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest +# MD5, SHA-1 and SHA-256 are supported algorithms. +# 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 all of +# the below criteria are met. +# * com.sun.management.jmxremote.password.hashpasswords property is set to true in +# management.properties file +# * the password file is writable +# * 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 the hashed password entry +# with the clear text password or the new hashed password. If the new password +# is in clear, it will be replaced with its hash when a 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. +# +# A 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. + +############################################################## +# File permissions of the jmxremote.password file +############################################################## +# This file must be made accessible by ONLY the owner, +# otherwise the program will exit with an error. # -# In a typical installation, this file can be read by anybody on the +# In a typical installation, this file can be accessed by anybody on the # local machine, and possibly by people on other machines. -# For # security, you should either restrict the access to this file, +# For security, you should either restrict the access to this file except for owner, # or specify another, less accessible file in the management config file # as described above. # + +############################################################## +# Sample of the jmxremote.password file +############################################################## # Following are two commented-out entries. The "measureRole" role has -# password "QED". The "controlRole" role has password "R&D". +# password "QED". The "controlRole" role has password "R&D". This is example +# of specifying passwords in clear # -# monitorRole QED -# controlRole R&D +# monitorRole QED +# controlRole R&D +# +# Once a login attempt is made, passwords will be hashed and the file will have +# below entries with clear passwords overwritten by their respective +# SHA-256 hash +# +# monitorRole 818kn2GKCT1IqMKnJhwtmow8v/9cv++8bJbhjO+ugX0= WqoaqEAVub/PenLY2wxLMLCdPRa+rFCWCWM0Zh/wR38= SHA-256 +# controlRole ALO98BOPW9rqvvtzzn7Lx7Q2uNWZdUf9PtY0g9aQ5lk= yobRA/4ygyJQTE2gEe4xQdYu7IexBHl0SSP9mEHFFhA= SHA-256 +# --- old/src/jdk.management.agent/share/conf/management.properties 2017-10-06 00:40:07.551427750 +0530 +++ new/src/jdk.management.agent/share/conf/management.properties 2017-10-06 00:40:07.451427443 +0530 @@ -301,6 +301,17 @@ # com.sun.management.jmxremote.password.file=filepath # +# ################# Hash passwords in password file ############## +# com.sun.management.jmxremote.password.hashpasswords = true|false +# Default for this property is true. +# Specifies if passswords in the above file should be hashed or not. +# If this property is true, and if the password file is writable, and if the +# system security policy allows writing into the password file, +# all the clear passwords in the password file will be replaced by +# its SHA-256 hash when the file is read by the server +# + +# # ################ RMI Access file location ##################### # # com.sun.management.jmxremote.access.file=filepath --- /dev/null 2017-10-05 15:54:58.620005553 +0530 +++ new/src/java.management/share/classes/com/sun/jmx/remote/security/HashedPasswordManager.java 2017-10-06 00:40:07.759428388 +0530 @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2017 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.sun.jmx.remote.security; + +import com.sun.jmx.remote.util.ClassLogger; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +/** + * This classes loads passwords from password file and optionally hashes them. + * + *

This class accepts Unicode UTF-8 encoded file + * + *

Each entry in password file contains username followed by password. + * Password can be in clear text or as hash. + * Hashed passwords must follow below format. + * hashedPassword = base64_encoded_salt W base64_encoded_hash W hash_algorithm + * where, + * W = spaces + * base64_encoded_hash = Hash_algorithm(password + salt) + * hash_algorithm = Algorithm string as specified in + * + * 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"; + + sbuf.append(header); + + 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 !!!"); + } + } + + // 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 !!!!"); + } + } + } +} --- /dev/null 2017-10-05 15:54:58.620005553 +0530 +++ new/test/javax/management/security/HashedPasswordFileTest.java 2017-10-06 00:40:08.059429307 +0530 @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* @test + * @summary Test Hashed passwords + * @library /test/lib + * @modules java.management + * @build HashedPasswordFileTest + * @run testng/othervm HashedPasswordFileTest + * + */ +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import javax.management.MBeanServer; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.annotations.AfterClass; + +import jdk.test.lib.Utils; +import jdk.test.lib.process.ProcessTools; + +@Test +public class HashedPasswordFileTest { + + private final String[] randomWords = {"accost", "savoie", "bogart", "merest", + "azuela", "hoodie", "bursal", "lingua", "wincey", "trilby", "egesta", + "wester", "gilgai", "weinek", "ochone", "sanest", "gainst", "defang", + "ranket", "mayhem", "tagger", "timber", "eggcup", "mhren", "colloq", + "dreamy", "hattie", "rootle", "bloody", "helyne", "beater", "cosine", + "enmity", "outbox", "issuer", "lumina", "dekker", "vetoed", "dennis", + "strove", "gurnet", "talkie", "bennie", "behove", "coates", "shiloh", + "yemeni", "boleyn", "coaxal", "irne"}; + + private final String[] hashAlgs = {"MD5", "SHA-1", "SHA-256"}; + + Random rnd = new Random(); + private final Random random = Utils.getRandomInstance(); + + private JMXConnectorServer cs; + + private String randomWord() { + int idx = rnd.nextInt(randomWords.length); + return randomWords[idx]; + } + + private String[] getHash(String algorithm, String password) { + try { + byte[] salt = new byte[32]; + 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) { + throw new RuntimeException(ex); + } + } + + private String getPasswordFilePath() { + String testDir = System.getProperty("test.src"); + String testFileName = "jmxremote.password"; + return testDir + File.separator + testFileName; + } + + private File createNewPasswordFile() throws IOException { + File file = new File(getPasswordFilePath()); + System.out.println("Created new file at : " + file.getAbsolutePath()); + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + return file; + } + + private Map generateClearTextPasswordFile() throws IOException { + File file = createNewPasswordFile(); + Map props = new HashMap<>(); + BufferedWriter br; + try (FileWriter fw = new FileWriter(file)) { + br = new BufferedWriter(fw); + int numentries = rnd.nextInt(5) + 3; + for (int i = 0; i < numentries; i++) { + String username = randomWord(); + String password = randomWord(); + props.put(username, password); + br.write(username + " " + password + "\n"); + } + br.flush(); + } + br.close(); + return props; + } + + private boolean isPasswordFileHashed() throws FileNotFoundException, IOException { + BufferedReader br; + boolean result; + try (FileReader fr = new FileReader(getPasswordFilePath())) { + br = new BufferedReader(fr); + result = br.lines().anyMatch(line -> { + if (line.startsWith("#")) { + return false; + } + String[] tokens = line.split("\\s+"); + return tokens.length == 3 || tokens.length == 4; + }); + } + br.close(); + return result; + } + + private Map generateHashedPasswordFile() throws IOException { + File file = createNewPasswordFile(); + Map props = new HashMap<>(); + BufferedWriter br; + try (FileWriter fw = new FileWriter(file)) { + br = new BufferedWriter(fw); + int numentries = rnd.nextInt(5) + 3; + for (int i = 0; i < numentries; i++) { + String username = randomWord(); + String password = randomWord(); + String alg = hashAlgs[rnd.nextInt(hashAlgs.length)]; + String[] b64str = getHash(alg, password); + br.write(username + " " + b64str[0] + " " + b64str[1] + " " + alg + "\n"); + props.put(username, password); + } + br.flush(); + } + br.close(); + return props; + } + + private JMXServiceURL createServerSide(boolean useHash) + throws MalformedURLException, IOException { + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + JMXServiceURL url = new JMXServiceURL("rmi", null, 0); + + HashMap env = new HashMap<>(); + env.put("jmx.remote.x.password.file", getPasswordFilePath()); + env.put("jmx.remote.x.password.hashpasswords", useHash ? "true" : "false"); + cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs); + cs.start(); + JMXServiceURL addr = cs.getAddress(); + return addr; + } + + @Test + public void testClearTextPasswordFile() throws IOException { + Boolean[] bvals = new Boolean[]{true, false}; + for (boolean bval : bvals) { + try { + Map credentials = generateClearTextPasswordFile(); + JMXServiceURL serverUrl = createServerSide(bval); + for (Map.Entry entry : credentials.entrySet()) { + HashMap env = new HashMap<>(); + env.put("jmx.remote.credentials", + new String[]{entry.getKey(), entry.getValue()}); + try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { + cc.getMBeanServerConnection(); + } + } + Assert.assertEquals(isPasswordFileHashed(), bval); + } finally { + cs.stop(); + } + } + } + + @Test + public void testReadOnlyPasswordFile() throws IOException { + Boolean[] bvals = new Boolean[]{true, false}; + for (boolean bval : bvals) { + try { + Map credentials = generateClearTextPasswordFile(); + File file = new File(getPasswordFilePath()); + file.setReadOnly(); + JMXServiceURL serverUrl = createServerSide(true); + for (Map.Entry entry : credentials.entrySet()) { + HashMap env = new HashMap<>(); + env.put("jmx.remote.credentials", + new String[]{entry.getKey(), entry.getValue()}); + try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { + cc.getMBeanServerConnection(); + } + } + Assert.assertEquals(isPasswordFileHashed(), false); + } finally { + cs.stop(); + } + } + } + + @Test + public void testHashedPasswordFile() throws IOException { + Boolean[] bvals = new Boolean[]{true, false}; + for (boolean bval : bvals) { + try { + Map credentials = generateHashedPasswordFile(); + JMXServiceURL serverUrl = createServerSide(bval); + Assert.assertEquals(isPasswordFileHashed(), true); + for (Map.Entry entry : credentials.entrySet()) { + HashMap env = new HashMap<>(); + env.put("jmx.remote.credentials", + new String[]{entry.getKey(), entry.getValue()}); + try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { + cc.getMBeanServerConnection(); + } + } + } finally { + cs.stop(); + } + } + } + + @Test + public void testDefaultAgent() throws IOException, InterruptedException, Exception { + List pbArgs = new ArrayList<>(); + int port = Utils.getFreePort(); + generateClearTextPasswordFile(); + + // This will run only on a POSIX compliant system + if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + return; + } + + // Make sure only owner is able to read/write the file or else + // default agent will fail to start + File file = new File(getPasswordFilePath()); + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(file.toPath(), perms); + + pbArgs.add("-cp"); + pbArgs.add(System.getProperty("test.class.path")); + + pbArgs.add("-Dcom.sun.management.jmxremote.port=" + port); + pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); + pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); + pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); + pbArgs.add(TestApp.class.getSimpleName()); + + ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( + pbArgs.toArray(new String[0])); + Process process = ProcessTools.startProcess( + TestApp.class.getSimpleName(), + pb); + + if (process.waitFor() != 0) { + throw new RuntimeException("Test Failed : Error starting default agent"); + } + Assert.assertEquals(isPasswordFileHashed(), true); + } + + @Test + public void testDefaultAgentNoHash() throws IOException, InterruptedException, Exception { + List pbArgs = new ArrayList<>(); + int port = Utils.getFreePort(); + generateClearTextPasswordFile(); + + // This will run only on a POSIX compliant system + if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + return; + } + + // Make sure only owner is able to read/write the file or else + // default agent will fail to start + File file = new File(getPasswordFilePath()); + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(file.toPath(), perms); + + pbArgs.add("-cp"); + pbArgs.add(System.getProperty("test.class.path")); + + pbArgs.add("-Dcom.sun.management.jmxremote.port=" + port); + pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); + pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); + pbArgs.add("-Dcom.sun.management.jmxremote.password.hashpasswords=false"); + pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); + pbArgs.add(TestApp.class.getSimpleName()); + + ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( + pbArgs.toArray(new String[0])); + Process process = ProcessTools.startProcess( + TestApp.class.getSimpleName(), + pb); + + if (process.waitFor() != 0) { + throw new RuntimeException("Test Failed : Error starting default agent"); + } + Assert.assertEquals(isPasswordFileHashed(), false); + } + + @AfterClass + public void cleanUp() { + File file = new File(getPasswordFilePath()); + if (file.exists()) { + file.delete(); + } + } +} + +class TestApp { + + public static void main(String[] args) throws MalformedURLException, IOException { + try { + JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + + System.getProperty("com.sun.management.jmxremote.port") + "/jmxrmi"); + Map env = new HashMap<>(1); + // any dummy credentials will do. We just have to trigger password hashing + env.put("jmx.remote.credentials", new String[]{"a", "a"}); + try (JMXConnector cc = JMXConnectorFactory.connect(url, env)) { + cc.getMBeanServerConnection(); + } + } catch (SecurityException ex) { + // Catch authentication failure here + } + } +}