ldap.ActiveLoginImpl.java Source code

Java tutorial

Introduction

Here is the source code for ldap.ActiveLoginImpl.java

Source

/**
* Copyright (c) 2001-2012 "Redbasin Networks, INC" [http://redbasin.org]
*
* This file is part of Redbasin OpenDocShare community project.
*
* Redbasin OpenDocShare is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package ldap;

import ldap.ActiveLogin;
import util.LdapConstants;
import util.LdapUtil;
import util.Base64;
import util.JCrypt;

import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.io.UnsupportedEncodingException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * (c) Chris Betts: Groupmind Project
 */
public class ActiveLoginImpl implements ActiveLogin {

    protected final Log logger = LogFactory.getLog(getClass());
    public static boolean debug = false;
    public static boolean verbose = false;

    DirContext context; // this is the link to the LDAP directory
    private SecureRandom random;

    public ActiveLoginImpl() {
        random = new SecureRandom();
    }

    public ActiveLoginImpl(boolean codeDebug, boolean verboseDebugTrace) throws NamingException {
        debug = codeDebug;
        verbose = verboseDebugTrace;
        /*
                try
                {
        //context = setupJNDIConnection(Config.DIRECTORY_URL, Config.DIRECTORY_ADMIN, Config.DIRECTORY_PWD, verboseDebugTrace);
        context = setupJNDIConnection(LdapConstants.ldapDirectoryUrl,LdapConstants.ldapDirectoryAdmin, LdapConstants.ldapDirectoryPwd, verboseDebugTrace);
         }
        catch (AuthenticationException e) {
            //logger.info("There was an error establishing the 'admin' connection to the directory");
            //logger.info("The directory rejected the administration credentials:");
            //logger.info("   user: " + Config.DIRECTORY_ADMIN);
            //logger.info("   user: " + LdapConstants.ldapDirectoryAdmin);
            //logger.info("   pwd:  " + Config.DIRECTORY_PWD + "\n" + e.getMessage());
            //logger.info("   pwd:  " + LdapConstants.ldapDirectoryPwd + "\n" + e.getMessage());
            //e.printStackTrace();
            //System.exit(-1);
              throw e;
        }
        catch (Exception e) {
            //logger.info("There was an error establishing the 'admin' connection to the directory");
            //logger.info("  Examine the stack trace below for details, including the LDAP error message" + e.getMessage());
            //e.printStackTrace();
            //System.exit(-1);
        }
        */
    }

    public DirContext getContext() {
        if (context == null) {
            logger.info("context is null or not initialized");
        }
        return context;
    }

    /**
     * Returns whether this user is listed in the admin users role
     *
     * @param login
     * @return
     * @throws Exception
     */
    public boolean isAdmin(String login, DirContext context, String DN) throws Exception {
        NamingEnumeration result = null;

        String[] returnAttributes = new String[] { "uniqueMember" };

        /* specify search constraints to search subtree */
        SearchControls constraints = new SearchControls();

        constraints.setSearchScope(SearchControls.OBJECT_SCOPE);
        constraints.setCountLimit(0);
        constraints.setTimeLimit(0);

        constraints.setReturningAttributes(returnAttributes);
        /*
                Entry user = null;
                try {
        user = searcher.getUser(LdapConstants.ldapAttrLogin, login, context);
                } catch (NamingException e) {
                   throw new LdapException("getUser NamingException" + e.getMessage(), e);
                }
           String DN = null;
                if (user == null) {
                   logger.info("USER DOES NOT EXIST");
                   return false;
                } else {
              DN = user.getName().toString();
                   if (DN != null) {
          logger.info("DN = " + DN);
                   }
           }
        */

        //result = context.search(LdapConstants.ldapAdminRoleDn, "(uniqueMember="+getUserDN(login)+")", constraints);
        result = context.search(LdapConstants.ldapAdminRoleDn, "(uniqueMember=" + DN + ")", constraints);

        if (result.hasMore()) {
            if (debug) {
                SearchResult sResult = (SearchResult) result.next();
                logger.info("Read Admin Roles Object with members: " + sResult.getAttributes().toString());
            }
            return true;
        } else if (debug)
            logger.info("Failed to find admin object with member " + DN);

        return false;
    }

