summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java
blob: 1bce12290bebe227ceecbe286470ead45d83297e (plain)
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
package com.gitblit.utils;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

/**
 * The class PasswordHashPbkdf2 implements password hashing and validation
 * with PBKDF2
 *
 * It uses the concept proposed by OWASP - Hashing Java:
 * https://www.owasp.org/index.php/Hashing_Java
 */
class PasswordHashPbkdf2 extends PasswordHash
{

	private static final Logger LOGGER = LoggerFactory.getLogger(PasswordHashPbkdf2.class);

	/**
	 * The PBKDF has some parameters that define security and workload.
	 * The Configuration class keeps these parameters.
	 */
	private static class Configuration
	{
		private final String algorithm;
		private final int iterations;
		private final int keyLen;
		private final int saltLen;

		private Configuration(String algorithm, int iterations, int keyLen, int saltLen) {
			this.algorithm = algorithm;
			this.iterations = iterations;
			this.keyLen = keyLen;
			this.saltLen = saltLen;
		}
	}


	private static final SecureRandom RANDOM = new SecureRandom();
	/**
	 * A list of Configurations is created to list the configurations supported by
	 * this implementation. The configuration id is stored in the hashed entry,
	 * identifying the Configuration in this array.
	 * When adding a new variant with different values for these parameters, add
	 * it to this array.
	 * The code uses the last configuration in the array as the most secure, to be used
	 * when creating new hashes when no configuration is specified.
	 */
	private static final Configuration[] configurations = {
			// Configuration 0, also default when none is specified in the stored hashed entry.
			new Configuration("PBKDF2WithHmacSHA256", 10000, 256, 32)
	};


	PasswordHashPbkdf2() {
		super(Type.PBKDF2);
	}


	/*
	 * We return a hashed entry, where the hash part (salt+hash) itself is prefixed
	 * again by the configuration id of the configuration that was used for the PBKDF,
	 * enclosed in '$':
	 * PBKDF2:$0$thesaltThehash
	 */
	@Override
	public String toHashedEntry(char[] password, String username) {
		if (password == null) {
			LOGGER.warn("The password argument may not be null when hashing a password.");
			throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
		}

		int configId = getLatestConfigurationId();
		Configuration config = configurations[configId];

		byte[] salt = new byte[config.saltLen];
		RANDOM.nextBytes(salt);
		byte[] hash = hash(password, salt, config);

		return type.name() + ":"
				+ "$" + configId + "$"
				+ StringUtils.toHex(salt)
				+ StringUtils.toHex(hash);
	}

	@Override
	public boolean matches(String hashedEntry, char[] password, String username) {
		if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
		if (password == null) return false;

		String hashedPart = getEntryValue(hashedEntry);
		int configId = getConfigIdFromStoredPassword(hashedPart);

		return isPasswordCorrect(password, hashedPart, configurations[configId]);
	}








	/**
	 * Return the id of the most updated configuration of parameters for the PBKDF.
	 * New password hashes should be generated with this one.
	 *
	 * @return An index into the configurations array for the latest configuration.
	 */
	private int getLatestConfigurationId() {
		return configurations.length-1;
	}


	/**
	 * Get the configuration id from the stored hashed password, that was used when the
	 * hash was created. The configuration id is the index into the configuration array,
	 * and is stored in the format $Id$ after the type identifier: TYPE:$Id$....
	 * If there is no identifier in the stored entry, id 0 is used, to keep backward
	 * compatibility.
	 * If an id is found that is not in the range of the declared configurations,
	 * 0 is returned. This may fail password validation. As of now there is only one
	 * configuration and even if there were more, chances are slim that anything else
	 * was used. So we try at least the first one instead of failing with an exception
	 * as the probability of success is high enough to save the user from a bad experience
	 * and to risk some hassle for the admin finding out in the logs why a login failed,
	 * when it does.
	 *
	 * @param hashPart
	 * 			The hash part of the stored entry, i.e. the part after the TYPE:
	 * @return The configuration id, or
	 *         0 if none was found.
	 */
	private static int getConfigIdFromStoredPassword(String hashPart) {
		String[] parts = hashPart.split("\\$", 3);
		// If there are not two parts, there is no '$'-enclosed part and we have no configuration information stored.
		// Return default 0.
		if (parts.length <= 2) return 0;

		// The first string wil be empty. Even if it isn't we ignore it because it doesn't contain our information.
		try {
			int configId = Integer.parseInt(parts[1]);
			if (configId < 0 || configId >= configurations.length) {
				LOGGER.warn("A user table password entry contains a configuration id that is not valid: {}." +
						"Assuming PBKDF configuration 0. This may fail to validate the password.", configId);
				return 0;
			}
			return configId;
		}
		catch (NumberFormatException e) {
			LOGGER.warn("A user table password entry contains a configuration id that is not a parsable number ({}${}$...)." +
					"Assuming PBKDF configuration 0. This may fail to validate the password.", parts[0], parts[1], e);
			return 0;
		}
	}





