1 /* 2 * Copyright (c) 2017, 2018, 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. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* @test 25 * @bug 5016517 8204661 26 * @summary Test Hashed passwords 27 * @library /test/lib 28 * @modules java.management 29 * jdk.management.agent/jdk.internal.agent 30 * @build HashedPasswordFileTest 31 * @run testng/othervm HashedPasswordFileTest 32 * 33 */ 34 35 import jdk.internal.agent.ConnectorAddressLink; 36 import jdk.test.lib.Utils; 37 import jdk.test.lib.process.ProcessTools; 38 import org.testng.Assert; 39 import org.testng.annotations.AfterClass; 40 import org.testng.annotations.Test; 41 42 import javax.management.MBeanServer; 43 import javax.management.remote.*; 44 import java.io.*; 45 import java.lang.management.ManagementFactory; 46 import java.nio.charset.StandardCharsets; 47 import java.nio.file.FileSystems; 48 import java.nio.file.Files; 49 import java.nio.file.attribute.PosixFilePermission; 50 import java.security.MessageDigest; 51 import java.security.NoSuchAlgorithmException; 52 import java.util.*; 53 import java.util.List; 54 import java.util.Set; 55 import java.util.concurrent.*; 56 57 @Test 58 public class HashedPasswordFileTest { 59 60 private final String[] randomWords = {"accost", "savoie", "bogart", "merest", 61 "azuela", "hoodie", "bursal", "lingua", "wincey", "trilby", "egesta", 62 "wester", "gilgai", "weinek", "ochone", "sanest", "gainst", "defang", 63 "ranket", "mayhem", "tagger", "timber", "eggcup", "mhren", "colloq", 64 "dreamy", "hattie", "rootle", "bloody", "helyne", "beater", "cosine", 65 "enmity", "outbox", "issuer", "lumina", "dekker", "vetoed", "dennis", 66 "strove", "gurnet", "talkie", "bennie", "behove", "coates", "shiloh", 67 "yemeni", "boleyn", "coaxal", "irne"}; 68 69 private final String[] hashAlgs = { 70 "MD2", 71 "MD5", 72 "SHA-1", 73 "SHA-224", 74 "SHA-256", 75 "SHA-384", 76 "SHA-512/224", 77 "SHA-512/256", 78 "SHA3-224", 79 "SHA3-256", 80 "SHA3-384", 81 "SHA3-512" 82 }; 83 84 private final Random random = Utils.getRandomInstance(); 85 86 private JMXConnectorServer cs; 87 randomWord()88 private String randomWord() { 89 int idx = random.nextInt(randomWords.length); 90 return randomWords[idx]; 91 } 92 getHash(String algorithm, String password)93 private String[] getHash(String algorithm, String password) { 94 try { 95 byte[] salt = new byte[64]; 96 random.nextBytes(salt); 97 98 MessageDigest digest = MessageDigest.getInstance(algorithm); 99 digest.reset(); 100 digest.update(salt); 101 byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); 102 103 String saltStr = Base64.getEncoder().encodeToString(salt); 104 String hashStr = Base64.getEncoder().encodeToString(hash); 105 106 return new String[]{saltStr, hashStr}; 107 } catch (NoSuchAlgorithmException ex) { 108 throw new RuntimeException(ex); 109 } 110 } 111 getPasswordFilePath()112 private String getPasswordFilePath() { 113 String testDir = System.getProperty("test.src"); 114 String testFileName = "jmxremote.password"; 115 return testDir + File.separator + testFileName; 116 } 117 createNewPasswordFile()118 private File createNewPasswordFile() throws IOException { 119 File file = new File(getPasswordFilePath()); 120 if (file.exists()) { 121 file.delete(); 122 } 123 file.createNewFile(); 124 return file; 125 } 126 generateClearTextPasswordFile()127 private Map<String, String> generateClearTextPasswordFile() throws IOException { 128 File file = createNewPasswordFile(); 129 Map<String, String> props = new HashMap<>(); 130 BufferedWriter br; 131 try (FileWriter fw = new FileWriter(file)) { 132 br = new BufferedWriter(fw); 133 int numentries = random.nextInt(5) + 3; 134 for (int i = 0; i < numentries; i++) { 135 String username; 136 do { 137 username = randomWord(); 138 } while (props.get(username) != null); 139 String password = randomWord(); 140 props.put(username, password); 141 br.write(username + " " + password + "\n"); 142 } 143 br.flush(); 144 } 145 br.close(); 146 return props; 147 } 148 isPasswordFileHashed()149 private boolean isPasswordFileHashed() throws IOException { 150 BufferedReader br; 151 boolean result; 152 try (FileReader fr = new FileReader(getPasswordFilePath())) { 153 br = new BufferedReader(fr); 154 result = br.lines().anyMatch(line -> { 155 if (line.startsWith("#")) { 156 return false; 157 } 158 String[] tokens = line.split("\\s+"); 159 return tokens.length == 3 || tokens.length == 4; 160 }); 161 } 162 br.close(); 163 return result; 164 } 165 generateHashedPasswordFile()166 private Map<String, String> generateHashedPasswordFile() throws IOException { 167 File file = createNewPasswordFile(); 168 Map<String, String> props = new HashMap<>(); 169 BufferedWriter br; 170 try (FileWriter fw = new FileWriter(file)) { 171 br = new BufferedWriter(fw); 172 int numentries = random.nextInt(5) + 3; 173 for (int i = 0; i < numentries; i++) { 174 String username; 175 do { 176 username = randomWord(); 177 } while (props.get(username) != null); 178 String password = randomWord(); 179 String alg = hashAlgs[random.nextInt(hashAlgs.length)]; 180 String[] b64str = getHash(alg, password); 181 br.write(username + " " + b64str[0] + " " + b64str[1] + " " + alg + "\n"); 182 props.put(username, password); 183 } 184 br.flush(); 185 } 186 br.close(); 187 return props; 188 } 189 createServerSide(boolean useHash)190 private JMXServiceURL createServerSide(boolean useHash) 191 throws IOException { 192 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 193 JMXServiceURL url = new JMXServiceURL("rmi", null, 0); 194 195 HashMap<String, Object> env = new HashMap<>(); 196 env.put("jmx.remote.x.password.file", getPasswordFilePath()); 197 env.put("jmx.remote.x.password.toHashes", useHash ? "true" : "false"); 198 cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs); 199 cs.start(); 200 return cs.getAddress(); 201 } 202 203 @Test testClearTextPasswordFile()204 public void testClearTextPasswordFile() throws IOException { 205 Boolean[] bvals = new Boolean[]{true, false}; 206 for (boolean bval : bvals) { 207 try { 208 Map<String, String> credentials = generateClearTextPasswordFile(); 209 JMXServiceURL serverUrl = createServerSide(bval); 210 for (Map.Entry<String, String> entry : credentials.entrySet()) { 211 HashMap<String, Object> env = new HashMap<>(); 212 env.put("jmx.remote.credentials", 213 new String[]{entry.getKey(), entry.getValue()}); 214 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 215 cc.getMBeanServerConnection(); 216 } 217 } 218 Assert.assertEquals(isPasswordFileHashed(), bval); 219 } finally { 220 cs.stop(); 221 } 222 } 223 } 224 225 @Test testReadOnlyPasswordFile()226 public void testReadOnlyPasswordFile() throws IOException { 227 Boolean[] bvals = new Boolean[]{true, false}; 228 for (boolean bval : bvals) { 229 try { 230 Map<String, String> credentials = generateClearTextPasswordFile(); 231 File file = new File(getPasswordFilePath()); 232 file.setReadOnly(); 233 JMXServiceURL serverUrl = createServerSide(bval); 234 for (Map.Entry<String, String> entry : credentials.entrySet()) { 235 HashMap<String, Object> env = new HashMap<>(); 236 env.put("jmx.remote.credentials", 237 new String[]{entry.getKey(), entry.getValue()}); 238 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 239 cc.getMBeanServerConnection(); 240 } 241 } 242 Assert.assertEquals(isPasswordFileHashed(), false); 243 } finally { 244 cs.stop(); 245 } 246 } 247 } 248 249 @Test testHashedPasswordFile()250 public void testHashedPasswordFile() throws IOException { 251 Boolean[] bvals = new Boolean[]{true, false}; 252 for (boolean bval : bvals) { 253 try { 254 Map<String, String> credentials = generateHashedPasswordFile(); 255 JMXServiceURL serverUrl = createServerSide(bval); 256 Assert.assertEquals(isPasswordFileHashed(), true); 257 for (Map.Entry<String, String> entry : credentials.entrySet()) { 258 HashMap<String, Object> env = new HashMap<>(); 259 env.put("jmx.remote.credentials", 260 new String[]{entry.getKey(), entry.getValue()}); 261 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 262 cc.getMBeanServerConnection(); 263 } 264 } 265 } finally { 266 cs.stop(); 267 } 268 } 269 } 270 271 private static class SimpleJMXClient implements Callable { 272 private final JMXServiceURL url; 273 private final Map<String, String> credentials; 274 SimpleJMXClient(JMXServiceURL url, Map<String, String> credentials)275 public SimpleJMXClient(JMXServiceURL url, Map<String, String> credentials) { 276 this.url = url; 277 this.credentials = credentials; 278 } 279 280 @Override call()281 public Object call() throws Exception { 282 for (Map.Entry<String, String> entry : credentials.entrySet()) { 283 HashMap<String, Object> env = new HashMap<>(); 284 env.put("jmx.remote.credentials", 285 new String[]{entry.getKey(), entry.getValue()}); 286 try (JMXConnector cc = JMXConnectorFactory.connect(url, env)) { 287 cc.getMBeanServerConnection(); 288 } 289 } 290 return null; 291 } 292 } 293 294 @Test testMultipleClients()295 public void testMultipleClients() throws Throwable { 296 Map<String, String> credentials = generateClearTextPasswordFile(); 297 JMXServiceURL serverUrl = createServerSide(true); 298 Assert.assertEquals(isPasswordFileHashed(), false); 299 // create random number of clients 300 int numClients = random.nextInt(20) + 10; 301 List<Future> futures = new ArrayList<>(); 302 ExecutorService executor = Executors.newFixedThreadPool(numClients); 303 for (int i = 0; i < numClients; i++) { 304 Future future = executor.submit(new SimpleJMXClient(serverUrl, credentials)); 305 futures.add(future); 306 } 307 try { 308 for (Future future : futures) { 309 future.get(); 310 } 311 } catch (InterruptedException ex) { 312 Thread.currentThread().interrupt(); 313 } catch (ExecutionException ex) { 314 throw ex.getCause(); 315 } finally { 316 executor.shutdown(); 317 } 318 319 Assert.assertEquals(isPasswordFileHashed(), true); 320 } 321 322 @Test testPasswordChange()323 public void testPasswordChange() throws IOException { 324 try { 325 Map<String, String> credentials = generateClearTextPasswordFile(); 326 JMXServiceURL serverUrl = createServerSide(true); 327 Assert.assertEquals(isPasswordFileHashed(), false); 328 329 for (Map.Entry<String, String> entry : credentials.entrySet()) { 330 HashMap<String, Object> env = new HashMap<>(); 331 env.put("jmx.remote.credentials", 332 new String[]{entry.getKey(), entry.getValue()}); 333 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 334 cc.getMBeanServerConnection(); 335 } 336 } 337 Assert.assertEquals(isPasswordFileHashed(), true); 338 339 // Read the file back. Add new entries. Change passwords for few 340 BufferedReader br = new BufferedReader(new FileReader(getPasswordFilePath())); 341 String line; 342 StringBuilder sbuild = new StringBuilder(); 343 while ((line = br.readLine()) != null) { 344 if (line.trim().startsWith("#")) { 345 sbuild.append(line).append("\n"); 346 continue; 347 } 348 349 // Change password for random entries 350 if (random.nextBoolean()) { 351 String[] tokens = line.split("\\s+"); 352 if ((tokens.length == 4 || tokens.length == 3)) { 353 String password = randomWord(); 354 credentials.put(tokens[0], password); 355 sbuild.append(tokens[0]).append(" ").append(password).append("\n"); 356 } 357 } else { 358 sbuild.append(line).append("\n"); 359 } 360 } 361 362 // Add new entries in clear 363 int newentries = random.nextInt(2) + 3; 364 for (int i = 0; i < newentries; i++) { 365 String username; 366 do { 367 username = randomWord(); 368 } while (credentials.get(username) != null); 369 String password = randomWord(); 370 credentials.put(username, password); 371 sbuild.append(username).append(" ").append(password).append("\n"); 372 } 373 374 // Add new entries as a hash 375 int numentries = random.nextInt(2) + 3; 376 for (int i = 0; i < numentries; i++) { 377 String username; 378 do { 379 username = randomWord(); 380 } while (credentials.get(username) != null); 381 String password = randomWord(); 382 String alg = hashAlgs[random.nextInt(hashAlgs.length)]; 383 String[] b64str = getHash(alg, password); 384 credentials.put(username, password); 385 sbuild.append(username).append(" ").append(b64str[0]) 386 .append(" ").append(b64str[1]).append(" ") 387 .append(alg).append("\n"); 388 } 389 390 try (BufferedWriter bw = new BufferedWriter(new FileWriter(getPasswordFilePath()))) { 391 bw.write(sbuild.toString()); 392 } 393 394 for (Map.Entry<String, String> entry : credentials.entrySet()) { 395 HashMap<String, Object> env = new HashMap<>(); 396 env.put("jmx.remote.credentials", 397 new String[]{entry.getKey(), entry.getValue()}); 398 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 399 cc.getMBeanServerConnection(); 400 } 401 } 402 } finally { 403 cs.stop(); 404 } 405 } 406 407 @Test testDefaultAgent()408 public void testDefaultAgent() throws Exception { 409 List<String> pbArgs = new ArrayList<>(); 410 generateClearTextPasswordFile(); 411 412 // This will run only on a POSIX compliant system 413 if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { 414 return; 415 } 416 417 // Make sure only owner is able to read/write the file or else 418 // default agent will fail to start 419 File file = new File(getPasswordFilePath()); 420 Set<PosixFilePermission> perms = new HashSet<>(); 421 perms.add(PosixFilePermission.OWNER_READ); 422 perms.add(PosixFilePermission.OWNER_WRITE); 423 Files.setPosixFilePermissions(file.toPath(), perms); 424 425 pbArgs.add("-cp"); 426 pbArgs.add(System.getProperty("test.class.path")); 427 428 pbArgs.add("-Dcom.sun.management.jmxremote.port=0"); 429 pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); 430 pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); 431 pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); 432 pbArgs.add("--add-exports"); 433 pbArgs.add("jdk.management.agent/jdk.internal.agent=ALL-UNNAMED"); 434 pbArgs.add(TestApp.class.getSimpleName()); 435 436 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( 437 pbArgs.toArray(new String[0])); 438 Process process = ProcessTools.startProcess( 439 TestApp.class.getSimpleName(), 440 pb); 441 442 if (process.waitFor() != 0) { 443 throw new RuntimeException("Test Failed : Error starting default agent"); 444 } 445 Assert.assertEquals(isPasswordFileHashed(), true); 446 } 447 448 @Test testDefaultAgentNoHash()449 public void testDefaultAgentNoHash() throws Exception { 450 List<String> pbArgs = new ArrayList<>(); 451 generateClearTextPasswordFile(); 452 453 // This will run only on a POSIX compliant system 454 if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { 455 return; 456 } 457 458 // Make sure only owner is able to read/write the file or else 459 // default agent will fail to start 460 File file = new File(getPasswordFilePath()); 461 Set<PosixFilePermission> perms = new HashSet<>(); 462 perms.add(PosixFilePermission.OWNER_READ); 463 perms.add(PosixFilePermission.OWNER_WRITE); 464 Files.setPosixFilePermissions(file.toPath(), perms); 465 466 pbArgs.add("-cp"); 467 pbArgs.add(System.getProperty("test.class.path")); 468 469 pbArgs.add("-Dcom.sun.management.jmxremote.port=0"); 470 pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); 471 pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); 472 pbArgs.add("-Dcom.sun.management.jmxremote.password.toHashes=false"); 473 pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); 474 pbArgs.add("--add-exports"); 475 pbArgs.add("jdk.management.agent/jdk.internal.agent=ALL-UNNAMED"); 476 pbArgs.add(TestApp.class.getSimpleName()); 477 478 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( 479 pbArgs.toArray(new String[0])); 480 Process process = ProcessTools.startProcess( 481 TestApp.class.getSimpleName(), 482 pb); 483 484 if (process.waitFor() != 0) { 485 throw new RuntimeException("Test Failed : Error starting default agent"); 486 } 487 Assert.assertEquals(isPasswordFileHashed(), false); 488 } 489 490 @AfterClass cleanUp()491 public void cleanUp() { 492 File file = new File(getPasswordFilePath()); 493 if (file.exists()) { 494 file.delete(); 495 } 496 } 497 } 498 499 class TestApp { 500 main(String[] args)501 public static void main(String[] args) throws IOException { 502 try { 503 Map<String, String> propsMap = ConnectorAddressLink.importRemoteFrom(0); 504 String jmxServiceUrl = propsMap.get("sun.management.JMXConnectorServer.0.remoteAddress"); 505 Map<String, Object> env = new HashMap<>(1); 506 // any dummy credentials will do. We just have to trigger password hashing 507 env.put("jmx.remote.credentials", new String[]{"a", "a"}); 508 try (JMXConnector cc = JMXConnectorFactory.connect(new JMXServiceURL(jmxServiceUrl), env)) { 509 cc.getMBeanServerConnection(); 510 } 511 } catch (SecurityException ex) { 512 // Catch authentication failure here 513 } 514 } 515 } 516