/* * 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 } } }