3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.authentication;
22 import java.security.NoSuchAlgorithmException;
23 import java.security.SecureRandom;
24 import java.security.spec.InvalidKeySpecException;
25 import java.util.Base64;
26 import java.util.EnumMap;
27 import javax.annotation.Nullable;
28 import javax.crypto.SecretKeyFactory;
29 import javax.crypto.spec.PBEKeySpec;
30 import org.apache.commons.codec.digest.DigestUtils;
31 import org.apache.commons.lang.RandomStringUtils;
32 import org.mindrot.jbcrypt.BCrypt;
33 import org.sonar.api.config.Configuration;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.user.UserDto;
37 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
38 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
39 import org.sonar.server.authentication.event.AuthenticationException;
41 import static com.google.common.base.Preconditions.checkArgument;
42 import static java.lang.String.format;
43 import static java.util.Objects.requireNonNull;
46 * Validates the password of a "local" user (password is stored in
49 public class CredentialsLocalAuthentication {
50 public static final String ERROR_NULL_HASH_METHOD = "null hash method";
51 public static final String ERROR_NULL_PASSWORD_IN_DB = "null password in DB";
52 public static final String ERROR_NULL_SALT = "null salt";
53 public static final String ERROR_WRONG_PASSWORD = "wrong password";
54 public static final String ERROR_PASSWORD_CANNOT_BE_NULL = "Password cannot be null";
55 public static final String ERROR_UNKNOWN_HASH_METHOD = "Unknown hash method [%s]";
56 private static final SecureRandom SECURE_RANDOM = new SecureRandom();
57 private static final String PBKDF2_ITERATIONS_PROP = "sonar.internal.pbkdf2.iterations";
58 private static final HashMethod DEFAULT = HashMethod.PBKDF2;
59 private static final int DUMMY_PASSWORD_AND_SALT_SIZE = 100;
61 private final DbClient dbClient;
62 private final EnumMap<HashMethod, HashFunction> hashFunctions = new EnumMap<>(HashMethod.class);
64 public enum HashMethod {
68 public CredentialsLocalAuthentication(DbClient dbClient, Configuration configuration) {
69 this.dbClient = dbClient;
70 hashFunctions.put(HashMethod.BCRYPT, new BcryptFunction());
71 hashFunctions.put(HashMethod.SHA1, new Sha1Function());
72 hashFunctions.put(HashMethod.PBKDF2, new PBKDF2Function(configuration.getInt(PBKDF2_ITERATIONS_PROP).orElse(null)));
75 void generateHashToAvoidEnumerationAttack(){
76 String randomSalt = RandomStringUtils.randomAlphabetic(DUMMY_PASSWORD_AND_SALT_SIZE);
77 String randomPassword = RandomStringUtils.randomAlphabetic(DUMMY_PASSWORD_AND_SALT_SIZE);
78 hashFunctions.get(HashMethod.PBKDF2).encryptPassword(randomSalt, randomPassword);
82 * This method authenticate a user with his password against the value stored in user.
83 * If authentication failed an AuthenticationException will be thrown containing the failure message.
84 * If the password must be updated because an old algorithm is used, the UserDto is updated but the session
87 public void authenticate(DbSession session, UserDto user, @Nullable String password, Method method) {
88 if (user.getHashMethod() == null) {
89 throw AuthenticationException.newBuilder()
90 .setSource(Source.local(method))
91 .setLogin(user.getLogin())
92 .setMessage(ERROR_NULL_HASH_METHOD)
96 HashMethod hashMethod;
98 hashMethod = HashMethod.valueOf(user.getHashMethod());
99 } catch (IllegalArgumentException ex) {
100 generateHashToAvoidEnumerationAttack();
101 throw AuthenticationException.newBuilder()
102 .setSource(Source.local(method))
103 .setLogin(user.getLogin())
104 .setMessage(format(ERROR_UNKNOWN_HASH_METHOD, user.getHashMethod()))
108 HashFunction hashFunction = hashFunctions.get(hashMethod);
110 AuthenticationResult result = hashFunction.checkCredentials(user, password);
111 if (!result.isSuccessful()) {
112 throw AuthenticationException.newBuilder()
113 .setSource(Source.local(method))
114 .setLogin(user.getLogin())
115 .setMessage(result.getFailureMessage())
119 // Upgrade the password if it's an old hashMethod
120 if (hashMethod != DEFAULT || result.needsUpdate) {
121 hashFunctions.get(DEFAULT).storeHashPassword(user, password);
122 dbClient.userDao().update(session, user);
127 * Method used to store the password as a hash in database.
128 * The crypted_password, salt and hash_method are set
130 public void storeHashPassword(UserDto user, String password) {
131 hashFunctions.get(DEFAULT).storeHashPassword(user, password);
134 private static class AuthenticationResult {
135 private final boolean successful;
136 private final String failureMessage;
137 private final boolean needsUpdate;
139 private AuthenticationResult(boolean successful, String failureMessage) {
140 this(successful, failureMessage, false);
143 private AuthenticationResult(boolean successful, String failureMessage, boolean needsUpdate) {
144 checkArgument((successful && failureMessage.isEmpty()) || (!successful && !failureMessage.isEmpty()), "Incorrect parameters");
145 this.successful = successful;
146 this.failureMessage = failureMessage;
147 this.needsUpdate = needsUpdate;
150 public boolean isSuccessful() {
154 public String getFailureMessage() {
155 return failureMessage;
158 public boolean isNeedsUpdate() {
163 public interface HashFunction {
164 AuthenticationResult checkCredentials(UserDto user, String password);
166 void storeHashPassword(UserDto user, String password);
168 default String encryptPassword(String salt, String password) {
169 throw new IllegalStateException("This method is not supported for this hash function");
174 * Implementation of deprecated SHA1 hash function
176 private static final class Sha1Function implements HashFunction {
178 public AuthenticationResult checkCredentials(UserDto user, String password) {
179 if (user.getCryptedPassword() == null) {
180 return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
182 if (user.getSalt() == null) {
183 return new AuthenticationResult(false, ERROR_NULL_SALT);
185 if (!user.getCryptedPassword().equals(hash(user.getSalt(), password))) {
186 return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
188 return new AuthenticationResult(true, "");
192 public void storeHashPassword(UserDto user, String password) {
193 requireNonNull(password, ERROR_PASSWORD_CANNOT_BE_NULL);
194 byte[] saltRandom = new byte[20];
195 SECURE_RANDOM.nextBytes(saltRandom);
196 String salt = DigestUtils.sha1Hex(saltRandom);
198 user.setHashMethod(HashMethod.SHA1.name())
199 .setCryptedPassword(hash(salt, password))
203 private static String hash(String salt, String password) {
204 return DigestUtils.sha1Hex("--" + salt + "--" + password + "--");
208 static final class PBKDF2Function implements HashFunction {
209 private static final char ITERATIONS_HASH_SEPARATOR = '$';
210 private static final int DEFAULT_ITERATIONS = 100_000;
211 private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
212 private static final int KEY_LEN = 512;
213 private static final String ERROR_INVALID_HASH_STORED = "invalid hash stored";
214 private final int generationIterations;
216 public PBKDF2Function(@Nullable Integer generationIterations) {
217 this.generationIterations = generationIterations != null ? generationIterations : DEFAULT_ITERATIONS;
221 public AuthenticationResult checkCredentials(UserDto user, String password) {
222 if (user.getCryptedPassword() == null) {
223 return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
225 if (user.getSalt() == null) {
226 return new AuthenticationResult(false, ERROR_NULL_SALT);
229 int pos = user.getCryptedPassword().indexOf(ITERATIONS_HASH_SEPARATOR);
231 return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
235 iterations = Integer.parseInt(user.getCryptedPassword().substring(0, pos));
236 } catch (NumberFormatException e) {
237 return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
239 String hash = user.getCryptedPassword().substring(pos + 1);
240 byte[] salt = Base64.getDecoder().decode(user.getSalt());
242 if (!hash.equals(hash(salt, password, iterations))) {
243 return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
245 boolean needsUpdate = iterations != generationIterations;
246 return new AuthenticationResult(true, "", needsUpdate);
250 public void storeHashPassword(UserDto user, String password) {
251 byte[] salt = new byte[20];
252 SECURE_RANDOM.nextBytes(salt);
253 String hashStr = hash(salt, password, generationIterations);
254 String saltStr = Base64.getEncoder().encodeToString(salt);
255 user.setHashMethod(HashMethod.PBKDF2.name())
256 .setCryptedPassword(composeEncryptedPassword(hashStr))
261 public String encryptPassword(String saltStr, String password) {
262 byte[] salt = Base64.getDecoder().decode(saltStr);
263 return composeEncryptedPassword(hash(salt, password, generationIterations));
266 private String composeEncryptedPassword(String hashStr) {
267 return format("%d%c%s", generationIterations, ITERATIONS_HASH_SEPARATOR, hashStr);
270 private static String hash(byte[] salt, String password, int iterations) {
272 SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGORITHM);
273 PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KEY_LEN);
274 byte[] hash = skf.generateSecret(spec).getEncoded();
275 return Base64.getEncoder().encodeToString(hash);
276 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
277 throw new RuntimeException(e);
283 * Implementation of deprecated bcrypt hash function
285 private static final class BcryptFunction implements HashFunction {
287 public AuthenticationResult checkCredentials(UserDto user, String password) {
288 if (user.getCryptedPassword() == null) {
289 return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
291 // This behavior is overridden in most of integration tests for performance reasons, any changes to BCrypt calls should be propagated to
293 if (!BCrypt.checkpw(password, user.getCryptedPassword())) {
294 return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
296 return new AuthenticationResult(true, "");
300 public void storeHashPassword(UserDto user, String password) {
301 requireNonNull(password, ERROR_PASSWORD_CANNOT_BE_NULL);
302 user.setHashMethod(HashMethod.BCRYPT.name())
303 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))