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