/*
 * This file is part of NeverNote 
 * Copyright 2009 Randy Baumgarte
 * 
 * This file may be licensed under the terms of of the
 * GNU General Public License Version 2 (the ``GPL'').
 *
 * Software distributed under the License is distributed
 * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
 * express or implied. See the GPL for the specific language
 * governing rights and limitations.
 *
 * You should have received a copy of the GPL along with this
 * program. If not, go to http://www.gnu.org/licenses/gpl.html
 * or write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
*/
package cx.fbn.nevernote.evernote;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.zip.CRC32;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.RC2ParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import cx.fbn.nevernote.utilities.Base64;

public class EnCrypt {

	// Convert a string of text to a hex string
     public static String asHex (byte buf[]) {
      StringBuffer strbuf = new StringBuffer(buf.length * 2);
      int i;

      for (i = 0; i < buf.length; i++) {
       if ((buf[i] & 0xff) < 0x10)
	    strbuf.append("0");

       strbuf.append(Long.toString(buf[i] & 0xff, 16));
      }

      return strbuf.toString();
     }

//	public static class EnCryptException extends Exception {
//    	 public EnCryptException(String message, Throwable cause) {
//    		 super(message, cause);
//    	 }
//     }
     
     /**
      * Choose the character set to use for encoding
      */
     private Charset getCharset() {
    	 
    	 // Just hard-coding choice here
    	boolean useUtf8 = true; 
 		
 		final Charset charSet;
 		if(useUtf8) {
 			charSet = Charset.forName("UTF-8");
 		} else {
 			charSet = Charset.defaultCharset();
 		}
 		return charSet;
     }
     
     
     // Useful for debugging, but not normally used
     @SuppressWarnings("unused")
	private byte[] encodeStringOld(String text) {

    	 int len = text.length()+4;
    	 int mod = (len%8);
    	 if (mod>0) {
    		 for (; mod !=0; len++) {
    			 mod = len%8;
    		 }
    		 len--;
    	 }
    	 len = len-4;
    	 StringBuffer textBuffer = new StringBuffer(text);
    	 textBuffer.setLength(len);

    	 // Setup parms for the cipher
    	 String encoded = crcHeader(textBuffer.toString()) +textBuffer;

    	 return encoded.getBytes();
     }

     /**
      * Main changes are
      * 
      * 1. Do padding based on encoded bytes, not string length (some chars -> 2 bytes)
      * 2. Use specific named charset
      */
     private byte[] encodeStringNew(String text) {
    	 
    	 final Charset charSet = getCharset();
 		
 		// Convert to bytes using given encoding, and align *bytes* to multiple of
 		// 8, with 4 bytes reserved for the crc
 		final byte[] bytes = text.getBytes(charSet);
 		int align8 = (bytes.length + 4) % 8;
 		int paddingNeeded = 8 - align8;
 		final byte[] paddedBytes = Arrays.copyOf(bytes, bytes.length + paddingNeeded);
 		
 		// Now calculate the crc, using the bytes
 		String crc = crcHeader(paddedBytes);
 		
 		byte[] crcBytes = crc.getBytes(charSet);
 		if(crcBytes.length != 4) {
 			System.err.println("CRC Bytes really should be 4 in length!");
 			return null;
 		}
 		
 		// Now combine crc bytes and string bytes into byte array
 		// for encryption
 		byte[] total = new byte[paddedBytes.length + crcBytes.length];
 		System.arraycopy(crcBytes, 0, total, 0, crcBytes.length);
 		System.arraycopy(paddedBytes, 0, total, crcBytes.length, paddedBytes.length);
 		
 		return total;
     }

     
     /**
      * Same as for encryption: use named charset, and
      * @param bytes
      * @return
      */
     private String decodeBytesNew(byte[] bytes) {
    	 
    	 Charset charSet = getCharset();
    	 
    	 byte[] crcBytes = Arrays.copyOfRange(bytes, 0, 4);
    	 byte[] textBytes = Arrays.copyOfRange(bytes, 4, bytes.length);
    	 
    	 CharBuffer crcChar = charSet.decode(ByteBuffer.wrap(crcBytes));
    	 CharBuffer textChar = charSet.decode(ByteBuffer.wrap(textBytes));
    	 
    	 // Get crc of text to see if same
    	 String cryptCRC = crcChar.toString();
    	 String realCRC = crcHeader(textBytes);
    	 
    	 if(realCRC.equals(cryptCRC)) {
    		 // Trim nulls at end
    		 while(textChar.get(textChar.limit() - 1) == 0 && textChar.limit() != 0) {
    			 textChar.limit(textChar.limit() - 1);
    		 }
    		 String str = textChar.toString();
    		 return str;
    	 }
    	 
    	 return null;
     }
     