	/**
	 * Hash.
	 *
	 * @param password
	 *          the password
	 * @param salt
	 *          the salt
	 * @param config
	 * 			Parameter configuration to use for the PBKDF
	 * @return Hashed result
	 */
	private static byte[] hash(char[] password, byte[] salt, Configuration config) {
		PBEKeySpec spec = new PBEKeySpec(password, salt, config.iterations, config.keyLen);
		Arrays.fill(password, Character.MIN_VALUE);
		try {
			SecretKeyFactory skf = SecretKeyFactory.getInstance(config.algorithm);
			return skf.generateSecret(spec).getEncoded();
		} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
			LOGGER.warn("Error while hashing password.", e);
			throw new IllegalStateException("Error while hashing password", e);
		} finally {
			spec.clearPassword();
		}
	}

	/**
	 * Checks if is password correct.
	 *
	 * @param passwordToCheck
	 *            the password to check
	 * @param salt
	 *            the salt
	 * @param expectedHash
	 *            the expected hash
	 * @return true, if is password correct
	 */
	private static boolean isPasswordCorrect(char[] passwordToCheck, byte[] salt, byte[] expectedHash, Configuration config) {
		byte[] hashToCheck = hash(passwordToCheck, salt, config);
		Arrays.fill(passwordToCheck, Character.MIN_VALUE);
		if (hashToCheck.length != expectedHash.length) {
			return false;
		}
		for (int i = 0; i < hashToCheck.length; i++) {
			if (hashToCheck[i] != expectedHash[i]) {
				return false;
			}
		}
		return true;
	}


	/**
	 * Gets the salt from stored password.
	 *
	 * @param storedPassword
	 *            the stored password
	 * @return the salt from stored password
	 */
	private static byte[] getSaltFromStoredPassword(String storedPassword, Configuration config) {
		byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
		return Arrays.copyOfRange(pw, 0, config.saltLen);
	}

	/**
	 * Gets the hash from stored password.
	 *
	 * @param storedPassword
	 *            the stored password
	 * @return the hash from stored password
	 */
	private static byte[] getHashFromStoredPassword(String storedPassword, Configuration config) {
		byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
		return Arrays.copyOfRange(pw, config.saltLen, pw.length);
	}

	/**
	 * Strips the configuration id prefix ($Id$) from a stored
	 * password and returns the decoded hash
	 *
	 * @param storedPassword
	 *            the stored password
	 * @return the stored hash with stripped prefix
	 */
	private static byte[] getStoredHashWithStrippedPrefix(String storedPassword) {
		String[] strings = storedPassword.split("\\$", 3);
		String saltAndHash = strings[strings.length -1];
		try {
			return Hex.decodeHex(saltAndHash.toCharArray());
		} catch (DecoderException e) {
			LOGGER.warn("Failed to decode stored password entry from hex to string.", e);
			throw new IllegalStateException("Error while reading stored credentials", e);
		}
	}

	/**
	 * Checks if password is correct.
	 *
	 * @param password
	 *            the password to validate
	 * @param storedPassword
	 *            the stored password, i.e. the password entry value, without the leading TYPE:
	 * @return true, if password is correct, false otherwise
	 */
	private static boolean isPasswordCorrect(char[] password, String storedPassword, Configuration config) {
		byte[] storedSalt = getSaltFromStoredPassword(storedPassword, config);
		byte[] storedHash = getHashFromStoredPassword(storedPassword, config);
		return isPasswordCorrect(password, storedSalt, storedHash, config);
	}
}