    /**
     * This updates the UserAccount.
     * It requires at a minimum a name;
     * and optionally any other attributes.
     *
     * Note that this will REPLACE any attributes passed, deleting any existing values
     * for the specified attribute (e.g. if the attribute is userPassword, the old userPassword will
     * be discarded, rather than there being two userPasswords in the entry).
     *
     * Modifying the naming attribute will probably result in an error (depending on the directory).
     *
     * @param account
     * @throws Exception
     */
    public void updateAccount(UserAccount account, DirContext context, String userDN) throws Exception {

        //if (account.get(Config.USER_NAMING_ATT) == null)
        if (account.get(LdapConstants.ldapDnAttrType) == null)
            throw new NamingException("UpdateAccount(), UserAccount has no naming Attribute");

        // should not be used
        //logger.info("Updating: \n" + account.getUserDN() + "\n" + account.toString());
        logger.info("Updating: \n" + userDN + "\n" + account.toString());

        // remove the naming attribute from the account before adding

        Attributes atts = copyAttributes(account); // create a local copy

        //atts.remove(Config.USER_NAMING_ATT);  // we can't modify the naming attribute this way, so don't try...
        //atts.remove(LdapConstants.ldapAttrUid);  // we can't modify the naming attribute this way, so don't try...
        atts.remove(LdapConstants.ldapDnAttrType); // we can't modify the naming attribute this way, so don't try...
        atts = hashPasswordAttribute(atts);
        // context.modifyAttributes(account.getUserDN(), DirContext.REPLACE_ATTRIBUTE, atts);
        context.modifyAttributes(userDN, DirContext.REPLACE_ATTRIBUTE, atts);
    }

    /**
     * This deletes a user account.  It requires a user naming attribute
     * (usually the uid).
     * <p/>
     * For consistancy, this should use UserAccount instead of a String login.
     *
     * @param login
     * @throws Exception
     */
    public void deleteAccount(String login, DirContext context, String userDN) throws Exception {
        //String dn = getUserDN(login);

        context.destroySubcontext(userDN);

    }

    /**
     * This adds a new user.  It requires at the minimum a name, it should also
     * usually have a surname and a password at a minimum.
     *
     * @param account
     * @throws Exception
     */
    public void addAccount(UserAccount account, DirContext context, String userBaseDN) throws Exception {
        // set some default values for the user entry if they haven't been manually added.

        //if (account.get(Config.USER_NAMING_ATT) == null)
        if (account.get(LdapConstants.ldapAttrCn) == null)
            throw new NamingException("addAccount(), UserAccount has no naming Attribute");

        if (account.get(LdapConstants.ldapObjectClass) == null) {
            //Attribute oc = new BasicAttribute("objectClass");
            Attribute oc = new BasicAttribute(LdapConstants.ldapObjectClass);

            if (LdapConstants.ldapObjectClassEmployeeEnable) {
                //oc.add("employee");
                oc.add(LdapConstants.ldapObjectClassEmployee);
            }

            //old redbasin stuff   
            /*   
                   if (LdapConstants.ldapAttrTopEnable) {
                           oc.add(LdapConstants.ldapAttrTop); 
                   }
                   if (LdapConstants.ldapAttrPersonEnable) {
                           oc.add(LdapConstants.ldapAttrPerson); 
                   }
                   if (LdapConstants.ldapAttrOrgPersonEnable) {
                           oc.add(LdapConstants.ldapAttrOrgPerson); 
                   }
                   if (LdapConstants.ldapAttrInetOrgPersonEnable) {
                           oc.add(LdapConstants.ldapAttrInetOrgPerson); 
                   }
            */
            account.put(oc);
        }

        /*  made changes  */
        /*
                if (account.get("cn") == null)
        account.put("cn", account.getUserID());
            
                if (account.get("sn") == null)
        account.put("sn", "xxx");  // put in default value for required attribute
        */
        if (account.get(LdapConstants.ldapAttrCn) == null)
            account.put(LdapConstants.ldapAttrCn, account.getUserID());

        if (account.get(LdapConstants.ldapAttrSn) == null)
            account.put(LdapConstants.ldapAttrSn, "xxx"); // put in default value for required attribute
        //logger.info("ADDING: \n" + account.getUserDN() + "\n" + account.toString());
        logger.info("ADDING: \n" + userBaseDN + "\n" + account.toString());

        /**
        * deal with the password adding later 
        */
        /*
           Attributes attributes = copyAttributes(account);
           UserAccount myaccount = hashPasswordAttribute(attributes);
        */
        // use this only when we add the user
        //context.createSubcontext(account.getUserDN(), account);
        context.createSubcontext(userBaseDN, account);
    }

    /**
     * This accepts a partial (?) UserAccount, that must have at least a
     * name, reads the full entry from the directory, and displays the result.
     *
     */
    //public void listAccount(UserAccount account) throws Exception
    //{
    //}

    /*          UTILITY METHODS
     *
     *          These do the actual work of talking to the directory
     */

    /**
       * Active Directory does not allow 'compare' ldap operations against passwords, so we either
       * use AD specific code (bad) or do a fake bind request (slow).  This uses the fake bind request;
       * for high performance enterprise apps a different approach is necessary (but enterprise apps would not
       * use AD, so it's a moot point).
       *
       * @param login
       * @param password
       * @return whether the user login and password are valid.
       * @throws NamingException
       */
    public boolean testBind(String login, String password, String userDN) throws NamingException {
        //String userDN = getUserDN(login);
        try {
            logger.info("Rebinding as user to test password");

            //setupJNDIConnection(Config.DIRECTORY_URL, userDN, password, verbose);
            setupJNDIConnection(LdapConstants.ldapDirectoryUrl, userDN, password, verbose);
            return true;
        } catch (AuthenticationException e) {
            //throw (e);
            logger.info(e.getMessage());
            return false;
        }
    }

    /**
     * This does a light copy of an attributes list (such as a UserAccount, if we need to modify it but
     * want to keep the original userAccount)
     * @param oldAtts
     * @return a light copy of the original attributes list.
     */
    public Attributes copyAttributes(Attributes oldAtts) {
        BasicAttributes atts = new BasicAttributes();
        NamingEnumeration attList = oldAtts.getAll();
        while (attList.hasMoreElements()) // shouldn't throw an exception, so use normal enumeration methods
        {
            Attribute att = (Attribute) attList.nextElement();
            atts.put(att);
        }
        return atts;
    }

    /**
     * open the directory connection.
     * @param url
     * @param tracing
     * @return
     * @throws NamingException
     */
    private DirContext setupJNDIConnection(String url, String userDN, String password, boolean tracing)
            throws NamingException {
        /*
         * First, set up a large number of environment variables to sensible default valuse
         */

        Hashtable env = new Hashtable();
        // sanity check
        if (url == null)
            throw new NamingException("URL not specified in openContext()!");

        // set the tracing level now, since it can't be set once the connection is open.
        if (tracing)
            env.put("com.sun.jndi.ldap.trace.ber", System.err); // echo trace to standard error output

        //env.put("java.naming.ldap.version", "3");               // always use ldap v3 - v2 too limited
        env.put(LdapConstants.ldapVersionStr, LdapConstants.ldapVersion); // always use ldap v3 - v2 too limited

        //env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");  // use default jndi provider
        env.put(Context.INITIAL_CONTEXT_FACTORY, LdapConstants.ldapContext); // use default jndi provider

        //env.put("java.naming.ldap.deleteRDN", "false");         // usually what we want
        env.put(LdapConstants.ldapDeleteRdn, LdapConstants.ldapDeleteRdnValue); // usually what we want

        //env.put(Context.REFERRAL, "ignore");                    //could be: follow, ignore, throw
        env.put(Context.REFERRAL, LdapConstants.ldapIgnore); //could be: follow, ignore, throw

        // env.put("java.naming.ldap.derefAliases", "finding");    // could be: finding, searching, etc.
        env.put(LdapConstants.ldapFindingAliases, LdapConstants.ldapFindingStr); // could be: finding, searching, etc.

        //env.put(Context.SECURITY_AUTHENTICATION, "simple");         // 'simple' = username + password
        env.put(Context.SECURITY_AUTHENTICATION, LdapConstants.ldapSecurityAuth); // 'simple' = username + password

        env.put(Context.SECURITY_PRINCIPAL, userDN); // add the full user dn

        env.put(Context.SECURITY_CREDENTIALS, password); // stupid jndi requires us to cast this to a string-

        env.put(Context.PROVIDER_URL, url); // the ldap url to connect to; e.g. "ldap://ca.com:389"

        /*
         *  Open the actual LDAP session using the above environment variables
         */

        DirContext newContext = new InitialDirContext(env);

        if (newContext == null)
            throw new NamingException(
                    "Internal Error with jndi connection: No Context was returned, however no exception was reported by jndi.");

        return newContext;

    }