     /**  
      * For reference: old version.  Useful for debugging
      * @param bytes
      * @return
      */  
     @SuppressWarnings("unused")
	private String decodeBytesOld(byte[] bytes) {

    	 // We have a result.  Separate it into the 4 byte header and the decrypted text
    	 StringBuffer buffer = new StringBuffer(new String(bytes));
    	 String cryptCRC = buffer.substring(0,4);
    	 String clearText = buffer.substring(4);
    	 String realCRC = crcHeader(clearText);
    	 // We need to get the real CRC of the decrypted text
    	 if (realCRC.equalsIgnoreCase(cryptCRC)) {
    		 int endPos = clearText.length();
    		 for (int i=buffer.length()-1; i>=0; i--) {
    			 if (buffer.charAt(i) == 0) 
    				 endPos--;
    			 else
    				 i=-1;
    		 }
    		 clearText = clearText.substring(0,endPos);
    		 return clearText;
    	 }

    	 return null;

     }

     
     
	// Encrypte the text and return the base64 string
	public String encrypt(String text, String passphrase, int keylen) {
		
		
		RC2ParameterSpec parm = new RC2ParameterSpec(keylen);
	   
		try {
			// Get a MD5 for the passphrase
			 MessageDigest md = MessageDigest.getInstance("MD5");
			 // NB Use specific Charset
		    md.update(passphrase.getBytes(getCharset()));
		    
		    // Setup parms for the cipher
		    SecretKeySpec skeySpec = new SecretKeySpec(md.digest(), "RC2");
			Cipher cipher = Cipher.getInstance("RC2/ECB/NoPadding");
			cipher.init(Cipher.ENCRYPT_MODE, skeySpec, parm);
			
			//byte[] oldBytes = encodeStringOld(text);
			byte[] newBytes = encodeStringNew(text);
			//boolean areSame = Arrays.equals(oldBytes, newBytes);
			//System.out.println("Same? " + areSame);
			byte[] d = cipher.doFinal(newBytes);
			
			return Base64.encodeBytes(d);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}		
		
		return null;
	}
	// Decrypt the base64 text and return the unsecure text
	public String decrypt(String text, String passphrase, int keylen) {
		RC2ParameterSpec parm = new RC2ParameterSpec(keylen);
	    MessageDigest md;
		try {
			// Get a MD5 for the passphrase
			md = MessageDigest.getInstance("MD5");
			md.update(passphrase.getBytes(getCharset()));
		    
		    // Setup parms for the cipher
		    SecretKeySpec skeySpec = new SecretKeySpec(md.digest(), "RC2");
			Cipher cipher = Cipher.getInstance("RC2/ECB/NOPADDING");
			cipher.init(Cipher.DECRYPT_MODE, skeySpec, parm);
			
			// Decode the encrypted text and decrypt
			byte[] dString = Base64.decode(text);
			byte[] d = cipher.doFinal(dString);
			
			//String clearTextOld = decodeBytesOld(d);
			String clearTextNew = decodeBytesNew(d);
			//if(clearTextNew != null) {
			//	System.out.println("Are same decrypted ? " + clearTextNew.equals(clearTextOld));
			//}
			return clearTextNew;
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}			
		return null;
	}
	// Utility function to return the CRC header of an encoded string.  This is
	// used to verify good decryption and put in front of a new encrypted string
	private String crcHeader(String text) {
		return crcHeader(text.getBytes());
	}
	private String crcHeader(byte[] bytes) {
		CRC32 crc = new CRC32();
		crc.update(bytes);
		int realCRC = (int)crc.getValue();
		
		// The first 4 chars of the hex string will equal the first
		// 4 chars of the decyphered text.  If they match we have a
		// good password.  This is what we return
		realCRC = realCRC ^ (-1);
		realCRC = realCRC >>> 0;
		String hexCRC = Integer.toHexString(realCRC).substring(0,4);
		return hexCRC.toString().toUpperCase();

	}

}