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