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