1 importjava.security.SecureRandom;2 importjavax.crypto.spec.PBEKeySpec;3 importjavax.crypto.SecretKeyFactory;4 importjava.security.NoSuchAlgorithmException;5 importjava.security.spec.InvalidKeySpecException;6 importjavax.xml.bind.DatatypeConverter;7
8 public classPasswordStorage9 {10
11 @SuppressWarnings("serial")12 static public class InvalidHashException extendsException {13 publicInvalidHashException(String message) {14 super(message);15 }16 publicInvalidHashException(String message, Throwable source) {17 super(message, source);18 }19 }20
21 @SuppressWarnings("serial")22 static public class CannotPerformOperationException extendsException {23 publicCannotPerformOperationException(String message) {24 super(message);25 }26 publicCannotPerformOperationException(String message, Throwable source) {27 super(message, source);28 }29 }30
31 public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";32
33 //These constants may be changed without breaking existing hashes.
34 public static final int SALT_BYTE_SIZE = 24;35 public static final int HASH_BYTE_SIZE = 18;36 public static final int PBKDF2_ITERATIONS = 64000;37
38 //These constants define the encoding and may not be changed.
39 public static final int HASH_SECTIONS = 5;40 public static final int HASH_ALGORITHM_INDEX = 0;41 public static final int ITERATION_INDEX = 1;42 public static final int HASH_SIZE_INDEX = 2;43 public static final int SALT_INDEX = 3;44 public static final int PBKDF2_INDEX = 4;45
46 public staticString createHash(String password)47 throwsCannotPerformOperationException48 {49 returncreateHash(password.toCharArray());50 }51
52 public static String createHash(char[] password)53 throwsCannotPerformOperationException54 {55 //Generate a random salt
56 SecureRandom random = newSecureRandom();57 byte[] salt = new byte[SALT_BYTE_SIZE];58 random.nextBytes(salt);59
60 //Hash the password
61 byte[] hash =pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);62 int hashSize =hash.length;63
64 //format: algorithm:iterations:hashSize:salt:hash
65 String parts = "sha1:" +
66 PBKDF2_ITERATIONS +
67 ":" + hashSize +
68 ":" +
69 toBase64(salt) +
70 ":" +
71 toBase64(hash);72 returnparts;73 }74
75 public static booleanverifyPassword(String password, String correctHash)76 throwsCannotPerformOperationException, InvalidHashException77 {78 returnverifyPassword(password.toCharArray(), correctHash);79 }80
81 public static boolean verifyPassword(char[] password, String correctHash)82 throwsCannotPerformOperationException, InvalidHashException83 {84 //Decode the hash into its parameters
85 String[] params = correctHash.split(":");86 if (params.length !=HASH_SECTIONS) {87 throw newInvalidHashException(88 "Fields are missing from the password hash."
89 );90 }91
92 //Currently, Java only supports SHA1.
93 if (!params[HASH_ALGORITHM_INDEX].equals("sha1")) {94 throw newCannotPerformOperationException(95 "Unsupported hash type."
96 );97 }98
99 int iterations = 0;100 try{101 iterations =Integer.parseInt(params[ITERATION_INDEX]);102 } catch(NumberFormatException ex) {103 throw newInvalidHashException(104 "Could not parse the iteration count as an integer.",105 ex106 );107 }108
109 if (iterations < 1) {110 throw newInvalidHashException(111 "Invalid number of iterations. Must be >= 1."
112 );113 }114
115
116 byte[] salt = null;117 try{118 salt =fromBase64(params[SALT_INDEX]);119 } catch(IllegalArgumentException ex) {120 throw newInvalidHashException(121 "Base64 decoding of salt failed.",122 ex123 );124 }125
126 byte[] hash = null;127 try{128 hash =fromBase64(params[PBKDF2_INDEX]);129 } catch(IllegalArgumentException ex) {130 throw newInvalidHashException(131 "Base64 decoding of pbkdf2 output failed.",132 ex133 );134 }135
136
137 int storedHashSize = 0;138 try{139 storedHashSize =Integer.parseInt(params[HASH_SIZE_INDEX]);140 } catch(NumberFormatException ex) {141 throw newInvalidHashException(142 "Could not parse the hash size as an integer.",143 ex144 );145 }146
147 if (storedHashSize !=hash.length) {148 throw newInvalidHashException(149 "Hash length doesn't match stored hash length."
150 );151 }152
153 //Compute the hash of the provided password, using the same salt,154 //iteration count, and hash length
155 byte[] testHash =pbkdf2(password, salt, iterations, hash.length);156 //Compare the hashes in constant time. The password is correct if157 //both hashes match.
158 returnslowEquals(hash, testHash);159 }160
161 private static boolean slowEquals(byte[] a, byte[] b)162 {163 int diff = a.length ^b.length;164 for(int i = 0; i < a.length && i < b.length; i++)165 diff |= a[i] ^b[i];166 return diff == 0;167 }168
169 private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, intbytes)170 throwsCannotPerformOperationException171 {172 try{173 PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);174 SecretKeyFactory skf =SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);175 returnskf.generateSecret(spec).getEncoded();176 } catch(NoSuchAlgorithmException ex) {177 throw newCannotPerformOperationException(178 "Hash algorithm not supported.",179 ex180 );181 } catch(InvalidKeySpecException ex) {182 throw newCannotPerformOperationException(183 "Invalid key spec.",184 ex185 );186 }187 }188
189 private static byte[] fromBase64(String hex)190 throwsIllegalArgumentException191 {192 returnDatatypeConverter.parseBase64Binary(hex);193 }194
195 private static String toBase64(byte[] array)196 {197 returnDatatypeConverter.printBase64Binary(array);198 }199
200 }