]> source.dussan.org Git - sonarqube.git/blob
758d0a6296a081597b8e0b0033dbb43a9808e2f8
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.server.authentication;
21
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.lang.RandomStringUtils;
31 import org.mindrot.jbcrypt.BCrypt;
32 import org.sonar.api.config.Configuration;
33 import org.sonar.db.DbClient;
34 import org.sonar.db.DbSession;
35 import org.sonar.db.user.UserDto;
36 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
37 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
38 import org.sonar.server.authentication.event.AuthenticationException;
39
40 import static com.google.common.base.Preconditions.checkArgument;
41 import static java.lang.String.format;
42
43 /**
44  * Validates the password of a "local" user (password is stored in
45  * database).
46  */
47 public class CredentialsLocalAuthentication {
48   public static final String ERROR_NULL_HASH_METHOD = "null hash method";
49   public static final String ERROR_NULL_PASSWORD_IN_DB = "null password in DB";
50   public static final String ERROR_NULL_SALT = "null salt";
51   public static final String ERROR_WRONG_PASSWORD = "wrong password";
52   public static final String ERROR_UNKNOWN_HASH_METHOD = "Unknown hash method [%s]";
53   private static final SecureRandom SECURE_RANDOM = new SecureRandom();
54   private static final String PBKDF2_ITERATIONS_PROP = "sonar.internal.pbkdf2.iterations";
55   private static final HashMethod DEFAULT = HashMethod.PBKDF2;
56   private static final int DUMMY_PASSWORD_AND_SALT_SIZE = 100;
57
58   private final DbClient dbClient;
59   private final EnumMap<HashMethod, HashFunction> hashFunctions = new EnumMap<>(HashMethod.class);
60
61   public enum HashMethod {
62     BCRYPT, PBKDF2
63   }
64
65   public CredentialsLocalAuthentication(DbClient dbClient, Configuration configuration) {
66     this.dbClient = dbClient;
67     hashFunctions.put(HashMethod.BCRYPT, new BcryptFunction());
68     hashFunctions.put(HashMethod.PBKDF2, new PBKDF2Function(configuration.getInt(PBKDF2_ITERATIONS_PROP).orElse(null)));
69   }
70
71   void generateHashToAvoidEnumerationAttack(){
72     String randomSalt = RandomStringUtils.randomAlphabetic(DUMMY_PASSWORD_AND_SALT_SIZE);
73     String randomPassword = RandomStringUtils.randomAlphabetic(DUMMY_PASSWORD_AND_SALT_SIZE);
74     hashFunctions.get(HashMethod.PBKDF2).encryptPassword(randomSalt, randomPassword);
75   }
76
77   /**
78    * This method authenticate a user with his password against the value stored in user.
79    * If authentication failed an AuthenticationException will be thrown containing the failure message.
80    * If the password must be updated because an old algorithm is used, the UserDto is updated but the session
81    * is not committed
82    */
83   public void authenticate(DbSession session, UserDto user, String password, Method method) {
84     HashMethod hashMethod = getHashMethod(user, method);
85     HashFunction hashFunction = hashFunctions.get(hashMethod);
86     AuthenticationResult result = authenticate(user, password, method, hashFunction);
87
88     // Upgrade the password if it's an old hashMethod
89     if (hashMethod != DEFAULT || result.needsUpdate) {
90       hashFunctions.get(DEFAULT).storeHashPassword(user, password);
91       dbClient.userDao().update(session, user);
92     }
93   }
94
95   private HashMethod getHashMethod(UserDto user, Method method) {
96     if (user.getHashMethod() == null) {
97       throw AuthenticationException.newBuilder()
98         .setSource(Source.local(method))
99         .setLogin(user.getLogin())
100         .setMessage(ERROR_NULL_HASH_METHOD)
101         .build();
102     }
103     try {
104       return HashMethod.valueOf(user.getHashMethod());
105     } catch (IllegalArgumentException ex) {
106       generateHashToAvoidEnumerationAttack();
107       throw AuthenticationException.newBuilder()
108         .setSource(Source.local(method))
109         .setLogin(user.getLogin())
110         .setMessage(format(ERROR_UNKNOWN_HASH_METHOD, user.getHashMethod()))
111         .build();
112     }
113   }
114
115   private static AuthenticationResult authenticate(UserDto user, String password, Method method, HashFunction hashFunction) {
116     AuthenticationResult result = hashFunction.checkCredentials(user, password);
117     if (!result.isSuccessful()) {
118       throw AuthenticationException.newBuilder()
119         .setSource(Source.local(method))
120         .setLogin(user.getLogin())
121         .setMessage(result.getFailureMessage())
122         .build();
123     }
124     return result;
125   }
126
127   /**
128    * Method used to store the password as a hash in database.
129    * The crypted_password, salt and hash_method are set
130    */
131   public void storeHashPassword(UserDto user, String password) {
132     hashFunctions.get(DEFAULT).storeHashPassword(user, password);
133   }
134
135   private static class AuthenticationResult {
136     private final boolean successful;
137     private final String failureMessage;
138     private final boolean needsUpdate;
139
140     private AuthenticationResult(boolean successful, String failureMessage) {
141       this(successful, failureMessage, false);
142     }
143
144     private AuthenticationResult(boolean successful, String failureMessage, boolean needsUpdate) {
145       checkArgument((successful && failureMessage.isEmpty()) || (!successful && !failureMessage.isEmpty()), "Incorrect parameters");
146       this.successful = successful;
147       this.failureMessage = failureMessage;
148       this.needsUpdate = needsUpdate;
149     }
150
151     public boolean isSuccessful() {
152       return successful;
153     }
154
155     public String getFailureMessage() {
156       return failureMessage;
157     }
158
159     public boolean isNeedsUpdate() {
160       return needsUpdate;
161     }
162   }
163
164   public interface HashFunction {
165     AuthenticationResult checkCredentials(UserDto user, String password);
166
167     void storeHashPassword(UserDto user, String password);
168
169     default String encryptPassword(String salt, String password) {
170       throw new IllegalStateException("This method is not supported for this hash function");
171     }
172   }
173
174   static final class PBKDF2Function implements HashFunction {
175     private static final char ITERATIONS_HASH_SEPARATOR = '$';
176     private static final int DEFAULT_ITERATIONS = 100_000;
177     private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
178     private static final int KEY_LEN = 512;
179     private static final String ERROR_INVALID_HASH_STORED = "invalid hash stored";
180     private final int generationIterations;
181
182     public PBKDF2Function(@Nullable Integer generationIterations) {
183       this.generationIterations = generationIterations != null ? generationIterations : DEFAULT_ITERATIONS;
184     }
185
186     @Override
187     public AuthenticationResult checkCredentials(UserDto user, String password) {
188       if (user.getCryptedPassword() == null) {
189         return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
190       }
191       if (user.getSalt() == null) {
192         return new AuthenticationResult(false, ERROR_NULL_SALT);
193       }
194
195       int pos = user.getCryptedPassword().indexOf(ITERATIONS_HASH_SEPARATOR);
196       if (pos < 1) {
197         return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
198       }
199       int iterations;
200       try {
201         iterations = Integer.parseInt(user.getCryptedPassword().substring(0, pos));
202       } catch (NumberFormatException e) {
203         return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
204       }
205       String hash = user.getCryptedPassword().substring(pos + 1);
206       byte[] salt = Base64.getDecoder().decode(user.getSalt());
207
208       if (!hash.equals(hash(salt, password, iterations))) {
209         return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
210       }
211       boolean needsUpdate = iterations != generationIterations;
212       return new AuthenticationResult(true, "", needsUpdate);
213     }
214
215     @Override
216     public void storeHashPassword(UserDto user, String password) {
217       byte[] salt = new byte[20];
218       SECURE_RANDOM.nextBytes(salt);
219       String hashStr = hash(salt, password, generationIterations);
220       String saltStr = Base64.getEncoder().encodeToString(salt);
221       user.setHashMethod(HashMethod.PBKDF2.name())
222         .setCryptedPassword(composeEncryptedPassword(hashStr))
223         .setSalt(saltStr);
224     }
225
226     @Override
227     public String encryptPassword(String saltStr, String password) {
228       byte[] salt = Base64.getDecoder().decode(saltStr);
229       return composeEncryptedPassword(hash(salt, password, generationIterations));
230     }
231
232     private String composeEncryptedPassword(String hashStr) {
233       return format("%d%c%s", generationIterations, ITERATIONS_HASH_SEPARATOR, hashStr);
234     }
235
236     private static String hash(byte[] salt, String password, int iterations) {
237       try {
238         SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGORITHM);
239         PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KEY_LEN);
240         byte[] hash = skf.generateSecret(spec).getEncoded();
241         return Base64.getEncoder().encodeToString(hash);
242       } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
243         throw new RuntimeException(e);
244       }
245     }
246   }
247
248   /**
249    * Implementation of deprecated bcrypt hash function
250    */
251   private static final class BcryptFunction implements HashFunction {
252     @Override
253     public AuthenticationResult checkCredentials(UserDto user, String password) {
254       if (user.getCryptedPassword() == null) {
255         return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
256       }
257       // This behavior is overridden in most of integration tests for performance reasons, any changes to BCrypt calls should be propagated to
258       // Byteman classes
259       if (!BCrypt.checkpw(password, user.getCryptedPassword())) {
260         return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
261       }
262       return new AuthenticationResult(true, "");
263     }
264
265     @Override
266     public void storeHashPassword(UserDto user, String password) {
267       user.setHashMethod(HashMethod.BCRYPT.name())
268         .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
269         .setSalt(null);
270     }
271   }
272 }