-
Notifications
You must be signed in to change notification settings - Fork 25.1k
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
Changes from 1 commit
a02c3dd
feafc97
ac10583
99a7f1c
f97d866
3625713
3635c11
ede105f
5577013
3ef1ea1
47f911f
4fad6cf
51ee743
3305765
6d28ef0
ef3dc4c
038bc6a
0ab9cb0
5e537fe
44949ca
15cbf2d
17d4028
e2f429a
af85aeb
8cd5ae2
c0d33a9
0f19d47
622a204
918301c
bb85c20
b18203c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
||
|
@@ -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<>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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": | ||
|
@@ -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": | ||
|
@@ -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": | ||
|
@@ -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) + "]"); | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a big deal, but maybe we should pull the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add an |
||
settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2_10000").build(); | ||
assertFalse(new PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)).isFailure()); | ||
|
||
|
@@ -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())); | ||
|
||
} | ||
} |
There was a problem hiding this comment.
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