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

import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAKeyGenParameterSpec;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.asymmetric.utils.KeyPairConverterAPI;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.cipher.configuration.AsymmetricCipherPolicy;
import com.scytl.cryptolib.asymmetric.cipher.configuration.AsymmetricCipherPolicyFromProperties;
import com.scytl.cryptolib.asymmetric.cipher.factory.AsymmetricCipherFactory;
import com.scytl.cryptolib.asymmetric.cipher.factory.CryptoAsymmetricCipher;
import com.scytl.cryptolib.asymmetric.keypair.configuration.ConfigEncryptionKeyPairAlgorithmAndSpec;
import com.scytl.cryptolib.asymmetric.keypair.configuration.KeyPairPolicy;
import com.scytl.cryptolib.asymmetric.keypair.configuration.KeyPairPolicyFromProperties;
import com.scytl.cryptolib.asymmetric.keypair.constants.KeyPairConstants;
import com.scytl.cryptolib.asymmetric.keypair.factory.CryptoKeyPairGenerator;
import com.scytl.cryptolib.asymmetric.keypair.factory.KeyPairGeneratorFactory;
import com.scytl.cryptolib.asymmetric.signer.configuration.DigitalSignerPolicy;
import com.scytl.cryptolib.asymmetric.signer.configuration.DigitalSignerPolicyFromProperties;
import com.scytl.cryptolib.asymmetric.signer.factory.CryptoDigitalSigner;
import com.scytl.cryptolib.asymmetric.signer.factory.DigitalSignerFactory;
import com.scytl.cryptolib.asymmetric.utils.KeyPairConverter;
import com.scytl.cryptolib.asymmetric.xml.signer.configuration.XmlDigitalSignerPolicy;
import com.scytl.cryptolib.asymmetric.xml.signer.configuration.XmlDigitalSignerPolicyFromProperties;
import com.scytl.cryptolib.asymmetric.xml.signer.factory.CryptoXmlDigitalSigner;
import com.scytl.cryptolib.asymmetric.xml.signer.factory.XmlDigitalSignerFactory;
import com.scytl.cryptolib.commons.constants.Constants;
import com.scytl.cryptolib.commons.utils.validations.Validate;

/**
 * Class which implements {@link AsymmetricServiceAPI}.
 * <p>
 * Instances of this class are immutable.
 */
public final class AsymmetricService implements AsymmetricServiceAPI {

    private final CryptoKeyPairGenerator _signingKeyPairGenerator;

    private final CryptoKeyPairGenerator _encryptionKeyPairGenerator;

    private final CryptoAsymmetricCipher _asymmetricCipher;

    private final CryptoDigitalSigner _digitalSigner;

    private final CryptoXmlDigitalSigner _xmlDigitalSigner;

    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 AsymmetricService() 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 AsymmetricService(final String path)
            throws GeneralCryptoLibException {

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

        KeyPairPolicy keyPairPolicy =
            new KeyPairPolicyFromProperties(path);
        _signingKeyPairGenerator =
            new KeyPairGeneratorFactory(keyPairPolicy).createSigning();
        _encryptionKeyPairGenerator =
            new KeyPairGeneratorFactory(keyPairPolicy).createEncryption();

        AsymmetricCipherPolicy asymmetricCipherPolicy =
            new AsymmetricCipherPolicyFromProperties(path);
        _asymmetricCipher =
            new AsymmetricCipherFactory(asymmetricCipherPolicy).create();

        DigitalSignerPolicy digitalSignerPolicy =
            new DigitalSignerPolicyFromProperties(path);
        _digitalSigner =
            new DigitalSignerFactory(digitalSignerPolicy).create();

        XmlDigitalSignerPolicy xmlDigitalSignerPolicy =
            new XmlDigitalSignerPolicyFromProperties(path);
        _xmlDigitalSigner =
            new XmlDigitalSignerFactory(xmlDigitalSignerPolicy).create();

        ConfigEncryptionKeyPairAlgorithmAndSpec keyPairSpec =
            keyPairPolicy.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 KeyPair getKeyPairForSigning() {

        return _signingKeyPairGenerator.genKeyPair();
    }

    @Override
    public KeyPair getKeyPairForEncryption() {

        return _encryptionKeyPairGenerator.genKeyPair();
    }

    @Override
    public byte[] encrypt(final PublicKey key, final byte[] data)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Public key");
        Validate.notNullOrEmpty(key.getEncoded(), "Public key content");
        validateKeySize(key, "encryption");
        Validate.notNullOrEmpty(data, "Data");

        return _asymmetricCipher.encrypt(key, data);
    }

