Changed default in-mem password hashing
- Introduced `ssha256` hashing (salted sha-256) which is now the default in-mem hashing algorithm (instead of bcrypt4) - changed the `md5` to be a real MD5 algorithm (used to be md5crypt. - introduced `apr1` hashing algo. which is the old md5crypt algorithm. Also updated the relevant docs & tests Original commit: elastic/x-pack-elasticsearch@c8c0be5b95
This commit is contained in:
parent
31f3afe684
commit
336d508172
|
@ -31,7 +31,7 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm
|
|||
|
||||
protected CachingUsernamePasswordRealm(String type, RealmConfig config) {
|
||||
super(type, config);
|
||||
hasher = Hasher.resolve(config.settings().get(CACHE_HASH_ALGO_SETTING, null), Hasher.BCRYPT4);
|
||||
hasher = Hasher.resolve(config.settings().get(CACHE_HASH_ALGO_SETTING, null), Hasher.SSHA256);
|
||||
TimeValue ttl = config.settings().getAsTime(CACHE_TTL_SETTING, DEFAULT_TTL);
|
||||
if (ttl.millis() > 0) {
|
||||
cache = CacheBuilder.newBuilder()
|
||||
|
|
|
@ -6,15 +6,18 @@
|
|||
package org.elasticsearch.shield.authc.support;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
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.apache.commons.codec.digest.*;
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.os.OsUtils;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.ShieldSettingsException;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -180,7 +183,7 @@ public enum Hasher {
|
|||
}
|
||||
},
|
||||
|
||||
MD5() {
|
||||
APR1() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||
|
@ -202,7 +205,9 @@ public enum Hasher {
|
|||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||
String hash = Base64.encodeBase64String(DigestUtils.sha1(textBytes));
|
||||
MessageDigest md = SHA1Provider.sha1();
|
||||
md.update(textBytes);
|
||||
String hash = Base64.encodeBase64String(md.digest());
|
||||
return (SHA1_PREFIX + hash).toCharArray();
|
||||
}
|
||||
|
||||
|
@ -213,7 +218,9 @@ public enum Hasher {
|
|||
return false;
|
||||
}
|
||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||
String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(textBytes));
|
||||
MessageDigest md = SHA1Provider.sha1();
|
||||
md.update(textBytes);
|
||||
String passwd64 = Base64.encodeBase64String(md.digest());
|
||||
return hashStr.substring(SHA1_PREFIX.length()).compareTo(passwd64) == 0;
|
||||
}
|
||||
},
|
||||
|
@ -236,6 +243,60 @@ public enum Hasher {
|
|||
}
|
||||
},
|
||||
|
||||
MD5() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
MessageDigest md = MD5Provider.md5();
|
||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||
String hash = Base64.encodeBase64String(md.digest());
|
||||
return (MD5_PREFIX + hash).toCharArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
String hashStr = new String(hash);
|
||||
if (!hashStr.startsWith(MD5_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
hashStr = hashStr.substring(MD5_PREFIX.length());
|
||||
MessageDigest md = MD5Provider.md5();
|
||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||
String computedHashStr = Base64.encodeBase64String(md.digest());
|
||||
return hashStr.equals(computedHashStr);
|
||||
}
|
||||
},
|
||||
|
||||
SSHA256() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
MessageDigest md = SHA256Provider.sha256();
|
||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||
char[] salt = SaltProvider.salt(8);
|
||||
md.update(CharArrays.toUtf8Bytes(salt));
|
||||
String hash = Base64.encodeBase64String(md.digest());
|
||||
char[] result = new char[SSHA256_PREFIX.length() + salt.length + hash.length()];
|
||||
System.arraycopy(SSHA256_PREFIX.toCharArray(), 0, result, 0, SSHA256_PREFIX.length());
|
||||
System.arraycopy(salt, 0, result, SSHA256_PREFIX.length(), salt.length);
|
||||
System.arraycopy(hash.toCharArray(), 0, result, SSHA256_PREFIX.length() + salt.length, hash.length());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
String hashStr = new String(hash);
|
||||
if (!hashStr.startsWith(SSHA256_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
hashStr = hashStr.substring(SSHA256_PREFIX.length());
|
||||
char[] saltAndHash = hashStr.toCharArray();
|
||||
MessageDigest md = SHA256Provider.sha256();
|
||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||
md.update(new String(saltAndHash, 0, 8).getBytes(Charsets.UTF_8));
|
||||
String computedHash = Base64.encodeBase64String(md.digest());
|
||||
return computedHash.equals(new String(saltAndHash, 8, saltAndHash.length - 8));
|
||||
}
|
||||
},
|
||||
|
||||
NOOP() {
|
||||
@Override
|
||||
public char[] hash(SecuredString text) {
|
||||
|
@ -253,6 +314,8 @@ public enum Hasher {
|
|||
private static final String SHA1_PREFIX = "{SHA}";
|
||||
private static final String SHA2_PREFIX_5 = "$5$";
|
||||
private static final String SHA2_PREFIX_6 = "$6$";
|
||||
private static final String MD5_PREFIX = "{MD5}";
|
||||
private static final String SSHA256_PREFIX = "{SSHA256}";
|
||||
private static final String PLAIN_PREFIX = "{plain}";
|
||||
static final boolean CRYPT_SUPPORTED = !OsUtils.WINDOWS;
|
||||
|
||||
|
@ -269,9 +332,11 @@ public enum Hasher {
|
|||
case "bcrypt7" : return BCRYPT7;
|
||||
case "bcrypt8" : return BCRYPT8;
|
||||
case "bcrypt9" : return BCRYPT9;
|
||||
case "apr1" : return APR1;
|
||||
case "sha1" : return SHA1;
|
||||
case "sha2" : return SHA2;
|
||||
case "md5" : return MD5;
|
||||
case "ssha256" : return SSHA256;
|
||||
case "noop" :
|
||||
case "clear_text" : return NOOP;
|
||||
default:
|
||||
|
@ -291,4 +356,91 @@ public enum Hasher {
|
|||
|
||||
public abstract boolean verify(SecuredString data, char[] hash);
|
||||
|
||||
static final class MD5Provider {
|
||||
|
||||
private static final MessageDigest digest;
|
||||
|
||||
static {
|
||||
try {
|
||||
digest = MessageDigest.getInstance(MessageDigestAlgorithms.MD5);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ShieldException("unsupported digest algorithm [" + MessageDigestAlgorithms.MD5 + "]. Please verify you are running on Java 7 or above", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest md5() {
|
||||
try {
|
||||
MessageDigest md5 = (MessageDigest) digest.clone();
|
||||
md5.reset();
|
||||
return md5;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new ShieldException("could not create MD5 digest", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SHA1Provider {
|
||||
|
||||
private static final MessageDigest digest;
|
||||
|
||||
static {
|
||||
try {
|
||||
digest = MessageDigest.getInstance(MessageDigestAlgorithms.SHA_1);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ShieldException("unsupported digest algorithm [" + MessageDigestAlgorithms.SHA_1 + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest sha1() {
|
||||
try {
|
||||
MessageDigest sha1 = (MessageDigest) digest.clone();
|
||||
sha1.reset();
|
||||
return sha1;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new ShieldException("could not create SHA1 digest", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SHA256Provider {
|
||||
|
||||
private static final MessageDigest digest;
|
||||
|
||||
static {
|
||||
try {
|
||||
digest = MessageDigest.getInstance(MessageDigestAlgorithms.SHA_256);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ShieldException("unsupported digest algorithm [" + MessageDigestAlgorithms.SHA_256 + "]. Please verify you are running on Java 7 or above", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest sha256() {
|
||||
try {
|
||||
MessageDigest sha = (MessageDigest) digest.clone();
|
||||
sha.reset();
|
||||
return sha;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new ShieldException("could not create [" + MessageDigestAlgorithms.SHA_256 + "] digest", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SaltProvider {
|
||||
|
||||
static final char[] ALPHABET = new char[] {
|
||||
'.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
|
||||
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
||||
};
|
||||
|
||||
public static char[] salt(int length) {
|
||||
Random random = ThreadLocalRandom.current();
|
||||
char[] salt = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
salt[i] = ALPHABET[(random.nextInt(ALPHABET.length))];
|
||||
}
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.bench;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomStrings;
|
||||
import org.elasticsearch.common.metrics.MeanMetric;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.junit.Ignore;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Ignore
|
||||
public class HasherBenchmark {
|
||||
|
||||
private static final int WARMING_ITERS = 1000;
|
||||
private static final int BENCH_ITERS = 10000;
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
test(Hasher.SSHA256).print();
|
||||
test(Hasher.MD5).print();
|
||||
test(Hasher.SHA1).print();
|
||||
test(Hasher.APR1).print();
|
||||
test(Hasher.BCRYPT4).print();
|
||||
}
|
||||
|
||||
protected static Metrics test(Hasher hasher) {
|
||||
|
||||
Metrics metrics = new Metrics(hasher);
|
||||
|
||||
System.out.print("warming up [" + hasher.name() + "]...");
|
||||
|
||||
for (int i = 0; i < WARMING_ITERS; i++) {
|
||||
SecuredString str = new SecuredString(RandomStrings.randomAsciiOfLength(ThreadLocalRandom.current(), 8).toCharArray());
|
||||
char[] hash = hasher.hash(str);
|
||||
hasher.verify(str, hash);
|
||||
}
|
||||
|
||||
System.out.println("done!");
|
||||
System.out.print("starting benchmark for [" + hasher.name() + "]...");
|
||||
|
||||
long start;
|
||||
|
||||
for (int i = 0; i < BENCH_ITERS; i++) {
|
||||
SecuredString str = new SecuredString(RandomStrings.randomAsciiOfLength(ThreadLocalRandom.current(), 8).toCharArray());
|
||||
|
||||
start = System.nanoTime();
|
||||
char[] hash = hasher.hash(str);
|
||||
metrics.hash.inc(System.nanoTime() - start);
|
||||
|
||||
start = System.nanoTime();
|
||||
hasher.verify(str, hash);
|
||||
metrics.verify.inc(System.nanoTime() - start);
|
||||
if (i % 1000000 == 0) {
|
||||
System.out.println("finished " + i + " iterations");
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("done!");
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private static class Metrics {
|
||||
|
||||
final String name;
|
||||
final MeanMetric hash = new MeanMetric();
|
||||
final MeanMetric verify = new MeanMetric();
|
||||
|
||||
public Metrics(Hasher hasher) {
|
||||
this.name = hasher.name();
|
||||
}
|
||||
|
||||
void print() {
|
||||
System.out.println(name);
|
||||
System.out.println("\tHash (total): " + TimeValue.timeValueNanos(hash.sum()).format());
|
||||
System.out.println("\tHash (avg): " + hash.mean() + " nanos");
|
||||
System.out.println("\tVerify (total): " + TimeValue.timeValueNanos(verify.sum()).format());
|
||||
System.out.println("\tVerify (avg): " + verify.mean() + " nanos");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,6 +64,11 @@ public class HasherTests extends ElasticsearchTestCase {
|
|||
testHasherSelfGenerated(Hasher.SHA2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSSHA256_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.SSHA256);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoop_SelfGenerated() throws Exception {
|
||||
testHasherSelfGenerated(Hasher.NOOP);
|
||||
|
@ -81,7 +86,9 @@ public class HasherTests extends ElasticsearchTestCase {
|
|||
assertThat(Hasher.resolve("bcrypt9"), sameInstance(Hasher.BCRYPT9));
|
||||
assertThat(Hasher.resolve("sha1"), sameInstance(Hasher.SHA1));
|
||||
assertThat(Hasher.resolve("sha2"), sameInstance(Hasher.SHA2));
|
||||
assertThat(Hasher.resolve("apr1"), sameInstance(Hasher.APR1));
|
||||
assertThat(Hasher.resolve("md5"), sameInstance(Hasher.MD5));
|
||||
assertThat(Hasher.resolve("ssha256"), sameInstance(Hasher.SSHA256));
|
||||
assertThat(Hasher.resolve("noop"), sameInstance(Hasher.NOOP));
|
||||
assertThat(Hasher.resolve("clear_text"), sameInstance(Hasher.NOOP));
|
||||
try {
|
||||
|
@ -95,7 +102,8 @@ public class HasherTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
private static void testHasherSelfGenerated(Hasher hasher) throws Exception {
|
||||
SecuredString passwd = SecuredStringTests.build("test123");
|
||||
assertTrue(hasher.verify(passwd, hasher.hash(passwd)));
|
||||
SecuredString passwd = SecuredStringTests.build(randomAsciiOfLength(10));
|
||||
char[] hash = hasher.hash(passwd);
|
||||
assertTrue(hasher.verify(passwd, hash));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue