org.cloudfoundry.identity.uaa.util.CachingPasswordEncoder Maven / Gradle / Ivy
/*
* ****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* ****************************************************************************
*/
package org.cloudfoundry.identity.uaa.util;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import static org.springframework.security.crypto.util.EncodingUtils.concatenate;
/**
* Wrapper around a slow password encoder that does a fast translation in memory only
* This uses a hash to as a key to store a list of
*/
public class CachingPasswordEncoder implements PasswordEncoder {
private final MessageDigest messageDigest;
private final byte[] secret;
private final byte[] salt;
private final BytesKeyGenerator saltGenerator;
private final int iterations;
private int maxKeys = 1000;
private int maxEncodedPasswords = 5;
private boolean enabled = true;
private int expiryInSeconds = 300;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
private volatile Cache> cache = null;
private PasswordEncoder passwordEncoder;
public CachingPasswordEncoder() throws NoSuchAlgorithmException {
messageDigest = MessageDigest.getInstance("SHA-256");
this.secret = Utf8.encode(new RandomValueStringGenerator().generate());
this.saltGenerator = KeyGenerators.secureRandom();
this.salt = saltGenerator.generateKey();
iterations = 25;
buildCache();
}
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public String encode(CharSequence rawPassword) throws AuthenticationException {
//we always use the Bcrypt mechanism, we never store repeated information
return getPasswordEncoder().encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) throws AuthenticationException {
if (isEnabled()) {
String cacheKey = cacheEncode(rawPassword);
return internalMatches(cacheKey, rawPassword, encodedPassword);
} else {
return getPasswordEncoder().matches(rawPassword, encodedPassword);
}
}
protected Set getOrCreateHashList(String cacheKey) {
Set result = cache.getIfPresent(cacheKey);
if (result==null) {
if (cache.size()>=getMaxKeys()) {
cache.invalidateAll();
}
cache.put(cacheKey, Collections.synchronizedSet(new LinkedHashSet<>()));
}
return cache.getIfPresent(cacheKey);
}
private boolean internalMatches(String cacheKey, CharSequence rawPassword, String encodedPassword) {
Set cacheValue = cache.getIfPresent(cacheKey);
boolean result = false;
List searchList = (cacheValue!=null ? new ArrayList(cacheValue) : Collections.emptyList());
for (String encoded : searchList) {
if (hashesEquals(encoded, encodedPassword)) {
result = true;
break;
}
}
if (!result) {
String encoded = BCrypt.hashpw(rawPassword.toString(), encodedPassword);
if (hashesEquals(encoded, encodedPassword)) {
result = true;
cacheValue = getOrCreateHashList(cacheKey);
if (cacheValue!=null) {
//this list should never grow very long.
//Only if you store multiple versions of the same password more than once
if (cacheValue.size() >= getMaxEncodedPasswords()) {
cacheValue.clear();
}
cacheValue.add(encoded);
}
}
}
return result;
}
protected String cacheEncode(CharSequence rawPassword) {
byte[] digest = digest(rawPassword);
return new String(Hex.encode(digest));
}
private byte[] digest(CharSequence rawPassword) {
byte[] digest = digest(concatenate(salt, secret, Utf8.encode(rawPassword)));
return concatenate(salt, digest);
}
private byte[] digest(byte[] value) {
synchronized (messageDigest) {
for (int i = 0; i < iterations; i++) {
value = messageDigest.digest(value);
}
return value;
}
}
private boolean hashesEquals(String a, String b) {
char[] caa = a.toCharArray();
char[] cab = b.toCharArray();
if (caa.length != cab.length) {
return false;
}
byte ret = 0;
for (int i = 0; i < caa.length; i++) {
ret |= caa[i] ^ cab[i];
}
return ret == 0;
}
public int getMaxKeys() {
return maxKeys;
}
public void setMaxKeys(int maxKeys) {
this.maxKeys = maxKeys;
buildCache();
}
public int getMaxEncodedPasswords() {
return maxEncodedPasswords;
}
public void setMaxEncodedPasswords(int maxEncodedPasswords) {
this.maxEncodedPasswords = maxEncodedPasswords;
buildCache();
}
public long getNumberOfKeys() {
return cache.size();
}
public ConcurrentMap> asMap() {
return cache.asMap();
}
public int getExpiryInSeconds() {
return expiryInSeconds;
}
public void setExpiryInSeconds(int expiryInSeconds) {
this.expiryInSeconds = expiryInSeconds;
buildCache();
}
protected void buildCache() {
cache = CacheBuilder.newBuilder()
.expireAfterWrite(expiryInSeconds, TimeUnit.SECONDS)
.build();
}
}