package org.spongycastle.crypto.examples;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.SecureRandom;

import org.spongycastle.crypto.CryptoException;
import org.spongycastle.crypto.KeyGenerationParameters;
import org.spongycastle.crypto.engines.DESedeEngine;
import org.spongycastle.crypto.generators.DESedeKeyGenerator;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.DESedeParameters;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex;

/**
 * DESExample is a simple DES based encryptor/decryptor.
 * <p>
 * The program is command line driven, with the input
 * and output files specified on the command line.
 * <pre>
 * java org.spongycastle.crypto.examples.DESExample infile outfile [keyfile]
 * </pre>
 * A new key is generated for each encryption, if key is not specified,
 * then the example will assume encryption is required, and as output
 * create deskey.dat in the current directory.  This key is a hex
 * encoded byte-stream that is used for the decryption.  The output
 * file is Hex encoded, 60 characters wide text file.
 * <p>
 * When encrypting;
 * <ul>
 *  <li>the infile is expected to be a byte stream (text or binary)
 *  <li>there is no keyfile specified on the input line
 * </ul>
 * <p>
 * When decrypting;
 * <ul>
 *  <li>the infile is expected to be the 60 character wide base64 
 *    encoded file
 *  <li>the keyfile is expected to be a base64 encoded file
 * </ul>
 * <p>
 * This example shows how to use the light-weight API, DES and
 * the filesystem for message encryption and decryption.
 *
 */
public class DESExample extends Object
{
    // Encrypting or decrypting ?
    private boolean encrypt = true;

    // To hold the initialised DESede cipher
    private PaddedBufferedBlockCipher cipher = null;

    // The input stream of bytes to be processed for encryption
    private BufferedInputStream in = null;

    // The output stream of bytes to be procssed
    private BufferedOutputStream out = null;

    // The key
    private byte[] key = null;

    /*
     * start the application
     */
    public static void main(String[] args)
    {
        boolean encrypt = true;
        String infile = null;
        String outfile = null;
        String keyfile = null;

        if (args.length < 2)
        {
            DESExample de = new DESExample();
            System.err.println("Usage: java "+de.getClass().getName()+
                                " infile outfile [keyfile]");
            System.exit(1);
        }

        keyfile = "deskey.dat";
        infile = args[0];
        outfile = args[1];

        if (args.length > 2)
        {
            encrypt = false;
            keyfile = args[2];
        }

        DESExample de = new DESExample(infile, outfile, keyfile, encrypt);
        de.process();
    }

    // Default constructor, used for the usage message
    public DESExample()
    {
    }

    /*
     * Constructor, that takes the arguments appropriate for
     * processing the command line directives.
     */
    public DESExample(
                String infile,
                String outfile,
                String keyfile,
                boolean encrypt)
    {
        /* 
         * First, determine that infile & keyfile exist as appropriate.
         *
         * This will also create the BufferedInputStream as required
         * for reading the input file.  All input files are treated
         * as if they are binary, even if they contain text, it's the
         * bytes that are encrypted.
         */
        this.encrypt = encrypt;
        try
        {
            in = new BufferedInputStream(new FileInputStream(infile));
        }
        catch (FileNotFoundException fnf)
        {
            System.err.println("Input file not found ["+infile+"]");
            System.exit(1);
        }

        try
        {
            out = new BufferedOutputStream(new FileOutputStream(outfile));
        }
        catch (IOException fnf)
        {
            System.err.println("Output file not created ["+outfile+"]");
            System.exit(1);
        }

        if (encrypt)
        {
            try
            {
                /*
                 * The process of creating a new key requires a 
                 * number of steps.
                 *
                 * First, create the parameters for the key generator
                 * which are a secure random number generator, and
                 * the length of the key (in bits).
                 */
                SecureRandom sr = null;
                try
                {
                    sr = new SecureRandom();
                    /*
                     * This following call to setSeed() makes the
                     * initialisation of the SecureRandom object
                     * _very_ fast, but not secure AT ALL.  
                     *
                     * Remove the line, recreate the class file and 
                     * then run DESExample again to see the difference.
                     *
                     * The initialisation of a SecureRandom object
                     * can take 5 or more seconds depending on the
                     * CPU that the program is running on.  That can
                     * be annoying during unit testing.
                     *     -- jon
                     */
                    sr.setSeed("www.bouncycastle.org".getBytes());
                }
                catch (Exception nsa)
                {
                    System.err.println("Hmmm, no SHA1PRNG, you need the "+
                                        "Sun implementation");
                    System.exit(1);
                }
                KeyGenerationParameters kgp = new KeyGenerationParameters(
                                    sr, 
                                    DESedeParameters.DES_EDE_KEY_LENGTH*8);

                /*
                 * Second, initialise the key generator with the parameters
                 */
                DESedeKeyGenerator kg = new DESedeKeyGenerator();
                kg.init(kgp);

                /*
                 * Third, and finally, generate the key
                 */
                key = kg.generateKey();

                /*
                 * We can now output the key to the file, but first
                 * hex encode the key so that we can have a look
                 * at it with a text editor if we so desire
                 */
                BufferedOutputStream keystream = 
                    new BufferedOutputStream(new FileOutputStream(keyfile));
                byte[] keyhex = Hex.encode(key);
                keystream.write(keyhex, 0, keyhex.length);
                keystream.flush();
                keystream.close();
            }
            catch (IOException createKey)
            {
                System.err.println("Could not decryption create key file "+
                                    "["+keyfile+"]");
                System.exit(1);
            }
        }
        else
        {
            try
            {
                // read the key, and decode from hex encoding
                BufferedInputStream keystream = 
                    new BufferedInputStream(new FileInputStream(keyfile));
                int len = keystream.available();
                byte[] keyhex = new byte[len];
                keystream.read(keyhex, 0, len);
                key = Hex.decode(keyhex);
            }
            catch (IOException ioe)
            {
                System.err.println("Decryption key file not found, "+
                                    "or not valid ["+keyfile+"]");
                System.exit(1);
            }
        }
    }

