/*
 * BIAES - Tim Tyler 2001.
 * 
 * BIAES - a bijective version of AES (Rijndael).
 *
 */

/*
 * ToDo
 * ====
 *
 * Add compression algorithm...
 * 
 * Consider chaining mode to prevent attacker getting plaintext/cyphertext pairs...?
 * Option to add random padding - designed to obscure the length of messages...
 *
 * Make an API that descends from the "Cipher" class...
 * Make generic padding wrapper - to allow use of the bijective padding scheme with other cyphers...
 * A better way to avoid null file?
 * Use some particular package...
 * 
 *
 * Done
 * ====
 * Make thread-safe...
 * Seed RNG with time...
 * IV -> simply "add" constant (based on IV) to key...
 * Change key at regular intervals...
 *  NO IV -> simply "add" constant to key...
 *  IV -> simply "add" constant (based on IV) to key...
 * If using IV, use "John Savard" padding, using secure RNG...
 * Secure RNG...
 * Key size configurable...
 * Hex keys with no IV...???
 * Hash IV with key...
 * Clean up code...
 * Java 1.1 note
 * Javadoc...
 * Web pages...
 * Documentation...
 * Command line FrEnd...
 * Programmer's API
 * Padding currently pads files with > 128-bit keys to the nearest 256-bit block boundary (not maximally efficient)...
 * Whitening...
 * Hex key...
 * Different key sizes -> different block sizes...
 * Use RNG to make IV...
 * Test with null file...
 * Add and subtract one to deal with null file...
 * Key...
 * Hash key...
 * Use CBC
 * Allow for IV to be set to zero and not transmitted...
 *
 */

   import java.awt.*;
   import java.applet.*;
   import java.awt.event.*;
   import java.net.URL;
   import java.io.*;
   import java.util.Random;
   import java.security.SecureRandom;
   import java.math.BigInteger;

