/*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you under the Apache License, Version 2.0 (the
*  "License"); you may not use this file except in compliance
*  with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing,
*  software distributed under the License is distributed on an
*   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
*  KIND, either express or implied.  See the License for the
*  specific language governing permissions and limitations
*  under the License.
*/
package org.apache.synapse.securevault;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.securevault.definition.CipherInformation;
import org.apache.synapse.securevault.definition.IdentityKeyStoreInformation;
import org.apache.synapse.securevault.definition.KeyStoreInformation;
import org.apache.synapse.securevault.definition.TrustKeyStoreInformation;
import org.apache.synapse.securevault.keystore.IdentityKeyStoreWrapper;
import org.apache.synapse.securevault.keystore.KeyStoreWrapper;
import org.apache.synapse.securevault.keystore.TrustKeyStoreWrapper;
import org.apache.synapse.securevault.secret.SecretInformation;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;

/**
 * Wraps the cipher and expose abstraction need for synapse ciphering
 */
public abstract class BaseCipher implements EncryptionProvider, DecryptionProvider {

    private CipherInformation cipherInformation;
    private KeyStoreInformation keystoreInformation;
    private static Log log = LogFactory.getLog(BaseCipher.class);
    /* Underlying cipher instance*/
    private Cipher cipher;
    protected KeyStoreWrapper keyStoreWrapper;
    private Key key;

    protected BaseCipher(CipherInformation cipherInformation,
                         KeyStoreInformation keystoreInformation) {
        this.cipherInformation = cipherInformation;
        this.keystoreInformation = keystoreInformation;
        if (keystoreInformation instanceof TrustKeyStoreInformation) {
            keyStoreWrapper = new TrustKeyStoreWrapper();
            ((TrustKeyStoreWrapper) keyStoreWrapper).init(
                    (TrustKeyStoreInformation) keystoreInformation);
        } else {
            keyStoreWrapper = new IdentityKeyStoreWrapper();
            IdentityKeyStoreInformation identityKeyStore =
                    (IdentityKeyStoreInformation) keystoreInformation;

            SecretInformation secretInformation = identityKeyStore.getKeyPasswordProvider();
            if (secretInformation != null) { //TODO validate
                ((IdentityKeyStoreWrapper) keyStoreWrapper).init(identityKeyStore,
                        secretInformation.getResolvedSecret());
            }
        }
        init();
    }

    protected BaseCipher(CipherInformation cipherInformation,
                         KeyStoreWrapper keyStoreWrapper) {
        this.keyStoreWrapper = keyStoreWrapper;
        this.cipherInformation = cipherInformation;
        init();
    }

    protected BaseCipher(CipherInformation cipherInformation, Key key) {
        this.key = key;
        this.cipherInformation = cipherInformation;
        init();
    }

    private void init() {

        String algorithm = cipherInformation.getAlgorithm();
        CipherOperationMode opMode = cipherInformation.getCipherOperationMode();
        if (key == null) {
            key = getKey(opMode);
        }
        if (log.isDebugEnabled()) {
            log.debug("Initializing cipher with algorithm " +
                    "'" + algorithm + "' in mode '" + opMode + "'");
        }
        try {
            String provider = cipherInformation.getProvider();
            if (provider != null && !"".equals(provider)) {
                try {
                    cipher = Cipher.getInstance(algorithm, provider.trim());
                } catch (NoSuchProviderException e) {
                    throw new SecureVaultException("Invalid Provider : " + provider, log);
                }
            } else {
                cipher = Cipher.getInstance(algorithm);
            }
            if (opMode == CipherOperationMode.ENCRYPT) {
                cipher.init(Cipher.ENCRYPT_MODE, key);
            } else if (opMode == CipherOperationMode.DECRYPT) {
                cipher.init(Cipher.DECRYPT_MODE, key);
            } else {
                throw new SecureVaultException("Invalid mode : " + opMode, log);
            }

        } catch (NoSuchAlgorithmException e) {
            throw new SecureVaultException("There is no algorithm support for " +
                    "'" + algorithm + "' in the operation mode '" + opMode + "'" + e, log);
        } catch (NoSuchPaddingException e) {
            throw new SecureVaultException("There is no padding scheme  for " +
                    "'" + algorithm + "' in the operation mode '" + opMode + "'" + e, log);
        } catch (InvalidKeyException e) {
            throw new SecureVaultException("Invalid key ", e, log);
        }
    }

    public CipherInformation getCipherInformation() {
        return cipherInformation;
    }

    public KeyStoreInformation getKeyStoreInformation() {
        return keystoreInformation;
    }

    /**
     * Returns the correct key for correct operation
     *
     * @param operationMode Ciper operation
     * @return A key
     */
    public abstract Key getKey(CipherOperationMode operationMode);

    /**
     * Do cryptographic operation
     *
     * @param inputStream Input Stream
     * @return result
     */
    private byte[] doCipherOperation(byte[] inputStream) {

        InputStream sourceStream = new ByteArrayInputStream(inputStream);
        if (cipherInformation.getInType() != null) {
            try {
                sourceStream = EncodingHelper.decode(
                        sourceStream, cipherInformation.getInType());
            } catch (IOException e) {
                throw new SecureVaultException("IOError when decoding the input " +
                        "stream for cipher ", e, log);
            }
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        CipherOutputStream out = new CipherOutputStream(baos, cipher);

        byte[] buffer = new byte[64];
        int length;
        try {
            while ((length = sourceStream.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
        } catch (IOException e) {
            throw new SecureVaultException("IOError when reading the input" +
                    " stream for cipher ", e, log);
        } finally {
            try {
                sourceStream.close();
                out.flush();
                out.close();
            } catch (IOException ignored) {
                // ignore exception
            }
        }

        if (cipherInformation.getOutType() != null) {
            return EncodingHelper.encode(baos, cipherInformation.getOutType());
        } else {
            return baos.toByteArray();
        }
    }

    @Override
    public byte[] encrypt(byte[] plainText) {
        return doCipherOperation(plainText);
    }

    @Override
    public byte[] decrypt(byte[] cipherText) {
        return doCipherOperation(cipherText);
    }
}
