PasswordEncode.java

package com.tradecloud.authentication;

import org.apache.log4j.Logger;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class PasswordEncode implements PasswordEncoder {

    public static final String MD5 = "MD5";
    public static final String SHA_1 = "SHA-1";
    public static final String UTF_16 = "UTF-16";
    private static final Logger log = Logger.getLogger(PasswordEncode.class);
    public static final String BASE64_ENCODING = "BASE64";
    public static final String BASE16_ENCODING = "HEX";

    /**
     * Default encode method.
     */
    public static String encode(String plaintext) {
        return PasswordEncode.createPasswordHash(SHA_1, PasswordEncode.BASE16_ENCODING, UTF_16, "", plaintext);
    }

    /**
     * Calculate a password hash using a MessageDigest.
     *
     * @param hashAlgorithm the MessageDigest algorithm name
     * @param hashEncoding  either base64 or hex to specify the type of encoding the
     *                      MessageDigest as a string.
     * @param hashCharset   the charset used to create the byte[] passed to the
     *                      MessageDigestfrom the password String. If null the platform
     *                      default is used.
     * @param username      ignored in default version
     * @param password      the password string to be hashed
     * @return the hashed string if successful, null if there is a digest
     * exception
     */
    public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset, String username, String password) {
        byte[] passBytes;
        String passwordHash = null;

        // convert password to byte data
        try {
            if (hashCharset == null)
                passBytes = password.getBytes();
            else
                passBytes = password.getBytes(hashCharset);
        } catch (UnsupportedEncodingException uee) {
            log.error("charset " + hashCharset + " not found. Using platform default.", uee);
            passBytes = password.getBytes();
        }

        // calculate the hash and apply the encoding.
        try {
            MessageDigest md = MessageDigest.getInstance(hashAlgorithm);

            md.update(passBytes);

            byte[] hash = md.digest();
            if (hashEncoding.equalsIgnoreCase(BASE64_ENCODING)) {
                passwordHash = PasswordEncode.encodeBase64(hash);
            } else if (hashEncoding.equalsIgnoreCase(BASE16_ENCODING)) {
                passwordHash = PasswordEncode.encodeBase16(hash);
            } else {
                log.error("Unsupported hash encoding format " + hashEncoding);
            }
        } catch (Exception e) {
            log.error("Password hash calculation failed ", e);
        }
        return passwordHash;
    }

    /**
     * BASE64 encoder implementation. Provides encoding methods, using the
     * BASE64 encoding rules, as defined in the MIME specification, <a
     * href="http://ietf.org/rfc/rfc1521.txt">rfc1521</a>.
     */
    public static String encodeBase64(byte[] bytes) {
        String base64 = null;
        try {
            base64 = new String(Base64.encode(bytes));
        } catch (Exception e) {
        }
        return base64;
    }

    /**
     * Hex encoding of hashes, as used by Catalina. Each byte is converted to
     * the corresponding two hex characters.
     */
    public static String encodeBase16(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            // top 4 bits
            char c = (char) ((b >> 4) & 0xf);
            if (c > 9)
                c = (char) ((c - 10) + 'a');
            else
                c = (char) (c + '0');
            sb.append(c);
            // bottom 4 bits
            c = (char) (b & 0xf);
            if (c > 9)
                c = (char) ((c - 10) + 'a');
            else
                c = (char) (c + '0');
            sb.append(c);
        }
        return sb.toString();
    }

    public String encodePassword(String rawPass, Object salt) {
        return PasswordEncode.createPasswordHash(SHA_1, PasswordEncode.BASE16_ENCODING, UTF_16, "", rawPass);
    }

    /**
     * This is the method invoked by the Spring security framework that compares
     * the users password with the stored one.
     */

    public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
        String newPassword = PasswordEncode.createPasswordHash(SHA_1, PasswordEncode.BASE16_ENCODING, UTF_16, "", rawPass);

        boolean result = encPass.equals(newPassword);

        // retry with raw password to enable comparison if hashed passwords get
        // used as input
//        if (!result) {
//            result = encPass.equals(rawPass);
//        }
        // Print error so that we know it was a password mis-match.
        if (!result) {
            log.error("\n\nPassword Comparison Failure: [" + encPass + "] != [" + newPassword + "].\n\n");
        } else {
            // log.debug("Passwords compared successfully. User login successful.");
        }

        return result;
    }

    /**
     * Adding this method for legacy deploys where we need to has passwords.
     * <p>
     * Tradecloud was originally deployed with MD5 hashing but we switched this
     * to fit with Redbox
     * <p>
     * To get an equivalent hash to what we would have got using Spring's
     * default MD5 hashing algorithm use this method
     */
    public static String encodePasswordWithMD5(String plaintext) throws NoSuchAlgorithmException {
        MessageDigest messageDigest = MessageDigest.getInstance(MD5);
        messageDigest.update(plaintext.getBytes(), 0, plaintext.length());
        String hashedPass = new BigInteger(1, messageDigest.digest()).toString(16);
        if (hashedPass.length() < 32) {
            hashedPass = "0" + hashedPass;
        }
        return hashedPass;
    }

    @Override
    public String encode(CharSequence charSequence) {
        try {
            return encodePasswordWithMD5((String) charSequence);
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalStateException("Password encoding " + e.getMessage());
        }
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return isPasswordValid(s, (String) charSequence, null);
    }
}