    /*
    *    New Password Hashing Methods
    *
    *    Utility Methods for hashing passwords, and comparing hashed passwords with plaintext
    *    passwords.  Requires BASE64 encoding class (separate in util package)
    */

    public Attributes hashPasswordAttribute(Attributes account) throws NamingException {
        Attribute pwdAtt = account.get(LdapConstants.ldapAttrUserPassword);
        if (pwdAtt == null || pwdAtt.get() == null)
            throw new NamingException("user password attribute missing!");

        logger.info("entered hashPassword()" + pwdAtt);
        Object o = pwdAtt.get();
        logger.info("entered hashPassword()");
        byte[] hash = hashPassword(o);
        logger.info("completed hashPassword()");

        account.remove(LdapConstants.ldapAttrUserPassword);
        logger.info("adding the ldapAttrUserPassword, " + hash);
        account.put(LdapConstants.ldapAttrUserPassword, hash);
        byte[] pwd = (byte[]) account.get("userPassword").get();
        if (pwd != null) {
            logger.info("getting the ldapAttrUserPassword, " + pwd);
        } else {
            logger.info("hash pwd is null when tried to retrieve it");
        }
        return account;
    }

    private byte[] hashPassword(Object o) {
        // not usually the case; password should be a byte array.
        String pwd;
        if (o instanceof String) {
            logger.info("o instanceof String ");
            pwd = (String) o;
            logger.info("o instanceof String  completed, " + pwd);
        } else {
            logger.info("StringEncode called");
            pwd = stringEncode((byte[]) o);
            logger.info("StringEncode() completed " + pwd);
        }

        /**
        * we decide the encryption scheme
        */
        if (!pwd.startsWith("{")) {
            logger.info("hashPwd() defaultEncryptionscheme called" + pwd);
            byte[] hash = hashPwd(pwd, LdapConstants.ldapEncryptionScheme, null);
            return hash;
        } else {
            logger.info("plainDecode for pwd called");
            return plainDecode(pwd); // password already hashed.
        }
    }

    /**
     * Calculates the pwd hash to be stored in the userPassword field.
     *
     * @param s    The password in plaintext that should be hashed.
     * @param type The encryption scheme (The {CRYPT} scheme is currently unsupported): t == 2 means MD5  (salt needs to
     *             be null) t == 3 means SMD5 (needs a salt != null) t == 4 means SHA  (salt needs to be null) t == 5 means SSHA
     *             (needs a salt != null)
     * @param salt The salt that is to be used together with the schemes {SMD5} and {SSHA}. Should be between 8 and 16
     *             Bytes. salt should be null for any other scheme.
     * @return The base64-encoded hashed pwd with the following format: - {MD5}base64(MD5-hash) for MD5 hashes -
     *         {SHA}base64(SHA-hash) for SHA hashes - {SMD5}base64(MD5-hash+salt bytes) for SMD5 hashes -
     *         {SSHA}base64(SHA-hash+salt bytes) for SSHA hashes Or
     *         {CRYPT}salt + base64(hash)
     *          null if t is not one of 1, 2, 3, 4, 5.
     */

