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:
uboness 2015-03-24 01:41:27 +01:00
parent 31f3afe684
commit 336d508172
4 changed files with 260 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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