    private void process()
    {
        /* 
         * Setup the DESede cipher engine, create a PaddedBufferedBlockCipher
         * in CBC mode.
         */
        cipher = new PaddedBufferedBlockCipher(
                                    new CBCBlockCipher(new DESedeEngine()));

        /*
         * The input and output streams are currently set up
         * appropriately, and the key bytes are ready to be
         * used.
         *
         */

        if (encrypt)
        {
            performEncrypt(key);
        }
        else
        {
            performDecrypt(key);
        }

        // after processing clean up the files
        try
        {
            in.close();
            out.flush();
            out.close();
        }
        catch (IOException closing)
        {

        }
    }
        
    /*
     * This method performs all the encryption and writes
     * the cipher text to the buffered output stream created
     * previously.
     */
    private void performEncrypt(byte[] key)
    {
        // initialise the cipher with the key bytes, for encryption
        cipher.init(true, new KeyParameter(key));

        /*
         * Create some temporary byte arrays for use in
         * encryption, make them a reasonable size so that
         * we don't spend forever reading small chunks from
         * a file.
         *
         * There is no particular reason for using getBlockSize()
         * to determine the size of the input chunk.  It just
         * was a convenient number for the example.  
         */
        // int inBlockSize = cipher.getBlockSize() * 5;
        int inBlockSize = 47;
        int outBlockSize = cipher.getOutputSize(inBlockSize);

        byte[] inblock = new byte[inBlockSize];
        byte[] outblock = new byte[outBlockSize];

        /* 
         * now, read the file, and output the chunks
         */
        try
        {
            int inL;
            int outL;
            byte[] rv = null;
            while ((inL=in.read(inblock, 0, inBlockSize)) > 0)
            {
                outL = cipher.processBytes(inblock, 0, inL, outblock, 0);
                /*
                 * Before we write anything out, we need to make sure
                 * that we've got something to write out. 
                 */
                if (outL > 0)
                {
                    rv = Hex.encode(outblock, 0, outL);
                    out.write(rv, 0, rv.length);
                    out.write('\n');
                }
            }

            try
            {
                /*
                 * Now, process the bytes that are still buffered
                 * within the cipher.
                 */
                outL = cipher.doFinal(outblock, 0);
                if (outL > 0)
                {
                    rv = Hex.encode(outblock, 0, outL);
                    out.write(rv, 0, rv.length);
                    out.write('\n');
                }
            }
            catch (CryptoException ce)
            {

            }
        }
        catch (IOException ioeread)
        {
            ioeread.printStackTrace();
        }
    }

    /*
     * This method performs all the decryption and writes
     * the plain text to the buffered output stream created
     * previously.
     */
    private void performDecrypt(byte[] key)
    {    
        // initialise the cipher for decryption
        cipher.init(false, new KeyParameter(key));

        /* 
         * As the decryption is from our preformatted file,
         * and we know that it's a hex encoded format, then
         * we wrap the InputStream with a BufferedReader
         * so that we can read it easily.
         */
        BufferedReader br = new BufferedReader(new InputStreamReader(in));

        /* 
         * now, read the file, and output the chunks
         */
        try
        {
            int outL;
            byte[] inblock = null;
            byte[] outblock = null;
            String rv = null;
            while ((rv = br.readLine()) != null)
            {
                inblock = Hex.decode(rv);
                outblock = new byte[cipher.getOutputSize(inblock.length)];

                outL = cipher.processBytes(inblock, 0, inblock.length, 
                                            outblock, 0);
                /*
                 * Before we write anything out, we need to make sure
                 * that we've got something to write out. 
                 */
                if (outL > 0)
                {
                    out.write(outblock, 0, outL);
                }
            }

            try
            {
                /*
                 * Now, process the bytes that are still buffered
                 * within the cipher.
                 */
                outL = cipher.doFinal(outblock, 0);
                if (outL > 0)
                {
                    out.write(outblock, 0, outL);
                }
            }
            catch (CryptoException ce)
            {

            }
        }
        catch (IOException ioeread)
        {
            ioeread.printStackTrace();
        }
    }

}

