/*
 * 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.products.ov.channel.cc.commons.keymanagement;

import static java.nio.file.Files.newInputStream;
import static java.text.MessageFormat.format;
import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Date;

import javax.security.auth.x500.X500Principal;
import javax.sql.DataSource;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.certificates.CertificatesServiceAPI;
import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.api.stores.StoresServiceAPI;
import com.scytl.cryptolib.api.stores.bean.KeyStoreType;
import com.scytl.cryptolib.certificates.constants.X509CertificateConstants;
import com.scytl.cryptolib.certificates.utils.LdapHelper;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.EncryptionParameters;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.channel.cc.commons.keymanagement.log.ControlComponentsCommonsLogConstants;
import com.scytl.products.ov.channel.cc.commons.keymanagement.log.ControlComponentsCommonsLogEvents;
import com.scytl.products.ov.channel.cc.commons.slogger.ServiceTransactionInfoProvider;
import com.scytl.products.ov.channel.cc.commons.util.FingerprintGenerator;

/**
 * <p>
 * Implementation of {@link KeyManager}.
 * <p>
 * This implementation uses an Oracle database as persistence storage of the
 * keys. Client should respect the following rules while implementing the
 * transaction management:
 * <ul>
 * <li>The supplied {@link DataSource} must be configured with the auto-commit
 * switched off.</li>
 * <li>All the methods like
 * {@code activateNodeKeys, createAndActivateNodeKeys, createXXX, getXXX, hasValidElectionSigningKeys}
 * must be invoked in transaction.</li>
 * <li>All the checked exceptions but the generic {@link KeyManagementException}
 * indicates some precondition check failure. They are thrown before any data is
 * actually written to the database. Thus in such a case the client can proceed
 * with the current transaction without a risk to damage the data.</li>
 * </ul>
 */
public final class KeyManagerImpl implements KeyManager {

    private final AsymmetricServiceAPI asymmetricService;

    private final StoresServiceAPI storesService;

    private final Generator generator;

    private final Database database;

    private final Cache cache;

    private final SecureLoggingWriter secureLoggingWriter;

    private final FingerprintGenerator fingerprintGenerator;

    private final ServiceTransactionInfoProvider serviceTransactionInfoProvider;

    private final String controlComponentId;

    private volatile NodeKeys nodeKeys;

    /**
     * Constructor. For internal use only.
     *
     * @param asymmetricService
     *            the asymmetric service
     * @param storesService
     *            the stores service
     * @param generator
     *            the generator
     * @param database
     *            the database
     * @param cache
     */
    KeyManagerImpl(AsymmetricServiceAPI asymmetricService, StoresServiceAPI storesService, Generator generator,
            Database database, Cache cache, SecureLoggingWriter secureLoggingWriter,
            FingerprintGenerator fingerprintGenerator, ServiceTransactionInfoProvider serviceTransactionInfoProvider,
            String controlComponentId) {
        this.asymmetricService = asymmetricService;
        this.storesService = storesService;
        this.generator = generator;
        this.database = database;
        this.cache = cache;
        this.secureLoggingWriter = secureLoggingWriter;
        this.fingerprintGenerator = fingerprintGenerator;
        this.serviceTransactionInfoProvider = serviceTransactionInfoProvider;
        this.controlComponentId = controlComponentId;
    }

    @Override
    public synchronized void activateNodeKeys(PasswordProtection password)
            throws KeyNotFoundException, InvalidPasswordException, KeyManagementException {
        requireNonNull(password, "Password is null.");
        activateNodeKeys(database.loadNodeKeys(password));
    }

    @Override
    public void createAndActivateNodeKeys(InputStream stream, String alias, PasswordProtection protection)
            throws InvalidKeyStoreException, InvalidPasswordException, InvalidNodeCAException,
            KeyAlreadyExistsException, KeyManagementException, IOException {
        requireNonNull(stream, "Stream is null.");
        KeyStore store;
        try {
            store = storesService.loadKeyStore(KeyStoreType.PKCS12, stream, protection.getPassword());
        } catch (GeneralCryptoLibException e) {
            if (e.getCause() instanceof IOException) {
                if (e.getCause().getCause() instanceof UnrecoverableEntryException) {
                    throw new InvalidPasswordException("Password is invalid.", e);
                } else {
                    throw (IOException) e.getCause();
                }
            } else {
                throw new KeyManagementException("Failed to create and activate node keys.", e);
            }
        }
        createAndActivateNodeKeys(store, alias, protection);
    }

