001    package org.trails.security.password;
002    
003    import java.security.MessageDigest;
004    import java.security.NoSuchAlgorithmException;
005    import java.util.Random;
006    
007    import org.apache.commons.logging.Log;
008    import org.apache.commons.logging.LogFactory;
009    
010    
011    /**
012     * A utility class for encoding a string with SHA-1 hash and comparing the equality of an encoded string
013     * Uses a randomly generated salt with a default length of 2-4 (public class members, changeable if needed)
014     * 
015     * Implementation adapted from the examples provided at:
016     * http://www.koders.com/java/fid9D416D88A1524FCC491B342D7B6A2E70694691D7.aspx
017     * http://www.bombaydigital.com/arenared/2003/10/10/1
018     * http://www.glenmccl.com/tip_010.htm
019     * 
020     */
021    public class DigestUtil {
022            private static final Log LOG = LogFactory.getLog(DigestUtil.class);
023            
024            public final static int SALT_MINLENGTH = 2;
025            public final static int SALT_MAXLENGTH = 4;
026            
027            // As on Xnix 
028            public static final char SALT_SEPARATOR = '$';
029            private static Random random = new Random();
030            private static MessageDigest messageDigest = null;
031            
032    
033    
034            /*
035             * @return Returns true if a hash for plainTextPassword equals hash for encodedPassword. 
036             * Returns false if either parameter is null or salt wasn't found from the encodedPassword 
037             * @param encodedPassword containing the salt and the hashed password
038             * @param plainTextPassword
039             */
040            public static boolean equalsEncoded(String encodedPassword, String plainTextPassword) {
041                    boolean result = false;
042                    if (encodedPassword != null && plainTextPassword != null){
043                            int index = encodedPassword.indexOf(SALT_SEPARATOR);
044                            if (index < 1) {
045                                    LOG.warn("Salt was not found from the encodedPassword parameter. Operation expects String encodedPassword, String plainTextPassword");
046                            }else{
047                                    String salt = encodedPassword.substring(0, index);
048                                    result = encodedPassword.substring(index +1).equals(new String(createHash(plainTextPassword, salt.getBytes())) );
049                            }
050                    }
051                    return result;
052            }
053            
054            /*
055             * @return Returns an encoded password of form <salt><SALT_SEPARATOR><passwordHash> for clearTextPassword passed in as a parameter
056             * Returns null if the parameter was null
057             */
058            public static String encode(String clearTextPassword) {
059                    String result = null;
060                    if (clearTextPassword != null){
061                            byte[] saltBytes = randomString(Math.min(SALT_MINLENGTH, SALT_MAXLENGTH), Math.max(SALT_MINLENGTH, SALT_MAXLENGTH) ).getBytes();
062                            result = new String(concatenate(saltBytes, createHash(clearTextPassword, saltBytes) ) );        
063                    }
064                    return result;
065            }
066            
067            private static byte[] createHash(String clearTextPassword, byte[] saltBytes) {
068                    // kaosko: We would save the cloning cost if md was a static class member, 
069                    // but it wouldn't be threadsafe. However login is already synchronized, so it should be safe to do this.
070                    if (messageDigest == null) {
071                            try {
072                                    messageDigest = MessageDigest.getInstance("SHA-1");
073                            } catch (NoSuchAlgorithmException e) {
074                                    LOG.fatal("Couldn't create SHA-1 MessageDigest, password encoding doesn't work. Are you using the right version of Java?");
075                                    return null;
076                            }
077                    }
078                    MessageDigest mdLocal = null;
079                    try {
080                            mdLocal = (MessageDigest)messageDigest.clone();
081                    } catch (CloneNotSupportedException e1) {
082                            LOG.fatal("Couldn't clone static MessageDigest, password encoding doesn't work. Are you using the right version of Java?");
083                            return null;
084                    }
085    
086                    mdLocal.update(clearTextPassword.getBytes());
087                    mdLocal.update(saltBytes);
088                    byte[] digest = mdLocal.digest();
089                    StringBuffer hexString = new StringBuffer();
090                    
091                    for (int i=0;i<digest.length;i++) {
092                      hexString.append(Integer.toHexString(0xFF & digest[i]));
093                    }
094                    return hexString.toString().getBytes();
095    
096            }
097    
098            /**
099             * Combine two byte arrays with a salt separator in between
100             */
101            private static byte[] concatenate(byte[] left, byte[] right) {
102                    byte[] result = new byte[left.length + 1 + right.length];
103                    System.arraycopy(left, 0, result, 0, left.length);
104                    result[left.length] = SALT_SEPARATOR;
105                    System.arraycopy(right, 0, result, left.length + 1, right.length);
106                    return result;
107            }       
108            
109            private static int rand(int low, int high) {
110                    int width = high - low + 1;
111                    int offset = random.nextInt() % width;
112                    if (offset < 0){
113                            offset = -offset;
114                    }
115                    return low + offset;
116            }
117    
118            private static String randomString(int low, int high) {
119                    int length = rand(low, high);
120                    byte byteArray[] = new byte[length];
121                    for (int i = 0; i < length; i++){
122                            byteArray[i] = (byte) rand('a', 'z');
123                    }
124                    return new String(byteArray);
125            }
126    }
127    
128    
129    
130