/*
 * 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.camel.component.crypto.cms;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;

import org.apache.camel.Exchange;
import org.apache.camel.component.crypto.cms.crypt.DefaultEnvelopedDataDecryptorConfiguration;
import org.apache.camel.component.crypto.cms.crypt.DefaultKeyTransRecipientInfo;
import org.apache.camel.component.crypto.cms.crypt.EnvelopedDataDecryptor;
import org.apache.camel.component.crypto.cms.crypt.EnvelopedDataEncryptor;
import org.apache.camel.component.crypto.cms.crypt.EnvelopedDataEncryptorConfiguration;
import org.apache.camel.component.crypto.cms.crypt.RecipientInfo;
import org.apache.camel.component.crypto.cms.exception.CryptoCmsException;
import org.apache.camel.component.crypto.cms.exception.CryptoCmsFormatException;
import org.apache.camel.component.crypto.cms.exception.CryptoCmsNoCertificateForRecipientsException;
import org.apache.camel.component.crypto.cms.exception.CryptoCmsNoKeyOrCertificateForAliasException;
import org.apache.camel.component.crypto.cms.util.ExchangeUtil;
import org.apache.camel.component.crypto.cms.util.KeystoreUtil;
import org.apache.camel.support.jsse.KeyStoreParameters;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class EnvelopedDataTest {

    @BeforeAll
    public static void setUpProvider() {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Test
    public void executeDESedeCBClength192() throws Exception {
        encryptDecrypt("system.jks", "rsa", "DESede/CBC/PKCS5Padding", 192);
    }

    @Test
    public void executeDESedeCBClength128() throws Exception {
        encryptDecrypt("system.jks", "rsa", "DESede/CBC/PKCS5Padding", 128);
    }

    @Test
    public void executeDESCBCkeyLength64() throws Exception {
        encryptDecrypt("system.jks", "rsa", "DES/CBC/PKCS5Padding", 64);
    }

    @Test
    public void executeDESCBCkeyLength56() throws Exception {
        encryptDecrypt("system.jks", "rsa", "DES/CBC/PKCS5Padding", 56);
    }

    @Test
    public void executeCAST5CBCkeyLength128() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 128);
    }

    @Test
    public void executeCAST5CBCkeyLength120() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 120);
    }

    @Test
    public void executeCAST5CBCkeyLength112() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 112);
    }

    @Test
    public void executeCAST5CBCkeyLength104() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 104);
    }

    @Test
    public void executeCAST5CBCkeyLength96() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 96);
    }

    @Test
    public void executeCAST5CBCkeyLength88() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 88);
    }

    @Test
    public void executeCAST5CBCkeyLength80() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 80);
    }

    @Test
    public void executeCAST5CBCkeyLength72() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 72);
    }

    @Test
    public void executeCAST5CBCkeyLength64() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 64);
    }

    @Test
    public void executeCAST5CBCkeyLength56() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 56);
    }

    @Test
    public void executeCAST5CBCkeyLength48() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 48);
    }

    @Test
    public void executeCAST5CBCkeyLength40() throws Exception {
        encryptDecrypt("system.jks", "rsa", "CAST5/CBC/PKCS5Padding", 40);
    }

    @Test
    public void executeRC2CBCkeyLength128() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 128);
    }

    @Test
    public void executeRC2CBCkeyLength120() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 120);
    }

    @Test
    public void executeRC2CBCkeyLength112() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 112);
    }

    @Test
    public void executeRC2CBCkeyLength104() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 104);
    }

    @Test
    public void executeRC2CBCkeyLength96() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 96);
    }

    @Test
    public void executeRC2CBCkeyLength88() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 88);
    }

    @Test
    public void executeRC2CBCkeyLength80() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 80);
    }

    @Test
    public void executeRC2CBCkeyLength72() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 72);
    }

    @Test
    public void executeRC2CBCkeyLength64() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 64);
    }

    @Test
    public void executeRC2CBCkeyLength56() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 56);
    }

    @Test
    public void executeRC2CBCkeyLength48() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 48);
    }

    @Test
    public void executeRC2CBCkeyLength40() throws Exception {
        encryptDecrypt("system.jks", "rsa", "RC2/CBC/PKCS5Padding", 40);
    }

    @Test
    public void executeCamelliaCBCKeySize128() throws Exception {
        encryptDecrypt("system.jks", "rsa", "Camellia/CBC/PKCS5Padding", 128);
    }

    /** Works if strong encryption policy jars are installed. */
    @Disabled
    @Test
    public void executeCamelliaCBCKeySize256() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "Camellia/CBC/PKCS5Padding", 256));
    }

    /** Works if strong encryption policy jars are installed. */
    @Disabled
    @Test
    public void executeCamelliaCBCKeySize192() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "Camellia/CBC/PKCS5Padding", 192));
    }

    @Test
    public void executeNoInWhiteListCamellia256CBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "Camellia256/CBC/PKCS5Padding", 256));
    }

    @Test
    public void executeNotInWhiteListCamellia192CBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "Camellia192/CBC/PKCS5Padding", 192));
    }

    /** Works if strong encryption policy jars are installed. */
    @Disabled
    @Test
    public void executeAESCBCKeySize256() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES/CBC/PKCS5Padding", 256));
    }

    /** Works if strong encryption policy jars are installed. */
    @Disabled
    @Test
    public void executeAESCBCKeySize192() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES/CBC/PKCS5Padding", 192));
    }

    @Test
    public void executeAESCBCKeySize128() throws Exception {
        encryptDecrypt("system.jks", "rsa", "AES/CBC/PKCS5Padding", 128);
    }

    @Test
    public void executeNotInWhiteListAES256CBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES256/CBC/PKCS5Padding", 256));
    }

    @Test
    public void executeNotInWhiteListAES192CBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES192/CBC/PKCS5Padding", 192));
    }

    @Test
    public void executerNoImplRSAECB() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "RSA/ECB/OAEP", 0));
    }

    @Test
    public void executeNotInWhiteListAESGCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES/GCM/NoPadding", 128));
    }

    @Test
    public void executeNotInWhiteListAES192GCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES192/GCM/NoPadding", 192));
    }

    @Test
    public void executeNotInWhiteListAES256GCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES256/GCM/NoPadding", 256));
    }

    @Test
    public void executeNotInWhiteListAES256CCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES256/CCM/NoPadding", 256));
    }

    @Test
    public void executeNotInWhiteListIDEACBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "IDEA/CBC/PKCS5Padding", 128));
    }

    @Test
    public void executeNotInWhiteListAESCCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES/CCM/NoPadding", 128));
    }

    @Test
    public void executeNotInWhiteListAES192CCM() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "AES192/CCM/NoPadding", 192));
    }

    @Test
    public void executeNotInWhiteListRC5CBC() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "RC5/CBC/PKCS5Padding", 0));
    }

    @Test
    public void wrongSecretKeyLength() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encrypt("system.jks", "DESede/CBC/PKCS5Padding", 200, "testMessage", "rsa"));
    }

    @Test
    public void wrongContentEncryptionAlgorithm() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> encryptDecrypt("system.jks", "rsa", "WrongDESede/CBC/PKCS5Padding", 200));
    }

    @Test
    public void wrongEncryptAlias() throws Exception {
        assertThrows(CryptoCmsNoKeyOrCertificateForAliasException.class,
                () -> encrypt("system.jks", "DESede/CBC/PKCS5Padding", 128, "testMessage", "wrongAlias"));
    }

    @Test
    public void encryptWrongAliasAndCorrectAlias() throws Exception {
        assertThrows(CryptoCmsNoKeyOrCertificateForAliasException.class,
                () -> encrypt("system.jks", "DESede/CBC/PKCS5Padding", 128, "testMessage", "wrongAlias", "rsa"));
    }

    @Test
    public void encryptTwoWrongAliases() throws Exception {
        assertThrows(CryptoCmsNoKeyOrCertificateForAliasException.class,
                () -> encrypt("system.jks", "DESede/CBC/PKCS5Padding", 128, "testMessage", "wrongAlias", "wrongAlias2"));
    }

    @Test
    public void encryptTwoCorrectAliases() throws Exception {
        encrypt("system.jks", "DESede/CBC/PKCS5Padding", 128, "testMessage", "rsa2", "rsa");
    }

    @Test
    public void wrongEncryptedMessage() throws Exception {
        assertThrows(CryptoCmsFormatException.class,
                () -> decrypt("system.jks", "TestMessage".getBytes()));
    }

    @Test
    public void wrongEncryptedEmptyMessage() throws Exception {
        assertThrows(CryptoCmsException.class,
                () -> decrypt("system.jks", new byte[0]));
    }

    @Test
    public void decryptionWithEmptyAlias() throws Exception {
        byte[] bytes = encrypt("system.jks", "DESede/CBC/PKCS5Padding", 192, "Test Message", "rsa");
        decrypt("system.jks", bytes);
    }

    @Test
    public void decryptionWithNullAliasWrongKeystore() throws Exception {
        byte[] bytes = encrypt("system.jks", "DESede/CBC/PKCS5Padding", 192, "Test Message", "rsa");
        assertThrows(CryptoCmsNoCertificateForRecipientsException.class,
                () -> decrypt("test.jks", bytes));
    }

    // @Test
    // public void invalidContentTypeEnvelopedData() throws Exception {
    // try {
    // encryptDecrypt("system.jks", "rsa", "DESede/CBC/PKCS5Padding", 192,
    // CmsEnvelopedDataDecryptorConfiguration.SIGNEDANDENVELOPEDDATA);
    // } catch (CmsException e) {
    // Assert.assertTrue(e.getMessage().contains("The PKCS#7/CMS decryptor step
    // does not accept PKCS#7/CMS messages of content type 'Enveloped Data'"));
    // return;
    // }
    // Assert.fail("Exception expected");
    // }

    private void encryptDecrypt(String keystoreName, String alias, String contentEncryptionAlgorithm, int secretKeyLength)
            throws Exception {
        String message = "Test Message";

        byte[] encrypted = encrypt(keystoreName, contentEncryptionAlgorithm, secretKeyLength, message, alias);
        byte[] decrypted = decrypt(keystoreName, encrypted);

        String actual = new String(decrypted, "UTF-8");
        assertEquals(message, actual);
    }

    private byte[] encrypt(
            String keystoreName, String contentEncryptionAlgorithm, int secretKeyLength, String message, String... aliases)
            throws UnsupportedEncodingException, Exception {
        KeyStoreParameters keystorePas = KeystoreUtil.getKeyStoreParameters(keystoreName);

        List<RecipientInfo> recipients = new ArrayList<>(aliases.length);
        for (String alias : aliases) {
            DefaultKeyTransRecipientInfo recipient = new DefaultKeyTransRecipientInfo();
            recipient.setCertificateAlias(alias);
            recipient.setKeyStoreParameters(keystorePas);
            recipients.add(recipient);
        }

        EnvelopedDataEncryptorConfiguration enConf = new EnvelopedDataEncryptorConfiguration(null);
        enConf.setContentEncryptionAlgorithm(contentEncryptionAlgorithm);
        for (RecipientInfo recipient : recipients) {
            enConf.setRecipient(recipient);
        }
        enConf.setSecretKeyLength(secretKeyLength); // optional
        // enConf.setBlockSize(2048); // optional
        enConf.init();
        EnvelopedDataEncryptor encryptor = new EnvelopedDataEncryptor(enConf);

        Exchange exchange = ExchangeUtil.getExchange();
        exchange.getIn().setBody(new ByteArrayInputStream(message.getBytes("UTF-8")));
        encryptor.process(exchange);
        byte[] encrypted = (byte[]) exchange.getMessage().getBody();
        return encrypted;
    }

    private byte[] decrypt(String keystoreName, byte[] encrypted) throws UnsupportedEncodingException, Exception, IOException {

        KeyStoreParameters keystore = KeystoreUtil.getKeyStoreParameters(keystoreName);

        Exchange exchangeDecrypt = ExchangeUtil.getExchange();
        exchangeDecrypt.getIn().setBody(new ByteArrayInputStream(encrypted));

        DefaultEnvelopedDataDecryptorConfiguration conf = new DefaultEnvelopedDataDecryptorConfiguration();
        conf.setKeyStoreParameters(keystore);
        EnvelopedDataDecryptor decryptor = new EnvelopedDataDecryptor(conf);

        decryptor.process(exchangeDecrypt);

        byte[] decrypted = (byte[]) exchangeDecrypt.getMessage().getBody();

        return decrypted;
    }

}