    @Override
    public void createAndActivateNodeKeys(KeyStore store, String alias, PasswordProtection password)
            throws InvalidKeyStoreException, InvalidPasswordException, InvalidNodeCAException,
            KeyAlreadyExistsException, KeyManagementException {
        requireNonNull(store, "Store is null.");
        requireNonNull(alias, "Alias is null.");
        requireNonNull(password, "Password is null");
        PrivateKeyEntry entry;
        try {
            if (!store.entryInstanceOf(alias, PrivateKeyEntry.class)) {
                throw new InvalidKeyStoreException(format("Invalid CCN CA alias ''{0}''", alias));
            }
            entry = (PrivateKeyEntry) store.getEntry(alias, password);
        } catch (UnrecoverableEntryException e) {
            throw new InvalidPasswordException("Password is invalid.", e);
        } catch (KeyStoreException | NoSuchAlgorithmException e) {
            throw new KeyManagementException("Failed to create and activate node keys.", e);
        }
        Certificate[] certificateChain = entry.getCertificateChain();
        for (Certificate certificate : certificateChain) {
            if (!(certificate instanceof X509Certificate)) {
                throw new InvalidNodeCAException("Certificate chain contains non-X509 certificates.");
            }
        }
        createAndActivateNodeKeys(entry.getPrivateKey(), (X509Certificate[]) certificateChain, password);
    }

    @Override
    public void createAndActivateNodeKeys(Path file, String alias, PasswordProtection password)
            throws InvalidKeyStoreException, InvalidPasswordException, InvalidNodeCAException,
            KeyAlreadyExistsException, KeyManagementException, NoSuchFileException, IOException {
        requireNonNull(file, "File is null.");
        try (InputStream stream = newInputStream(file)) {
            createAndActivateNodeKeys(stream, alias, password);
        }
    }

    @Override
    public void createAndActivateNodeKeys(PrivateKey nodeCAPrivateKey, X509Certificate[] nodeCACertificateChain,
            PasswordProtection password)
            throws InvalidNodeCAException, KeyAlreadyExistsException, KeyManagementException {
        requireNonNull(nodeCAPrivateKey, "CCN CA private key is null.");
        requireNonNull(nodeCACertificateChain, "CCN CA certificate chain is null.");
        if (nodeCACertificateChain.length == 0) {
            throw new IllegalArgumentException("CCN CA certificate chain is empty.");
        }
        checkNodeCAKeysMatch(nodeCAPrivateKey, nodeCACertificateChain[0].getPublicKey());
        requireNonNull(password, "Password is null.");
        synchronized (this) {
            NodeKeys nodeKeys = generator.generateNodeKeys(nodeCAPrivateKey, nodeCACertificateChain);
            database.saveNodeKeys(nodeKeys, password);
            activateNodeKeys(nodeKeys);
        }
    }

