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. 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 * @summary Test Hashed passwords 26 * @library /test/lib 27 * @modules java.management 28 * @build HashedPasswordFileTest 29 * @run testng/othervm HashedPasswordFileTest 30 * 31 */ 32 import java.io.BufferedReader; 33 import java.io.BufferedWriter; 34 import java.io.File; 35 import java.io.FileNotFoundException; 36 import java.io.FileReader; 37 import java.io.FileWriter; 38 import java.io.IOException; 39 import java.lang.management.ManagementFactory; 40 import java.net.MalformedURLException; 41 import java.nio.charset.StandardCharsets; 42 import java.nio.file.FileSystems; 43 import java.nio.file.Files; 44 import java.nio.file.attribute.PosixFilePermission; 45 import java.security.MessageDigest; 46 import java.security.NoSuchAlgorithmException; 47 import java.util.ArrayList; 48 import java.util.Base64; 49 import java.util.HashMap; 50 import java.util.HashSet; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Random; 54 import java.util.Set; 55 import javax.management.MBeanServer; 56 import javax.management.remote.JMXConnector; 57 import javax.management.remote.JMXConnectorFactory; 58 import javax.management.remote.JMXConnectorServer; 59 import javax.management.remote.JMXConnectorServerFactory; 60 import javax.management.remote.JMXServiceURL; 61 62 import org.testng.Assert; 63 import org.testng.annotations.Test; 64 import org.testng.annotations.AfterClass; 65 66 import jdk.test.lib.Utils; 67 import jdk.test.lib.process.ProcessTools; 68 69 @Test 70 public class HashedPasswordFileTest { 71 72 private final String[] randomWords = {"accost", "savoie", "bogart", "merest", 73 "azuela", "hoodie", "bursal", "lingua", "wincey", "trilby", "egesta", 74 "wester", "gilgai", "weinek", "ochone", "sanest", "gainst", "defang", 75 "ranket", "mayhem", "tagger", "timber", "eggcup", "mhren", "colloq", 76 "dreamy", "hattie", "rootle", "bloody", "helyne", "beater", "cosine", 77 "enmity", "outbox", "issuer", "lumina", "dekker", "vetoed", "dennis", 78 "strove", "gurnet", "talkie", "bennie", "behove", "coates", "shiloh", 79 "yemeni", "boleyn", "coaxal", "irne"}; 80 81 private final String[] hashAlgs = {"MD5", "SHA-1", "SHA-256"}; 82 83 Random rnd = new Random(); 84 private final Random random = Utils.getRandomInstance(); 85 86 private JMXConnectorServer cs; 87 88 private String randomWord() { 89 int idx = rnd.nextInt(randomWords.length); 90 return randomWords[idx]; 91 } 92 93 private String[] getHash(String algorithm, String password) { 94 try { 95 byte[] salt = new byte[32]; 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 112 private String getPasswordFilePath() { 113 String testDir = System.getProperty("test.src"); 114 String testFileName = "jmxremote.password"; 115 return testDir + File.separator + testFileName; 116 } 117 118 private File createNewPasswordFile() throws IOException { 119 File file = new File(getPasswordFilePath()); 120 System.out.println("Created new file at : " + file.getAbsolutePath()); 121 if (file.exists()) { 122 file.delete(); 123 } 124 file.createNewFile(); 125 return file; 126 } 127 128 private Map<String, String> generateClearTextPasswordFile() throws IOException { 129 File file = createNewPasswordFile(); 130 Map<String, String> props = new HashMap<>(); 131 BufferedWriter br; 132 try (FileWriter fw = new FileWriter(file)) { 133 br = new BufferedWriter(fw); 134 int numentries = rnd.nextInt(5) + 3; 135 for (int i = 0; i < numentries; i++) { 136 String username = randomWord(); 137 String password = randomWord(); 138 props.put(username, password); 139 br.write(username + " " + password + "\n"); 140 } 141 br.flush(); 142 } 143 br.close(); 144 return props; 145 } 146 147 private boolean isPasswordFileHashed() throws FileNotFoundException, IOException { 148 BufferedReader br; 149 boolean result; 150 try (FileReader fr = new FileReader(getPasswordFilePath())) { 151 br = new BufferedReader(fr); 152 result = br.lines().anyMatch(line -> { 153 if (line.startsWith("#")) { 154 return false; 155 } 156 String[] tokens = line.split("\\s+"); 157 return tokens.length == 3 || tokens.length == 4; 158 }); 159 } 160 br.close(); 161 return result; 162 } 163 164 private Map<String, String> generateHashedPasswordFile() throws IOException { 165 File file = createNewPasswordFile(); 166 Map<String, String> props = new HashMap<>(); 167 BufferedWriter br; 168 try (FileWriter fw = new FileWriter(file)) { 169 br = new BufferedWriter(fw); 170 int numentries = rnd.nextInt(5) + 3; 171 for (int i = 0; i < numentries; i++) { 172 String username = randomWord(); 173 String password = randomWord(); 174 String alg = hashAlgs[rnd.nextInt(hashAlgs.length)]; 175 String[] b64str = getHash(alg, password); 176 br.write(username + " " + b64str[0] + " " + b64str[1] + " " + alg + "\n"); 177 props.put(username, password); 178 } 179 br.flush(); 180 } 181 br.close(); 182 return props; 183 } 184 185 private JMXServiceURL createServerSide(boolean useHash) 186 throws MalformedURLException, IOException { 187 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 188 JMXServiceURL url = new JMXServiceURL("rmi", null, 0); 189 190 HashMap<String, Object> env = new HashMap<>(); 191 env.put("jmx.remote.x.password.file", getPasswordFilePath()); 192 env.put("jmx.remote.x.password.hashpasswords", useHash ? "true" : "false"); 193 cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs); 194 cs.start(); 195 JMXServiceURL addr = cs.getAddress(); 196 return addr; 197 } 198 199 @Test 200 public void testClearTextPasswordFile() throws IOException { 201 Boolean[] bvals = new Boolean[]{true, false}; 202 for (boolean bval : bvals) { 203 try { 204 Map<String, String> credentials = generateClearTextPasswordFile(); 205 JMXServiceURL serverUrl = createServerSide(bval); 206 for (Map.Entry<String, String> entry : credentials.entrySet()) { 207 HashMap<String, Object> env = new HashMap<>(); 208 env.put("jmx.remote.credentials", 209 new String[]{entry.getKey(), entry.getValue()}); 210 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 211 cc.getMBeanServerConnection(); 212 } 213 } 214 Assert.assertEquals(isPasswordFileHashed(), bval); 215 } finally { 216 cs.stop(); 217 } 218 } 219 } 220 221 @Test 222 public void testReadOnlyPasswordFile() throws IOException { 223 Boolean[] bvals = new Boolean[]{true, false}; 224 for (boolean bval : bvals) { 225 try { 226 Map<String, String> credentials = generateClearTextPasswordFile(); 227 File file = new File(getPasswordFilePath()); 228 file.setReadOnly(); 229 JMXServiceURL serverUrl = createServerSide(true); 230 for (Map.Entry<String, String> entry : credentials.entrySet()) { 231 HashMap<String, Object> env = new HashMap<>(); 232 env.put("jmx.remote.credentials", 233 new String[]{entry.getKey(), entry.getValue()}); 234 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 235 cc.getMBeanServerConnection(); 236 } 237 } 238 Assert.assertEquals(isPasswordFileHashed(), false); 239 } finally { 240 cs.stop(); 241 } 242 } 243 } 244 245 @Test 246 public void testHashedPasswordFile() throws IOException { 247 Boolean[] bvals = new Boolean[]{true, false}; 248 for (boolean bval : bvals) { 249 try { 250 Map<String, String> credentials = generateHashedPasswordFile(); 251 JMXServiceURL serverUrl = createServerSide(bval); 252 Assert.assertEquals(isPasswordFileHashed(), true); 253 for (Map.Entry<String, String> entry : credentials.entrySet()) { 254 HashMap<String, Object> env = new HashMap<>(); 255 env.put("jmx.remote.credentials", 256 new String[]{entry.getKey(), entry.getValue()}); 257 try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) { 258 cc.getMBeanServerConnection(); 259 } 260 } 261 } finally { 262 cs.stop(); 263 } 264 } 265 } 266 267 @Test 268 public void testDefaultAgent() throws IOException, InterruptedException, Exception { 269 List<String> pbArgs = new ArrayList<>(); 270 int port = Utils.getFreePort(); 271 generateClearTextPasswordFile(); 272 273 // This will run only on a POSIX compliant system 274 if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { 275 return; 276 } 277 278 // Make sure only owner is able to read/write the file or else 279 // default agent will fail to start 280 File file = new File(getPasswordFilePath()); 281 Set<PosixFilePermission> perms = new HashSet<>(); 282 perms.add(PosixFilePermission.OWNER_READ); 283 perms.add(PosixFilePermission.OWNER_WRITE); 284 Files.setPosixFilePermissions(file.toPath(), perms); 285 286 pbArgs.add("-cp"); 287 pbArgs.add(System.getProperty("test.class.path")); 288 289 pbArgs.add("-Dcom.sun.management.jmxremote.port=" + port); 290 pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); 291 pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); 292 pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); 293 pbArgs.add(TestApp.class.getSimpleName()); 294 295 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( 296 pbArgs.toArray(new String[0])); 297 Process process = ProcessTools.startProcess( 298 TestApp.class.getSimpleName(), 299 pb); 300 301 if (process.waitFor() != 0) { 302 throw new RuntimeException("Test Failed : Error starting default agent"); 303 } 304 Assert.assertEquals(isPasswordFileHashed(), true); 305 } 306 307 @Test 308 public void testDefaultAgentNoHash() throws IOException, InterruptedException, Exception { 309 List<String> pbArgs = new ArrayList<>(); 310 int port = Utils.getFreePort(); 311 generateClearTextPasswordFile(); 312 313 // This will run only on a POSIX compliant system 314 if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { 315 return; 316 } 317 318 // Make sure only owner is able to read/write the file or else 319 // default agent will fail to start 320 File file = new File(getPasswordFilePath()); 321 Set<PosixFilePermission> perms = new HashSet<>(); 322 perms.add(PosixFilePermission.OWNER_READ); 323 perms.add(PosixFilePermission.OWNER_WRITE); 324 Files.setPosixFilePermissions(file.toPath(), perms); 325 326 pbArgs.add("-cp"); 327 pbArgs.add(System.getProperty("test.class.path")); 328 329 pbArgs.add("-Dcom.sun.management.jmxremote.port=" + port); 330 pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true"); 331 pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath()); 332 pbArgs.add("-Dcom.sun.management.jmxremote.password.hashpasswords=false"); 333 pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false"); 334 pbArgs.add(TestApp.class.getSimpleName()); 335 336 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder( 337 pbArgs.toArray(new String[0])); 338 Process process = ProcessTools.startProcess( 339 TestApp.class.getSimpleName(), 340 pb); 341 342 if (process.waitFor() != 0) { 343 throw new RuntimeException("Test Failed : Error starting default agent"); 344 } 345 Assert.assertEquals(isPasswordFileHashed(), false); 346 } 347 348 @AfterClass 349 public void cleanUp() { 350 File file = new File(getPasswordFilePath()); 351 if (file.exists()) { 352 file.delete(); 353 } 354 } 355 } 356 357 class TestApp { 358 359 public static void main(String[] args) throws MalformedURLException, IOException { 360 try { 361 JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" 362 + System.getProperty("com.sun.management.jmxremote.port") + "/jmxrmi"); 363 Map<String, Object> env = new HashMap<>(1); 364 // any dummy credentials will do. We just have to trigger password hashing 365 env.put("jmx.remote.credentials", new String[]{"a", "a"}); 366 try (JMXConnector cc = JMXConnectorFactory.connect(url, env)) { 367 cc.getMBeanServerConnection(); 368 } 369 } catch (SecurityException ex) { 370 // Catch authentication failure here 371 } 372 } 373 }