[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:
uboness 2015-01-24 21:21:24 +01:00
parent b768ea9551
commit 4fb18bb65a
7 changed files with 154 additions and 16 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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
*/

View File

@ -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);

View File

@ -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();

View File

@ -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));
}
}

View File

@ -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("эластичный поиск-弾性検索"));
}
}