    @Override
    public void createChoiceCodeKeys(ChoiceCodeKeysSpec spec)
            throws KeyAlreadyExistsException, KeyNotFoundException, KeyManagementException,
            FingerprintGeneratorException {
        requireNonNull(spec, "Spec is null.");
        checkNodeKeysActivated();
        ElectionSigningKeys electionSigningKeys = database.loadElectionSigningKeys(spec.electionEventId());
        ChoiceCodeKeys keys = null;
        try {
            keys = generator.generateChoiceCodeKeys(spec, electionSigningKeys);
        } catch (UnsupportedEcryptionParametersException | ElGamalKeyPairFailedGenerationException e) {
            // [SL GENCCKEYS-2]
            logErrorGeneratingKeyPairCEK(spec);
            throw e;
        } catch (FailedSigningException e) {
            // [SL GENCCKEYSSIGN-6]
            logErrorSigningPkCEK(spec, electionSigningKeys.publicKey());
            throw e;
        }
        // pubkeyId is a fingerprint needed for secureLogs
        ElGamalPublicKey decryptionPublicKey = keys.decryptionPublicKey();
        String decryptionPubkeyId = fingerprintGenerator.generate(decryptionPublicKey);
        ElGamalPublicKey generationPublicKey = keys.generationPublicKey();
        String generationPubkeyId = fingerprintGenerator.generate(generationPublicKey);

        String pubKeyIds = String.join(",", generationPubkeyId, decryptionPubkeyId);
        // [SL GENCCEK-1]
        // Generate Choice Codes Generation/Decryption key pair -- Key pair
        // successfully
        // generated
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.KEY_PAIR_GENERATED_CEK)
                .electionEvent(spec.electionEventId()).objectId(spec.verificationCardSetId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubKeyIds)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
        // [SL GENCCKEYSSTORE 4]
        // Choice Codes Decryption public key and Choice Codes Generation public
        // key successfully signed
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.PUBLIC_KEY_SIGNED_CEK)
                .electionEvent(spec.electionEventId()).objectId(spec.verificationCardSetId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubKeyIds)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());

        try {
            database.saveChoiceCodeKeys(spec.electionEventId(), spec.verificationCardSetId(), keys);
            // [SL GENCCEK-3]
            // Generate Choice Codes Generation/Decryption key pair -- Key pair
            // succesfully
            // stored
            secureLoggingWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.KEY_PAIR_STORED_CEK)
                    .electionEvent(spec.electionEventId()).objectId(spec.verificationCardSetId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                        serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubKeyIds)
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .createLogInfo());
        } catch (KeyManagementException e) {
            // [SL GENCCEK-3]
            logErrorStoringKeyPairCEK(spec);
            throw e;
        }
    }

    private String generateFingerprint(final PublicKey publicKey) throws FingerprintGeneratorException {

        if (publicKey == null) {
            throw new IllegalArgumentException("The received publicKey is null");
        }
        final byte[] publicKeyAsBytes = publicKey.getEncoded();

        return fingerprintGenerator.generate(publicKeyAsBytes);
    }

    /**
     * @param spec
     * @throws FingerprintGeneratorException
     */
    private void logErrorSigningPkCEK(ChoiceCodeKeysSpec spec, PublicKey publicKey)
            throws FingerprintGeneratorException {
        String pubKeyIds = generateFingerprint(publicKey);
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_SIGNING_PUBLIC_KEY_CEK)
                .electionEvent(spec.electionEventId()).objectId(spec.verificationCardSetId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubKeyIds)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_SIGNING_PUBLIC_KEY_CEK.getInfo())
                .createLogInfo());
    }

    /**
     * @param spec
     */
    private void logErrorStoringKeyPairCEK(final ChoiceCodeKeysSpec spec) {
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_STORING_KEY_PAIR_CEK)
                .objectId(spec.verificationCardSetId()).electionEvent(spec.electionEventId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_STORING_KEY_PAIR_CEK.getInfo())
                .createLogInfo());
    }

    /**
     * @param spec
     */
    private void logErrorGeneratingKeyPairCEK(final ChoiceCodeKeysSpec spec) {
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_GENERATING_KEY_PAIR_CEK)
                .electionEvent(spec.electionEventId()).objectId(spec.verificationCardSetId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_GENERATING_KEY_PAIR_CEK.getInfo())
                .createLogInfo());
    }

    @Override
    public void createElectionSigningKeys(String electionEventId, Date validFrom, Date validTo)
            throws KeyAlreadyExistsException, KeyManagementException, FingerprintGeneratorException {
        requireNonNull(electionEventId, "Election event identifier is null.");
        requireNonNull(validFrom, "Valid from date is null.");
        requireNonNull(validTo, "Valid to date is null.");
        checkNodeKeysActivated();
        ElectionSigningKeys keys = null;
        try {
            keys = generator.generateElectionSigningKeys(electionEventId, validFrom, validTo, nodeKeys);
        } catch (KeyManagementException e) {
            // [SL GENCCCRT-4]
            logErrorSigningCertificateGenerated(electionEventId);
            throw e;
        }

        logSigningCertificateGenerated(electionEventId, keys);

        database.saveElectionSigningKeys(electionEventId, keys);

        logStoredGeneratedSigningKeyPair(electionEventId, keys);
    }

    private void logStoredGeneratedSigningKeyPair(String electionEventId, ElectionSigningKeys keys)
            throws FingerprintGeneratorException {
        String pubkeyId = generateFingerprint(keys.publicKey());
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.KEY_PAIR_GENERATED_STORED)
                .electionEvent(electionEventId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logSigningCertificateGenerated(String electionEventId, ElectionSigningKeys keys)
            throws FingerprintGeneratorException {
        String pubkeyId = generateFingerprint(keys.publicKey());

        // [SL GENCCSK-3]
        // Generate Control Component Nodes signing key pair -- Control
        // Component Signing Certificate successfully generated

        X509Certificate certificate = keys.certificate();
        String certCn = getSubjectCommonName(certificate);
        String issuerCn = getIssuerCommonName(certificate);
        BigInteger certSn = certificate.getSerialNumber();
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.CONTROL_COMPONENT_SIGNING_CERTIFICATED_GENERATED)
                .electionEvent(electionEventId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CERT_CN, certCn)
                .additionalInfo(ControlComponentsCommonsLogConstants.CERT_SN, certSn.toString())
                .additionalInfo(ControlComponentsCommonsLogConstants.ISSUER_CN, issuerCn)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logErrorSigningCertificateGenerated(final String electionEventId) {
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.ERROR_CONTROL_COMPONENT_SIGNING_CERTIFICATED)
                .electionEvent(electionEventId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_CONTROL_COMPONENT_SIGNING_CERTIFICATED.getInfo())
                .createLogInfo());
    }

    private String getSubjectCommonName(final X509Certificate certificate) {
        return getPrincipalCommonName(certificate.getSubjectX500Principal());
    }

    private String getIssuerCommonName(final X509Certificate certificate) {
        return getPrincipalCommonName(certificate.getIssuerX500Principal());
    }

    private String getPrincipalCommonName(final X500Principal principal) {
        return new LdapHelper().getAttributeFromDistinguishedName(principal.getName(),
            X509CertificateConstants.COMMON_NAME_ATTRIBUTE_NAME);
    }

    @Override
    public void createElectionSigningKeys(String electionEventId, ZonedDateTime validFrom, ZonedDateTime validTo)
            throws KeyAlreadyExistsException, KeyManagementException, FingerprintGeneratorException {

        requireNonNull(validFrom, "Valid from date is null.");
        requireNonNull(validTo, "Valid to date is null.");
        createElectionSigningKeys(electionEventId, Date.from(validFrom.toInstant()), Date.from(validTo.toInstant()));
    }

    @Override
    public void createMixingKeys(MixingKeysSpec spec)
            throws KeyAlreadyExistsException, KeyNotFoundException, KeyManagementException,
            FingerprintGeneratorException {
        requireNonNull(spec, "Spec is null.");
        checkNodeKeysActivated();
        ElectionSigningKeys electionSigningKeys = database.loadElectionSigningKeys(spec.electionEventId());
        MixingKeys keys = null;
        try {
            keys = generator.generateMixingKeys(spec, electionSigningKeys);
        } catch (UnsupportedEcryptionParametersException | ElGamalKeyPairFailedGenerationException e) {
            // [SL GENELK-2]
            logErrorGeneratingKpELK(spec);
        } catch (FailedSigningException e) {
            // [SL GENELKSIGN-6]
            logErrorSingingPkELK(spec, electionSigningKeys.publicKey());
            throw e;
        }

        String pubkeyId = fingerprintGenerator.generate(keys.publicKey());
        // [SL GENELK-1]
        // Generate Control Component Nodes mixing key -- Mixing key pair
        // successfully generated
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.KEY_PAIR_GENERATED_ELK)
                .electionEvent(spec.electionEventId()).objectId(spec.electoralAuthorityId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());

        // [SL GENELK-5]
        // Generate Control Component Nodes mixing key -- Public key
        // successfully signed
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.PUBLIC_KEY_SIGNED_ELK)
                .electionEvent(spec.electionEventId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());

        try {
            database.saveMixingKeys(spec.electionEventId(), spec.electoralAuthorityId(), keys);
        } catch (KeyManagementException e) {
            // [SL GENELKSTORE-4]
            logErrorStoringKpELK(spec);
            throw e;
        }

        // [SL GENELK-3]
        // Generate Control Component Nodes mixing key -- Key pair succesfully
        // stored
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.KEY_PAIR_STORED_ELK)
                .electionEvent(spec.electionEventId()).objectId(spec.electoralAuthorityId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    /**
     * @param spec
     * @throws FingerprintGeneratorException
     */
    private void logErrorSingingPkELK(MixingKeysSpec spec, PublicKey publicKey) throws FingerprintGeneratorException {
        String pubkeyId = generateFingerprint(publicKey);
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_SIGNING_PUBLIC_KEY_ELK)
                .electionEvent(spec.electionEventId()).objectId(spec.electoralAuthorityId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.PUBKEY_IDS, pubkeyId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_SIGNING_PUBLIC_KEY_ELK.getInfo())
                .createLogInfo());
    }

    /**
     * @param spec
     */
    private void logErrorStoringKpELK(final MixingKeysSpec spec) {
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_STORING_KEY_PAIR_ELK)
                .electionEvent(spec.electionEventId()).objectId(spec.electoralAuthorityId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_STORING_KEY_PAIR_ELK.getInfo())
                .createLogInfo());
    }

    /**
     * @param spec
     */
    private void logErrorGeneratingKpELK(final MixingKeysSpec spec) {
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.ERROR_GENERATING_KEY_PAIR_ELK)
                .electionEvent(spec.electionEventId()).objectId(spec.electoralAuthorityId())
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_GENERATING_KEY_PAIR_ELK.getInfo())
                .createLogInfo());
    }

    @Override
    public ElGamalPrivateKey getChoiceCodeDecryptionPrivateKey(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).decryptionPrivateKey();
    }

    @Override
    public ElGamalPublicKey getChoiceCodeDecryptionPublicKey(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).decryptionPublicKey();
    }

    @Override
    public byte[] getChoiceCodeDecryptionPublicKeySignature(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).decryptionPublicKeySignature();
    }

    @Override
    public EncryptionParameters getChoiceCodeEncryptionParameters(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).encryptionParameters();
    }

    @Override
    public ElGamalPrivateKey getChoiceCodeGenerationPrivateKey(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).generationPrivateKey();
    }

    @Override
    public ElGamalPublicKey getChoiceCodeGenerationPublicKey(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).generationPublicKey();
    }

    @Override
    public byte[] getChoiceCodeGenerationPublicKeySignature(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        return getChoiceCodeKeys(electionEventId, verificationCardSetId).generationPublicKeySignature();
    }

    @Override
    public X509Certificate getElectionSigningCertificate(String electionEventId)
            throws KeyNotFoundException, KeyManagementException {
        X509Certificate certificate = getElectionSigningKeys(electionEventId).certificate();

        return certificate;
    }

    @Override
    public X509Certificate[] getElectionSigningCertificateChain(String electionEventId)
            throws KeyNotFoundException, KeyManagementException {
        X509Certificate[] certificateChain = getElectionSigningKeys(electionEventId).certificateChain();

        return certificateChain;

    }

    @Override
    public PrivateKey getElectionSigningPrivateKey(final String electionEventId)
            throws KeyNotFoundException, KeyManagementException {
        return getElectionSigningKeys(electionEventId).privateKey();
    }

    @Override
    public PublicKey getElectionSigningPublicKey(final String electionEventId)
            throws KeyNotFoundException, KeyManagementException {
        return getElectionSigningKeys(electionEventId).publicKey();
    }

    @Override
    public ElGamalPrivateKey getMixingPrivateKey(String electionEventId, String electoralAuthorityId)
            throws KeyNotFoundException, KeyManagementException {
        return getMixingKeys(electionEventId, electoralAuthorityId).privateKey();
    }

    @Override
    public ElGamalPublicKey getMixingPublicKey(String electionEventId, String electoralAuthorityId)
            throws KeyNotFoundException, KeyManagementException {
        return getMixingKeys(electionEventId, electoralAuthorityId).publicKey();
    }

    @Override
    public byte[] getMixingPublicKeySignature(String electionEventId, String electoralAuthorityId)
            throws KeyNotFoundException, KeyManagementException {
        return getMixingKeys(electionEventId, electoralAuthorityId).publicKeySignature();
    }

    @Override
    public X509Certificate getPlatformCACertificate() throws KeyNotFoundException, KeyManagementException {
        checkNodeKeysActivated();

        // The existence of a non-empty certificate chain is assumed, as the
        // object cannot be built otherwise.
        return nodeKeys.caCertificateChain()[nodeKeys.caCertificateChain().length - 1];
    }

    @Override
    public boolean hasChoiceCodeKeys(String electionEventId, String verificationCardSetId)
            throws KeyManagementException {
        return database.hasChoiceCodeKeys(electionEventId, verificationCardSetId);
    }

    @Override
    public boolean hasMixingKeys(String electionEventId, String electoralAuthorityId) throws KeyManagementException {
        return database.hasMixingKeys(electionEventId, electoralAuthorityId);
    }

    @Override
    public boolean hasNodeKeysActivated() {
        return nodeKeys != null;
    }

    @Override
    public boolean hasValidElectionSigningKeys(String electionEventId, Date validFrom, Date validTo)
            throws KeyManagementException {
        requireNonNull(validFrom, "Valid from date is null.");
        requireNonNull(validTo, "Valid to date is null.");
        X509Certificate certificate;
        try {
            certificate = getElectionSigningCertificate(electionEventId);
        } catch (KeyNotFoundException e) {

            return false;
        }
        return !certificate.getNotBefore().after(validFrom) && !certificate.getNotAfter().before(validTo);

    }

    @Override
    public boolean hasValidElectionSigningKeys(String electionEventId, ZonedDateTime validFrom, ZonedDateTime validTo)
            throws KeyManagementException {
        requireNonNull(validFrom, "Valid from date is null.");
        requireNonNull(validTo, "Valid to date is null.");
        return hasValidElectionSigningKeys(electionEventId, Date.from(validFrom.toInstant()),
            Date.from(validTo.toInstant()));
    }

    @Override
    public X509Certificate nodeCACertificate() {
        checkNodeKeysActivated();
        return nodeKeys.caCertificate();
    }

    @Override
    public PrivateKey nodeCAPrivateKey() {
        checkNodeKeysActivated();
        return nodeKeys.caPrivateKey();
    }

    @Override
    public PublicKey nodeCAPublicKey() {
        return nodeKeys.caPublicKey();
    }

    @Override
    public X509Certificate nodeEncryptionCertificate() {
        checkNodeKeysActivated();
        return nodeKeys.encryptionCertificate();
    }

    @Override
    public PrivateKey nodeEncryptionPrivateKey() {
        checkNodeKeysActivated();
        return nodeKeys.encryptionPrivateKey();
    }

    @Override
    public PublicKey nodeEncryptionPublicKey() {
        return nodeKeys.encryptionPublicKey();
    }

    @Override
    public X509Certificate nodeLogEncryptionCertificate() {
        checkNodeKeysActivated();
        return nodeKeys.logEncryptionCertificate();
    }

    @Override
    public PrivateKey nodeLogEncryptionPrivateKey() {
        checkNodeKeysActivated();
        return nodeKeys.logEncryptionPrivateKey();
    }

    @Override
    public PublicKey nodeLogEncryptionPublicKey() {
        return nodeKeys.logEncryptionPublicKey();
    }

    @Override
    public X509Certificate nodeLogSigningCertificate() {
        checkNodeKeysActivated();
        return nodeKeys.logSigningCertificate();
    }

    @Override
    public PrivateKey nodeLogSigningPrivateKey() {
        checkNodeKeysActivated();
        return nodeKeys.logSigningPrivateKey();
    }

    @Override
    public PublicKey nodeLogSigningPublicKey() {
        return nodeKeys.logSigningPublicKey();
    }

    @Override
    public void shutdown() {
        cache.shutdown();
    }

    @Override
    public void startup() {
        cache.startup();
    }

    private void activateNodeKeys(final NodeKeys nodeKeys) {
        PrivateKey privateKey = nodeKeys.encryptionPrivateKey();
        PublicKey publicKey = nodeKeys.encryptionPublicKey();
        database.setEncryptionKeys(privateKey, publicKey);
        this.nodeKeys = nodeKeys;
    }

    private void checkNodeCAKeysMatch(PrivateKey nodeCAPrivateKey, PublicKey nodeCAPublicKey)
            throws InvalidNodeCAException {
        byte[] bytes = nodeCAPublicKey.getEncoded();
        try {
            byte[] signature = asymmetricService.sign(nodeCAPrivateKey, bytes);
            if (!asymmetricService.verifySignature(signature, nodeCAPublicKey, bytes)) {
                throw new InvalidNodeCAException("CCN CA keys do not match.");
            }
        } catch (GeneralCryptoLibException e) {
            throw new InvalidNodeCAException("CCN CA keys are invalid.", e);
        }
    }

    private void checkNodeKeysActivated() throws IllegalStateException {
        if (!hasNodeKeysActivated()) {
            throw new IllegalStateException("Node keys are not activated.");
        }
    }

    private ChoiceCodeKeys getChoiceCodeKeys(String electionEventId, String verificationCardSetId)
            throws KeyNotFoundException, KeyManagementException {
        requireNonNull(electionEventId, "Election event identifier is null.");
        requireNonNull(verificationCardSetId, "Verification card set identifier is null.");
        checkNodeKeysActivated();
        ChoiceCodeKeys keys = cache.getChoiceCodeKeys(electionEventId, verificationCardSetId);
        if (keys == null) {
            keys = database.loadChoiceCodeKeys(electionEventId, verificationCardSetId);
            cache.putChoiceCodeKeys(electionEventId, verificationCardSetId, keys);
        }
        return keys;
    }

    private ElectionSigningKeys getElectionSigningKeys(String electionEventId)
            throws KeyNotFoundException, KeyManagementException {
        requireNonNull(electionEventId, "Election event identifier is null.");
        checkNodeKeysActivated();
        ElectionSigningKeys keys = cache.getElectionSigningKeys(electionEventId);
        if (keys == null) {
            keys = database.loadElectionSigningKeys(electionEventId);
            cache.putElectionSigningKeys(electionEventId, keys);
        }
        return keys;
    }

    private MixingKeys getMixingKeys(String electionEventId, String electoralAuthorityId)
            throws KeyNotFoundException, KeyManagementException {
        requireNonNull(electionEventId, "Election event identifier is null.");
        requireNonNull(electoralAuthorityId, "Electoral authority identifier is null.");
        checkNodeKeysActivated();

        MixingKeys keys = cache.getMixingKeys(electionEventId, electoralAuthorityId);
        if (keys == null) {
            keys = database.loadMixingKeys(electionEventId, electoralAuthorityId);
            cache.putMixingKeys(electionEventId, electoralAuthorityId, keys);
        }
        return keys;
    }

    /**
     * Builder for creating {@link KeyManagerImpl} instances.
     */
    public static final class Builder {
        private AsymmetricServiceAPI asymmetricService;

        private StoresServiceAPI storesService;

        private CertificatesServiceAPI certificatesService;

        private ElGamalServiceAPI elGamalService;

        private PrimitivesServiceAPI primitivesService;

        private DataSource dataSource;

        private Duration cacheExpiryPeriod;

        private String nodeId;

        private SecureLoggingWriter secureLoggingWriter;

        private FingerprintGenerator fingerprintGenerator;

        private ServiceTransactionInfoProvider serviceTransactionInfoProvider;

        /**
         * Builds a new {@link KeyManagerImpl} instance.
         *
         * @return a new instance.
         */
        public KeyManagerImpl build() {
            requireNonNull(asymmetricService, "Asymmetric service is null.");
            requireNonNull(storesService, "Stores service is null.");
            requireNonNull(certificatesService, "Certificates service is null.");
            requireNonNull(elGamalService, "ElGamal service is null.");
            requireNonNull(primitivesService, "Primitives service is null.");
            requireNonNull(dataSource, "Data source is null.");
            requireNonNull(cacheExpiryPeriod, "Cache expiry period is null.");
            requireNonNull(nodeId, "Node identifier is null.");
            requireNonNull(secureLoggingWriter, "SecureLoggingWriter is null.");
            requireNonNull(fingerprintGenerator, "FingerprintGenerator is null.");
            requireNonNull(serviceTransactionInfoProvider, "ServiceTransactionInfoProvider is null.");

            Codec codec = newCodec();
            Generator generator = newGenerator(codec);
            Database database = newDatabase(codec, generator);

            Cache cache = new CacheImpl(cacheExpiryPeriod);
            return new KeyManagerImpl(asymmetricService, storesService, generator, database, cache, secureLoggingWriter,
                fingerprintGenerator, serviceTransactionInfoProvider, nodeId);
        }

        /**
         * Sets the asymmetric service.
         *
         * @param asymmetricService
         *            the asymmetric service
         * @return this instance.
         */
        public Builder setAsymmetricService(AsymmetricServiceAPI asymmetricService) {
            this.asymmetricService = asymmetricService;
            return this;
        }

        /**
         * Sets the cache expiry period.
         * 
         * @param cacheExpiryPeriod
         *            the cache expiry period
         * @return this instance.
         */
        public Builder setCacheExpiryPeriod(Duration cacheExpiryPeriod) {
            this.cacheExpiryPeriod = cacheExpiryPeriod;
            return this;
        }

        /**
         * Sets the certificates service.
         *
         * @param certificatesService
         *            the certificates service
         * @return this instance.
         */
        public Builder setCertificatesService(CertificatesServiceAPI certificatesService) {
            this.certificatesService = certificatesService;
            return this;
        }

        /**
         * Sets the data source.
         *
         * @param dataSource
         *            the data source
         * @return this instance.
         */
        public Builder setDataSource(final DataSource dataSource) {
            this.dataSource = dataSource;
            return this;
        }

        /**
         * Sets the ElGamal service.
         *
         * @param elGamalService
         *            the ElGamal service
         * @return this instance.
         */
        public Builder setElGamalService(ElGamalServiceAPI elGamalService) {
            this.elGamalService = elGamalService;
            return this;
        }

        public Builder setFingerprintGenerator(FingerprintGenerator fingerprintGenerator) {
            this.fingerprintGenerator = fingerprintGenerator;
            return this;
        }

        /**
         * Sets the node identifier.
         *
         * @param nodeId
         *            the node identifier
         * @return this instance.
         */
        public Builder setNodeId(final String nodeId) {
            this.nodeId = nodeId;
            return this;
        }

        /**
         * Sets the primitives service.
         *
         * @param primitivesService
         *            the primitives service
         * @return this instance.
         */
        public Builder setPrimitivesService(PrimitivesServiceAPI primitivesService) {
            this.primitivesService = primitivesService;
            return this;
        }

        /**
         * Sets the stores service.
         *
         * @param storesService
         *            the stores service
         * @return this instance.
         */
        public Builder setStoresService(final StoresServiceAPI storesService) {
            this.storesService = storesService;
            return this;
        }

        public Builder setSecureLoggingWriter(SecureLoggingWriter secureLoggingWriter) {
            this.secureLoggingWriter = secureLoggingWriter;
            return this;
        }

        public Builder setServiceTransactionInfoProvider(
                ServiceTransactionInfoProvider serviceTransactionInfoProvider) {
            this.serviceTransactionInfoProvider = serviceTransactionInfoProvider;
            return this;
        }

        private Codec newCodec() {
            return new CodecImpl(storesService, asymmetricService);
        }

        private Database newDatabase(final Codec codec, final Generator generator) {
            return new DatabaseImpl(dataSource, codec, generator, nodeId);
        }

        private Generator newGenerator(final Codec codec) {
            return new GeneratorImpl(asymmetricService, certificatesService, elGamalService, primitivesService, codec,
                nodeId);
        }
    }

}
