Skip to content

Commit ba0ecae

Browse files
authored
Configurable password hashing algorithm/cost(#31234) (#32092)
* Configurable password hashing algorithm/cost (#31234) Make password hashing algorithm/cost configurable for the stored passwords of users for the realms that this applies (native, reserved). Replaces predefined choice of bcrypt with cost factor 10. This also introduces PBKDF2 with configurable cost (number of iterations) as an algorithm option for password hashing both for storing passwords and for the user cache. Password hash validation algorithm selection takes into consideration the stored hash prefix and only a specific number of algorithnm and cost factor options for brypt and pbkdf2 are whitelisted and can be selected in the relevant setting. * resolveHasher defaults to NOOP (#31723) This changes the default behavior when resolving the hashing algorithm from unrecognised hash strings, which was introduced in #31234 A hash string that doesn't start with an algorithm identifier can either be a malformed/corrupted hash or a plaintext password when Hasher.NOOP is used(against warnings). Do not make assumptions about which of the two is true for such strings and default to Hasher.NOOP. Hash verification will subsequently fail for malformed hashes. Finally, do not log the potentially malformed hash as this can very well be a plaintext password. * Fix RealmInteg test failures As part of the changes in #31234,the password verification logic determines the algorithm used for hashing the password from the format of the stored password hash itself. Thus, it is generally possible to validate a password even if it's associated stored hash was not created with the same algorithm than the one currently set in the settings. At the same time, we introduced a check for incoming client change password requests to make sure that the request's password is hashed with the same algorithm that is configured to be used in the node settings. In the spirit of randomizing the algorithms used, the {@code SecurityClient} used in the {@code NativeRealmIntegTests} and {@code ReservedRealmIntegTests} would send all requests dealing with user passwords by randomly selecting a hashing algorithm each time. This meant that some change password requests were using a different password hashing algorithm than the one used for the node and the request would fail. This commit changes this behavior in the two aforementioned Integ tests to use the same password hashing algorithm for the node and the clients, no matter what the request is.
1 parent eed8dc9 commit ba0ecae

File tree

61 files changed

+1037
-410
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1037
-410
lines changed

server/src/main/java/org/elasticsearch/common/settings/Setting.java

+12
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,18 @@ public static Setting<String> simpleString(String key, Validator<String> validat
10171017
return new Setting<>(new SimpleKey(key), null, s -> "", Function.identity(), validator, properties);
10181018
}
10191019

1020+
/**
1021+
* Creates a new Setting instance with a String value
1022+
*
1023+
* @param key the settings key for this setting.
1024+
* @param defaultValue the default String value.
1025+
* @param properties properties for this setting like scope, filtering...
1026+
* @return the Setting Object
1027+
*/
1028+
public static Setting<String> simpleString(String key, String defaultValue, Property... properties) {
1029+
return new Setting<>(key, s -> defaultValue, Function.identity(), properties);
1030+
}
1031+
10201032
public static int parseInt(String s, int minValue, String key) {
10211033
return parseInt(s, minValue, Integer.MAX_VALUE, key);
10221034
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.elasticsearch.common.settings.Setting;
1010
import org.elasticsearch.common.settings.Setting.Property;
1111
import org.elasticsearch.xpack.core.security.SecurityField;
12+
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
1213
import org.elasticsearch.xpack.core.ssl.SSLClientAuth;
1314
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
1415
import org.elasticsearch.xpack.core.ssl.VerificationMode;
@@ -20,13 +21,20 @@
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.List;
24+
import java.util.Locale;
25+
import java.util.function.Function;
2326

2427
import static org.elasticsearch.xpack.core.security.SecurityField.USER_SETTING;
2528

2629
/**
2730
* A container for xpack setting constants.
2831
*/
2932
public class XPackSettings {
33+
34+
private XPackSettings() {
35+
throw new IllegalStateException("Utility class should not be instantiated");
36+
}
37+
3038
/** Setting for enabling or disabling security. Defaults to true. */
3139
public static final Setting<Boolean> SECURITY_ENABLED = Setting.boolSetting("xpack.security.enabled", true, Setting.Property.NodeScope);
3240

@@ -113,6 +121,17 @@ public class XPackSettings {
113121
DEFAULT_CIPHERS = ciphers;
114122
}
115123

124+
/*
125+
* Do not allow insecure hashing algorithms to be used for password hashing
126+
*/
127+
public static final Setting<String> PASSWORD_HASHING_ALGORITHM = new Setting<>(
128+
"xpack.security.authc.password_hashing.algorithm", "bcrypt", Function.identity(), (v, s) -> {
129+
if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
130+
throw new IllegalArgumentException("Invalid algorithm: " + v + ". Only pbkdf2 or bcrypt family algorithms can be used for " +
131+
"password hashing.");
132+
}
133+
}, Setting.Property.NodeScope);
134+
116135
public static final List<String> DEFAULT_SUPPORTED_PROTOCOLS = Arrays.asList("TLSv1.2", "TLSv1.1", "TLSv1");
117136
public static final SSLClientAuth CLIENT_AUTH_DEFAULT = SSLClientAuth.REQUIRED;
118137
public static final SSLClientAuth HTTP_CLIENT_AUTH_DEFAULT = SSLClientAuth.NONE;
@@ -151,6 +170,7 @@ public static List<Setting<?>> getAllSettings() {
151170
settings.add(SQL_ENABLED);
152171
settings.add(USER_SETTING);
153172
settings.add(ROLLUP_ENABLED);
173+
settings.add(PASSWORD_HASHING_ALGORITHM);
154174
return Collections.unmodifiableList(settings);
155175
}
156176

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequestBuilder.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,22 @@ public ChangePasswordRequestBuilder username(String username) {
4141
return this;
4242
}
4343

44-
public static char[] validateAndHashPassword(SecureString password) {
44+
public static char[] validateAndHashPassword(SecureString password, Hasher hasher) {
4545
Validation.Error error = Validation.Users.validatePassword(password.getChars());
4646
if (error != null) {
4747
ValidationException validationException = new ValidationException();
4848
validationException.addValidationError(error.toString());
4949
throw validationException;
5050
}
51-
return Hasher.BCRYPT.hash(password);
51+
return hasher.hash(password);
5252
}
5353

5454
/**
5555
* Sets the password. Note: the char[] passed to this method will be cleared.
5656
*/
57-
public ChangePasswordRequestBuilder password(char[] password) {
57+
public ChangePasswordRequestBuilder password(char[] password, Hasher hasher) {
5858
try (SecureString secureString = new SecureString(password)) {
59-
char[] hash = validateAndHashPassword(secureString);
59+
char[] hash = validateAndHashPassword(secureString, hasher);
6060
request.passwordHash(hash);
6161
}
6262
return this;
@@ -65,7 +65,8 @@ public ChangePasswordRequestBuilder password(char[] password) {
6565
/**
6666
* Populate the change password request from the source in the provided content type
6767
*/
68-
public ChangePasswordRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException {
68+
public ChangePasswordRequestBuilder source(BytesReference source, XContentType xContentType, Hasher hasher) throws
69+
IOException {
6970
// EMPTY is ok here because we never call namedObject
7071
try (InputStream stream = source.streamInput();
7172
XContentParser parser = xContentType.xContent()
@@ -80,7 +81,7 @@ public ChangePasswordRequestBuilder source(BytesReference source, XContentType x
8081
if (token == XContentParser.Token.VALUE_STRING) {
8182
String password = parser.text();
8283
final char[] passwordChars = password.toCharArray();
83-
password(passwordChars);
84+
password(passwordChars, hasher);
8485
assert CharBuffer.wrap(passwordChars).chars().noneMatch((i) -> (char) i != (char) 0) : "expected password to " +
8586
"clear the char[] but it did not!";
8687
} else {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java

+4-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.elasticsearch.action.ActionRequestBuilder;
1010
import org.elasticsearch.action.support.WriteRequestBuilder;
1111
import org.elasticsearch.client.ElasticsearchClient;
12-
import org.elasticsearch.common.Nullable;
1312
import org.elasticsearch.common.Strings;
1413
import org.elasticsearch.common.ValidationException;
1514
import org.elasticsearch.common.bytes.BytesReference;
@@ -33,8 +32,6 @@
3332
public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest, PutUserResponse, PutUserRequestBuilder>
3433
implements WriteRequestBuilder<PutUserRequestBuilder> {
3534

36-
private final Hasher hasher = Hasher.BCRYPT;
37-
3835
public PutUserRequestBuilder(ElasticsearchClient client) {
3936
this(client, PutUserAction.INSTANCE);
4037
}
@@ -53,7 +50,7 @@ public PutUserRequestBuilder roles(String... roles) {
5350
return this;
5451
}
5552

56-
public PutUserRequestBuilder password(@Nullable char[] password) {
53+
public PutUserRequestBuilder password(char[] password, Hasher hasher) {
5754
if (password != null) {
5855
Validation.Error error = Validation.Users.validatePassword(password);
5956
if (error != null) {
@@ -96,7 +93,8 @@ public PutUserRequestBuilder enabled(boolean enabled) {
9693
/**
9794
* Populate the put user request using the given source and username
9895
*/
99-
public PutUserRequestBuilder source(String username, BytesReference source, XContentType xContentType) throws IOException {
96+
public PutUserRequestBuilder source(String username, BytesReference source, XContentType xContentType, Hasher hasher) throws
97+
IOException {
10098
Objects.requireNonNull(xContentType);
10199
username(username);
102100
// EMPTY is ok here because we never call namedObject
@@ -113,7 +111,7 @@ public PutUserRequestBuilder source(String username, BytesReference source, XCon
113111
if (token == XContentParser.Token.VALUE_STRING) {
114112
String password = parser.text();
115113
char[] passwordChars = password.toCharArray();
116-
password(passwordChars);
114+
password(passwordChars, hasher);
117115
Arrays.fill(passwordChars, (char) 0);
118116
} else {
119117
throw new ElasticsearchParseException(

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/CachingUsernamePasswordRealmSettings.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import java.util.Set;
1414

1515
public final class CachingUsernamePasswordRealmSettings {
16-
public static final Setting<String> CACHE_HASH_ALGO_SETTING = Setting.simpleString("cache.hash_algo", Setting.Property.NodeScope);
16+
public static final Setting<String> CACHE_HASH_ALGO_SETTING = Setting.simpleString("cache.hash_algo", "ssha256",
17+
Setting.Property.NodeScope);
1718
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20);
1819
public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope);
1920
private static final int DEFAULT_MAX_USERS = 100_000; //100k users

0 commit comments

Comments
 (0)