/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.web.security.hash;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.web.security.security.SecureRandomService;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
/**
* A {@link SaltedHashService} implementation using a {@link javax.crypto.SecretKeyFactory} for salted hashing.
*
* @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a>
*
*/
public class JCASaltedHashService implements SaltedHashService {
public static final String PBKDF2_WITH_HMAC_SHA1 = "PBKDF2WithHmacSHA1";
public static final int DEFAULT_SALT_BYTE_LENGTH = 9;
public static final int DEFAULT_HASH_BYTE_LENGTH = 9;
public static final int DEFAULT_ITERATION_COUNT = 1000;
/**
* The number of iterations submitted to {@link SecretKeyFactory#generateSecret(java.security.spec.KeySpec)}.
* <p>
* From <a href="http://en.wikipedia.org/wiki/PBKDF2">http://en.wikipedia.org/wiki/PBKDF2</a> (retrieved 2013-01-08):
* <cite>When the
* standard was written in 2000, the recommended minimum number of iterations was 1000, but the parameter is intended to be
* increased over time as CPU speeds increase.
* [...] Apple's iOS 3 uses 2,000 iterations and iOS 4 uses 10,000.
* </cite>
*/
private int iterationCount;
/**
* Salt length in bytes. Use values divisible by 3 to get salted hash strings without Base64 padding characters.
*/
private int saltByteLength;
/**
* Hash length in bytes. Use values divisible by 3 to get salted hash strings without Base64 padding characters.
*/
private int hashByteLength;
/**
* Name of the hashing algorithm which is submitted to {@link SecretKeyFactory#getInstance(String)}.
*/
private String algorithm;
/**
* Pluggable codec for serialization and deserialization of {@link SaltedHash} objects.
*/
private SaltedHashCodec codec;
/**
*
*/
private final Logger log = LoggerFactory.getLogger(JCASaltedHashService.class);
/**
* @param algorithm
* @param iterationCount
* @param saltLength
* @param hashLength
* @param codec
*/
public JCASaltedHashService(String algorithm, int iterationCount, int saltLength, int hashLength, SaltedHashCodec codec) {
super();
this.algorithm = algorithm;
this.iterationCount = iterationCount;
this.saltByteLength = saltLength;
this.hashByteLength = hashLength;
this.codec = codec;
}
/**
*
*/
public JCASaltedHashService() {
this(PBKDF2_WITH_HMAC_SHA1, DEFAULT_ITERATION_COUNT, DEFAULT_SALT_BYTE_LENGTH, DEFAULT_HASH_BYTE_LENGTH,
XmlSafeSaltedHashCodec.INSTANCE);
}
/*
* (non-Javadoc)
*
* @see org.exoplatform.web.security.hash.SaltedHashService#getSaltedHash(java.lang.String, java.security.SecureRandom)
*/
@Override
public String getSaltedHash(String password) throws SaltedHashException {
try {
byte[] salt = new byte[saltByteLength];
PortalContainer container = PortalContainer.getInstance();
SecureRandom random = ((SecureRandomService) container.getComponentInstanceOfType(SecureRandomService.class)).getSecureRandom();
random.nextBytes(salt);
SaltedHash saltedHash = new SaltedHash(algorithm, iterationCount, salt, hash(this.algorithm, password, salt,
iterationCount, hashByteLength));
return codec.encode(saltedHash);
} catch (InvalidKeySpecException e) {
throw new SaltedHashException("Could not create salted hash from password.", e);
} catch (NoSuchAlgorithmException e) {
throw new SaltedHashException("Could not create salted hash from password.", e);
}
}
/*
* (non-Javadoc)
*
* @see org.exoplatform.web.security.hash.SaltedHashService#validate(java.lang.String, java.lang.String)
*/
@Override
public boolean validate(String password, String encodedSaltedHash) throws SaltedHashException {
try {
SaltedHash saltedHash = codec.decode(encodedSaltedHash);
byte[] expectedHash = hash(saltedHash.getAlgorithm(), password, saltedHash.getSalt(),
saltedHash.getIterationCount(), saltedHash.getHash().length);
if (log.isDebugEnabled()) {
log.debug("About to validate submitted hash " + Arrays.toString(expectedHash) + " against stored hash "
+ Arrays.toString(saltedHash.getHash()));
}
return Arrays.equals(expectedHash, saltedHash.getHash());
} catch (NoSuchAlgorithmException e) {
throw new SaltedHashException("Could not validate password against salted hash.", e);
} catch (InvalidKeySpecException e) {
throw new SaltedHashException("Could not validate password against salted hash.", e);
}
}
/**
* Computes the hash as a byte array.
*
* @param algorithm
* @param password
* @param salt
* @param iterationCount
* @param hashLength
* @return
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
private static byte[] hash(String algorithm, String password, byte[] salt, int iterationCount, int hashLength)
throws InvalidKeySpecException, NoSuchAlgorithmException {
SecretKey key = SecretKeyFactory.getInstance(algorithm).generateSecret(
new PBEKeySpec(password.toCharArray(), salt, iterationCount, hashLength * 8));
return key.getEncoded();
}
/**
* @see #iterationCount
* @return the iterationCount
*/
public int getIterationCount() {
return iterationCount;
}
/**
* @see #iterationCount
* @param iterationCount the iterationCount to set
*/
public void setIterationCount(int iterationCount) {
this.iterationCount = iterationCount;
}
/**
* @see #saltByteLength
* @return the saltLength
*/
public int getSaltByteLength() {
return saltByteLength;
}
/**
* @see #saltByteLength
* @param saltLength the saltLength to set
*/
public void setSaltLength(int saltLength) {
this.saltByteLength = saltLength;
}
/**
* @see #hashByteLength
* @return the hashLength
*/
public int getHashByteLength() {
return hashByteLength;
}
/**
* @see #hashByteLength
* @param hashLength the hashLength to set
*/
public void setHashLength(int hashLength) {
this.hashByteLength = hashLength;
}
/**
* @see #algorithm
* @return the algorithm
*/
public String getAlgorithm() {
return algorithm;
}
/**
* @see #algorithm
* @param algorithm the algorithm to set
*/
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
/**
* @see #codec
* @return the codec
*/
public SaltedHashCodec getCodec() {
return codec;
}
/**
* @see #codec
* @param codec the codec to set
*/
public void setCodec(SaltedHashCodec codec) {
this.codec = codec;
}
}