/**
 * BIAES - a bijective version of AES (Rijndael).<p>
 *
 * This program uses bijective padding in conjunction with AES (Rijndael).<p>
 * It uses either a hashed keyphrase - or 128, 192, or 256-bit keys.<p>
 *
 * This code has been placed in the public domain.<p>
 * You can do what you like with it.<p>
 * Note that this code comes with no warranty.<p>
 *
 * Note that the "Rijndael_Algorithm" file has additional copyright notices.<p>
 *
 * @author  Tim Tyler tim@tt1.org
 */
   public class BIAES {
      // PRNG code deliberately not thread-safe - any lack of determinism from multiple thread access is welcomed...
      static int byte_c = 0;
      static int byte_v = 0;
      static SecureRandom srng;
   
      int granularity; // in bytes...
   
      TerminatedFile file = new TerminatedFile();
      Random rnd = new Random();
   
      byte[] output_array;
   
   
   
      final int RANDOM = 0;
      final int BIJECTIVE = 1;
   
      final int pad_size = 96; // in bytes...
   
   // ENCRYPT
   
   /**
    * Encrypt data using Rijndael in CBC mode, with padding...
    * 
    */
      public byte[] encrypt(byte[] input_array, String key_phrase, boolean iv) {
         file.length = input_array.length;
         file.data = new byte[file.length + pad_size];
      
         System.arraycopy(input_array, 0, file.data, 0, file.length);
      
         file = encrypt(file, key_phrase, iv);
      
         byte[] temp_array;
      
         temp_array = new byte[file.length];
      
         System.arraycopy(file.data, 0, temp_array, 0, file.length);
      
         return temp_array;
      }
   
   
   /**
    * Encrypt data from input_file and store the result in output_file using Rijndael in CBC mode, with padding...
    * 
    */
      public void encrypt(String input_file, String output_file, String key_phrase, boolean iv) {
         int i;
         int iv_size = 16;
         byte[] temp_array;
      
         try {
            File src = new File(input_file);
         
            FileInputStream in = new FileInputStream(input_file);
         
            ByteArrayOutputStream bytes;
         
            bytes = new ByteArrayOutputStream();
            int _array_size = 1024;  // choose a size...
            byte[] _array = new byte[_array_size];
         
            int rb;
         
            while ((rb = in.read(_array, 0, _array_size)) > -1) {
               bytes.write(_array, 0, rb);
            }
         
            // pad with <pad_size> 00 bytes...
            for (i = 0; i < pad_size; i++) {
               bytes.write(new byte[] {0}, 0, 1); 
            }
         
            bytes.close();
            in.close();
         
            file.data = bytes.toByteArray();
         
            file.length = file.data.length - pad_size;
         }
            catch (Exception e) {
               Log.log("Error while getting file for encryption:");
               e.printStackTrace(Log.getPrintStream());
            }
      
         file = encrypt(file, key_phrase, iv);
      
         temp_array = new byte[file.length];
      
         System.arraycopy(file.data, 0, temp_array, 0, file.length);
      
         try {
            FileOutputStream fos = new FileOutputStream(output_file);
         
            fos.write(temp_array);
         
            fos.close();
         }
            catch (Exception e) {
               Log.log("Error while outputting encrypted file:");
               e.printStackTrace(Log.getPrintStream());
            }
      }
   
   
      TerminatedFile encrypt(TerminatedFile input_file, String key_phrase, boolean iv) {
         int i;
         int iv_size = 16;
         Object key = null;
         Block initial_value;
         int key_size;
         int padding = iv ? RANDOM : BIJECTIVE;
         int block_count;
         KeyManager km = new KeyManager();
      
         initial_value = InitialValue.generateIV(iv);
      
         km.getHexKey(key_phrase);
         key_size = km.getKeySize();
      
         if (iv) {
            key_phrase = km.appendByteArrayToString(key_phrase,initial_value.data);
         }
      
         byte[] key_byte_array = km.getKeyByteArray(key_phrase);
         key = makeKey(key_byte_array);
      
         km.setUpKeyConstants(initial_value);
      
         granularity = km.getBestBlockSize();
      
         if (padding == BIJECTIVE) {
            file.setGranularity(granularity);
            file.addConstant(1);
            file.makePaddableByteFile();
            file.addPadding(iv ? iv_size : 0);
            file.unmakePaddableBlockFile();
         
            if (granularity > 16) {
               if (file.length > 32) {
                  file.start = 32;
               
                  file.makePaddableBlockFile();
                  file.removePadding();
                  file.unmakePaddableByteFile();
               
                  file.setGranularity(16);
               
                  file.makePaddableByteFile();
                  file.addPadding(iv ? iv_size : 0);
                  file.unmakePaddableBlockFile();
               
                  file.start = 0;
               }
            }
         }
         else
         {
            int nob2p = 16 - (file.length & 15);
         
            if (file.length < 16) {
               nob2p += 16;
            }
         
            file.addRandomPadding(nob2p);
            file.data[file.length - 1] = (byte)((file.data[file.length - 1] & ((nob2p <= 16) ? 0xF0 : 0xE0)) | (nob2p - 1));
         }
      
         int new_length = file.length + iv_size; // allow room for IV...
         output_array = new byte[new_length + pad_size]; // more space...
      
         // insert the IV...
         System.arraycopy(initial_value.data, 0, output_array, 0, initial_value.block_size);
      
         if (!iv) {
            file.xorChecksum();
         }
      
         byte[] temp_array = new byte[16];
      
         block_count = iv ? 0 : 0;
      
         // encryption loop
         for (i = iv_size; i < new_length; i += 16) {
            for (int j = 0; j < 16; j++) {
               temp_array[j] = (byte)(output_array[i + j - iv_size] ^ file.data[i + j - iv_size]); // chaining operation...
            }
         
            temp_array = Rijndael_Algorithm.blockEncrypt(temp_array, 0, key); // Rijndael block encryption...
            System.arraycopy(temp_array, 0, output_array, i, 16); // copy a block across...
         
            if ((++block_count & 31) == 31) {
               // change key...
               key_byte_array = km.key_block.addByteArray(key_byte_array);
               key = makeKey(key_byte_array);
            }
         }
      
         if (iv) { // leave IV in place in the output...
            file.data = output_array;
            file.length = new_length;
         }
         else // chop off IV...
         {
            System.arraycopy(output_array, 16, file.data, 0, new_length - iv_size); // copy a block across...
            file.length = new_length - iv_size;
         }
      
         if (padding == BIJECTIVE) {
            if (granularity > 16) {
               if (file.length > 32) {
                  file.start = file.start = 32 + (iv ? iv_size : 0);
               
                  file.makePaddableBlockFile();
                  file.removePadding();
                  file.unmakePaddableByteFile();
               
                  file.setGranularity(granularity);
               
                  file.makePaddableByteFile();
                  file.addPadding(0);
                  file.unmakePaddableBlockFile();
               }
            }
         
            file.start = iv ? iv_size : 0;
         
            // file.setGranularity(granularity); // overkill
            file.makePaddableBlockFile();
            file.removePadding();
            file.unmakePaddableByteFile();
            file.subtractConstant(1);
         }
      
         return file;
      }
   
   
   // ==========================================================================================
   
   // DECRYPT
   
   /**
    * Decrypt data using Rijndael in CBC mode, with padding...
    * 
    */
      public byte[] decrypt(byte[] input_array, String key_phrase, boolean iv) {
         byte[] temp_array;
      
         file.length = input_array.length;
         file.data = new byte[file.length + pad_size];
      
         System.arraycopy(input_array, 0, file.data, 0, file.length);
      
         file = decrypt(file, key_phrase, iv);
      
         temp_array = new byte[file.length];
      
         System.arraycopy(file.data, 0, temp_array, 0, file.length);
      
         return temp_array;
      }
   
   
   /**
    * Decrypt data from input_file and store the result in output_file using Rijndael in CBC mode, with padding...
    * 
    */
      public void decrypt(String input_file, String output_file, String key_phrase, boolean iv) {
         int i;
         int iv_size = 16;
         byte[] temp_array;
      
         try {
            File src = new File(input_file);
         
            FileInputStream in = new FileInputStream(input_file);
         
            ByteArrayOutputStream bytes;
         
            bytes = new ByteArrayOutputStream();
            int _array_size = 1024;  // choose a size...
            byte[] _array = new byte[_array_size];
         
            int rb;
         
            while ((rb = in.read(_array, 0, _array_size)) > -1) {
               bytes.write(_array, 0, rb);
            }
         
            // pad with <pad_size> 00 bytes...
            for (i = 0; i < pad_size; i++) {
               bytes.write(new byte[] {0}, 0, 1);
            }
         
            bytes.close();
            in.close();
         
            file.data = bytes.toByteArray();
         
            file.length = file.data.length - pad_size;
         }
            catch (Exception e) {
               Log.log("Error while getting file for encryption:");
               e.printStackTrace(Log.getPrintStream());
            }
      
         file = decrypt(file, key_phrase, iv);
      
         temp_array = new byte[file.length];
      
         System.arraycopy(file.data, 0, temp_array, 0, file.length);
      
         try {
            FileOutputStream fos = new FileOutputStream(output_file);
         
            fos.write(temp_array);
         
            fos.close();
         }
            catch (Exception e) {
               Log.log("Error while outputting encrypted file:");
               e.printStackTrace(Log.getPrintStream());
            }
      }
   
   
      TerminatedFile decrypt(TerminatedFile input_file, String key_phrase, boolean iv) {
         int i;
         int iv_size = 16;
         Object key = null;
         Block initial_value;
         int key_size;
         int padding = iv ? RANDOM : BIJECTIVE;
         int block_count;
         KeyManager km = new KeyManager();
      
         // insert IV
         if (!iv) { // add 00s as IV...
            System.arraycopy(file.data, 0, file.data, iv_size, file.length); // move data up...
            for (int j = 0; j < iv_size; j++) { // insert 00 00 ... 00 00 IV...
               file.data[j] = (byte)(0x00);
            }
         
            file.length += iv_size;
         }
      
         initial_value = InitialValue.returnIV(file.data);
      
         km.getHexKey(key_phrase);
         key_size = km.getKeySize();
      
         if (iv) {
            key_phrase = km.appendByteArrayToString(key_phrase,initial_value.data);
         }
      
         byte[] key_byte_array = km.getKeyByteArray(key_phrase);
      
         key = makeKey(key_byte_array);
      
         km.setUpKeyConstants(initial_value);
      
         block_count = 0;
      
         granularity = km.getBestBlockSize();
         file.start = 16; // after IV...
      
         if (padding == BIJECTIVE) {
            file.setGranularity(granularity);
            file.addConstant(1);   
            file.makePaddableByteFile();  
            file.addPadding(iv ? iv_size : 0);
            file.unmakePaddableBlockFile();
         
            if (granularity > 16) {
               if (file.length > 48) {
                  file.start = 48;
                  file.setGranularity(granularity);
               
                  file.makePaddableBlockFile();
                  file.removePadding();
                  file.unmakePaddableByteFile();
               
                  file.setGranularity(16);
               
                  file.makePaddableByteFile();
                  file.addPadding(0);
                  file.unmakePaddableBlockFile();
               }
            }
         }
      
      
         int new_length = file.length - iv_size;
      
         output_array = new byte[new_length + pad_size];
      
         byte[] temp_array;
      
         for (i = 0; i < new_length; i += 16) {
            temp_array = Rijndael_Algorithm.blockDecrypt(file.data, i + iv_size, key);
            for (int j = 0; j < 16; j++) {
               output_array[i + j] = (byte)(temp_array[j] ^ file.data[i + j]);
            }
         
            if ((++block_count & 31) == 31) {
               // change key...
               key_byte_array = km.key_block.addByteArray(key_byte_array);
               key = makeKey(key_byte_array);
            }
         }
      
         file.data = output_array;
         file.length = new_length;
      
         file.start = 0;
      
         if (!iv) {
            file.xorChecksum();
         }
      
         if (padding == BIJECTIVE) {
            if (granularity > 16) {
               if (file.length > 32) {
                  file.start = 32;
               
                  file.setGranularity(16);
                  file.makePaddableBlockFile();
                  file.removePadding();
                  file.unmakePaddableByteFile();
               
                  file.setGranularity(granularity);
               
                  file.makePaddableByteFile();
                  file.addPadding(0); // was 0...
                  file.unmakePaddableBlockFile();
               }
            }
         
            file.start = 0;
         
         // file.setGranularity(granularity); // overkill...
            file.makePaddableBlockFile();
            file.removePadding();
            file.unmakePaddableByteFile();
            file.subtractConstant(1);
         }
         else
         {
            int strip_len;
            strip_len = file.data[file.length - 1] & ((file.length <= 32) ? 0x1F : 0x0F);
            file.length -= (strip_len + 1);
         }
      
         return file;
      }
   
   
      Object makeKey(byte[] key_byte_array) {
         Object key = null;
      
         try {
            key = Rijndael_Algorithm.makeKey(key_byte_array);
         }
            catch (Exception e) {
            // can never happen...
            }
      
         return key;
      }
   
   
      static byte getRandomByte() {
         if (srng == null) {
            // seed based on current time - done this way for speed :-|
            long l = System.currentTimeMillis();
            byte[] ba = new byte[8];
            for (int i = 0; i < 8; i++) {
               ba[i] = (byte)l;
               l = l >> 8;
            }
         
            srng = new SecureRandom(ba);
            // srng = new SecureRandom(); // this would be the more secure (but slow) way...
         }
      
         if ((byte_c) != 0) {
            byte_c -= 1;
            byte_v = byte_v >>> 8;
            return (byte)byte_v;
         }
         else
         {
            byte_c = 3;
            byte_v = srng.nextInt();
            return (byte)byte_v;
         }
      }
   
   
      void runCLI(String args[]) {
         boolean allowed = true;
      
         allowed = !(args.length == 0);
      
         if (allowed) {
            allowed = !("-?".equals(args[0]));
            if (allowed) {
               if (args.length < 2) {
                  Log.log("Not enough arguments");
                  allowed = false;
               }
               else
               {
                  if (args.length > 5) {
                     Log.log("Too many arguments");
                     allowed = false;
                  }
               }
            
               if (allowed) {
                  int i = 0;
               
                  boolean iv = false;
                  boolean encrypt = false;
               
                  encrypt = ("-e".equals(args[i]));
               
                  if (encrypt) {
                     i++;
                  }
               
                  iv = ("-iv".equals(args[i]));
               
                  if (iv) {
                     i++;
                  }
               
                  if (args.length - i == 3) {
                     String f_in = args[i++];
                     String f_out = args[i++];
                     String key = args[i++];
                  
                     if (encrypt) {
                        encrypt(f_in, f_out, key, iv);
                     }
                     else
                     {
                        decrypt(f_in, f_out, key, iv);
                     }
                  }
                  else
                  {
                     Log.log("Incorrect arguments.");
                  }
               }
            }
         }
      
         if (!allowed) {
            Log.log("BIAES");
            Log.log("=====");
            Log.log("BIAES - a desktop encryption program using Rijndael (AES).");
            Log.log("Syntax: BIAES [-e] [-iv] <infile> <outfile> <key>");
            Log.log("Flags:  -e  - encrypt (default is decryption);");
            Log.log("        -iv - use random initial value;");
            Log.log("Any fields with spaces in should be enclosed in quotation marks.");
            Log.log("This is version 1.0 of BIAES.");
         }
      }
   
      public static void main(String args[]) {
         BIAES biaes = new BIAES();
         biaes.runCLI(args);
      }
   
   }