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
286
287
288
289
290
291
292
|
/*
* Copyright 2017 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit.utils;
import java.util.Arrays;
/**
* This is the superclass for classes responsible for handling password hashing.
*
* It provides a factory-like interface to create an instance of a class that
* is responsible for the mechanics of a specific password hashing method.
* It also provides the common interface, leaving implementation specifics
* to subclasses of itself, which are the factory products.
*
* @author Florian Zschocke
* @since 1.9.0
*/
public abstract class PasswordHash {
/**
* The types of implemented password hashing schemes.
*/
public enum Type {
MD5,
CMD5,
PBKDF2;
static Type fromName(String name) {
if (name == null) return null;
for (Type type : Type.values()) {
if (type.name().equalsIgnoreCase(name)) return type;
}
// Compatibility with type id "PBKDF2WITHHMACSHA256", which is also handled by PBKDF2 type.
if (name.equalsIgnoreCase("PBKDF2WITHHMACSHA256")) return Type.PBKDF2;
// Recognise the name used for CMD5 in the settings file.
if (name.equalsIgnoreCase("combined-md5")) return Type.CMD5;
return null;
}
}
/**
* The hashing scheme type handled by an instance of a subclass
*/
final Type type;
/**
* Constructor for subclasses to initialize the final type field.
* @param type
* Type of hashing scheme implemented by this instance.
*/
PasswordHash(Type type) {
this.type = type;
}
/**
* When no hash type is specified, this determines the default that should be used.
*/
public static Type getDefaultType() {
return Type.PBKDF2;
}
/**
* Create an instance of a password hashing class for the given hash type.
*
* @param type
* Type of hash to be used.
* @return A class that can calculate the given hash type and verify a user password,
* or null if the given hash type is not a valid one.
*/
public static PasswordHash instanceOf(String type) {
Type hashType = Type.fromName(type);
if (hashType == null) return null;
switch (hashType) {
case MD5:
return new PasswordHashMD5();
case CMD5:
return new PasswordHashCombinedMD5();
case PBKDF2:
return new PasswordHashPbkdf2();
default:
return null;
}
}
/**
* Create an instance of a password hashing class of the correct type for a given
* hashed password from the user password table. The stored hashed password needs
* to be prefixed with the hash type identifier.
*
* @param hashedEntry
* Hashed password string from the user table.
* @return
* A class that can calculate the given hash type and verify a user password,
* or null if no instance can be created for the hashed user password.
*/
public static PasswordHash instanceFor(String hashedEntry) {
Type type = getEntryType(hashedEntry);
if (type != null) return instanceOf(type.name());
return null;
}
/**
* Test if a given string is a hashed password entry. This method simply checks if the
* given string is prefixed by a known hash type identifier.
*
* @param storedPassword
* A stored user password.
* @return True if the given string is detected to be hashed with a known hash type,
* false otherwise.
*/
public static boolean isHashedEntry(String storedPassword) {
return null != getEntryType(storedPassword);
}
/**
* Some hash methods are considered more secure than others. This method determines for a certain type
* of password hash if it is inferior than a given other type and stored passwords should be
* upgraded to the given hashing method.
*
* @param algorithm
* Password hashing type to be checked if it is superior than the one of this instance.
* @return True, if the given type in parameter {@code algorithm} is better and stored passwords should be upgraded to it,
* false, otehrwise.
*/
public boolean needsUpgradeTo(String algorithm) {
Type hashType = Type.fromName(algorithm);
if (hashType == null) return false;
if (this.type == hashType) return false;
// Right now we keep it really simple. With the existing types, only PBKDF2 is considered secure,
// everything else is inferior. This will need to be updated once more secure hashing algorithms
// are implemented, or the workload/parameters of the PBKDF2 are changed.
return hashType == Type.PBKDF2;
}
/**
* Convert the given password to a hashed password entry to be stored in the user table.
* The resulting string is prefixed by the hashing scheme type followed by a colon:
* TYPE:theactualhashinhex
*
* @param password
* Password to be hashed.
* @param username
* User name, only used for the Combined-MD5 (user+MD5) hashing type.
* @return
* Hashed password entry to be stored in the user table.
*/
abstract public String toHashedEntry(char[] password, String username);
/**
* Convert the given password to a hashed password entry to be stored in the user table.
* The resulting string is prefixed by the hashing scheme type followed by a colon:
* TYPE:theactualhashinhex
*
* @param password
* Password to be hashed.
* @param username
* User name, only used for the Combined-MD5 (user+MD5) hashing type.
* @return
* Hashed password entry to be stored in the user table.
*/
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(password.toCharArray(), username);
}
/**
* Test if a given password (and user name) match a hashed password.
* The instance of the password hash class has to be created with
* {code instanceFor}, so that it matches the type of the hashed password
* entry to test against.
*
*
* @param hashedEntry
* The hashed password entry from the user password table.
* @param password
* Clear text password to test against the hashed one.
* @param username
* User name, needed for the MD5+USER hash type.
* @return True, if the password (and username) match the hashed password,
* false, otherwise.
*/
public boolean matches(String hashedEntry, char[] password, String username) {
if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
if (password == null) return false;
String hashed = toHashedEntry(password, username);
Arrays.fill(password, Character.MIN_VALUE);
return hashed.equalsIgnoreCase(hashedEntry);
}
static Type getEntryType(String hashedEntry) {
if (hashedEntry == null) return null;
int indexOfSeparator = hashedEntry.indexOf(':');
if (indexOfSeparator <= 0) return null;
String typeId = hashedEntry.substring(0, indexOfSeparator);
return Type.fromName(typeId);
}
static String getEntryValue(String hashedEntry) {
if (hashedEntry == null) return null;
int indexOfSeparator = hashedEntry.indexOf(':');
return hashedEntry.substring(indexOfSeparator +1);
}
/************************************** Implementations *************************************************/
private static class PasswordHashMD5 extends PasswordHash
{
PasswordHashMD5() {
super(Type.MD5);
}
// To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
// for MD5 the (String,String) variant of the method is the one implementing the hashing.
@Override
public String toHashedEntry(char[] password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(new String(password), username);
}
@Override
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return type.name() + ":"
+ StringUtils.getMD5(password);
}
}
private static class PasswordHashCombinedMD5 extends PasswordHash
{
PasswordHashCombinedMD5() {
super(Type.CMD5);
}
// To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
// for Combined-MD5 the (String,String) variant of the method is the one implementing the hashing.
@Override
public String toHashedEntry(char[] password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(new String(password), username);
}
@Override
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
if (username == null) throw new IllegalArgumentException("The username argument may not be null when hashing a password with Combined-MD5.");
if (StringUtils.isEmpty(username)) throw new IllegalArgumentException("The username argument may not be empty when hashing a password with Combined-MD5.");
return type.name() + ":"
+ StringUtils.getMD5(username.toLowerCase() + password);
}
@Override
public boolean matches(String hashedEntry, char[] password, String username) {
if (username == null || StringUtils.isEmpty(username)) return false;
return super.matches(hashedEntry, password, username);
}
}
}
|