/*
* Copyright (C) The MX4J Contributors.
* All rights reserved.
*
* This software is distributed under the terms of the MX4J License version 1.0.
* See the terms of the MX4J License in the documentation provided with this software.
*/
package mx4j.tools.remote;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.management.remote.JMXAuthenticator;
import javax.management.remote.JMXPrincipal;
import javax.security.auth.Subject;
import mx4j.util.Base64Codec;
/**
* Implementation of the JMXAuthenticator interface to be used on server side
* to secure access to {@link javax.management.remote.JMXConnectorServer JMXConnectorServer}s. <br/>
* Usage:
* <pre>
* JMXAuthenticator authenticator = new PasswordAuthenticator(new File("users.properties"));
* Map environment = new HashMap();
* environment.put(JMXConnectorServer.AUTHENTICATOR, authenticator);
* JMXServiceURL address = new JMXServiceURL("rmi", "localhost", 0);
* MBeanServer server = ...;
* JMXConnectorServer cntorServer = JMXConnectorServerFactory.newJMXConnectorServer(address, environment, server);
* </pre>
* The format of the users.properties file is that of a standard properties file: <br/>
* <user>=<password><br/>
* where <password> can be stored in 2 ways:
* <ul>
* <li>Clear text: the password is written in clear text</li>
* <li>Obfuscated text: the password is obfuscated</li>
* </ul>
* The obfuscated form can be obtained running this class as a main class:
* <pre>
* java -cp mx4j-remote.jar mx4j.tools.remote.PasswordAuthenticator
* </pre>
* and following the instructions printed on the console. The output will be a string that should be
* copy/pasted as the password into the properties file.<br/>
* The obfuscated password is obtained by digesting the clear text password using a
* {@link java.security.MessageDigest} algorithm, and then by Base64-encoding the resulting bytes.<br/>
* <br/>
* On client side, you are allowed to connect to a server side secured with the PasswordAuthenticator
* only if you provide the correct credentials:
* <pre>
* String[] credentials = new String[2];
* // The user will travel as clear text
* credentials[0] = "user";
* // You may send the password in clear text, but it's better to obfuscate it
* credentials[1] = PasswordAuthenticator.obfuscatePassword("password");
* Map environment = new HashMap();
* environment.put(JMXConnector.CREDENTIALS, credentials);
* JMXServiceURL address = ...;
* JMXConnector cntor = JMXConnectorFactory.connect(address, environment);
* </pre>
* Note that {@link #obfuscatePassword(java.lang.String,java.lang.String) obfuscating} the passwords only works if the server side has been
* setup with the PasswordAuthenticator.
* However, the PasswordAuthenticator can be used with other JSR 160 implementations, such as Sun's reference
* implementation.
*
* @version $Revision: 1.3 $
*/
public class PasswordAuthenticator implements JMXAuthenticator
{
private static final String LEFT_DELIMITER = "OBF(";
private static final String RIGHT_DELIMITER = "):";
/**
* Runs this class as main class to obfuscate passwords.
* When no arguments are provided, it prints out the usage.
*
* @see #obfuscatePassword(java.lang.String,java.lang.String)
*/
public static void main(String[] args) throws Exception
{
if (args.length == 1)
{
if (!"-help".equals(args[0]))
{
printPassword("MD5", args[0]);
return;
}
}
else if (args.length == 3)
{
if ("-alg".equals(args[0]))
{
printPassword(args[1], args[2]);
return;
}
}
printUsage();
}
private static void printPassword(String algorithm, String input)
{
String password = obfuscatePassword(input, algorithm);
System.out.println(password);
}
private static void printUsage()
{
System.out.println();
System.out.println("Usage: java -cp <lib>/mx4j-tools.jar mx4j.tools.remote.PasswordAuthenticator <options> <password>");
System.out.println("Where <options> is one of the following:");
System.out.println(" -help Prints this message");
System.out.println(" -alg <digest algorithm> Specifies the digest algorithm (default is MD5)");
System.out.println();
}
/**
* Obfuscates the given password using MD5 as digest algorithm
*
* @see #obfuscatePassword(java.lang.String,java.lang.String)
*/
public static String obfuscatePassword(String password)
{
return obfuscatePassword(password, "MD5");
}
/**
* Obfuscates the given password using the given digest algorithm.<br/>
* Obfuscation consists of 2 steps: first the clear text password is {@link java.security.MessageDigest#digest digested}
* using the specified algorithm, then the resulting bytes are Base64-encoded.<br/>
* For example, the obfuscated version of the password "password" is "OBF(MD5):X03MO1qnZdYdgyfeuILPmQ=="
* or "OBF(SHA-1):W6ph5Mm5Pz8GgiULbPgzG37mj9g=". <br/>
* OBF stands for "obfuscated", in parenthesis the algorithm used to digest the password.
*/
public static String obfuscatePassword(String password, String algorithm)
{
try
{
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] digestedBytes = digest.digest(password.getBytes());
byte[] obfuscatedBytes = Base64Codec.encodeBase64(digestedBytes);
return LEFT_DELIMITER + algorithm + RIGHT_DELIMITER + new String(obfuscatedBytes);
}
catch (NoSuchAlgorithmException x)
{
throw new SecurityException("Could not find digest algorithm " + algorithm);
}
}
private Map passwords;
/**
* Creates a new PasswordAuthenticator that reads user/password pairs from the specified properties file.
* The file format is described in the javadoc of this class.
*
* @see #obfuscatePassword
*/
public PasswordAuthenticator(File passwordFile) throws IOException
{
this(new FileInputStream(passwordFile));
}
/**
* Creates a new PasswordAuthenticator that reads user/password pairs from the specified InputStream.
* The file format is described in the javadoc of this class.
*
* @see #obfuscatePassword
*/
public PasswordAuthenticator(InputStream is) throws IOException
{
passwords = readPasswords(is);
}
private Map readPasswords(InputStream is) throws IOException
{
Properties properties = new Properties();
try
{
properties.load(is);
}
finally
{
is.close();
}
return new HashMap(properties);
}
public Subject authenticate(Object credentials) throws SecurityException
{
if (!(credentials instanceof String[])) throw new SecurityException("Bad credentials");
String[] creds = (String[])credentials;
if (creds.length != 2) throw new SecurityException("Bad credentials");
String user = creds[0];
String password = creds[1];
if (password == null) throw new SecurityException("Bad password");
if (!passwords.containsKey(user)) throw new SecurityException("Unknown user " + user);
String storedPassword = (String)passwords.get(user);
if (!isPasswordCorrect(password, storedPassword)) throw new SecurityException("Bad password");
Set principals = new HashSet();
principals.add(new JMXPrincipal(user));
return new Subject(true, principals, Collections.EMPTY_SET, Collections.EMPTY_SET);
}
private boolean isPasswordCorrect(String password, String storedPassword)
{
if (password.startsWith(LEFT_DELIMITER))
{
if (storedPassword.startsWith(LEFT_DELIMITER))
{
return password.equals(storedPassword);
}
else
{
String algorithm = getAlgorithm(password);
String obfuscated = obfuscatePassword(storedPassword, algorithm);
return password.equals(obfuscated);
}
}
else
{
if (storedPassword.startsWith(LEFT_DELIMITER))
{
// Password was sent in clear, bad practice
String algorithm = getAlgorithm(storedPassword);
String obfuscated = obfuscatePassword(password, algorithm);
return obfuscated.equals(storedPassword);
}
else
{
return password.equals(storedPassword);
}
}
}
private String getAlgorithm(String obfuscatedPassword)
{
try
{
return obfuscatedPassword.substring(LEFT_DELIMITER.length(), obfuscatedPassword.indexOf(RIGHT_DELIMITER));
}
catch (IndexOutOfBoundsException x)
{
throw new SecurityException("Bad password");
}
}
}