    public byte[] hashPwd(String s, String type, byte[] salt) {
        try {
            MessageDigest md;
            StringBuffer hexString = new StringBuffer();

            if (type.equals(LdapConstants.SSHA)) {
                if (salt == null)
                    salt = getRandomSalt();
                md = MessageDigest.getInstance(LdapConstants.SSHA);
                hexString.append("{" + LdapConstants.SSHA + "}");
            } else if (type.equals(LdapConstants.SHA)) {
                md = MessageDigest.getInstance(LdapConstants.SHA);
                hexString.append("{" + LdapConstants.SHA + "}");
            } else if (type.equals(LdapConstants.SMD5)) {
                if (salt == null)
                    salt = getRandomSalt();
                md = MessageDigest.getInstance(LdapConstants.SMD5);
                hexString.append("{" + LdapConstants.SMD5 + "}");
            } else if (type.equals(LdapConstants.MD5)) {
                md = MessageDigest.getInstance(LdapConstants.MD5);
                //hexString.append("{" + MD5 + "}");
                hexString.append("{" + LdapConstants.MD5 + "}");
            } else if (type.equals(LdapConstants.crypt)) {
                return hashCrypt(s, salt);
            } else {
                logger.warn(
                        "skipping hashing for password with type = " + type + " and has salt " + (salt == null));
                return (null);
            }

            md.reset();
            //md.update(s.getBytes("UTF-8"));
            md.update(s.getBytes(LdapConstants.UTF8));

            if (salt != null) {
                // The way the salted hashes work is the following:
                // h=HASH(pwd+salt)
                //
                // To be able to restore the salt it needs to be appended
                // to the resulting hash.
                // {SMD5|SSHA}base64(h+salt)

                // So, lets append the salt to the pwd-buffer in md.
                md.update(salt);

                // Calculate the hash-value of s+salt
                byte[] buff = md.digest();

                // And append the salt to the hashed pwd.
                byte[] new_buf = new byte[buff.length + salt.length];
                for (int x = 0; x < buff.length; x++)
                    new_buf[x] = buff[x];

                for (int x = buff.length; x < new_buf.length; x++)
                    new_buf[x] = salt[x - buff.length];

                // New_buf now contains the 16(MD5), resp. 20(SHA1) hash
                // bytes and the salt.
                hexString.append(Base64.binaryToString(new_buf));
            } else {
                byte[] buff = md.digest();
                hexString.append(Base64.binaryToString(buff));
            }

            //return hexString.toString().getBytes("UTF-8");
            return hexString.toString().getBytes(LdapConstants.UTF8);
        } catch (UnsupportedEncodingException e) {
            logger.warn("Unexpected error encoding password ", e);
            e.printStackTrace();
            return new byte[0];
        } catch (java.security.NoSuchAlgorithmException e) {
            logger.warn("Unexpected error encoding password ", e);
            e.printStackTrace();
            return new byte[0];
        }
    }

    /**
     * Generate four bytes of reasonably random seed for salted algorithms where no seed
     * has been supplied.
     * @return four random bytes.
     */
    private byte[] getRandomSalt() {
        return random.generateSeed(4);
    }

    public byte[] hashCrypt(String pwd, byte[] salt) throws UnsupportedEncodingException // not really!
    {
        // StringBuffer hexString = new StringBuffer("{CRYPT}");
        StringBuffer hexString = new StringBuffer(LdapConstants.cryptWithBraces);
        hexString.append(JCrypt.crypt(salt, pwd));
        //return hexString.toString().getBytes("UTF-8");
        return hexString.toString().getBytes(LdapConstants.UTF8);
    }

