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