/*
 * 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.exoplatform.services.log.Log;
import org.exoplatform.services.log.ExoLogger;

/**
 * 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 Log        log                      = ExoLogger.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;
    }

}