    @Override
    public byte[] decrypt(final PrivateKey key, final byte[] data)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Private key");
        Validate.notNullOrEmpty(key.getEncoded(), "Private key content");
        validateKeySize(key, "decryption");
        Validate.notNullOrEmpty(data, "Encrypted data");

        return _asymmetricCipher.decrypt(key, data);
    }

    @Override
    public byte[] sign(final PrivateKey key, final byte[]... data)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Private key");
        Validate.notNullOrEmpty(key.getEncoded(), "Private key content");
        validateKeySize(key, "signing");
        Validate.notNullOrEmpty(data, "Data element array");
        if (data.length == 1) {
            Validate.notNullOrEmpty(data[0], "Data");
        } else {
            for (byte[] dataElement : data) {
                Validate.notNullOrEmpty(dataElement, "A data element");
            }
        }

        return _digitalSigner.sign(key, data);
    }

    @Override
    public byte[] sign(final PrivateKey key, final InputStream in)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Private key");
        Validate.notNullOrEmpty(key.getEncoded(), "Private key content");
        validateKeySize(key, "signing");
        Validate.notNull(in, "Data input stream");

        return _digitalSigner.sign(key, in);
    }

    @Override
    public boolean verifySignature(final byte[] signatureBytes,
            final PublicKey key, final byte[]... data)
            throws GeneralCryptoLibException {

        Validate.notNullOrEmpty(signatureBytes, "Signature");
        Validate.notNull(key, "Public key");
        Validate.notNullOrEmpty(key.getEncoded(), "Public key content");
        validateKeySize(key, "signature verification");
        Validate.notNullOrEmpty(data, "Data element array");
        if (data.length == 1) {
            Validate.notNullOrEmpty(data[0], "Data");
        } else {
            for (byte[] dataElement : data) {
                Validate.notNullOrEmpty(dataElement, "A data element");
            }
        }

        return _digitalSigner.verifySignature(signatureBytes, key, data);
    }

    @Override
    public boolean verifySignature(final byte[] signatureBytes,
            final PublicKey key, final InputStream in)
            throws GeneralCryptoLibException {

        Validate.notNullOrEmpty(signatureBytes, "Signature");
        Validate.notNull(key, "Public key");
        Validate.notNullOrEmpty(key.getEncoded(), "Public key content");
        validateKeySize(key, "signature verification");
        Validate.notNull(in, "Data input stream");

        return _digitalSigner.verifySignature(signatureBytes, key, in);
    }

    /**
     * Introduced as a part of an XML signer functionality to the service.
     */
    @Override
    public void signXml(final PrivateKey key,
            final Certificate[] certChain, final InputStream inStream,
            final OutputStream outStream, final String signatureParentNode)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Private key");
        Validate.notNullOrEmpty(key.getEncoded(), "Private key content");
        validateKeySize(key, "XML signing");
        if (certChain != null) {
            for (Certificate cert : certChain) {
                Validate.notNull(cert, "Certificate in chain");
                try {
                    Validate.notNullOrEmpty(cert.getEncoded(),
                        "Content of certificate in chain");
                } catch (CertificateEncodingException e) {
                    throw new GeneralCryptoLibException(
                        "Could not validate content of certificate in chain.",
                        e);
                }
            }
        }
        Validate.notNull(inStream, "XML input stream");
        Validate.notNull(outStream, "Signed XML output stream");
        Validate.notNullOrBlank(signatureParentNode,
            "XML signature parent node");

        _xmlDigitalSigner.sign(key, certChain, inStream, outStream,
            signatureParentNode);
    }

    /**
     * Introduced as a part of an XML signer functionality to the service.
     */
    @Override
    public boolean verifyXmlSignature(final PublicKey key,
            final InputStream inStream, final String signatureParentNode)
            throws GeneralCryptoLibException {

        Validate.notNull(key, "Public key");
        Validate.notNullOrEmpty(key.getEncoded(), "Public key content");
        validateKeySize(key, "XML signature verification");
        Validate.notNull(inStream, "Signed XML input stream");
        Validate.notNullOrBlank(signatureParentNode,
            "XML signature parent node");

        return _xmlDigitalSigner
            .verify(key, inStream, signatureParentNode);
    }

    /**
     * Introduced as a part of an import and export of private and public keys
     * to and from PEM Strings.
     */
    @Override
    public KeyPairConverterAPI getKeyPairConverter() {
        return new KeyPairConverter();
    }

    private void validateKeySize(final PublicKey publicKey,
            final String keyType) throws GeneralCryptoLibException {

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

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

    private void validateKeySize(final PrivateKey privateKey,
            final String keyType) throws GeneralCryptoLibException {

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

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