package org.bouncycastle.jce.provider.test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.bouncycastle.util.test.SimpleTest;
import org.bouncycastle.util.test.TestFailedException;

/**
 * check that cipher input/output streams are working correctly
 */
public class CipherStreamTest
    extends SimpleTest
{

    private static byte[] RK = Hex.decode("0123456789ABCDEF");
    private static byte[] RIN = Hex.decode("4e6f772069732074");
    private static byte[] ROUT = Hex.decode("3afbb5c77938280d");

    private static byte[] SIN = Hex.decode(
                    "00000000000000000000000000000000"
                  + "00000000000000000000000000000000"
                  + "00000000000000000000000000000000"
                  + "00000000000000000000000000000000");
    private static final byte[] SK = Hex.decode("80000000000000000000000000000000");
    private static final byte[] SIV = Hex.decode("0000000000000000");
    private static final byte[] SOUT = Hex.decode(
          "4DFA5E481DA23EA09A31022050859936"
        + "DA52FCEE218005164F267CB65F5CFD7F"
        + "2B4F97E0FF16924A52DF269515110A07"
        + "F9E460BC65EF95DA58F740B7D1DBB0AA");

    private static final byte[] XSK = Hex.decode("d5c7f6797b7e7e9c1d7fd2610b2abf2bc5a7885fb3ff78092fb3abe8986d35e2");
    private static final byte[] XSIV = Hex.decode("744e17312b27969d826444640e9c4a378ae334f185369c95");
    private static final byte[] XSIN = Hex.decode("7758298c628eb3a4b6963c5445ef66971222be5d1a4ad839715d1188071739b77cc6e05d5410f963a64167629757");
    private static final byte[] XSOUT= Hex.decode("27b8cfe81416a76301fd1eec6a4d99675069b2da2776c360db1bdfea7c0aa613913e10f7a60fec04d11e65f2d64e");

    private static final byte[] CHAK = Hex.decode("80000000000000000000000000000000");
    private static final byte[] CHAIV = Hex.decode("0000000000000000");
    private static final byte[] CHAIN =  Hex.decode(
                                              "00000000000000000000000000000000"
                                            + "00000000000000000000000000000000"
                                            + "00000000000000000000000000000000"
                                            + "00000000000000000000000000000000");
    private static final byte[] CHAOUT = Hex.decode("FBB87FBB8395E05DAA3B1D683C422046"
                                                    + "F913985C2AD9B23CFC06C1D8D04FF213"
                                                    + "D44A7A7CDB84929F915420A8A3DC58BF"
                                                    + "0F7ECB4B1F167BB1A5E6153FDAF4493D");

    private static final byte[] HCIN = new byte[64];
    private static final byte[] HCIV = new byte[32];

    private static final byte[] HCK256A = new byte[32];
    private static final byte[] HC256A = Hex.decode(
          "5B078985D8F6F30D42C5C02FA6B67951"
        + "53F06534801F89F24E74248B720B4818"
        + "CD9227ECEBCF4DBF8DBF6977E4AE14FA"
        + "E8504C7BC8A9F3EA6C0106F5327E6981");

    private static final byte[] HCK128A = new byte[16];
    private static final byte[] HC128A = Hex.decode(
          "82001573A003FD3B7FD72FFB0EAF63AA"
        + "C62F12DEB629DCA72785A66268EC758B"
        + "1EDB36900560898178E0AD009ABF1F49"
        + "1330DC1C246E3D6CB264F6900271D59C");

    private static final byte[] GRAIN_V1 = Hex.decode("0123456789abcdef1234");
    private static final byte[] GRAIN_V1_IV = Hex.decode("0123456789abcdef");
    private static final byte[] GRAIN_V1_IN = new byte[10];
    private static final byte[] GRAIN_V1_OUT = Hex.decode("7f362bd3f7abae203664");

    private static final byte[] GRAIN_128 = Hex.decode("0123456789abcdef123456789abcdef0");
    private static final byte[] GRAIN_128_IV = Hex.decode("0123456789abcdef12345678");
    private static final byte[] GRAIN_128_IN = new byte[16];
    private static final byte[] GRAIN_128_OUT = Hex.decode("afb5babfa8de896b4b9c6acaf7c4fbfd");

    public CipherStreamTest()
    {
    }

    private void runTest(
        String  name)
        throws Exception
    {
        String lCode = "ABCDEFGHIJKLMNOPQRSTUVWXY0123456789";
        KeyGenerator            kGen;

        if (name.indexOf('/') < 0)
        {
            kGen = KeyGenerator.getInstance(name, "BC");
        }
        else
        {
            kGen = KeyGenerator.getInstance(name.substring(0, name.indexOf('/')), "BC");
        }

        byte[]                  data = lCode.getBytes();
        Cipher                  in = Cipher.getInstance(name, "BC");
        Cipher                  out = Cipher.getInstance(name, "BC");
        Key                     key = kGen.generateKey();
        ByteArrayInputStream    bIn = new ByteArrayInputStream(data);
        ByteArrayOutputStream   bOut = new ByteArrayOutputStream();

        in.init(Cipher.ENCRYPT_MODE, key);
        if (in.getIV() != null)
        {
            out.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(in.getIV()));
        }
        else
        {
            out.init(Cipher.DECRYPT_MODE, key);
        }

        CipherInputStream       cIn = new CipherInputStream(bIn, in);
        CipherOutputStream      cOut = new CipherOutputStream(bOut, out);

        int c;

        while ((c = cIn.read()) >= 0)
        {
            cOut.write(c);
        }

        cIn.close();

        cOut.flush();
        cOut.close();

        String  res = new String(bOut.toByteArray());

        if (!res.equals(lCode))
        {
            fail("Failed - decrypted data doesn't match.");
        }


        //
        // short buffer test
        //
        try
        {
            byte[] enc = in.doFinal(data);
            byte[] out1 = new byte[enc.length / 2];

            try
            {
                out.doFinal(enc, 0, enc.length, out1, 0);

                fail("ShortBufferException not triggered");
            }
            catch (ShortBufferException e)
            {
                byte[] out2 = new byte[in.getOutputSize(enc.length)];

                int count = out.doFinal(enc, 0, enc.length, out2, 0);

                if (!areEqual(out2, count, data))
                {
                    fail("" + name + " failed decryption - expected " + new String(Hex.encode(data)) + " got " + new String(Hex.encode(out2)));
                }
            }
        }
        catch (TestFailedException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            fail("" + name + " failed short buffer decryption - " + e.toString());
        }
    }


    private boolean areEqual(byte[] a, int aLen, byte[] b)
    {
        if (b.length != aLen)
        {
            return false;
        }

        for (int i = 0; i != aLen; i++)
        {
            if (a[i] != b[i])
            {
                return false;
            }
        }

        return true;
    }

    private void testAlgorithm(String name, byte[] keyBytes, byte[] iv, byte[] plainText, byte[] cipherText)
        throws Exception
    {
        SecretKey key = new SecretKeySpec(keyBytes, name);
        Cipher    in = Cipher.getInstance(name, "BC");
        Cipher    out = Cipher.getInstance(name, "BC");

        if (iv != null)
        {
            in.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
            out.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        }
        else
        {
            in.init(Cipher.ENCRYPT_MODE, key);
            out.init(Cipher.DECRYPT_MODE, key);
        }

        byte[] enc = in.doFinal(plainText);
        if (!areEqual(enc, cipherText))
        {
            fail(name + ": cipher text doesn't match got " + new String(Hex.encode(enc)));
        }

        byte[] dec = out.doFinal(enc);

        if (!areEqual(dec, plainText))
        {
            fail(name + ": plain text doesn't match");
        }
    }

    private void testException(
        String  name)
    {
        try
        {
            byte[] key128 = {
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143,
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143 };

            byte[] key256 = {
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143,
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143,
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143,
                    (byte)128, (byte)131, (byte)133, (byte)134,
                    (byte)137, (byte)138, (byte)140, (byte)143 };

            byte[] keyBytes;
            if (name.equals("HC256") || name.equals("XSalsa20"))
            {
                keyBytes = key256;
            }
            else
            {
                keyBytes = key128;
            }

            SecretKeySpec cipherKey = new SecretKeySpec(keyBytes, name);
            Cipher ecipher = Cipher.getInstance(name, "BC");
            ecipher.init(Cipher.ENCRYPT_MODE, cipherKey);

            byte[] cipherText = new byte[0];
            try
            {
                // According specification Method engineUpdate(byte[] input,
                // int inputOffset, int inputLen, byte[] output, int
                // outputOffset)
                // throws ShortBufferException - if the given output buffer is
                // too
                // small to hold the result
                ecipher.update(new byte[20], 0, 20, cipherText);

                fail("failed exception test - no ShortBufferException thrown");
            }
            catch (ShortBufferException e)
            {
                // ignore
            }

            try
            {
                Cipher c = Cipher.getInstance(name, "BC");

                Key k = new PublicKey()
                {

                    public String getAlgorithm()
                    {
                        return "STUB";
                    }

                    public String getFormat()
                    {
                        return null;
                    }

                    public byte[] getEncoded()
                    {
                        return null;
                    }

                };

                c.init(Cipher.ENCRYPT_MODE, k);

                fail("failed exception test - no InvalidKeyException thrown for public key");
            }
            catch (InvalidKeyException e)
            {
                // okay
            }

            try
            {
                Cipher c = Cipher.getInstance(name, "BC");

                Key k = new PrivateKey()
                {

                    public String getAlgorithm()
                    {
                        return "STUB";
                    }

                    public String getFormat()
                    {
                        return null;
                    }

                    public byte[] getEncoded()
                    {
                        return null;
                    }

                };

                c.init(Cipher.DECRYPT_MODE, k);

                fail("failed exception test - no InvalidKeyException thrown for private key");
            }
            catch (InvalidKeyException e)
            {
                // okay
            }
        }
        catch (Exception e)
        {
            fail("unexpected exception.", e);
        }
    }

    public void performTest()
        throws Exception
    {
        runTest("RC4");
        testException("RC4");
        testAlgorithm("RC4", RK, null, RIN, ROUT);
        runTest("Salsa20");
        testException("Salsa20");
        testAlgorithm("Salsa20", SK, SIV, SIN, SOUT);
        runTest("XSalsa20");
        testException("XSalsa20");
        testAlgorithm("XSalsa20", XSK, XSIV, XSIN, XSOUT);
        runTest("ChaCha");
        testException("ChaCha");
        testAlgorithm("ChaCha", CHAK, CHAIV, CHAIN, CHAOUT);
        runTest("HC128");
        testException("HC128");
        testAlgorithm("HC128", HCK128A, HCIV, HCIN, HC128A);
        runTest("HC256");
        testException("HC256");
        testAlgorithm("HC256", HCK256A, HCIV, HCIN, HC256A);
        runTest("VMPC");
        testException("VMPC");
        //testAlgorithm("VMPC", a, iv, in, a);
        runTest("VMPC-KSA3");
        testException("VMPC-KSA3");
        //testAlgorithm("VMPC-KSA3", a, iv, in, a);
        testAlgorithm("Grainv1", GRAIN_V1, GRAIN_V1_IV, GRAIN_V1_IN, GRAIN_V1_OUT);
        testAlgorithm("Grain128", GRAIN_128, GRAIN_128_IV, GRAIN_128_IN, GRAIN_128_OUT);
        runTest("DES/ECB/PKCS7Padding");
        runTest("DES/CFB8/NoPadding");
    }

    public String getName()
    {
        return "CipherStreamTest";
    }


    public static void main(
        String[]    args)
    {
        Security.addProvider(new BouncyCastleProvider());

        runTest(new CipherStreamTest());
    }
}
