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("users.properties"));
053 * Map environment = new HashMap();
054 * environment.put(JMXConnectorServer.AUTHENTICATOR, authenticator);
055 * JMXServiceURL address = new JMXServiceURL("rmi", "localhost", 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/> <user>=<password><br/> where <password> 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] = "user";
085 * // You may send the password in clear text, but it's better to obfuscate it
086 * credentials[1] = PasswordAuthenticator.obfuscatePassword("password");
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 }