    /**
     * Verifies a given password against the password stored in the userPassword-attribute.
     * <p/>
     * The userPassword-value should follow the following format: - {MD5}base64(MD5-hash) - {SHA}base64(SHA-hash) -
     * {SMD5}base64(MD5-hash+salt bytes) - {SSHA}base64(SHA-hash+salt bytes) - plaintext password
     * <p/>
     * If the userPassword value does not start with one of the prefixes {MD5}, {SMD5}, {SHA} or {SSHA} it will be
     * handled as a plaintext pwd.
     * <p/>
     * The Unix {CRYPT}-Scheme is currently not supported.
     *
     * @param originalPassword The original pwd stored in the userPassword value.
     * @param newPassword      The password in plaintext that should be verified against the hashed pwd stored in the userPassword
     *                         field.
     * @return True - if the given plaintext pwd matches with the hashed pwd in the userPassword field, otherwise false.
     *         Returns also false for the {CRYPT}-scheme as it is currently unsupported.
     */
    protected boolean passwordVerify(String originalPassword, String newPassword) {

        logger.info("passwordVerify() entered ");
        if (originalPassword.toUpperCase().startsWith("{CRYPT}")) {
            logger.info("crypt = ");
            // newPassword is the given pwd in cleartext, so
            // we create the MD5 hash for the given pwd
            // and store it again in newPassword.

            String salt = originalPassword.substring(7, 9);

            newPassword = new String(hashPwd(newPassword, LdapConstants.crypt, salt.getBytes()));

            originalPassword = "{CRYPT}" + originalPassword.substring(7); // force prefix to uppsercase
        } else if (originalPassword.startsWith("{MD5}")) {
            logger.info("original password starts with MD5");
            // newPassword is the given pwd in cleartext, so
            // we create the MD5 hash for the given pwd
            // and store it again in newPassword.
            newPassword = new String(hashPwd(newPassword, LdapConstants.MD5, null));
        } else if (originalPassword.startsWith("{SMD5}")) {
            logger.info("original password starts with SMD5");
            // SMD5 means "Salted MD5". The "salt" is a
            // String of an arbitrary length appended to
            // the given MD5 hash in originalPassword.

            // To get the salt bytes first we need to base64-decode
            // the pwd hash and store it in tmp.
            byte[] tmp = Base64.stringToBinary(originalPassword.substring(6));

            // The length of an MD5-hash is always 16 Bytes. To get
            // the size of the salt we need to subtract 16 Bytes from
            // the total length of the base64-decoded String.
            if (tmp != null) {
                /*
                int len = tmp.length - 16;
                if (len > 0)
                {
                // Get the salt Bytes. As the MD5-Hash takes 16 Bytes,
                // the remaining Bytes will be the salt.
                byte[] salt = new byte[len];
                for (int x = 0; x < len; x++)
                    salt[x] = tmp[x + 16];
                    
                // newPassword is the given pwd in cleartext, so
                // we create the hash for the given pwd
                // and store it again in newPassword.
                newPassword = new String(hashPwd(newPassword, SMD5_CONST, salt));
                }
                */
                newPassword = new String(hashPwd(newPassword, LdapConstants.SMD5, getSalt(16, tmp)));

            }
        } else if (originalPassword.startsWith("{SHA}")) {
            logger.info("original password starts with SHA");
            // newPassword is the given pwd in cleartext, so
            // we create the hash for the given pwd
            // and store it again in newPassword.
            newPassword = new String(hashPwd(newPassword, LdapConstants.SHA, null));
            logger.info("newpassowrd = " + newPassword);
        } else if (originalPassword.startsWith("{SSHA}")) {
            logger.info("original password starts with SSHA");
            // SSHA means "Salted SHA-1". The "salt" is a
            // String of an arbitrary length appended to
            // the given SHA-1 hash in originalPassword.

            // To get the salt bytes first we need to base64-decode
            // the pwd hash and store it in tmp.
            byte[] tmp = Base64.stringToBinary(originalPassword.substring(6));

            // The length of an SHA-1-hash is always 20 Bytes. To get
            // the size of the salt we need to subtract 20 Bytes from
            // the total length of the base64-decoded String.
            if (tmp != null) {
                /*
                int len = tmp.length - 20;
                if (len > 0)
                {
                // Get the salt Bytes. As the SHA-1-Hash takes 20 Bytes,
                // the remaining Bytes will be the salt.
                byte[] salt = new byte[len];
                for (int x = 0; x < len; x++)
                    salt[x] = tmp[x + 20];
                    
                // newPassword is the given pwd in cleartext, so
                // we create the hash for the given pwd
                // and store it again in newPassword.
                newPassword = new String(hashPwd(newPassword, SSHA_CONST, salt));
                }
                */
                newPassword = new String(hashPwd(newPassword, LdapConstants.SSHA, getSalt(20, tmp)));
            }
        } else {
            // newPassword is already cleartext, no need for
            // hashing it.
            logger.info("newPassword without plainDecode" + newPassword);
            newPassword = new String(plainDecode(newPassword));
            logger.info("newPassword with plainDecode" + newPassword);
        }

        // Compare the two hashed pwd-Strings and return true if
        // they match, otherwise false. If the Unix {CRYPT}-scheme
        // is used we return false as well.
        if (newPassword.equals(originalPassword))
            return (true);

        logger.info("newPassword = " + newPassword + " originalPassword = " + originalPassword);
        return (false);
    }

