1 /* 2 * Copyright (c) 2017 Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package com.sun.jmx.remote.security; 26 27 import com.sun.jmx.remote.util.ClassLogger; 28 29 import java.io.File; 30 import java.io.FileWriter; 31 import java.io.IOException; 32 import java.nio.charset.StandardCharsets; 33 import java.nio.file.Files; 34 import java.nio.file.Paths; 35 import java.security.MessageDigest; 36 import java.security.NoSuchAlgorithmException; 37 import java.security.SecureRandom; 38 import java.util.Arrays; 39 import java.util.Base64; 40 import java.util.HashMap; 41 import java.util.Map; 42 import java.util.concurrent.atomic.AtomicBoolean; 43 import java.util.stream.Stream; 44 45 /** 46 * This classes loads passwords from password file and optionally hashes them. 47 * 48 * <p> This class accepts Unicode UTF-8 encoded file 49 * 50 * <p> Each entry in password file contains username followed by password. 51 * Password can be in clear text or as hash. 52 * Hashed passwords must follow below format. 53 * hashedPassword = base64_encoded_salt W base64_encoded_hash W hash_algorithm 54 * where, 55 * W = spaces 56 * base64_encoded_hash = Hash_algorithm(password + salt) 57 * hash_algorithm = Algorithm string as specified in 58 * <a href="{@docRoot}/../technotes/guides/security/StandardNames.html#MessageDigest"> 59 * This is an optional field. If not specified, SHA-256 will be assumed. 60 * 61 * <p> If passwords are in clear, they will be over-written by their hash if 62 * hashing is requested by setting com.sun.management.jmxremote.password.hashpasswords 63 * property to true in management.properties file and if the password file is 64 * writable and if the system security policy allows writing into the password 65 * file, if a security manager is configured 66 * 67 * <p> In order to change password for a role, replace hashed password entry 68 * with clear text password or new hashed password. If new password is in clear, 69 * it will be replaced with its hash when new login attempt is made. 70 * 71 * <p> A given role should have at most one entry in this file. If a role 72 * has no entry, it has no access. 73 * If multiple entries are found for the same role name, then the last one 74 * is used. 75 * 76 * <p> User generated hashed password file can also be used instead of clear-text 77 * password file. If generated by user, hashed passwords must follow 78 * format specified above. 79 */ 80 81 final public class HashedPasswordManager { 82 83 private static final class UserCredentials { 84 final private String userName; 85 final private String hashAlgorithm; 86 final private String b64Salt; 87 final private String b64Hash; 88 89 public UserCredentials(String userName, String hashAlgorithm, String b64Salt, String b64Hash) { 90 this.userName = userName; 91 this.hashAlgorithm = hashAlgorithm; 92 this.b64Salt = b64Salt; 93 this.b64Hash = b64Hash; 94 } 95 } 96 97 private static final String DefaultHashAlgorithm = "SHA-256"; 98 private static final int DefaultSaltLength = 32; 99 100 private final SecureRandom random = new SecureRandom(); 101 private final Map<String, UserCredentials> userCredentialsMap = new HashMap<>(); 102 private final String passwordFile; 103 private final boolean shouldHashPasswords; 104 105 private static final ClassLogger logger 106 = new ClassLogger("javax.management.remote.misc", 107 "HashedPasswordManager"); 108 109 /** 110 * Creates new password manager for input password file 111 * 112 * @param filename UTF-8 encoded input file to read passwords from 113 * @param shouldHashPasswords Request for clear passwords to be hashed 114 * @throws IOException if unable to access input password file 115 * @throws SecurityException if read/write file permissions are not granted 116 */ 117 public HashedPasswordManager(String filename, boolean shouldHashPasswords) 118 throws SecurityException, IOException { 119 this.passwordFile = filename; 120 this.shouldHashPasswords = shouldHashPasswords; 121 loadPasswords(); 122 } 123 124 /* 125 * Checks if the Security manager allows writing to the file 126 * as well as if the file is writable 127 */ 128 private boolean canWriteToFile(String fileName) { 129 File f = new File(fileName); 130 SecurityManager security = System.getSecurityManager(); 131 if (security != null) { 132 try { 133 security.checkWrite(fileName); // // Check we have write permissions 134 return f.canWrite(); // Check file is not read-only 135 } catch (SecurityException ex) { 136 return false; 137 } 138 } else { 139 return f.canWrite(); 140 } 141 } 142 143 private String[] getHash(String algorithm, String password) { 144 try { 145 byte[] salt = new byte[DefaultSaltLength]; 146 random.nextBytes(salt); 147 148 MessageDigest digest = MessageDigest.getInstance(algorithm); 149 digest.reset(); 150 digest.update(salt); 151 byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); 152 153 String saltStr = Base64.getEncoder().encodeToString(salt); 154 String hashStr = Base64.getEncoder().encodeToString(hash); 155 156 return new String[]{saltStr, hashStr}; 157 } catch (NoSuchAlgorithmException ex) { 158 if(logger.warningOn()) { 159 logger.warning("getHash", "Invalid algorithm : " + algorithm); 160 } 161 // We should never reach here as default Hash Algorithm 162 // must be always present 163 return new String[]{"", ""}; 164 } 165 } 166 167 /** 168 * Authenticate supplied credentials against one present in file 169 * 170 * @param userName Input username 171 * @param inputPassword Input password 172 * @return true if authentication succeeds, false otherwise 173 */ 174 public synchronized boolean authenticate(String userName, String inputPassword) { 175 if (userCredentialsMap.containsKey(userName)) { 176 try { 177 UserCredentials us = userCredentialsMap.get(userName); 178 byte[] salt = Base64.getDecoder().decode(us.b64Salt); 179 byte[] targetHash = Base64.getDecoder().decode(us.b64Hash); 180 MessageDigest digest = MessageDigest.getInstance(us.hashAlgorithm); 181 digest.reset(); 182 digest.update(salt); 183 byte[] hash = digest.digest(inputPassword.getBytes(StandardCharsets.UTF_8)); 184 return Arrays.equals(hash, targetHash); 185 } catch (NoSuchAlgorithmException ex) { 186 if(logger.warningOn()) { 187 logger.warning("authenticate", "Unrecognized hash algorithm : " + 188 userCredentialsMap.get(userName).hashAlgorithm 189 + " - for user : " + userName); 190 } 191 return false; 192 } 193 } else { 194 if(logger.warningOn()) { 195 logger.warning("authenticate", "Unknown user : " + userName); 196 } 197 return false; 198 } 199 } 200 201 /** 202 * Load passwords from password file. 203 * 204 * <p> 205 * This method should be called for every login attempt to load new/changed 206 * credentials, if any. 207 * 208 * <p> 209 * If hashing is requested, clear passwords will be over-written with their 210 * SHA-256 hash 211 * 212 * @throws IOException If unable to access the file 213 * @throws SecurityException If read/write file permissions are not granted 214 */ 215 public synchronized void loadPasswords() 216 throws IOException, SecurityException { 217 218 SecurityManager security = System.getSecurityManager(); 219 if (security != null) { 220 security.checkRead(passwordFile); 221 } 222 223 Stream<String> lines = Files.lines(Paths.get(passwordFile)); 224 AtomicBoolean hasClearPasswords = new AtomicBoolean(false); 225 StringBuilder sbuf = new StringBuilder(); 226 String header = "# The passwords in this file are hashed.\n" 227 + "# In order to change password for a role, replace hashed " 228 + "password entry\n" 229 + "# with clear text password or new hashed password. " 230 + "If new password is in clear,\n # it will be replaced with its " 231 + "hash when new login attempt is made.\n\n"; 232 233 sbuf.append(header); 234 235 lines.forEach(line -> { 236 if (line.trim().startsWith("#")) { // Ignore comments 237 sbuf.append(line).append("\n"); 238 return; 239 } 240 String[] tokens = line.split("\\s+"); 241 switch (tokens.length) { 242 case 2: { 243 // Password is in clear 244 String[] b64str = getHash(DefaultHashAlgorithm, tokens[1]); 245 UserCredentials us = new UserCredentials(tokens[0],DefaultHashAlgorithm,b64str[0],b64str[1]); 246 sbuf.append(us.userName).append(" ").append(us.b64Salt). 247 append(" ").append(us.b64Hash).append(" "). 248 append(us.hashAlgorithm).append("\n"); 249 userCredentialsMap.put(tokens[0], us); 250 hasClearPasswords.set(true); 251 if (logger.debugOn()) { 252 logger.debug("loadPasswords", 253 "Found atleast one clear password"); 254 } 255 break; 256 } 257 case 3: 258 case 4: { 259 // Passwords are hashed 260 UserCredentials us = new UserCredentials(tokens[0], (tokens.length == 4 ? tokens[3] : DefaultHashAlgorithm), 261 tokens[1], tokens[2]); 262 sbuf.append(line).append("\n"); 263 userCredentialsMap.put(tokens[0], us); 264 break; 265 } 266 default: 267 sbuf.append(line).append("\n"); 268 break; 269 } 270 }); 271 272 if (!shouldHashPasswords && hasClearPasswords.get()) { 273 if (logger.warningOn()) { 274 logger.warning("loadPasswords", 275 "Passwords in " + passwordFile + " are in clear but are requested " 276 + "not to be hashed !!!"); 277 } 278 } 279 280 // Even if we are unable to write hashed passwords to password file, 281 // passwords will be hashed in memory so that jvm heap dump should not 282 // give away clear passwords 283 if (shouldHashPasswords && hasClearPasswords.get()) { 284 if (canWriteToFile(passwordFile)) { 285 try (FileWriter fw = new FileWriter(passwordFile)) { 286 fw.write(sbuf.toString()); 287 } 288 if(logger.debugOn()) { 289 logger.debug("loadPasswords", 290 "Wrote hash passwords to file"); 291 } 292 } else if (logger.warningOn()) { 293 logger.warning("loadPasswords", 294 "Passwords in " + passwordFile + " are in clear and password file is read-only. " 295 + "Passwords cannot be hashed !!!!"); 296 } 297 } 298 } 299 }