[Perf] Introduced additional hashers
Introduced three new hasher implementations: - `bcrypt5` - a bcrypt hasher configured with a salt generated with 5 iterations - `bcrypt7` - a bcrypt hasher configured with a salt generated with 7 iterations - `noop` - a hasher that doesn't hash and works with the original text Also, due to poor performance and based on the external security audit review feedback, the default realm caching hash is now changed to `bcrypt5` (used to be `sha2`). Original commit: elastic/x-pack-elasticsearch@53d4f40564
This commit is contained in:
parent
b768ea9551
commit
4fb18bb65a
|
@ -19,22 +19,24 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm {
|
||||
|
||||
public static final String CACHE_HASH_ALGO_SETTING = "cache.hash_algo";
|
||||
public static final String CACHE_TTL_SETTING = "cache.ttl";
|
||||
public static final String CACHE_MAX_USERS_SETTING = "cache.max_users";
|
||||
|
||||
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20);
|
||||
private static final int DEFAULT_MAX_USERS = 100000; //100k users
|
||||
public static final String CACHE_TTL = "cache.ttl";
|
||||
public static final String CACHE_MAX_USERS = "cache.max_users";
|
||||
|
||||
private final Cache<String, UserWithHash> cache;
|
||||
private final Hasher hasher;
|
||||
final Hasher hasher;
|
||||
|
||||
protected CachingUsernamePasswordRealm(String type, RealmConfig config) {
|
||||
super(type, config);
|
||||
hasher = Hasher.resolve(config.settings().get("cache.hash_algo", null), Hasher.SHA2);
|
||||
TimeValue ttl = config.settings().getAsTime(CACHE_TTL, DEFAULT_TTL);
|
||||
hasher = Hasher.resolve(config.settings().get(CACHE_HASH_ALGO_SETTING, null), Hasher.BCRYPT5);
|
||||
TimeValue ttl = config.settings().getAsTime(CACHE_TTL_SETTING, DEFAULT_TTL);
|
||||
if (ttl.millis() > 0) {
|
||||
cache = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS)
|
||||
.maximumSize(config.settings().getAsInt(CACHE_MAX_USERS, DEFAULT_MAX_USERS))
|
||||
.maximumSize(config.settings().getAsInt(CACHE_MAX_USERS_SETTING, DEFAULT_MAX_USERS))
|
||||
.build();
|
||||
} else {
|
||||
cache = null;
|
||||
|
|
|
@ -10,10 +10,10 @@ import org.apache.commons.codec.digest.Crypt;
|
|||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.codec.digest.Md5Crypt;
|
||||
import org.apache.commons.codec.digest.Sha2Crypt;
|
||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||
import org.elasticsearch.common.os.OsUtils;
|
||||
import org.elasticsearch.shield.ShieldSettingsException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
|
@ -78,6 +78,40 @@ public enum Hasher {
|
|||
}
|
||||
},
|
||||
|
||||
BCRYPT5() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
String salt = org.elasticsearch.shield.authc.support.BCrypt.gensalt(5);
|
||||
return BCrypt.hashpw(text, salt).toCharArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
String hashStr = new String(hash);
|
||||
if (!hashStr.startsWith(BCRYPT_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
return BCrypt.checkpw(text, hashStr);
|
||||
}
|
||||
},
|
||||
|
||||
BCRYPT7() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
String salt = org.elasticsearch.shield.authc.support.BCrypt.gensalt(7);
|
||||
return BCrypt.hashpw(text, salt).toCharArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
String hashStr = new String(hash);
|
||||
if (!hashStr.startsWith(BCRYPT_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
return BCrypt.checkpw(text, hashStr);
|
||||
}
|
||||
},
|
||||
|
||||
MD5() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
|
@ -132,6 +166,18 @@ public enum Hasher {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
NOOP() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
return text.copyChars();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
return Arrays.equals(text.internalChars(), hash);
|
||||
}
|
||||
};
|
||||
|
||||
private static final String APR1_PREFIX = "$apr1$";
|
||||
|
@ -147,11 +193,15 @@ public enum Hasher {
|
|||
return defaultHasher;
|
||||
}
|
||||
switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "htpasswd" : return HTPASSWD;
|
||||
case "bcrypt" : return BCRYPT;
|
||||
case "sha1" : return SHA1;
|
||||
case "sha2" : return SHA2;
|
||||
case "md5" : return MD5;
|
||||
case "htpasswd" : return HTPASSWD;
|
||||
case "bcrypt" : return BCRYPT;
|
||||
case "bcrypt5" : return BCRYPT5;
|
||||
case "bcrypt7" : return BCRYPT7;
|
||||
case "sha1" : return SHA1;
|
||||
case "sha2" : return SHA2;
|
||||
case "md5" : return MD5;
|
||||
case "noop" :
|
||||
case "clear_text" : return NOOP;
|
||||
default:
|
||||
return defaultHasher;
|
||||
}
|
||||
|
|
|
@ -77,7 +77,9 @@ public class SecuredString implements CharSequence {
|
|||
|
||||
/**
|
||||
* Note: This is a dangerous call that exists for performance/optimization
|
||||
* DO NOT modify the array returned by this method. To clear the array call SecureString.clear().
|
||||
* DO NOT modify the array returned by this method and DO NOT cache it (as it will be cleared).
|
||||
*
|
||||
* To clear the array call SecureString.clear().
|
||||
*
|
||||
* @return the internal characters that MUST NOT be cleared manually
|
||||
*/
|
||||
|
@ -86,6 +88,13 @@ public class SecuredString implements CharSequence {
|
|||
return chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A copy of the internal charachters. May be usd for caching.
|
||||
*/
|
||||
public char[] copyChars() {
|
||||
return Arrays.copyOf(chars, chars.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return utf8 encoded bytes
|
||||
*/
|
||||
|
|
|
@ -139,7 +139,7 @@ public class LdapRealmTest extends LdapTest {
|
|||
String userTemplate = VALID_USER_TEMPLATE;
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE))
|
||||
.put(LdapRealm.CACHE_TTL, -1)
|
||||
.put(LdapRealm.CACHE_TTL_SETTING, -1)
|
||||
.build();
|
||||
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.support;
|
|||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
|
@ -15,11 +16,33 @@ import org.junit.Test;
|
|||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
public class CachingUsernamePasswordRealmTests extends ElasticsearchTestCase {
|
||||
|
||||
@Test
|
||||
public void testSettings() throws Exception {
|
||||
|
||||
String hashAlgo = randomFrom("bcrypt", "bcrypt5", "bcrypt7", "sha1", "sha2", "md5", "clear_text", "noop");
|
||||
int maxUsers = randomIntBetween(10, 100);
|
||||
TimeValue ttl = TimeValue.timeValueMinutes(randomIntBetween(10, 20));
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put(CachingUsernamePasswordRealm.CACHE_HASH_ALGO_SETTING, hashAlgo)
|
||||
.put(CachingUsernamePasswordRealm.CACHE_MAX_USERS_SETTING, maxUsers)
|
||||
.put(CachingUsernamePasswordRealm.CACHE_TTL_SETTING, ttl)
|
||||
.build();
|
||||
|
||||
RealmConfig config = new RealmConfig("test_realm", settings);
|
||||
CachingUsernamePasswordRealm realm = new CachingUsernamePasswordRealm("test", config) {
|
||||
@Override
|
||||
protected User doAuthenticate(UsernamePasswordToken token) {
|
||||
return new User.Simple("username", "r1", "r2", "r3");
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(realm.hasher, sameInstance(Hasher.resolve(hashAlgo)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCache(){
|
||||
AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm();
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.support;
|
||||
|
||||
import org.elasticsearch.shield.ShieldSettingsException;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
@ -40,6 +43,16 @@ public class HasherTests extends ElasticsearchTestCase {
|
|||
testHasherSelfGenerated(Hasher.BCRYPT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBcrypt5_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.BCRYPT5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBcrypt7_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.BCRYPT7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMd5_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.MD5);
|
||||
|
@ -60,4 +73,29 @@ public class HasherTests extends ElasticsearchTestCase {
|
|||
assertTrue(hasher.verify(passwd, hasher.hash(passwd)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoop_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.NOOP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolve() throws Exception {
|
||||
assertThat(Hasher.resolve("htpasswd"), sameInstance(Hasher.HTPASSWD));
|
||||
assertThat(Hasher.resolve("bcrypt"), sameInstance(Hasher.BCRYPT));
|
||||
assertThat(Hasher.resolve("bcrypt5"), sameInstance(Hasher.BCRYPT5));
|
||||
assertThat(Hasher.resolve("bcrypt7"), sameInstance(Hasher.BCRYPT7));
|
||||
assertThat(Hasher.resolve("sha1"), sameInstance(Hasher.SHA1));
|
||||
assertThat(Hasher.resolve("sha2"), sameInstance(Hasher.SHA2));
|
||||
assertThat(Hasher.resolve("md5"), sameInstance(Hasher.MD5));
|
||||
assertThat(Hasher.resolve("noop"), sameInstance(Hasher.NOOP));
|
||||
assertThat(Hasher.resolve("clear_text"), sameInstance(Hasher.NOOP));
|
||||
try {
|
||||
Hasher.resolve("unknown_hasher");
|
||||
fail("expected a shield setting error when trying to resolve an unknown hasher");
|
||||
} catch (ShieldSettingsException sse) {
|
||||
// expected
|
||||
}
|
||||
Hasher hasher = randomFrom(Hasher.values());
|
||||
assertThat(Hasher.resolve("unknown_hasher", hasher), sameInstance(hasher));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.junit.Test;
|
|||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SecuredStringTests {
|
||||
|
@ -95,4 +97,18 @@ public class SecuredStringTests {
|
|||
String password2 = new String(utf8, Charsets.UTF_8);
|
||||
assertThat(password2, equalTo(password));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyChars() throws Exception {
|
||||
String password = "эластичный поиск-弾性検索";
|
||||
SecuredString securePass = new SecuredString(password.toCharArray());
|
||||
char[] copy = securePass.copyChars();
|
||||
assertThat(copy, not(sameInstance(securePass.internalChars())));
|
||||
assertThat(copy, equalTo(securePass.internalChars()));
|
||||
|
||||
// just a sanity check to make sure that clearing the secured string
|
||||
// doesn't modify the returned copied chars
|
||||
securePass.clear();
|
||||
assertThat(new String(copy), equalTo("эластичный поиск-弾性検索"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue