/*
 * 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.digital.envelope.service;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAKeyGenParameterSpec;

import com.scytl.cryptolib.api.digital.envelope.DigitalEnvelopeServiceAPI;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.keypair.configuration.ConfigEncryptionKeyPairAlgorithmAndSpec;
import com.scytl.cryptolib.asymmetric.keypair.constants.KeyPairConstants;
import com.scytl.cryptolib.commons.constants.Constants;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.digital.envelope.configuration.DigitalEnvelopePolicy;
import com.scytl.cryptolib.digital.envelope.configuration.DigitalEnvelopePolicyFromProperties;
import com.scytl.cryptolib.digital.envelope.cryptoapi.CryptoAPIDigitalEnvelope;
import com.scytl.cryptolib.digital.envelope.factory.CryptoDigitalEnvelope;
import com.scytl.cryptolib.digital.envelope.factory.CryptoDigitalEnvelopeGenerator;
import com.scytl.cryptolib.digital.envelope.factory.CryptoDigitalEnvelopeOpener;
import com.scytl.cryptolib.digital.envelope.factory.DigitalEnvelopeFactory;

/**
 * Class which implements the interface {@link DigitalEnvelopeServiceAPI}.
 * <p>
 * Instances of this class are immutable.
 */
public class DigitalEnvelopeService implements DigitalEnvelopeServiceAPI {

    private final CryptoDigitalEnvelopeGenerator _digitalEnvelopeGenerator;

    private final CryptoDigitalEnvelopeOpener _digitalEnvelopeOpener;

    private final String _asymmetricKeyPairAlgorithm;

    private final int _asymmetricKeyByteLength;

    /**
     * Default constructor which initializes all properties to default values.
     * These default values are obtained from the path indicated by
     * {@link Constants#CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH}.
     * 
     * @throws GeneralCryptoLibException
     *             if the default properties are invalid.
     */
    public DigitalEnvelopeService() throws GeneralCryptoLibException {

        this(Constants.CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH);
    }

    /**
     * Constructor which initializes its state using the properties file located
     * at the specified path.
     * <P>
     * Note: a single file is used, which must contain all the properties
     * considered by this module.
     * 
     * @param path
     *            the path of the properties file to be used to configure the
     *            service.
     * @throws GeneralCryptoLibException
     *             if the path is invalid or the properties are invalid.
     */
    public DigitalEnvelopeService(final String path)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(path, "Properties file path");

        DigitalEnvelopePolicy digitalEnvelopePolicy =
            new DigitalEnvelopePolicyFromProperties(path);

        DigitalEnvelopeFactory digitalEnvelopeFactory =
            new DigitalEnvelopeFactory(digitalEnvelopePolicy);

        _digitalEnvelopeGenerator =
            digitalEnvelopeFactory.createGenerator();

        _digitalEnvelopeOpener = digitalEnvelopeFactory.createOpener();

        ConfigEncryptionKeyPairAlgorithmAndSpec keyPairSpec =
            digitalEnvelopePolicy.getEncryptingKeyPairAlgorithmAndSpec();
        _asymmetricKeyPairAlgorithm = keyPairSpec.getAlgorithm();
        if (_asymmetricKeyPairAlgorithm.equals(KeyPairConstants.RSA_ALG)) {
            _asymmetricKeyByteLength =
                ((RSAKeyGenParameterSpec) keyPairSpec.getSpec())
                    .getKeysize() / Byte.SIZE;
        } else {
            throw new CryptoLibException(
                "Unrecognized asymmetric key pair algorithm: "
                    + _asymmetricKeyPairAlgorithm);
        }
    }

    @Override
    public CryptoDigitalEnvelope createDigitalEnvelope(final byte[] data,
            final PublicKey... publicKeys)
            throws GeneralCryptoLibException {

        validateGeneratorInput(data, publicKeys);

        return _digitalEnvelopeGenerator.generate(data, publicKeys);
    }

    @Override
    public byte[] openDigitalEnvelope(
            final CryptoAPIDigitalEnvelope digitalEnvelope,
            final PrivateKey privateKey) throws GeneralCryptoLibException {

        validateOpenerInput(digitalEnvelope, privateKey);

        return _digitalEnvelopeOpener.open(digitalEnvelope, privateKey);
    }

    private void validateGeneratorInput(final byte[] data,
            final PublicKey... publicKeys)
            throws GeneralCryptoLibException {

        Validate.notNullOrEmpty(data, "Data");
        Validate.notNullOrEmpty(publicKeys, "Public key array");
        if (publicKeys.length == 1) {
            Validate.notNull(publicKeys[0], "Public key");
            Validate.notNullOrEmpty(publicKeys[0].getEncoded(),
                "Public key content");
            validateKeySize(publicKeys[0]);
        } else {
            for (PublicKey key : publicKeys) {
                Validate.notNull(key, "A public key");
                Validate.notNullOrEmpty(key.getEncoded(),
                    "Content of a public key");
                validateKeySize(key);
            }
        }
    }

    private void validateOpenerInput(
            final CryptoAPIDigitalEnvelope digitalEnvelope,
            final PrivateKey privateKey) throws GeneralCryptoLibException {

        Validate.notNull(digitalEnvelope, "Digital envelope");
        Validate.notNull(privateKey, "Private key");
        Validate.notNullOrEmpty(privateKey.getEncoded(),
            "Private key content");
        validateKeySize(privateKey);
    }

    private void validateKeySize(final PublicKey publicKey)
            throws GeneralCryptoLibException {

        if (_asymmetricKeyPairAlgorithm.equals(KeyPairConstants.RSA_ALG)) {
            int publicKeyLength =
                ((RSAPublicKey) publicKey).getModulus().bitLength()
                    / Byte.SIZE;

            Validate
                .equals(
                    publicKeyLength,
                    _asymmetricKeyByteLength,
                    "Byte length of public key",
                    "byte length of corresponding key in cryptographic policy for digital envelope service");
        } else {
            throw new GeneralCryptoLibException(
                "Unrecognized asymmetric key pair algorithm: "
                    + _asymmetricKeyPairAlgorithm);
        }
    }

    private void validateKeySize(final PrivateKey privateKey)
            throws GeneralCryptoLibException {

        if (_asymmetricKeyPairAlgorithm.equals(KeyPairConstants.RSA_ALG)) {
            int privateKeyLength =
                ((RSAPrivateKey) privateKey).getModulus().bitLength()
                    / Byte.SIZE;

            Validate
                .equals(
                    privateKeyLength,
                    _asymmetricKeyByteLength,
                    "Byte length of private key",
                    "byte length of corresponding key in cryptographic policy for digital envelope service");
        } else {
            throw new GeneralCryptoLibException(
                "Unrecognized asymmetric key pair algorithm: "
                    + _asymmetricKeyPairAlgorithm);
        }
    }
}
