]> source.dussan.org Git - sonarqube.git/blob
757dd01075a1a39c3aaf16e91de4bfcfff912a89
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 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.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;
40
41 import static com.google.common.base.Preconditions.checkArgument;
42 import static java.lang.String.format;
43 import static java.util.Objects.requireNonNull;
44
45 /**
46  * Validates the password of a "local" user (password is stored in
47  * database).
48  */
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;
60
61   private final DbClient dbClient;
62   private final EnumMap<HashMethod, HashFunction> hashFunctions = new EnumMap<>(HashMethod.class);
63
64   public enum HashMethod {
65     SHA1, BCRYPT, PBKDF2
66   }
67
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)));
73   }
74
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);
79   }
80
81   /**
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
85    * is not committed
86    */
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)
93         .build();
94     }
95
96     HashMethod hashMethod;
97     try {
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()))
105         .build();
106     }
107
108     HashFunction hashFunction = hashFunctions.get(hashMethod);
109
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())
116         .build();
117     }
118
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);
123     }
124   }
125
126   /**
127    * Method used to store the password as a hash in database.
128    * The crypted_password, salt and hash_method are set
129    */
130   public void storeHashPassword(UserDto user, String password) {
131     hashFunctions.get(DEFAULT).storeHashPassword(user, password);
132   }
133
134   private static class AuthenticationResult {
135     private final boolean successful;
136     private final String failureMessage;
137     private final boolean needsUpdate;
138
139     private AuthenticationResult(boolean successful, String failureMessage) {
140       this(successful, failureMessage, false);
141     }
142
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;
148     }
149
150     public boolean isSuccessful() {
151       return successful;
152     }
153
154     public String getFailureMessage() {
155       return failureMessage;
156     }
157
158     public boolean isNeedsUpdate() {
159       return needsUpdate;
160     }
161   }
162
163   public interface HashFunction {
164     AuthenticationResult checkCredentials(UserDto user, String password);
165
166     void storeHashPassword(UserDto user, String password);
167
168     default String encryptPassword(String salt, String password) {
169       throw new IllegalStateException("This method is not supported for this hash function");
170     }
171   }
172
173   /**
174    * Implementation of deprecated SHA1 hash function
175    */
176   private static final class Sha1Function implements HashFunction {
177     @Override
178     public AuthenticationResult checkCredentials(UserDto user, String password) {
179       if (user.getCryptedPassword() == null) {
180         return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
181       }
182       if (user.getSalt() == null) {
183         return new AuthenticationResult(false, ERROR_NULL_SALT);
184       }
185       if (!user.getCryptedPassword().equals(hash(user.getSalt(), password))) {
186         return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
187       }
188       return new AuthenticationResult(true, "");
189     }
190
191     @Override
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);
197
198       user.setHashMethod(HashMethod.SHA1.name())
199         .setCryptedPassword(hash(salt, password))
200         .setSalt(salt);
201     }
202
203     private static String hash(String salt, String password) {
204       return DigestUtils.sha1Hex("--" + salt + "--" + password + "--");
205     }
206   }
207
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;
215
216     public PBKDF2Function(@Nullable Integer generationIterations) {
217       this.generationIterations = generationIterations != null ? generationIterations : DEFAULT_ITERATIONS;
218     }
219
220     @Override
221     public AuthenticationResult checkCredentials(UserDto user, String password) {
222       if (user.getCryptedPassword() == null) {
223         return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
224       }
225       if (user.getSalt() == null) {
226         return new AuthenticationResult(false, ERROR_NULL_SALT);
227       }
228
229       int pos = user.getCryptedPassword().indexOf(ITERATIONS_HASH_SEPARATOR);
230       if (pos < 1) {
231         return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
232       }
233       int iterations;
234       try {
235         iterations = Integer.parseInt(user.getCryptedPassword().substring(0, pos));
236       } catch (NumberFormatException e) {
237         return new AuthenticationResult(false, ERROR_INVALID_HASH_STORED);
238       }
239       String hash = user.getCryptedPassword().substring(pos + 1);
240       byte[] salt = Base64.getDecoder().decode(user.getSalt());
241
242       if (!hash.equals(hash(salt, password, iterations))) {
243         return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
244       }
245       boolean needsUpdate = iterations != generationIterations;
246       return new AuthenticationResult(true, "", needsUpdate);
247     }
248
249     @Override
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))
257         .setSalt(saltStr);
258     }
259
260     @Override
261     public String encryptPassword(String saltStr, String password) {
262       byte[] salt = Base64.getDecoder().decode(saltStr);
263       return composeEncryptedPassword(hash(salt, password, generationIterations));
264     }
265
266     private String composeEncryptedPassword(String hashStr) {
267       return format("%d%c%s", generationIterations, ITERATIONS_HASH_SEPARATOR, hashStr);
268     }
269
270     private static String hash(byte[] salt, String password, int iterations) {
271       try {
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);
278       }
279     }
280   }
281
282   /**
283    * Implementation of deprecated bcrypt hash function
284    */
285   private static final class BcryptFunction implements HashFunction {
286     @Override
287     public AuthenticationResult checkCredentials(UserDto user, String password) {
288       if (user.getCryptedPassword() == null) {
289         return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
290       }
291       // This behavior is overridden in most of integration tests for performance reasons, any changes to BCrypt calls should be propagated to
292       // Byteman classes
293       if (!BCrypt.checkpw(password, user.getCryptedPassword())) {
294         return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
295       }
296       return new AuthenticationResult(true, "");
297     }
298
299     @Override
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)))
304         .setSalt(null);
305     }
306   }
307 }