-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
PasswordHash.java
285 lines (249 loc) · 8.53 KB
/
PasswordHash.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* For handling digested secrets (such as passwords).
* Use {@link #PasswordHash(String, byte[], byte[])} to package and manipulate
* secrets that have already been hashed, and {@link #PasswordHash(String)} for
* plaintext secrets. Compare a plaintext candidate to a hashed secret with
* {@link #matches(String)}.
*
* @author mwood
*/
public class PasswordHash {
private static final Logger log = LogManager.getLogger();
private static final ConfigurationService config
= DSpaceServicesFactory.getInstance().getConfigurationService();
private static final Charset UTF_8 = Charset.forName("UTF-8"); // Should always succeed: UTF-8 is required
private static final String DEFAULT_DIGEST_ALGORITHM = "SHA-512"; // XXX magic
private static final String ALGORITHM_PROPERTY = "authentication-password.digestAlgorithm";
private static final int SALT_BYTES = 128 / 8; // XXX magic we want 128 bits
private static final int HASH_ROUNDS = 1024; // XXX magic 1024 rounds
private static final int SEED_BYTES = 64; // XXX magic
private static final int RESEED_INTERVAL = 100; // XXX magic
/**
* A secure random number generator instance.
*/
private static SecureRandom rng = null;
/**
* How many times has the RNG been called without re-seeding?
*/
private static int rngUses;
private String algorithm;
private byte[] salt;
private byte[] hash;
/**
* Don't allow empty instances.
*/
private PasswordHash() {
}
/**
* Construct a hash structure from existing data, just for passing around.
*
* @param algorithm the digest algorithm used in producing {@code hash}.
* If empty, set to null. Other methods will treat this as unsalted MD5.
* If you want salted multi-round MD5, specify "MD5".
* @param salt the salt hashed with the secret, or null.
* @param hash the hashed secret.
*/
public PasswordHash(String algorithm, byte[] salt, byte[] hash) {
if ((null != algorithm) && algorithm.isEmpty()) {
this.algorithm = null;
} else {
this.algorithm = algorithm;
}
this.salt = salt;
this.hash = hash;
}
/**
* Convenience: like {@link #PasswordHash(String, byte[], byte[])} but with
* hexadecimal-encoded {@code String}s.
*
* @param algorithm the digest algorithm used in producing {@code hash}.
* If empty, set to null. Other methods will treat this as unsalted MD5.
* If you want salted multi-round MD5, specify "MD5".
* @param salt hexadecimal digits encoding the bytes of the salt, or null.
* @param hash hexadecimal digits encoding the bytes of the hash.
* @throws DecoderException if salt or hash is not proper hexadecimal.
*/
public PasswordHash(String algorithm, String salt, String hash)
throws DecoderException {
if ((null != algorithm) && algorithm.isEmpty()) {
this.algorithm = null;
} else {
this.algorithm = algorithm;
}
if (null == salt) {
this.salt = null;
} else {
this.salt = Hex.decodeHex(salt.toCharArray());
}
if (null == hash) {
this.hash = null;
} else {
this.hash = Hex.decodeHex(hash.toCharArray());
}
}
/**
* Construct a hash structure from a cleartext password using the configured
* digest algorithm.
*
* @param password the secret to be hashed.
*/
public PasswordHash(String password) {
// Generate some salt
salt = generateSalt();
// What digest algorithm to use?
algorithm = config.getPropertyAsType(ALGORITHM_PROPERTY, DEFAULT_DIGEST_ALGORITHM);
// Hash it!
try {
hash = digest(salt, algorithm, password);
} catch (NoSuchAlgorithmException e) {
log.error(e::getMessage);
hash = new byte[] {0};
}
}
/**
* Is this the string whose hash I hold?
*
* @param secret string to be hashed and compared to this hash.
* @return true if secret hashes to the value held by this instance.
*/
public boolean matches(String secret) {
byte[] candidate;
try {
candidate = digest(salt, algorithm, secret);
} catch (NoSuchAlgorithmException e) {
log.error(e::getMessage);
return false;
}
return Arrays.equals(candidate, hash);
}
/**
* Get the hash.
*
* @return the value of hash
*/
public byte[] getHash() {
return hash;
}
/**
* Get the hash, as a String.
*
* @return hash encoded as hexadecimal digits, or null if none.
*/
public String getHashString() {
if (null != hash) {
return new String(Hex.encodeHex(hash));
} else {
return null;
}
}
/**
* Get the salt.
*
* @return the value of salt
*/
public byte[] getSalt() {
return salt;
}
/**
* Get the salt, as a String.
*
* @return salt encoded as hexadecimal digits, or null if none.
*/
public String getSaltString() {
if (null != salt) {
return new String(Hex.encodeHex(salt));
} else {
return null;
}
}
/**
* Get the value of algorithm
*
* @return the value of algorithm
*/
public String getAlgorithm() {
return algorithm;
}
/**
* The digest algorithm used if none is configured.
*
* @return name of the default digest.
*/
static public String getDefaultAlgorithm() {
return DEFAULT_DIGEST_ALGORITHM;
}
/**
* Generate an array of random bytes.
*/
private synchronized byte[] generateSalt() {
// Initialize a random-number generator
if (null == rng) {
rng = new SecureRandom();
log.info("Initialized a random number stream using {} provided by {}",
rng::getAlgorithm, rng::getProvider);
rngUses = 0;
}
if (rngUses++ > RESEED_INTERVAL) { // re-seed the generator periodically to break up possible patterns
log.debug("Re-seeding the RNG");
rng.setSeed(rng.generateSeed(SEED_BYTES));
rngUses = 0;
}
salt = new byte[SALT_BYTES];
rng.nextBytes(salt);
return salt;
}
/**
* Generate a salted hash of a string using a given algorithm.
*
* @param salt random bytes to salt the hash.
* @param algorithm name of the digest algorithm to use. Assume unsalted MD5 if null.
* @param secret the string to be hashed. Null is treated as an empty string ("").
* @return hash bytes.
* @throws NoSuchAlgorithmException if algorithm is unknown.
*/
private byte[] digest(byte[] salt, String algorithm, String secret)
throws NoSuchAlgorithmException {
MessageDigest digester;
if (null == secret) {
secret = "";
}
// Special case: old unsalted one-trip MD5 hash.
if (null == algorithm) {
digester = MessageDigest.getInstance("MD5");
digester.update(secret.getBytes(UTF_8));
return digester.digest();
}
// Set up a digest
digester = MessageDigest.getInstance(algorithm);
// Grind up the salt with the password, yielding a hash
if (null != salt) {
digester.update(salt);
}
digester.update(secret.getBytes(UTF_8)); // Round 0
for (int round = 1; round < HASH_ROUNDS; round++) {
byte[] lastRound = digester.digest();
digester.reset();
digester.update(lastRound);
}
return digester.digest();
}
}