    /**
     * This method extracts the 'salt' from the END of the password string.  This works because
     * the base password is known to be of set length 'len'
     * @param len the length of the 'base' part of the password (i.e., the non-salt part)
     * @param pwdBytes
     * @return the salt part of a password bytes string
     */
    private final byte[] getSalt(int len, byte[] pwdBytes) {
        if (pwdBytes != null && len > 0) {
            int saltLength = pwdBytes.length - len;
            if (saltLength > 0) {
                // Get the salt Bytes. As the base pwd takes 'len' bytes
                // the remaining Bytes will be the salt.
                byte[] salt = new byte[saltLength];
                for (int x = 0; x < saltLength; x++)
                    salt[x] = pwdBytes[x + len];

                return salt;
            }
        }
        return null;
    }

    protected byte[] plainDecode(String s) {
        try {
            return s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) // this really, really should never ever happen...
        {
            logger.info("UNEXPECTED INTERNAL ERROR - JAVA UNABLE TO DECODE UTF-8 ?!, error= " + e.getMessage());
            e.printStackTrace();
            return new byte[0];
        }
    }

    /**
     * Converts between a byte array and text
     */
    public String stringEncode(byte[] b) {
        if (b == null || b.length == 0) {
            logger.info("stringEncode, b is null");
            return new String();
        } else {
            try {
                logger.info("stringEncode(UTF-8)");
                return new String(b, "UTF-8");
            } catch (UnsupportedEncodingException e) // this should never, ever happen
            {
                logger.info("Unexpected error decoding password - JDK unable to handle UTF-8?? " + e.getMessage());
                e.printStackTrace();
                return new String(b); // Fall back on the platform default... not sure this is the best thing to do - CB
            }
        }
    }

    // short test method
    public void testMain() throws Exception {
        //Here are some usage examples to demonstrate how the password hashing API works.

        //1,2: This shows how we automatically hash a password attribute in a new user.

        String defaultEncryptionScheme = LdapConstants.SHA; // set the default encryption scheme - usually we only have one scheme per directory

        Attributes test = new BasicAttributes();
        test.put("cn", "test user");
        test.put("userPassword", "secret");
        test.put("objectClass", "person");

        //ActiveLoginImpl login = new ActiveLoginImpl();

        //logger.info("1: invoking hashPasswordAttribute: "); 
        Attributes mytest = hashPasswordAttribute(test);
        String pwd = stringEncode((byte[]) mytest.get("userPassword").get());
        // logger.info("1: show automatically hashed password attribute: " + pwd);

        //logger.info("2: verify SHA hashed password against plaintext = " + passwordVerify("{SHA}5en6G6MezRroT3XKqkdPOmY/BfQ=", "secret"));
        //logger.info("2: verify SHA hashed password against plaintext = " + passwordVerify(pwd, "secret"));

        //3,4 This shows using a salted hash; we automatically generate the salt
        defaultEncryptionScheme = LdapConstants.SSHA; // set the default encryption scheme to a salted hash
        test.put("userPassword", "secret"); // reset password
        // logger.info("3) 1: invoking hashPasswordAttribute: "); 
        test = hashPasswordAttribute(test); // hash password generating random salt

        String saltedPwd = stringEncode((byte[]) test.get("userPassword").get());

        // these are commented out.
        //logger.info("3: show salted hashed password attribute:        " + saltedPwd);

        //logger.info("4: verify SSHA salted hash password against plaintext = " + passwordVerify(saltedPwd, "secret"));

        //5,6 This shows using an old-style 'crypt' hash directly

        //logger.info("5: show creation of crypt password 'secret' with salt 'KD' = " + stringEncode(hashPwd("secret", LdapConstants.crypt ,plainDecode("KD"))));

        // logger.info("6: And verify crypt hashed password = " +   passwordVerify("{crypt}KDdVi0RbEzCac", "secret"));
    }

}