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