/*
 * 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.scytl.keystore.factory;

import static java.text.MessageFormat.format;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.codehaus.jackson.map.ObjectMapper;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.binary.ByteArrays;
import com.scytl.cryptolib.commons.configuration.Provider;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.primitives.derivation.factory.CryptoPBKDFDeriver;
import com.scytl.cryptolib.scytl.keystore.constants.ScytlKeyStoreConstants;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.cryptolib.scytl.keystore.factory.CryptoScytlKeyStoreWithPBKDFHelper.ENTRY_TYPE;
import com.scytl.cryptolib.symmetric.cipher.factory.CryptoSymmetricCipher;
import com.scytl.cryptolib.symmetric.cipher.factory.SymmetricCipherFactory;
import com.scytl.cryptolib.symmetric.key.configuration.ConfigSecretKeyAlgorithmAndSpec;

/**
 * Class which provides functionality to perform operations with a key store.
 * <p>
 * Note that the {@link CryptoScytlKeyStoreWithPBKDF} given is used in
 * derivation process. New entries can be added to store and already existent
 * entries can be updated if it is used a valid password.
 */
public final class CryptoScytlKeyStoreWithPBKDF
        implements CryptoAPIScytlKeyStore {

    private static final String ADD_ENTRY_ERROR =
        "Could not add the entry.";

    private static final String CREATE_ERROR =
        "Could not create the key store.";

    private static final String LOAD_ERROR = "Could not load key store.";
    
    private final DerivedKeyCache cache = new DerivedKeyCacheImpl();

    private final CryptoPBKDFDeriver _cryptoPBKDFDeriver;

    private final KeyStore _keyStore;

    private final Provider _keyStoreProvider;

    private final String _keyStoreType;

    private final byte[] _salt;

    private final List<String> _secretKeyAliases = new ArrayList<>();

    private final List<String> _privateKeyAliases = new ArrayList<>();

    private final List<String> _elGamalPrivateKeyAliases =
        new ArrayList<>();

    private final Map<String, byte[]> _secretKeys = new HashMap<>();

    private final Map<String, byte[]> _elGamalPrivateKeys =
        new HashMap<>();

    private final CryptoSymmetricCipher _cryptoSymmetricCipher;

    private final ConfigSecretKeyAlgorithmAndSpec _configSecretKeyAlgorithmAndSpec;

    /**
     * Creates an instance of the class and initializes it by provided
     * parameters.
     *
     * @param keyStoreType
     *            the type of KeyStore to be created.
     * @param keyStoreProvider
     *            the provider of the {@link java.security.KeyStore}.
     * @param cryptoPBKDFDeriver
     *            the password deriver.
     * @param symmetricCipherFactory
     *            the symmetric cipher factory.
     * @param configSecretKeyAlgorithmAndSpec
     *            the secret key type.
     */
    CryptoScytlKeyStoreWithPBKDF(final String keyStoreType,
            final Provider keyStoreProvider,
            final CryptoPBKDFDeriver cryptoPBKDFDeriver,
            final SymmetricCipherFactory symmetricCipherFactory,
            final ConfigSecretKeyAlgorithmAndSpec configSecretKeyAlgorithmAndSpec) {

        _keyStoreType = keyStoreType;
        _keyStoreProvider = keyStoreProvider;
        _cryptoPBKDFDeriver = cryptoPBKDFDeriver;

        _salt = _cryptoPBKDFDeriver.generateRandomSalt();
        _keyStore = createEmptyKeyStore();

        _configSecretKeyAlgorithmAndSpec = configSecretKeyAlgorithmAndSpec;
        _cryptoSymmetricCipher = symmetricCipherFactory.create();
    }

    /**
     * Creates an instance of the class and initializes it by provided
     * parameters.
     *
     * @param keyStoreType
     *            the type of KeyStore to be created.
     * @param keyStoreProvider
     *            the provider of the {@link java.security.KeyStore}.
     * @param cryptoPBKDFDeriver
     *            the password deriver.
     * @param in
     *            the stream to read the store from.
     * @param password
     *            the password.
     * @param symmetricCipherFactory
     *            the symmetric cipher factory.
     * @param configSecretKeyAlgorithmAndSpec
     *            the secret key type.
     * @throws IOException
     *             if the store cannot be read from {@code in}.
     * @throws GeneralCryptoLibException
     *             if there is problems while reading store.
     */
    CryptoScytlKeyStoreWithPBKDF(final String keyStoreType,
            final Provider keyStoreProvider,
            final CryptoPBKDFDeriver cryptoPBKDFDeriver,
            final InputStream in, final char[] password,
            final SymmetricCipherFactory symmetricCipherFactory,
            final ConfigSecretKeyAlgorithmAndSpec configSecretKeyAlgorithmAndSpec)
            throws GeneralCryptoLibException, IOException {

        boolean saltInitialized = false;

        _keyStoreType = keyStoreType;
        _keyStoreProvider = keyStoreProvider;
        _cryptoPBKDFDeriver = cryptoPBKDFDeriver;
        _salt = new byte[_cryptoPBKDFDeriver.getSaltBytesLength()];

        _configSecretKeyAlgorithmAndSpec = configSecretKeyAlgorithmAndSpec;
        _cryptoSymmetricCipher = symmetricCipherFactory.create();

        byte[] storeAsBytes = {};
        try {
            ZipInputStream zin =
                new ZipInputStream(new BufferedInputStream(in));
            ZipEntry entry = zin.getNextEntry();
            while (entry != null) {
                String[] entryName = entry.getName().split("\\.");
                ENTRY_TYPE entryType =
                    ENTRY_TYPE.valueOf(entryName[0].toUpperCase());
                switch (entryType) {
                case SALT:
                    if (zin.read(_salt) > 0) {
                        saltInitialized = true;
                    }
                    break;
                case SECRET:
                    _secretKeys.put(entryName[1], readBytes(zin));
                    _secretKeyAliases.add(entryName[1]);
                    break;
                case ELGAMAL:
                    _elGamalPrivateKeys.put(entryName[1], readBytes(zin));
                    _elGamalPrivateKeyAliases.add(entryName[1]);
                    break;
                case STORE:
                    storeAsBytes = readBytes(zin);
                    break;
                default:
                    throw new GeneralCryptoLibException(
                        format("Unknown entry type ''{0}''.", entryType));
                }
                entry = zin.getNextEntry();
            }

        } catch (ZipException e) {
            throw new GeneralCryptoLibException(
                "There was a problem reading the store. " + e.getMessage(),
                e);

        }
        if (!saltInitialized) {
            throw new GeneralCryptoLibException(
                "There was a problem reading the store. Salt was not initialized.");
        }

        _keyStore = loadKeyStore(storeAsBytes, password);

        try {
            String keyAlias;

            for (Enumeration<String> aliases = _keyStore.aliases(); aliases
                .hasMoreElements();) {

                keyAlias = aliases.nextElement();
                if (_keyStore.entryInstanceOf(keyAlias,
                    KeyStore.SecretKeyEntry.class)) {
                    _secretKeyAliases.add(keyAlias);
                } else {
                    _privateKeyAliases.add(keyAlias);
                }
            }
        } catch (KeyStoreException e) {
            throw new CryptoLibException(e);
        }
    }

    private byte[] readBytes(final InputStream stream) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try {
            int length;
            byte[] buffer = new byte[1024 * 10];
            while ((length = stream.read(buffer)) > 0) {
                bytes.write(buffer, 0, length);
            }
        } finally {
            bytes.close();
        }
        return bytes.toByteArray();
    }

    /**
     * Creates an instance of the class and initializes it by provided
     * parameters.
     *
     * @param fromJson
     *            indicates that store is in JSON.
     * @param keyStoreType
     *            the type of KeyStore to be created.
     * @param keyStoreProvider
     *            the provider of the {@link java.security.KeyStore}.
     * @param cryptoPBKDFDeriver
     *            the password deriver.
     * @param in
     *            the stream to read the store from.
     * @param password
     *            the password.
     * @param symmetricCipherFactory
     *            the symmetric cipher factory.
     * @param configSecretKeyAlgorithmAndSpec
     *            the secret key type.
     * @throws GeneralCryptoLibException
     *             if there is problems while reading store.
     */
    public CryptoScytlKeyStoreWithPBKDF(final boolean fromJson,
            final String keyStoreType, final Provider keyStoreProvider,
            final CryptoPBKDFDeriver cryptoPBKDFDeriver,
            final InputStream in, final char[] password,
            final SymmetricCipherFactory symmetricCipherFactory,
            final ConfigSecretKeyAlgorithmAndSpec configSecretKeyAlgorithmAndSpec)
            throws GeneralCryptoLibException {

        _keyStoreType = keyStoreType;
        _keyStoreProvider = keyStoreProvider;
        _cryptoPBKDFDeriver = cryptoPBKDFDeriver;
        _salt = new byte[_cryptoPBKDFDeriver.getSaltBytesLength()];

        _configSecretKeyAlgorithmAndSpec = configSecretKeyAlgorithmAndSpec;
        _cryptoSymmetricCipher = symmetricCipherFactory.create();

        ObjectMapper mapper = new ObjectMapper();
        try {
            HashMap<?, ?> jsonTree = mapper.readValue(in, HashMap.class);
            String saltEncoded = (String) jsonTree.get("salt");
            System.arraycopy(
                Base64.getDecoder().decode(saltEncoded),
                0, _salt, 0, _cryptoPBKDFDeriver.getSaltBytesLength());

            HashMap<?, ?> secrets =
                (HashMap<?, ?>) jsonTree.get("secrets");
            if (secrets != null) {
                for (Map.Entry<?, ?> entry : secrets.entrySet()) {
                    String alias = entry.getKey().toString();
                    byte[] key =
                        Base64.getDecoder().decode(entry.getValue().toString());
                    _secretKeys.put(alias, key);
                    _secretKeyAliases.add(alias);
                }
            }

            HashMap<?, ?> elGamalPrivateKeys =
                (HashMap<?, ?>) jsonTree.get("egPrivKeys");
            if (elGamalPrivateKeys != null) {
                for (Map.Entry<?, ?> entry : elGamalPrivateKeys
                    .entrySet()) {
                    String alias = entry.getKey().toString();
                    byte[] key =
                        Base64.getDecoder().decode(entry.getValue().toString());
                    _elGamalPrivateKeys.put(alias, key);
                    _elGamalPrivateKeyAliases.add(alias);
                }
            }

            String storeEncoded = (String) jsonTree.get("store");
            byte[] storeAsByte = Base64.getDecoder().decode(storeEncoded);

            if (storeAsByte == null) {
                _keyStore = createEmptyKeyStore();
            } else {
                _keyStore = loadKeyStore(storeAsByte, password);

                try {
                    String keyAlias;

                    for (Enumeration<String> aliases =
                        _keyStore.aliases(); aliases.hasMoreElements();) {

                        keyAlias = aliases.nextElement();
                        if (_keyStore.entryInstanceOf(keyAlias,
                            KeyStore.SecretKeyEntry.class)) {
                            _secretKeyAliases.add(keyAlias);
                        } else {
                            _privateKeyAliases.add(keyAlias);
                        }
                    }
                } catch (KeyStoreException e) {
                    throw new CryptoLibException(e);
                }
            }

        } catch (IOException e) {
            throw new CryptoLibException(e);
        }

    }

    private KeyStore createEmptyKeyStore() {
        KeyStore store;
        try {
            store = getInstance(_keyStoreType, _keyStoreProvider);
            store.load(null, null);
            return store;

        } catch (GeneralSecurityException | IOException e) {
            throw new CryptoLibException(CREATE_ERROR, e);
        }
    }

    private KeyStore getInstance(final String keyStoreType,
            final Provider provider) throws GeneralSecurityException {
        if (provider == Provider.DEFAULT) {
            return KeyStore.getInstance(keyStoreType);
        } else {
            return KeyStore.getInstance(keyStoreType,
                provider.getProviderName());
        }
    }

    private char[] derivePassword(byte[] key) {
        return new BigInteger(key).toString(Character.MAX_RADIX)
            .toCharArray();
    }

    private byte[] deriveKey(final char[] password)
            throws GeneralCryptoLibException {
        byte[] key = cache.get(password);
        if (key == null) {
            key = _cryptoPBKDFDeriver.deriveKey(password, _salt)
                .getEncoded();
        }
        return key;
    }

    @Override
    public Certificate[] getCertificateChain(final String alias)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "Certificate chain alias");

        try {
            return _keyStore.getCertificateChain(alias);
        } catch (KeyStoreException e) {
            throw new CryptoLibException(e);
        }
    }

    /**
     * Returns the {@link SecretKey} associated with the given alias, using the
     * given password to recover it.
     *
     * @see java.security.KeyStore#getKey(String, char[])
     * @param alias
     *            the alias, a text of minimum 1 char, maximum 50 chars, from
     *            the alphabet [a-z0-9_-].
     * @param password
     *            the password used to seal the {@link SecretKey} in the store.
     *            The password must contain a minimum of 16 characters and a
     *            maximum of 1000 characters.
     * @return the Secret Key.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    @Override
    public SecretKey getSecretKeyEntry(final String alias,
            final char[] password) throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "Secret key alias");
        Validate.notNullOrBlank(password, "Secret key password");

        byte[] data = _secretKeys.get(alias);
        if (data == null || data.length == 0) {
            return null;
        }
        
        String algorithm = _configSecretKeyAlgorithmAndSpec.getAlgorithm();
        
        byte[] key = deriveKey(password);
        SecretKey secretKey = new SecretKeySpec(key, algorithm);

        byte[] decrypt =
            _cryptoSymmetricCipher.decrypt(secretKey, data);

        checkDecryptedLength(alias, decrypt);
        checkDecryptedAlias(alias, decrypt);

        cache.putForSecretKey(alias, password, key);

        return new SecretKeySpec(decrypt, alias.length(),
            decrypt.length - alias.length(), algorithm);
    }

    private void checkDecryptedLength(final String alias,
            final byte[] decrypt) throws GeneralCryptoLibException {
        if (decrypt.length != _configSecretKeyAlgorithmAndSpec
            .getKeyLength() / Byte.SIZE + alias.length()) {
            throw new GeneralCryptoLibException("Data was tampered");
        }
    }

    /**
     * Returns the {@link PrivateKey} associated with the given alias, using the
     * given password to recover it.
     *
     * @see java.security.KeyStore#getKey(String, char[])
     * @param alias
     *            the alias, a text of minimum 1 char, maximum 50 chars, from
     *            the alphabet [a-z0-9_-].
     * @param password
     *            the password used to seal the key in the store. The password
     *            must contain a minimum of 16 characters and a maximum of 1000
     *            characters.
     * @return the {@link SecretKey}.
     * @throws GeneralCryptoLibException
     *             if the retrieving of secret key failed.
     */
    public PrivateKey getPrivateKeyEntry(final String alias,
            final char[] password) throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "Private key alias");
        Validate.notNullOrBlank(password, "Private key password");

        byte[] key = deriveKey(password);
        char[] derivedPassword = derivePassword(key);
        PrivateKey privateKey;
        try {
            privateKey =
                (PrivateKey) _keyStore.getKey(alias, derivedPassword);
        } catch (GeneralSecurityException e) {
            throw new CryptoLibException(
                "Could not retrieve private key from key store, using derived password",
                e);
        }
        cache.putForPrivateKey(alias, password, key);
        return privateKey;
    }

    @Override
    public List<String> getSecretKeyAliases() {
        return new ArrayList<>(_secretKeyAliases);
    }

    @Override
    public List<String> getPrivateKeyAliases() {
        return new ArrayList<>(_privateKeyAliases);
    }

    @Override
    public List<String> getElGamalPrivateKeyAliases() {
        return new ArrayList<>(_elGamalPrivateKeyAliases);
    }

    private KeyStore loadKeyStore(final byte[] bytes,
            final char[] password) throws GeneralCryptoLibException {
        byte[] key = deriveKey(password);
        char[] derivedPassword = derivePassword(key);
        KeyStore store;
        try (InputStream stream = new ByteArrayInputStream(bytes)) {
            store = getInstance(_keyStoreType, _keyStoreProvider);
            store.load(stream, derivedPassword);
        } catch (GeneralSecurityException | IOException e) {
            throw new GeneralCryptoLibException(LOAD_ERROR, e);
        }
        cache.putForKeyStore(password, key);
        return store;
    }

    /**
     * Assigns the given {@link PrivateKey} and the given {@link Certificate}
     * chain to the given alias, protecting it with the given password.
     *
     * @see java.security.KeyStore#setKeyEntry(String, java.security.Key,
     *      char[], Certificate[])
     * @param alias
     *            the alias, a text of minimum 1 char, maximum 50 chars, from
     *            the alphabet [a-z0-9_-].
     * @param key
     *            the {@link PrivateKey} to be associated with the alias.
     * @param password
     *            the password used to seal the private key and the chain in the
     *            store. The password must contain a minimum of 16 characters
     *            and a maximum of 1000 characters.
     * @param chain
     *            the {@link Certificate} chain for the corresponding
     *            {@link PrivateKey}. The chain should contain the leaf at the
     *            first position of the array.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    @Override
    public void setPrivateKeyEntry(final String alias,
            final PrivateKey key, final char[] password,
            final Certificate[] chain) throws GeneralCryptoLibException {

        validatePrivateKeyEntry(alias, key, password, chain);

        byte[] derivedKey = deriveKey(password);
        char[] derivedPassword = derivePassword(derivedKey);
        try {
            _keyStore.setKeyEntry(alias, key, derivedPassword, chain);
        } catch (KeyStoreException e) {
            throw new CryptoLibException(ADD_ENTRY_ERROR, e);
        }
        _privateKeyAliases.add(alias);
        cache.putForPrivateKey(alias, password, derivedKey);
    }

    /**
     * Assigns the given {@link SecretKey} to the given alias, protecting it
     * with the given password.
     *
     * @see java.security.KeyStore#setKeyEntry(String, Key, char[],
     *      Certificate[])
     * @param alias
     *            the alias, a text of minimum 1 char, maximum 50 chars, from
     *            the alphabet [a-z0-9_-].
     * @param secretKey
     *            the {@link SecretKey} to be associated with the alias.
     * @param password
     *            the password used to seal the {@link SecretKey} in the store.
     *            The password must contain a minimum of 16 characters and a
     *            maximum of 1000 characters.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    @Override
    public void setSecretKeyEntry(final String alias,
            final SecretKey secretKey, final char[] password)
            throws GeneralCryptoLibException {

        validateSecretKeyEntry(alias, secretKey, password);

        byte[] encodedKey = secretKey.getEncoded();
        byte[] data = ByteArrays.concatenate(
            alias.getBytes(StandardCharsets.UTF_8), encodedKey);

        byte[] derivedKey = deriveKey(password);
        SecretKeySpec secretKeySpec = new SecretKeySpec(derivedKey,
            _configSecretKeyAlgorithmAndSpec.getAlgorithm());

        byte[] encrypted =
            _cryptoSymmetricCipher.encrypt(secretKeySpec, data);
        _secretKeys.put(alias, encrypted);
        _secretKeyAliases.add(alias);
        cache.putForSecretKey(alias, password, derivedKey);
    }

    @Override
    public void setElGamalPrivateKeyEntry(final String alias,
            final ElGamalPrivateKey key, final char[] password)
            throws GeneralCryptoLibException {

        validateElGamalKeyEntry(alias, key, password);

        byte[] encodedKey = key.toJson().getBytes(StandardCharsets.UTF_8);
        byte[] data = ByteArrays.concatenate(
            alias.getBytes(StandardCharsets.UTF_8), encodedKey);

        byte[] derivedKey = deriveKey(password);
        SecretKeySpec secretKeySpec = new SecretKeySpec(derivedKey,
            _configSecretKeyAlgorithmAndSpec.getAlgorithm());
        
        byte[] encrypted =
            _cryptoSymmetricCipher.encrypt(secretKeySpec, data);
        _elGamalPrivateKeys.put(alias, encrypted);
        _elGamalPrivateKeyAliases.add(alias);
        cache.putForElGamalPrivateKey(alias, password, derivedKey);
    }

    @Override
    public ElGamalPrivateKey getElGamalPrivateKeyEntry(final String alias,
            final char[] password) throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "ElGamal private key alias");
        Validate.notNullOrBlank(password, "ElGamal private key password");

        byte[] data = _elGamalPrivateKeys.get(alias);
        if (data == null || data.length == 0) {
            return null;
        }

        byte[] key = deriveKey(password);
        SecretKey secretKey = new SecretKeySpec(key,
            _configSecretKeyAlgorithmAndSpec.getAlgorithm());

        byte[] decrypt =
            _cryptoSymmetricCipher.decrypt(secretKey, data);
        checkDecryptedAlias(alias, decrypt);

        cache.putForElGamalPrivateKey(alias, password, key);

        int length = decrypt.length - alias.length();
        String json = new String(decrypt, alias.length(), length,
            StandardCharsets.UTF_8);
        return ElGamalPrivateKey.fromJson(json);
    }

    private void checkDecryptedAlias(final String alias,
            final byte[] decrypt) throws GeneralCryptoLibException {
        byte[] decryptedAlias = Arrays.copyOf(decrypt, alias.length());
        byte[] aliasBytes = alias.getBytes(StandardCharsets.UTF_8);
        if (!Arrays.equals(decryptedAlias, aliasBytes)) {
            throw new GeneralCryptoLibException("Data was tampered");
        }
    }

    /**
     * {@inheritDoc}
     *
     * @param out
     *            {@inheritDoc}
     * @param password
     *            {@inheritDoc} A password of a minimum of
     *            {@value ScytlKeyStoreConstants#MINIMUM_SKS_PASSWORD_LENGTH }
     *            characters and a maximum of
     *            {@value ScytlKeyStoreConstants#MAXIMUM_SKS_PASSWORD_LENGTH}.
     * @throws GeneralCryptoLibException
     */
    @Override
    public void store(final OutputStream out, final char[] password)
            throws GeneralCryptoLibException {

        Validate.notNull(out, "Key store output stream");
        Validate.notNullOrBlank(password, "Key store password");
        Validate.inRange(password.length,
            ScytlKeyStoreConstants.MINIMUM_SKS_PASSWORD_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_PASSWORD_LENGTH,
            "Key store password length", "", "");

        byte[] derivedKey = deriveKey(password);
        char[] derivePassword = derivePassword(derivedKey);

        ZipOutputStream zos =
            new ZipOutputStream(new BufferedOutputStream(out));
        try {

            ZipEntry saltEntry = new ZipEntry(ENTRY_TYPE.SALT.name());
            zos.putNextEntry(saltEntry);
            zos.write(_salt);
                    
            for (Map.Entry<String, byte[]> entry : _secretKeys.entrySet()) {
                ZipEntry secretEntry = new ZipEntry(String.format("%s.%s", ENTRY_TYPE.SECRET.name(), entry.getKey()));
                zos.putNextEntry(secretEntry);
                zos.write(entry.getValue());
            }
            
            for (Map.Entry<String, byte[]> entry : _elGamalPrivateKeys.entrySet()) {
                ZipEntry keyEntry = new ZipEntry(String.format("%s.%s", ENTRY_TYPE.ELGAMAL.name(), entry.getKey()));
                zos.putNextEntry(keyEntry);
                zos.write(entry.getValue());
            }
            
            ZipEntry storeEntry = new ZipEntry(ENTRY_TYPE.STORE.name());
            zos.putNextEntry(storeEntry);
            try {
                _keyStore.store(zos, derivePassword);
            } catch (GeneralSecurityException e) {
                throw new CryptoLibException(e);
            }

            zos.finish();
            zos.flush();

        } catch (IOException e) {
            throw new CryptoLibException(CREATE_ERROR, e);
        }
        cache.putForKeyStore(password, derivedKey);
    }

    @Override
    public String toJSON(char[] password)
            throws GeneralCryptoLibException {
        byte[] key = deriveKey(password);
        char[] derivedPassword = derivePassword(key);
        byte[] keyStoreJS;
        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
            _keyStore.store(stream, derivedPassword);
            keyStoreJS = stream.toByteArray();
        } catch (GeneralSecurityException | IOException e) {
            throw new GeneralCryptoLibException(
                "Failed to serialize to JSON.", e);
        }
        cache.putForKeyStore(password, key);
        return CryptoScytlKeyStoreWithPBKDFHelper.toJSON(_salt,
            _secretKeys, _elGamalPrivateKeys, keyStoreJS);
    }

    private void validatePrivateKeyEntry(final String alias,
            final PrivateKey key, final char[] password,
            final Certificate[] chain) throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "Private key alias");
        Validate.inRange(alias.length(),
            ScytlKeyStoreConstants.MINIMUM_SKS_ENTRY_ALIAS_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_ENTRY_ALIAS_LENGTH,
            "Private key alias length", "", "");
        Validate.onlyContains(alias,
            Pattern.compile(
                ScytlKeyStoreConstants.ALLOWED_SKS_ENTRY_ALIAS_CHARACTERS),
            "Private key alias");
        Validate.notNull(key, "Private key");
        Validate.notNullOrEmpty(key.getEncoded(), "Private key content");
        Validate.notNullOrBlank(password, "Private key password");
        Validate.inRange(password.length,
            ScytlKeyStoreConstants.MINIMUM_SKS_PASSWORD_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_PASSWORD_LENGTH,
            "Private key password length", "", "");
        Validate.notNullOrEmpty(chain, "Certificate chain");
        for (Certificate certificate : chain) {
            Validate.notNull(certificate, "Certificate in chain");
            try {
                Validate.notNullOrEmpty(certificate.getEncoded(),
                    "Content of certificate in chain");
            } catch (CertificateEncodingException e) {
                throw new GeneralCryptoLibException(
                    "Could not validate content of certificate in chain.",
                    e);
            }
        }
    }

    private void validateSecretKeyEntry(final String alias,
            final SecretKey secretKey, final char[] password)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "Secret key alias");
        Validate.inRange(alias.length(),
            ScytlKeyStoreConstants.MINIMUM_SKS_ENTRY_ALIAS_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_ENTRY_ALIAS_LENGTH,
            "Secret key alias length", "", "");
        Validate.onlyContains(alias,
            Pattern.compile(
                ScytlKeyStoreConstants.ALLOWED_SKS_ENTRY_ALIAS_CHARACTERS),
            "Secret key alias");
        Validate.notNull(secretKey, "Secret key");
        Validate.notNullOrEmpty(secretKey.getEncoded(),
            "Secret key content");
        Validate.notNullOrBlank(password, "Secret key password");
        Validate.inRange(password.length,
            ScytlKeyStoreConstants.MINIMUM_SKS_PASSWORD_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_PASSWORD_LENGTH,
            "Secret key password length", "", "");
    }

    private void validateElGamalKeyEntry(final String alias,
            final ElGamalPrivateKey key, final char[] password)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(alias, "ElGamal private key alias");
        Validate.inRange(alias.length(),
            ScytlKeyStoreConstants.MINIMUM_SKS_ENTRY_ALIAS_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_ENTRY_ALIAS_LENGTH,
            "ElGamal private key alias length", "", "");
        Validate.onlyContains(alias,
            Pattern.compile(
                ScytlKeyStoreConstants.ALLOWED_SKS_ENTRY_ALIAS_CHARACTERS),
            "ElGamal private key alias");
        Validate.notNull(key, "ElGamal private key");
        Validate.notNullOrBlank(password, "ElGamal private key password");
        Validate.inRange(password.length,
            ScytlKeyStoreConstants.MINIMUM_SKS_PASSWORD_LENGTH,
            ScytlKeyStoreConstants.MAXIMUM_SKS_PASSWORD_LENGTH,
            "ElGamal private key password length", "", "");
    }
}
