/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.primitives.derivation.factory;

import static java.text.MessageFormat.format;

import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIDerivedKey;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIPBKDFDeriver;
import com.scytl.cryptolib.primitives.derivation.constants.DerivationConstants;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomBytes;

/**
 * Class that provides the functionality for password based key derivation. This
 * class implements {@link CryptoAPIPBKDFDeriver}
 * <P>
 * Note that the {@link SecretKeyFactory} that is used to generate the derived
 * password, as well as the {@link CryptoRandomBytes}, which is used to generate
 * the random bytes, are stored in this class and cannot be modified.
 */
public final class CryptoPBKDFDeriver implements CryptoAPIPBKDFDeriver {

    private static final String KEY_DERIVATION_ERROR = "Error while deriving the key";

    private final SecretKeyFactory _secretKeyFactory;

    private final CryptoRandomBytes _cryptoRandomBytes;

    private final int _iterations;

    private final int _keyLength;

    private final int _saltBytesLength;

    /**
     * Constructs a new CryptoPBKDFDeriver with the specified value.
     * 
     * @param secretKeyFactory
     *            an already initialized secret key factory.
     * @param cryptoRandomBytes
     *            the random byte generator used to generate the salt.
     * @param saltBitLength
     *            the length in bits of the salt.
     * @param iterations
     *            the number of iterations to be performed while deriving the
     *            password.
     * @param keyLength
     *            the length in bytes of the derived key.
     * @throws GeneralCryptoLibException
     *             if the salt bit length is invalid.
     */
    CryptoPBKDFDeriver(final SecretKeyFactory secretKeyFactory,
            final CryptoRandomBytes cryptoRandomBytes,
            final int saltBitLength, final int iterations,
            final int keyLength) throws GeneralCryptoLibException {

        _secretKeyFactory = secretKeyFactory;

        _cryptoRandomBytes = cryptoRandomBytes;

        _iterations = iterations;

        _keyLength = keyLength;

        if (saltBitLength % Byte.SIZE != 0) {
            throw new CryptoLibException(
                "The salt bit lengths is not a multiple of the number of bits per byte.");
        }

        Validate.isPositive(saltBitLength, "Salt length in bits");

        _saltBytesLength = saltBitLength / Byte.SIZE;
    }

    /**
     * Derives a {@link CryptoAPIDerivedKey} from a given password. The password
     * must contain a minimum of 16 characters and a maximum of 1000 characters.
     * 
     * @param password
     *            the password.
     * @return the derived {@link CryptoAPIDerivedKey}.
     * @throws GeneralCryptoLibException
     *             if the password is invalid.
     */
    @Override
    public CryptoAPIDerivedKey deriveKey(final char[] password)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(password, "Password");
        Validate.inRange(password.length,
            DerivationConstants.MINIMUM_PBKDF_PASSWORD_LENGTH,
            DerivationConstants.MAXIMUM_PBKDF_PASSWORD_LENGTH,
            "Password length", "", "");

        byte[] salt = generateRandomSalt();

        return deriveKey(password, salt);
    }

    /**
     * Derives a {@link CryptoAPIDerivedKey} from a given password and a given
     * salt. The password must contain a minimum of 16 characters and a maximum
     * of 1000 characters.
     * 
     * @param password
     *            the password.
     * @param salt
     *            the salt.
     * @return the derived {@link CryptoAPIDerivedKey}.
     * @throws GeneralCryptoLibException
     *             if the password or the salt are invalid.
     */
    @Override
    public CryptoAPIDerivedKey deriveKey(final char[] password,
            final byte[] salt) throws GeneralCryptoLibException {

        Validate.notNullOrBlank(password, "Password");
        Validate.inRange(password.length,
            DerivationConstants.MINIMUM_PBKDF_PASSWORD_LENGTH,
            DerivationConstants.MAXIMUM_PBKDF_PASSWORD_LENGTH,
            "Password length", "", "");
        Validate.notNullOrEmpty(salt, "Salt");
        Validate.notLessThan(salt.length,
            DerivationConstants.MINIMUM_PBKDF_SALT_LENGTH_IN_BYTES,
            "Salt length", "minimum allowed PBKDF salt length");

        PBEKeySpec pbeKeySpec =
            new PBEKeySpec(password, salt, _iterations, _keyLength);

        SecretKey key;
        try {
            key = _secretKeyFactory.generateSecret(pbeKeySpec);
        } catch (InvalidKeySpecException e) {
            throw new CryptoLibException(KEY_DERIVATION_ERROR, e);
        }
        byte[] encoded = key.getEncoded();
        // The only purpose of the code below is not to let the JIT
        // compiler to eliminate the key instance using something like
        // scalar replacement optimization. Otherwise it can lead to
        // unpredictable errors when the key content is an array of zeros.
        // See CRYP-1100 for details. Possibly in the future versions of
        // JVM the problem will disappear automatically.
        if (Arrays.equals(encoded, new byte[encoded.length])) {
            System.err.println(
                format("Derived key ''{0}'' is probably corrupted.", key));
        }
        return new CryptoDerivedKey(encoded);
    }

    /**
     * Generates a random salt.
     * 
     * @return the generated salt.
     */
    public byte[] generateRandomSalt() {
        try {
            return _cryptoRandomBytes.nextRandom(getSaltBytesLength());
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    /**
     * Returns the length of the salt in bytes.
     * 
     * @return the salt byte length.
     */
    public int getSaltBytesLength() {
        return _saltBytesLength;
    }
}
