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 }