package test;


import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.Key;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class BlowFishCrypterTest {
	/** Holds the chars for hex encoding */
    public static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
    
	/** Block size of blowfish encrypted strings */
	private final int _blockSize = 8;

	/** Used to cache the blowfish encryption cipher */
	private Cipher _encryptCipher;

	/** Used to cache the blowfish decryption cipher */
	private Cipher _decryptCipher;

	
	public static void main(String[] args) {
		// Debug path
		debugPathProperty("java.class.path");
		debugPathProperty("sun.boot.class.path");
		
		
		String data = "My secret info";
		
		BlowFishCrypterTest crypter = new BlowFishCrypterTest();
		
		String encryptedData = crypter.encrypt(data);
		
		String decryptedData = crypter.decrypt(encryptedData);
		
		if (data.equals( decryptedData ) ) {
			System.out.println("It works.");
		} else {
			System.out.println("We expected '" + data + "' but we got " + decryptedData + "'");
		
		}

	}
	
	private Key _blowfishKey;
	
	/** @return the ecryption key */
	private Key secretBlowfishKey() {
		if ( _blowfishKey == null ) {
			// Random 16-char secret password
			String secret = "QEJxqsK8h1KnHikr";
			_blowfishKey = new SecretKeySpec(secret.getBytes(), "Blowfish");
			System.out.println("_blowfishKey = " + _blowfishKey);
		}
		return _blowfishKey;
	}
	
	/**
	 * Creates a blowfish cipher for a given mode. The two possible modes for a
	 * blowfish cipher are: ENCRYPT and DECRYPT.
	 * 
	 * @param mode
	 *            of the cipher (encrypting or decrypting)
	 * @return a blowfish cipher initialized with the given mode and with the
	 *         <code>secretKey</code> from the above method.
	 */
	protected Cipher createBlowfishCipher(int mode) {
		try {
			Cipher cipher = Cipher.getInstance("Blowfish/ECB/NoPadding");
			cipher.init(mode, secretBlowfishKey());
			return cipher;
		}
		catch (java.security.NoSuchAlgorithmException ex) {
			throw new RuntimeException("Couldn't find the Blowfish algorithm; perhaps you do not have the SunJCE security provider installed properly?", ex);
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Method used to return the shared instance of the blowfish encryption
	 * cipher.
	 * 
	 * @return blowfish encryption cipher
	 */
	protected Cipher encryptCipher() {
		if (_encryptCipher == null) {
			_encryptCipher = createBlowfishCipher(Cipher.ENCRYPT_MODE);
			System.out.println("_encryptCipher = " + _encryptCipher + "; Provider = " + _encryptCipher.getProvider());
		}
		return _encryptCipher;
	}

	/**
	 * Method used to return the shared instance of the blowfish decryption
	 * cipher.
	 * 
	 * @return blowfish decryption cipher
	 */
	protected Cipher decryptCipher() {
		if (_decryptCipher == null) {
			_decryptCipher = createBlowfishCipher(Cipher.DECRYPT_MODE);
			System.out.println("_decryptCipher = " + _decryptCipher + "; Provider = " + _decryptCipher.getProvider());
		}
		return _decryptCipher;
	}

	/**
	 * Decodes a blowfish encoded string. Note that the originally encoded
	 * string should have been encoded with the same secret key as is used for
	 * the decoding cipher or else you are going to get garbage. To encode a
	 * string have a look at <code>blowfishEncode</code>.
	 * 
	 * @param cryptedText
	 *            blowfish encoded string to be decoded
	 * @return decode clear text string
	 */
	public String decrypt(String cryptedText) {
		if (cryptedText == null) {
			return null;
		}
		int length = cryptedText.length();
		if (length % 16 != 0) {
			return null;
		}
		ByteArrayOutputStream result = new ByteArrayOutputStream();
		byte[] clearText = null;
		byte[] encryptedBytes = new byte[_blockSize];

		int i = 0;
		for (int j = 0; j < length;) {

			char c1 = cryptedText.charAt(j++);
			int b1 = c1 < 'a' ? c1 - '0' : c1 - 'a' + 10;
			char c2 = cryptedText.charAt(j++);
			int b2 = c2 < 'a' ? c2 - '0' : c2 - 'a' + 10;
			encryptedBytes[i++] = (byte) ((b1 << 4) + b2);
			if (i == _blockSize) {
				// we filled a block
				try {
					clearText = decryptCipher().doFinal(encryptedBytes);
				}
				catch (Exception e) {
					throw new RuntimeException(e);
				}
				for (int k = 0; k < _blockSize; k++) {
					if (clearText[k] != 0) {
						result.write(clearText[k]);
					}
				}
				i = 0;
			}
		}

		if (i != 0) {
			for (int j = i; j < _blockSize; i++) {
				encryptedBytes[j] = 0;
			}
			try {
				clearText = decryptCipher().doFinal(encryptedBytes);
			}
			catch (Exception e) {
				throw new RuntimeException(e);
			}
			for (int k = 0; k < _blockSize; k++) {
				result.write(clearText[k]);
			}
		}
		return fromUTF8Bytes(result.toByteArray());
	}

	/**
	 * Blowfish encodes a given string using the secret key specified in the
	 * System property: <b>ERBlowfishCipherKey</b>. The blowfish cipher is a
	 * two way cipher meaning that given the secret key you can de-cipher what
	 * the original string is. For one-way encryption look at methods dealing
	 * with the Sha alogrithm. To decode a blowfish encoded string use the
	 * method: <code>blowfishDecode</code>. The resultant string from
	 * encoding a string is safe for use in urls and cookies.
	 * 
	 * @param clearText
	 *            string to be encrypted
	 * @return encrypted string
	 */
	public String encrypt(String clearText) {
		if (clearText == null) {
			return null;
		}
		byte clearTextBytes[] = toUTF8Bytes(clearText);
		StringBuffer result = new StringBuffer();
		int pos = 0, length = clearTextBytes.length;
		byte[] bytesToEncrypt = new byte[_blockSize];
		byte[] encryptedBytes = null;
		while (pos < length) {
			int k = 0;
			for (int j = pos; j < length && j < pos + _blockSize; k++, j++) {
				bytesToEncrypt[k] = clearTextBytes[j];
			}
			if (k < _blockSize) {
				for (int l = k; l < _blockSize; l++) {
					bytesToEncrypt[l] = 0;
				}
			}

			try {
				encryptedBytes = encryptCipher().doFinal(bytesToEncrypt);
			}
			catch (Exception e) {
				throw new RuntimeException(e);
			}
			for (k = 0; k < _blockSize; k++) {
				result.append(HEX_CHARS[(encryptedBytes[k] >>> 4) & 0xf]);
				result.append(HEX_CHARS[encryptedBytes[k] & 0xf]);
			}
			pos += _blockSize;
		}
		return result.toString();
	}
	
	
	
	// --------------- UTILITIES -----------------------
	
    /**
     * Utility to convert from UTF-8 bytes without the try/catch. Throws an NSForwardException in the unlikely case that your encoding can't be found.
     * @param bytes string to convert
     */
    public static String fromUTF8Bytes(byte bytes[]) {
    	return fromBytes(bytes, "UTF-8");
    }

    /**
     * Utility to convert from bytes without the try/catch. Throws an NSForwardException in the unlikely case that your encoding can't be found.
     * @param bytes string to convert
     * @param encoding
     */
    public static String fromBytes(byte bytes[], String encoding) {
    	if(bytes == null) {
    		return null;
    	}
    	try {
			return new String(bytes, encoding);
		}
		catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
    }
    

    /**
     * Utility to convert to UTF-8 bytes without the try/catch. Throws an NSForwardException in the unlikely case that your encoding can't be found.
     * @param string string to convert
     */
    public static byte[] toUTF8Bytes(String string) {
    	return toBytes(string, "UTF-8");
    }

    /**
     * Utility to convert to bytes without the try/catch. Throws an NSForwardException in the unlikely case that your encoding can't be found.
     * @param string string to convert
     * @param encoding
     */
    public static byte[] toBytes(String string, String encoding) {
    	if(string == null) {
    		return null;
    	}
    	try {
			return string.getBytes(encoding);
		}
		catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
    }
    
    
    public static void debugPathProperty(String property) {
    	String value = System.getProperty(property);
    	
    	// Make it legible in the console
    	value = value.replace(':', '\n');
    	
    	System.out.println(property + ":\n" + value + "\n");
    }

}