001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.servicemix.jbi.jmx;
018    
019    /*
020     * Copyright (C) The MX4J Contributors. All rights reserved.
021     * 
022     * This software is distributed under the terms of the MX4J License version 1.0.
023     * See the terms of the MX4J License in the documentation provided with this
024     * software.
025     */
026    
027    import java.io.File;
028    import java.io.FileInputStream;
029    import java.io.IOException;
030    import java.io.InputStream;
031    import java.security.MessageDigest;
032    import java.security.NoSuchAlgorithmException;
033    import java.util.Collections;
034    import java.util.HashMap;
035    import java.util.HashSet;
036    import java.util.Map;
037    import java.util.Properties;
038    import java.util.Set;
039    import javax.management.remote.JMXAuthenticator;
040    import javax.management.remote.JMXPrincipal;
041    import javax.security.auth.Subject;
042    
043    import mx4j.util.Base64Codec;
044    
045    /**
046     * Implementation of the JMXAuthenticator interface to be used on server side to
047     * secure access to
048     * {@link javax.management.remote.JMXConnectorServer JMXConnectorServer}s.
049     * <br/> Usage:
050     * 
051     * <pre>
052     * JMXAuthenticator authenticator = new PasswordAuthenticator(new File(&quot;users.properties&quot;));
053     * Map environment = new HashMap();
054     * environment.put(JMXConnectorServer.AUTHENTICATOR, authenticator);
055     * JMXServiceURL address = new JMXServiceURL(&quot;rmi&quot;, &quot;localhost&quot;, 0);
056     * MBeanServer server = ...;
057     * JMXConnectorServer cntorServer = JMXConnectorServerFactory.newJMXConnectorServer(address, environment, server);
058     * </pre>
059     * 
060     * The format of the users.properties file is that of a standard properties
061     * file: <br/> &lt;user&gt;=&lt;password&gt;<br/> where &lt;password&gt; can be
062     * stored in 2 ways:
063     * <ul>
064     * <li>Clear text: the password is written in clear text</li>
065     * <li>Obfuscated text: the password is obfuscated</li>
066     * </ul>
067     * The obfuscated form can be obtained running this class as a main class:
068     * 
069     * <pre>
070     * java -cp mx4j-remote.jar mx4j.tools.remote.PasswordAuthenticator
071     * </pre>
072     * 
073     * and following the instructions printed on the console. The output will be a
074     * string that should be copy/pasted as the password into the properties file.<br/>
075     * The obfuscated password is obtained by digesting the clear text password
076     * using a {@link java.security.MessageDigest} algorithm, and then by
077     * Base64-encoding the resulting bytes.<br/> <br/> On client side, you are
078     * allowed to connect to a server side secured with the PasswordAuthenticator
079     * only if you provide the correct credentials:
080     * 
081     * <pre>
082     * String[] credentials = new String[2];
083     * // The user will travel as clear text
084     * credentials[0] = &quot;user&quot;;
085     * // You may send the password in clear text, but it's better to obfuscate it
086     * credentials[1] = PasswordAuthenticator.obfuscatePassword(&quot;password&quot;);
087     * Map environment = new HashMap();
088     * environment.put(JMXConnector.CREDENTIALS, credentials);
089     * JMXServiceURL address = ...;
090     * JMXConnector cntor = JMXConnectorFactory.connect(address, environment);
091     * </pre>
092     * 
093     * Note that
094     * {@link #obfuscatePassword(java.lang.String,java.lang.String) obfuscating} the
095     * passwords only works if the server side has been setup with the
096     * PasswordAuthenticator. However, the PasswordAuthenticator can be used with
097     * other JSR 160 implementations, such as Sun's reference implementation.
098     * 
099     * @version $Revision: 1.3 $
100     */
101    public class PasswordAuthenticator implements JMXAuthenticator {
102    
103        private static final String LEFT_DELIMITER = "OBF(";
104        private static final String RIGHT_DELIMITER = "):";
105    
106        private Map passwords;
107    
108        /**
109         * Creates a new PasswordAuthenticator that reads user/password pairs from
110         * the specified properties file. The file format is described in the
111         * javadoc of this class.
112         * 
113         * @see #obfuscatePassword
114         */
115        public PasswordAuthenticator(File passwordFile) throws IOException {
116            this(new FileInputStream(passwordFile));
117        }
118    
119        /**
120         * Creates a new PasswordAuthenticator that reads user/password pairs from
121         * the specified InputStream. The file format is described in the javadoc of
122         * this class.
123         * 
124         * @see #obfuscatePassword
125         */
126        public PasswordAuthenticator(InputStream is) throws IOException {
127            passwords = readPasswords(is);
128        }
129    
130        /**
131         * Runs this class as main class to obfuscate passwords. When no arguments
132         * are provided, it prints out the usage.
133         * 
134         * @see #obfuscatePassword(java.lang.String,java.lang.String)
135         */
136        public static void main(String[] args) throws Exception {
137            if (args.length == 1 && !"-help".equals(args[0])) {
138                printPassword("MD5", args[0]);
139                return;
140            } else if (args.length == 3 && "-alg".equals(args[0])) {
141                printPassword(args[1], args[2]);
142                return;
143            }
144            printUsage();
145        }
146    
147        private static void printPassword(String algorithm, String input) {
148            String password = obfuscatePassword(input, algorithm);
149            System.out.println(password);
150        }
151    
152        private static void printUsage() {
153            System.out.println();
154            System.out.println("Usage: java -cp <lib>/mx4j-tools.jar mx4j.tools.remote.PasswordAuthenticator <options> <password>");
155            System.out.println("Where <options> is one of the following:");
156            System.out.println("   -help                     Prints this message");
157            System.out.println("   -alg <digest algorithm>   Specifies the digest algorithm (default is MD5)");
158            System.out.println();
159        }
160    
161        /**
162         * Obfuscates the given password using MD5 as digest algorithm
163         * 
164         * @see #obfuscatePassword(java.lang.String,java.lang.String)
165         */
166        public static String obfuscatePassword(String password) {
167            return obfuscatePassword(password, "MD5");
168        }
169    
170        /**
171         * Obfuscates the given password using the given digest algorithm.<br/>
172         * Obfuscation consists of 2 steps: first the clear text password is
173         * {@link java.security.MessageDigest#digest digested} using the specified
174         * algorithm, then the resulting bytes are Base64-encoded.<br/> For
175         * example, the obfuscated version of the password "password" is
176         * "OBF(MD5):X03MO1qnZdYdgyfeuILPmQ==" or
177         * "OBF(SHA-1):W6ph5Mm5Pz8GgiULbPgzG37mj9g=". <br/> OBF stands for
178         * "obfuscated", in parenthesis the algorithm used to digest the password.
179         */
180        public static String obfuscatePassword(String password, String algorithm) {
181            try {
182                MessageDigest digest = MessageDigest.getInstance(algorithm);
183                byte[] digestedBytes = digest.digest(password.getBytes());
184                byte[] obfuscatedBytes = Base64Codec.encodeBase64(digestedBytes);
185                return LEFT_DELIMITER + algorithm + RIGHT_DELIMITER + new String(obfuscatedBytes);
186            } catch (NoSuchAlgorithmException x) {
187                throw new SecurityException("Could not find digest algorithm " + algorithm);
188            }
189        }
190    
191        private Map readPasswords(InputStream is) throws IOException {
192            Properties properties = new Properties();
193            try {
194                properties.load(is);
195            } finally {
196                is.close();
197            }
198            return new HashMap(properties);
199        }
200    
201        public Subject authenticate(Object credentials) throws SecurityException {
202            if (!(credentials instanceof String[])) {
203                throw new SecurityException("Bad credentials");
204            }
205            String[] creds = (String[]) credentials;
206            if (creds.length != 2) {
207                throw new SecurityException("Bad credentials");
208            }
209    
210            String user = creds[0];
211            String password = creds[1];
212    
213            if (password == null) {
214                throw new SecurityException("Bad password");
215            }
216    
217            if (!passwords.containsKey(user)) {
218                throw new SecurityException("Unknown user " + user);
219            }
220    
221            String storedPassword = (String) passwords.get(user);
222            if (!isPasswordCorrect(password, storedPassword)) {
223                throw new SecurityException("Bad password");
224            }
225    
226            Set principals = new HashSet();
227            principals.add(new JMXPrincipal(user));
228            return new Subject(true, principals, Collections.EMPTY_SET, Collections.EMPTY_SET);
229        }
230    
231        private boolean isPasswordCorrect(String password, String storedPassword) {
232            if (password.startsWith(LEFT_DELIMITER)) {
233                if (storedPassword.startsWith(LEFT_DELIMITER)) {
234                    return password.equals(storedPassword);
235                } else {
236                    String algorithm = getAlgorithm(password);
237                    String obfuscated = obfuscatePassword(storedPassword, algorithm);
238                    return password.equals(obfuscated);
239                }
240            } else {
241                if (storedPassword.startsWith(LEFT_DELIMITER)) {
242                    // Password was sent in clear, bad practice
243                    String algorithm = getAlgorithm(storedPassword);
244                    String obfuscated = obfuscatePassword(password, algorithm);
245                    return obfuscated.equals(storedPassword);
246                } else {
247                    return password.equals(storedPassword);
248                }
249            }
250        }
251    
252        private String getAlgorithm(String obfuscatedPassword) {
253            try {
254                return obfuscatedPassword.substring(LEFT_DELIMITER.length(), obfuscatedPassword.indexOf(RIGHT_DELIMITER));
255            } catch (IndexOutOfBoundsException x) {
256                throw new SecurityException("Bad password");
257            }
258        }
259    }