/*::.
==================================================================================================================================
=================================================¦ Copyright © 2007 Allen Baker ¦=================================================
                                                 +------------------------------+
File:       FileCipher.java
Originator: Allen Baker (2007.05.27 21:40)
LayoutRev:  5
================================================================================================================================== */



/*.
==========================================================================================
Package
------------------------------------------------------------------------------------------ */
package cosmicabyss.com.lib;



/*.
==========================================================================================
Imports
------------------------------------------------------------------------------------------ */
import java.io.*;
import java.util.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;



/*::
======================================================================================================================== *//**
This class encrypt/decrypt data in files

<P>
   <DL>
      <DT>
         <B>
            Example usage:
         </B>
         <DD>
            <BLOCKQUOTE>
               <PRE id="unindent">
                  no example provided
               </PRE>
            </BLOCKQUOTE>
         </DD>
      </DT>
      <DT>
         <B>
            View Source:
         </B>
         <DD>
            <A href="FileCipher.java.html">
               FileCipher.java
            </A>
         </DD>
      </DT>
      <DT>
         <B>
            Author:
         </B>
         <DD>
            <A href="mailto:sourcecode.v01@cosmicabyss.com">
               Allen Baker
            </A>
         </DD>
      </DT>
   </DL>
*//*
======================================================================================================================== */
public class FileCipher extends BaseCipher
   {



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method creates an instance of the FileCipher class.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#000">View source</A>

   @param
      pFileName is the name of the file that this object will encrypt and/or decrypt.
   *//*
   ---------------------------------------------------------------------------------------------------- */
   public <Type> FileCipher
      (
      Type  pFileName
      )
      throws Exception
      {
      super();
      File  file = new File(UString.toString(pFileName));
      iFileName  = new XString(file.getCanonicalPath());
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method creates an instance of the FileCipher class.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#001">View source</A>

   @param
      pFileName is the name of the file that this object will encrypt and/or decrypt.
   @param
      pPassword is the password that will be used to encrypt and/or decrypt the file.
   *//*
   ---------------------------------------------------------------------------------------------------- */
   public <Type> FileCipher
      (
      Type      pFileName,
      Password  pPassword
      )
      throws Exception
      {
      super(pPassword);
      File  file = new File(UString.toString(pFileName));
      iFileName  = new XString(file.getCanonicalPath());
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method creates an instance of the FileCipher class.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#002">View source</A>

   @param
      pFileName is the name of the file that this object will encrypt and/or decrypt.
   @param
      pPassword is the password that will be used to encrypt and/or decrypt the file.
   @param
      pCipherIterations is the number of times the file will be run through the encryption and/or
      decryption algorithm when the encrypt and/or decrypt method is called. If this value is 1 or
      less, the file will be run through 1 time.
   *//*
   ---------------------------------------------------------------------------------------------------- */
   public <Type> FileCipher
      (
      Type      pFileName,
      Password  pPassword,
      int       pCipherIterations
      )
      throws Exception
      {
      super(pPassword,pCipherIterations);
      File  file = new File(UString.toString(pFileName));
      iFileName  = new XString(file.getCanonicalPath());
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method encrypts the file.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#003">View source</A>

   *//*
   ---------------------------------------------------------------------------------------------------- */
   public void encrypt() throws Exception
      {
      /*.
      ==========================================================================================
      If the file is already encrypted, don't encrypt it again
      ------------------------------------------------------------------------------------------ */
      if (iState == ENCRYPTED) return;
      /*.
      ==========================================================================================
      Get the parameters to use for each iteration
      ------------------------------------------------------------------------------------------ */
      Queue<XString>  passwordQueue  = this.passwordQueue();
      Queue<XString>  saltQueue      = this.saltQueue();
      Queue<XString>  algorithmQueue = this.algorithmQueue();
      /*.
      ==========================================================================================
      Encrypt the file iCipherIterations times
      ------------------------------------------------------------------------------------------ */
      XFile  iFile = new XFile(iFileName);
      XFile  oFile = null;
      int    i     = 0;
      for (i=0; i<iCipherIterations; i++)
         {
         /*.
         ==========================================================================================
         We'll output the encrypted version of the file to a temp file so if the encryption process
         fails midway through, we don't corrupt the original file
         ------------------------------------------------------------------------------------------ */
         oFile = new XFile(File.createTempFile("encrypt-" + i + "-",".tmp"));
         /*.
         ==========================================================================================
         Get the password, salt, and algorithm for this iteration
         ------------------------------------------------------------------------------------------ */
         XString pwd = passwordQueue.next();
         XString slt = saltQueue.next();
         XString alg = algorithmQueue.next();
         /*.
         ==========================================================================================
         Encrypt the file
         ------------------------------------------------------------------------------------------ */
         this.encrypt
            (
            new XString(iFile.getCanonicalPath()),
            new XString(oFile.getCanonicalPath()),
            pwd,
            slt,
            alg
            );
         /*.
         ==========================================================================================
         Delete the trail of temp files after they are no longer needed
         ------------------------------------------------------------------------------------------ */
         if (i > 0)
            {
            if ( ! iFile.delete())
               {
               throw new Exception("Cannot delete " + iFile.getCanonicalPath());
               }
            }
         /*.
         ==========================================================================================
         For the next iteration, the input file is the temp file we just used as the output file in
         this iteration.
         ------------------------------------------------------------------------------------------ */
         iFile = oFile;
         }
      /*.
      ==========================================================================================
      If the right number of encryption iterations were done ...
      ------------------------------------------------------------------------------------------ */
      if (i == iCipherIterations)
         {
         /*.
         ==========================================================================================
         Delete the original file
         ------------------------------------------------------------------------------------------ */
         XFile  originalFile = new XFile(iFileName);
         if ( ! originalFile.wipe())
            {
            throw new Exception("Cannot delete " + originalFile.getCanonicalPath());
            }
         /*.
         ==========================================================================================
         And replace it with the last encrypted file.
         ------------------------------------------------------------------------------------------ */
         if ( ! oFile.renameTo(originalFile))
            {
            throw new Exception("Cannot rename " + oFile.getCanonicalPath());
            }
         /*.
         ==========================================================================================
         And remember that the file is encrypted
         ------------------------------------------------------------------------------------------ */
         iState = ENCRYPTED;
         }
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method decrypts the file.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#004">View source</A>

   *//*
   ---------------------------------------------------------------------------------------------------- */
   public void decrypt() throws Exception
      {
      /*.
      ==========================================================================================
      If the file is already decrypted, don't decrypt it again
      ------------------------------------------------------------------------------------------ */
      if (iState == DECRYPTED) return;
      /*.
      ==========================================================================================
      Get the parameters to use for each iteration
      ------------------------------------------------------------------------------------------ */
      Stack<XString>  passwordStack  = this.passwordStack();
      Stack<XString>  saltStack      = this.saltStack();
      Stack<XString>  algorithmStack = this.algorithmStack();
      /*.
      ==========================================================================================
      Decrypt the file iCipherIterations times
      ------------------------------------------------------------------------------------------ */
      XFile  iFile = new XFile(iFileName);
      XFile  oFile = null;
      int    i     = 0;
      for (i=0; i<iCipherIterations; i++)
         {
         /*.
         ==========================================================================================
         We'll output the decrypted version of the file to a temp file so if the decryption process
         fails midway through, we don't corrupt the original file
         ------------------------------------------------------------------------------------------ */
         oFile = new XFile(File.createTempFile("decrypt-" + i + "-",".tmp"));
         /*.
         ==========================================================================================
         Get the password, salt, and algorithm for this iteration
         ------------------------------------------------------------------------------------------ */
         XString pwd = passwordStack.pop();
         XString slt = saltStack.pop();
         XString alg = algorithmStack.pop();
         /*.
         ==========================================================================================
         Decrypt the file
         ------------------------------------------------------------------------------------------ */
         this.decrypt
            (
            new XString(iFile.getCanonicalPath()),
            new XString(oFile.getCanonicalPath()),
            pwd,
            slt,
            alg
            );
         /*.
         ==========================================================================================
         Delete the trail of temp files after they are no longer needed
         ------------------------------------------------------------------------------------------ */
         if (i > 0)
            {
            if ( ! iFile.delete())
               {
               throw new Exception("Cannot delete " + iFile.getCanonicalPath());
               }
            }
         /*.
         ==========================================================================================
         For the next iteration, the input file is the temp file we just used as the output file in
         this iteration.
         ------------------------------------------------------------------------------------------ */
         iFile = oFile;
         }
      /*.
      ==========================================================================================
      If the right number of decryption iterations were done ...
      ------------------------------------------------------------------------------------------ */
      if (i == iCipherIterations)
         {
         /*.
         ==========================================================================================
         Delete the original file
         ------------------------------------------------------------------------------------------ */
         XFile  originalFile = new XFile(iFileName);
         if ( ! originalFile.delete())
            {
            throw new Exception("Cannot delete " + originalFile.getCanonicalPath());
            }
         /*.
         ==========================================================================================
         And replace it with the last decrypted file.
         ------------------------------------------------------------------------------------------ */
         if ( ! oFile.renameTo(originalFile))
            {
            throw new Exception("Cannot rename " + oFile.getCanonicalPath());
            }
         /*.
         ==========================================================================================
         And remember that the file is decrypted
         ------------------------------------------------------------------------------------------ */
         iState = DECRYPTED;
         }
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method sets the ConsoleStream for this object.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#005">View source</A>

   @return
      A reference to this object

   @param
      pConsole is a ConsoleStream through which this objects sends its output.
   *//*
   ---------------------------------------------------------------------------------------------------- */
   public FileCipher setOut(ConsoleStream pConsole)
      {
      cOut = pConsole;
      return this;
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This boilerplate method is used for testing an instantiated object of this class and may include any
   code the developer chooses.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#006">View source</A>

   *//*
   ---------------------------------------------------------------------------------------------------- */
   public FileCipher test() throws Exception
      {
      cOut.titledPrintf
         (
         "\"HELLO WORLD!\"",
         "%s  %s  %s",
         "I'm an object of the", CLASS_NAME, "class, and I approved this message."
         );
      return this;
      }



   /*:.
   ==============================================================================================================
   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[  Protected  ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
   ============================================================================================================== */



   /*:.
   ==============================================================================================================
   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[  Private  ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
   ============================================================================================================== */



   /*.
   ==========================================================================================
   Class Constants
      <BLOCKQUOTE>
         <PRE id="unindent">
            CLASS_NAME    : the name of this class
            DFLT_LINE_LEN : the default line length for word wrapping
            UNKNOWN       : the file is in an unknown   encryption state
            ENCRYPTED     : the file is in an encrypted encryption state
            DECRYPTED     : the file is in a  decrypted encryption state
         </PRE>
      </BLOCKQUOTE>
   ------------------------------------------------------------------------------------------ */
   private static final XString  CLASS_NAME    = new XString(FileCipher.class.getName());
   private static final int      DFLT_LINE_LEN = ConsoleMessage.defaultLineLength();
   private static final int      UNKNOWN       = 0;
   private static final int      ENCRYPTED     = 1;
   private static final int      DECRYPTED     = 2;
   /*.
   ==========================================================================================
   Class variables
      cOut : console output.
   ------------------------------------------------------------------------------------------ */
   private static ConsoleStream  cOut = ConsoleStream.getSingleton();
   /*.
   ==========================================================================================
   Instance variables
      <BLOCKQUOTE>
         <PRE id="unindent">
            iState    : current encryption state of the file
            iFileName : name of the file to en/de-crypt
         </PRE>
      </BLOCKQUOTE>
   ------------------------------------------------------------------------------------------ */
   private int      iState     = UNKNOWN;
   private XString  iFileName;



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method encrypts a file using password-based encryption.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#007">View source</A>

   @param
      pFileIn is the name of the file that will be encrypted. PFileOut must not be the same as pFileIn.
      pFileOut and pFileIn must not be null.
   @param
      pFileOut is the name that will be given to the encrypted file that is produced. PFileOut must not
      be the same as pFileIn. PFileOut and pFileIn must not be null.
   @param
      pPassword is the password that will be used to encrypt the file. The same password must be used
      to decrypt the file.
   @param
      pSalt is a random XString that the PBE cipher uses to randomize the key. It's a bit like a second
      password; and in fact, the user can treat it as exactly that -- a second password..
   @param
      pAlgorithnm is the name of the cipher algorithm that will be used to encrypt the file. An example
      of a valid value for pAlgorithm is: "PBEWithMD5AndDES"
   *//*
   ---------------------------------------------------------------------------------------------------- */
   private void encrypt
      (
      XString  pFileIn,
      XString  pFileOut,
      XString  pPassword,
      XString  pSalt,
      XString  pAlgorithm
      )
      throws Exception
      {
      /*.
      ==========================================================================================
      Check the parameters.
      ------------------------------------------------------------------------------------------ */
      if (pFileIn == null)
         {
         throw new Exception("pFileIn is null");
         }
      if (pFileOut == null)
         {
         throw new Exception("pFileOut is null");
         }
      if (pFileIn.equals(pFileOut))
         {
         throw new Exception("pFileIn and pFileOut are the same file");
         }
      /*.
      ==========================================================================================
      Tattle
      ------------------------------------------------------------------------------------------ */
      cOut.println("   " + UString.alignLeft(pAlgorithm.string(),iLongestAlgoName, ' ') + " " + Const.CHAR_BLACK_SQUARE + " "  + pPassword + " "  + Const.CHAR_BLACK_SQUARE);
      /*.
      ==========================================================================================
      Warn the user if the salt and password are the same
      ------------------------------------------------------------------------------------------ */
      if (pPassword.equals(pSalt))
         {
         cOut.println(Const.WARNING,"Salt matches password");
         }
      /*.
      ==========================================================================================
      Create a cipher object that will encrypt the data
      ------------------------------------------------------------------------------------------ */
      Cipher  cipher = this.encryptionCipher(pPassword,pSalt,pAlgorithm);
      /*.
      ==========================================================================================
      Set up the files
      ------------------------------------------------------------------------------------------ */
      FileInputStream     fis;
      FileOutputStream    fos;
      CipherOutputStream  cos;
      fis = new FileInputStream(pFileIn.string());
      fos = new FileOutputStream(pFileOut.string());
      cos = new CipherOutputStream(fos, cipher);
      /*.
      ==========================================================================================
      Encrypt it
      ------------------------------------------------------------------------------------------ */
      byte[]  b = new byte[UMath.ONE_MEG];
      int  i = fis.read(b);
      while (i != -1)
         {
         cos.write(b, 0, i);
         i = fis.read(b);
         }
      /*.
      ==========================================================================================
      Flush the output buffers
      ------------------------------------------------------------------------------------------ */
      cos.flush();
      fos.flush();
      /*.
      ==========================================================================================
      Close the files
      ------------------------------------------------------------------------------------------ */
      fis.close();
      cos.close();
      fos.close();
      }



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method decrypts a file using password-based decryption.

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#008">View source</A>

   @param
      pFileIn is the name of the encrypted file that will be decrypted. PFileOut must not be the same
      as pFileIn. PFileOut and pFileIn must not be null.
   @param
      pFileOut is the name that will be given to the decrypted file that is produced. PFileOut must not
      be the same as pFileIn. PFileOut and pFileIn must not be null.
   @param
      pPassword is the password that will be used to decrypt the file. It must be the same as the
      password that was used to encrypt the file.
   @param
      pSalt is a random XString that the PBE cipher uses to randomize the key. It's a bit like a second
      password; and in fact, the user can treat it as exactly that -- a second password..
   @param
      pAlgorithnm is the name of the cipher algorithm that will be used to decrypt the file. An example
      of a valid value for pAlgorithm is: "PBEWithMD5AndDES"
   *//*
   ---------------------------------------------------------------------------------------------------- */
   private void decrypt
      (
      XString  pFileIn,
      XString  pFileOut,
      XString  pPassword,
      XString  pSalt,
      XString  pAlgorithm
      )
      throws Exception
      {
      /*.
      ==========================================================================================
      Check the parameters.
      ------------------------------------------------------------------------------------------ */
      if (pFileIn == null)
         {
         throw new Exception("pFileIn is null");
         }
      if (pFileOut == null)
         {
         throw new Exception("pFileOut is null");
         }
      if (pFileIn.equals(pFileOut))
         {
         throw new Exception("pFileIn and pFileOut are the same file");
         }
      /*.
      ==========================================================================================
      Tattle
      ------------------------------------------------------------------------------------------ */
      cOut.println("   " + UString.alignLeft(pAlgorithm.string(),iLongestAlgoName, ' ') + " " + Const.CHAR_BLACK_SQUARE + " "  + pPassword + " "  + Const.CHAR_BLACK_SQUARE);
      /*.
      ==========================================================================================
      Create a cipher object that will decrypt the data
      ------------------------------------------------------------------------------------------ */
      Cipher  cipher = this.decryptionCipher(pPassword,pSalt,pAlgorithm);
      /*.
      ==========================================================================================
      Set up the files
      ------------------------------------------------------------------------------------------ */
      FileInputStream   fis;
      FileOutputStream  fos;
      CipherInputStream cis;
      fis = new FileInputStream(pFileIn.string());
      cis = new CipherInputStream(fis, cipher);
      fos = new FileOutputStream(pFileOut.string());
      /*.
      ==========================================================================================
      Decrypt it
      ------------------------------------------------------------------------------------------ */
      byte[] b = new byte[UMath.ONE_MEG];
      int i = cis.read(b);
      while (i != -1)
         {
         fos.write(b, 0, i);
         i = cis.read(b);
         }
      /*.
      ==========================================================================================
      Flush the output buffer
      ------------------------------------------------------------------------------------------ */
      fos.flush();
      /*.
      ==========================================================================================
      Close the files
      ------------------------------------------------------------------------------------------ */
      fis.close();
      cis.close();
      fos.close();
      }



   /*:.
   ==============================================================================================================
   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[  Inner Classes  ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
   ============================================================================================================== */



   /*:.
   ==============================================================================================================
   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[  Public Static Methods  ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
   ============================================================================================================== */



   /*:                                    
   ====================================================================================================
   [][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
   ==================================================================================================== *//**
   This method allows this class file to be unit tested as a standalone application. It's the method
   that's called when the class is invoked from the command line by using the java application launcher
   - "java". Main() is not a required method, but the practice of putting one in each class and
   wrapping class test code within it allows easy unit testing of the class; and main does not need to
   be removed when testing is complete.

   <P>
      <DL>
         <DT>
            <B>
               Command line usage:
            </B>
            <DD>
               Java cosmicabyss.com.lib.FileCipher
            </DD>
         </DT>
      </DL>

   <P><B>Implementation: </B><A HREF="FileCipher.java.html#009">View source</A>

   @param
      pArgs contains the command line arguments with which this class was invoked as an application.
   *//*
   ---------------------------------------------------------------------------------------------------- */
   public static void main(String[] pArgs) throws Exception
      {
      /*.
      ==========================================================================================
      Greetings !
      ------------------------------------------------------------------------------------------ */
      cOut.banner(CLASS_NAME);
      /*.
      ==========================================================================================
      Create an object and send its output to the ConsoleStream
      ------------------------------------------------------------------------------------------ */
      Password    p        = new Password("myNewSecretPassword");
      XString     fileName = new XString("c:/temp/test.txt");
      FileCipher  obj = new FileCipher(fileName, p);
      /*.
      ==========================================================================================
      Test code
      ------------------------------------------------------------------------------------------ */
      obj.test();
      /*.
      ==========================================================================================
      Used in waiting for keyboard input
      ------------------------------------------------------------------------------------------ */
      byte[]  b = new byte[10];
      /*.
      ==========================================================================================
      Encrypt the file
      ------------------------------------------------------------------------------------------ */
      cOut.println("encrypting " + fileName + " ...");
      obj.encrypt();
      cOut.println("encrypted.");
      /*.
      ==========================================================================================
      Give user time to see for himself
      ------------------------------------------------------------------------------------------ */
      cOut.println("");
      cOut.println("Press <ENTER> to continue: ");
      System.in.read(b);
      /*.
      ==========================================================================================
      Decrypt the file
      ------------------------------------------------------------------------------------------ */
      obj = new FileCipher(fileName, p);
      cOut.println("decrypting " + fileName + " ...");
      obj.decrypt();
      cOut.println("decrypted.");
      }



   }  // class FileCipher



   /*:.
   ==============================================================================================================
   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[  Notes  ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
   ============================================================================================================== */