/*
 * 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/>
 * &lt;user&gt;=&lt;password&gt;<br/>
 * where &lt;password&gt; 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");
      }
   }
}
