Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable password hashing algorithm/cost #31234

Merged
merged 31 commits into from
Jun 28, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a02c3dd
Make password hashing algorithm/cost configurable
jkakavas Jun 10, 2018
feafc97
Rework and test default cost
jkakavas Jun 10, 2018
ac10583
Remove irrelevant test
jkakavas Jun 10, 2018
99a7f1c
Remove * import
jkakavas Jun 10, 2018
f97d866
Fix a few more tests
jkakavas Jun 10, 2018
3625713
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 13, 2018
3635c11
Addresses feedback
jkakavas Jun 14, 2018
ede105f
Revert unnecessary scope changes
jkakavas Jun 14, 2018
5577013
Adjust tests
jkakavas Jun 14, 2018
3ef1ea1
Remove unecessary setting constructor
jkakavas Jun 14, 2018
47f911f
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 14, 2018
4fad6cf
remove unused imports
jkakavas Jun 14, 2018
51ee743
fix leftover test
jkakavas Jun 15, 2018
3305765
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 15, 2018
6d28ef0
Handle unknown hash formats with error
jkakavas Jun 15, 2018
ef3dc4c
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 15, 2018
038bc6a
Handle invalid hash during validation
jkakavas Jun 17, 2018
0ab9cb0
Handle invalid hash during validation
jkakavas Jun 18, 2018
5e537fe
Addresses feedback
jkakavas Jun 19, 2018
44949ca
Address feedback
jkakavas Jun 19, 2018
15cbf2d
Address feedback
jkakavas Jun 25, 2018
17d4028
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 25, 2018
e2f429a
Remove unused imports
jkakavas Jun 25, 2018
af85aeb
Address feedback
jkakavas Jun 25, 2018
8cd5ae2
remove now invalid test
jkakavas Jun 26, 2018
c0d33a9
Recomputed test password hashes with correct salt length
jkakavas Jun 26, 2018
0f19d47
Address feedback
jkakavas Jun 27, 2018
622a204
Fix checkstyle
jkakavas Jun 27, 2018
918301c
Addresses final feedback
jkakavas Jun 28, 2018
bb85c20
Reflect change in behavior in tests
jkakavas Jun 28, 2018
b18203c
Merge remote-tracking branch 'origin/master' into configurable-pwd-hash
jkakavas Jun 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ public static Setting<String> simpleString(String key, Validator<String> validat
* @param key the settings key for this setting.
* @param defaultValue the default String value.
* @param properties properties for this setting like scope, filtering...
* @return
* @return the Setting Object
*/
public static Setting<String> simpleString(String key, String defaultValue, Property... properties) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add javadocs? I know there aren't docs for other methods here but we should be in the habit of adding them for public methods

return new Setting<>(key, s -> defaultValue, Function.identity(), properties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.xpack.core.security.SecurityField;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.ssl.SSLClientAuth;
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
import org.elasticsearch.xpack.core.ssl.VerificationMode;
Expand All @@ -19,6 +20,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

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

Expand Down Expand Up @@ -110,8 +112,16 @@ private XPackSettings() {
DEFAULT_CIPHERS = ciphers;
}

public static final Setting<String> PASSWORD_HASHING_ALGORITHM = Setting.simpleString(
"xpack.security.authc.password_hashing.algorithm", "bcrypt", Setting.Property.NodeScope);
/*
* Do not allow insecure hashing algorithms to be used for password hashing
*/
public static final Setting<String> PASSWORD_HASHING_ALGORITHM = new Setting<>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we discussed in the team meeting or somewhere else. Just adding a comment for discussion if we need to have extensible mechanism via security extensions. If need be customers can use 'Argon2' or 'scrypt' using non-default implementations? Is it too early to support them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part has not been discussed, but it is a good idea for a future enhancement. I'd rather wait on opening this up though; once we do that then it has to be supported by us and everything we add creates overhead. We also do not know if there is demand for this and I'd guess that the majority of users would not have a preference as long as we use something that is secure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR was in the context of supporting a FIPS-140 compliant solution, hence only PBKDF2 was added. I also haven't seen any argon2 or scrypt JAVA implementations that have been used / tested sufficiently (there are bindings for the Argon2 C implementation though.. )

I'm definitely not against allowing for extensions with other algorithm implementations, but not in the context of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, Jay and Ioannis. I just thought if this was something that we wanted to consider for security extensions. Yes, I would not trust yet not proven fairly new implementations, just wanted to bring it up so we can keep it in our thoughts.

"xpack.security.authc.password_hashing.algorithm", "bcrypt", Function.identity(), (v, s) -> {
if (Hasher.getAvailableAlgoStoredHash().contains(v) == false) {
throw new IllegalArgumentException("Invalid algorithm: " + v + ". Only pbkdf2 or bcrypt family algorithms can be used for " +
"password hashing.");
}
}, Setting.Property.NodeScope);

public static final List<String> DEFAULT_SUPPORTED_PROTOCOLS = Arrays.asList("TLSv1.2", "TLSv1.1", "TLSv1");
public static final SSLClientAuth CLIENT_AUTH_DEFAULT = SSLClientAuth.REQUIRED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Collectors;

public enum Hasher {
Expand Down Expand Up @@ -370,8 +369,18 @@ public boolean verify(SecureString text, char[] hash) {
private static final String SSHA256_PREFIX = "{SSHA256}";
private static final String PBKDF2_PREFIX = "{PBKDF2}";
private static final int PBKDF2_DEFAULT_COST = 10000;
private static final int PBKDF2_KEY_LENGTH = 256;
private static final int BCRYPT_DEFAULT_COST = 10;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();

/**
* Returns a {@link Hasher} instance of the appropriate algorithm and associated cost as
* indicated by the {@code name}. Name identifiers for the default costs for
* BCRYPT and PBKDF2 return the he default BCRYPT and PBKDF2 Hasher instead of the specific
* instances for the associated cost.
*
* @param name The name of the algorithm and cost combination identifier
* @return the hasher associated with the identifier
*/
public static Hasher resolve(String name) {
switch (name.toLowerCase(Locale.ROOT)) {
case "bcrypt":
Expand All @@ -389,7 +398,7 @@ public static Hasher resolve(String name) {
case "bcrypt9":
return BCRYPT9;
case "bcrypt10":
return BCRYPT10;
return BCRYPT;
case "bcrypt11":
return BCRYPT11;
case "bcrypt12":
Expand All @@ -403,7 +412,7 @@ public static Hasher resolve(String name) {
case "pbkdf2_1000":
return PBKDF2_1000;
case "pbkdf2_10000":
return PBKDF2_10000;
return PBKDF2;
case "pbkdf2_50000":
return PBKDF2_50000;
case "pbkdf2_100000":
Expand All @@ -429,28 +438,25 @@ public static Hasher resolve(String name) {
/**
* Returns a {@link Hasher} instance that can be used to verify the {@code hash} by inspecting the
* hash prefix and determining the algorithm used for its generation.
* The default BCRYPT and PBKDF2 Hashers are used for verifying all the hashes of these algorithm
* families, instead of the specific instances for the given cost factor, as the cost factor is
* deduced from the hash and all {@link Hasher} share the same {@link #verifyPbkdf2Hash(SecureString, char[]) verifyPbkdf2Hash}
* and {@link #verifyBcryptHash(SecureString, char[]) verifyBcryptHash} methods respectively.
*
* @param hash the char array from which the hashing algorithm is to be deduced
* @return the hasher that can be used for validation
*/
public static Hasher resolveFromHash(char[] hash) {
String hashString = new String(hash);
if (hashString.startsWith(BCRYPT_PREFIX)) {
return Hasher.BCRYPT;
} else if (hashString.startsWith(PBKDF2_PREFIX)) {
return Hasher.PBKDF2;
} else if (hashString.startsWith(SHA1_PREFIX)) {
if (CharArrays.charsBeginsWith(BCRYPT_PREFIX, hash)) {
int cost = Integer.parseInt(new String(Arrays.copyOfRange(hash, BCRYPT_PREFIX.length(), hash.length - 54)));
return cost == BCRYPT_DEFAULT_COST ? Hasher.BCRYPT : resolve("bcrypt" + cost);
} else if (CharArrays.charsBeginsWith(PBKDF2_PREFIX, hash)) {
int cost = Integer.parseInt(new String(Arrays.copyOfRange(hash, PBKDF2_PREFIX.length(), hash.length - 90)));
return cost == PBKDF2_DEFAULT_COST ? Hasher.PBKDF2 : resolve("pbkdf2_" + cost);
} else if (CharArrays.charsBeginsWith(SHA1_PREFIX, hash)) {
return Hasher.SHA1;
} else if (hashString.startsWith(MD5_PREFIX)) {
} else if (CharArrays.charsBeginsWith(MD5_PREFIX, hash)) {
return Hasher.MD5;
} else if (hashString.startsWith(SSHA256_PREFIX)) {
} else if (CharArrays.charsBeginsWith(SSHA256_PREFIX, hash)) {
return Hasher.SSHA256;
} else {
throw new IllegalArgumentException("unknown hash format for hash [" + hashString + "]");
throw new IllegalArgumentException("unknown hash format for hash [" + new String(hash) + "]");
}
}

Expand Down Expand Up @@ -495,8 +501,8 @@ private static char[] getPbkdf2Hash(SecureString data, int cost) {
private static boolean verifyPbkdf2Hash(SecureString data, char[] hash) {
// Base64 string length : (4*(n/3)) rounded up to the next multiple of 4 because of padding, i.e. 44 for 32 bytes
final int tokenLength = 44;
char[] hashChars = new char[tokenLength];
char[] saltChars = new char[tokenLength];
char[] hashChars = null;
char[] saltChars = null;
try {
if (CharArrays.charsBeginsWith(PBKDF2_PREFIX, hash) == false) {
return false;
Expand All @@ -506,17 +512,21 @@ private static boolean verifyPbkdf2Hash(SecureString data, char[] hash) {
int cost = Integer.parseInt(new String(Arrays.copyOfRange(hash, PBKDF2_PREFIX.length(), hash.length - (2 * tokenLength + 2))));
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
PBEKeySpec keySpec = new PBEKeySpec(data.getChars(), Base64.getDecoder().decode(CharArrays.toUtf8Bytes(saltChars)),
cost, PBKDF2_KEY_LENGTH);
cost, 256);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the constant more since it provided context to this "magic number"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I eventually removed it since I didn't use it in calculating the 44 "magic number" in code and it seemed redundant. I can make it more obvious in the comment that n in (4*(n/3)) is 32 because its the key size (256) in bytes

char[] computedPwdHash = CharArrays.utf8BytesToChars(Base64.getEncoder()
.encode(secretKeyFactory.generateSecret(keySpec).getEncoded()));
boolean result = CharArrays.constantTimeEquals(computedPwdHash, hashChars);
final boolean result = CharArrays.constantTimeEquals(computedPwdHash, hashChars);
Arrays.fill(computedPwdHash, '\u0000');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big deal, but maybe we should pull the computedPwdHash array out of the try and set it to null initially. Then we can fill it in the finally with the others?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just trying to zerofill as soon after the use as possible. Do you see any advantages other than consistency?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An exception is thrown during the conversion of utf8 bytes to chars would cause this array to hang around in memory. Like I said, if you prefer how you have it, I am fine with it :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Nope, I was just trying to see what you're getting at thanks. I'll change it

return result;
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new ElasticsearchException("Can't use PBKDF2 for password hashing", e);
} finally {
Arrays.fill(hashChars, '\u0000');
Arrays.fill(saltChars, '\u0000');
if (null != hashChars) {
Arrays.fill(hashChars, '\u0000');
}
if (null != saltChars) {
Arrays.fill(saltChars, '\u0000');
}
}
}

Expand Down Expand Up @@ -547,9 +557,8 @@ public static List<String> getAvailableAlgoStoredHash() {
* Generates an array of {@code length} random bytes using {@link java.security.SecureRandom}
*/
private static byte[] generateSalt(int length) {
Random random = new SecureRandom();
byte[] salt = new byte[length];
random.nextBytes(salt);
SECURE_RANDOM.nextBytes(salt);
return salt;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.bootstrap.BootstrapContext;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;

import javax.crypto.SecretKeyFactory;
import java.security.NoSuchAlgorithmException;
Expand All @@ -22,11 +21,6 @@ public class PasswordHashingAlgorithmBootstrapCheck implements BootstrapCheck {
@Override
public BootstrapCheckResult check(BootstrapContext context) {
final String selectedAlgorithm = XPackSettings.PASSWORD_HASHING_ALGORITHM.get(context.settings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I told you to move this here, I am sorry. Setting validation should happen on the setting and not be part of a bootstrap check. The availability of the PBKDF2 algorithm is fine as a bootstrap check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misread the original comment, I'll revert the setting validation and only check the algo availability here.

if (Hasher.getAvailableAlgoStoredHash().contains(selectedAlgorithm.toLowerCase(Locale.ROOT)) == false) {
final String errorMessage = String.format(Locale.ROOT, "Only the following values [%s] are permitted for the [%s] setting. ",
Hasher.getAvailableAlgoStoredHash(), XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey());
return BootstrapCheckResult.failure(errorMessage);
}
if (selectedAlgorithm.toLowerCase(Locale.ROOT).startsWith("pbkdf2")) {
try {
SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ public Security(Settings settings, final Path configPath) {
// fetched
final List<BootstrapCheck> checks = new ArrayList<>();
checks.addAll(Arrays.asList(
new TokenSSLBootstrapCheck(),
new PkiRealmBootstrapCheck(settings, getSslService()),
new TokenSSLBootstrapCheck(),
new PkiRealmBootstrapCheck(settings, getSslService()),
new TLSLicenseBootstrapCheck(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation needs to be cleaned up

new PasswordHashingAlgorithmBootstrapCheck()));
checks.addAll(InternalRealms.getBootstrapChecks(settings, env));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordResponse;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.core.security.user.XPackUser;
Expand All @@ -41,6 +43,13 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene
listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal"));
return;
}
final String requestPwdHashAlgo = Hasher.resolveFromHash(request.passwordHash()).name();
final String configPwdHashAlgo = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings)).name();
if (requestPwdHashAlgo.equalsIgnoreCase(configPwdHashAlgo) == false) {
listener.onFailure(new IllegalArgumentException("incorrect password hashing algorithm [" + requestPwdHashAlgo + "] used while" +
" [" + configPwdHashAlgo + "] is configured."));
return;
}
nativeUsersStore.changePassword(request, new ActionListener<Void>() {
@Override
public void onResponse(Void v) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class PasswordHashingAlgorithmBootstrapCheckTests extends ESTestCase {
public void testPasswordHashingAlgorithmBootstrapCheck() {
Settings settings = Settings.EMPTY;
assertFalse(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());

// The following two will always pass because for now we only test in environments where PBKDF2WithHMACSHA512 is supported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add an assume statement that validates PBKDF2 is available?

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2_10000").build();
assertFalse(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());

Expand All @@ -29,26 +29,5 @@ public void testPasswordHashingAlgorithmBootstrapCheck() {

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "BCRYPT11").build();
assertFalse(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "SHA1").build();
assertTrue(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());
assertThat(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).getMessage(),
containsString(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey()));

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "MD5").build();
assertTrue(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());
assertThat(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).getMessage(),
containsString(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey()));

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "SSHA256").build();
assertTrue(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());
assertThat(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).getMessage(),
containsString(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey()));

settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "Argon").build();
assertTrue(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure());
assertThat(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).getMessage(),
containsString(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey()));

}
}
Loading