/*
 * Copyright  2004 Mass Dosage
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package com.ant;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

/**
 * Ant task for generating symmetric encryption keys and encrypting and
 * decrypting files.
 * 
 * @author mass
 */
public class Crypter extends Task {

	/**
	 * The default cipher transformation used for encryption or decryption.
	 */
	public static final String DEFAULT_CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";

	/**
	 * The default algorithm used if generating a key.
	 */
	public static final String DEFAULT_KEY_ALGORITHM = "AES/CBC";

	private String keyFile;
	private File inputFile;
	private File outputFile;
	private boolean encrypt = true;
	private boolean generateKey = false;
	private String salt = "EYnJl9GPHi44mt";
	private String cipherTransformation = DEFAULT_CIPHER_TRANSFORMATION;
	private String keyAlgorithm = DEFAULT_KEY_ALGORITHM;

	private IvParameterSpec ivSpec;

	/**
	 * Reads the contents of the key file and converts this into a
	 * <code>Key</code>.
	 * 
	 * @return The <code>Key</code> object.
	 * @throws BuildException
	 *             If the contents of the key file cannot be read.
	 */
	private SecretKey readKey() throws BuildException {
		if (this.keyFile == null) {
			throw new BuildException("No 'keyFile' specified, cannot continue.");
		}

		try {

			this.logInfo(" key: " + keyFile + " " + keyFile.length());
			this.logInfo(" salt: " + salt );
			/*KeyGenerator kgen = KeyGenerator.getInstance("AES");
			SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
			sr.setSeed(keyFile.getBytes());
			kgen.init(128, sr); // 192 and 256 bits may not be available
			SecretKey skey = kgen.generateKey();*/
			
			//String salt="sale";
						
			MessageDigest digest = MessageDigest.getInstance("SHA-1");
			
			for(int i=0; i<128;i++){
				digest.update(salt.getBytes());
				digest.update(keyFile.getBytes());
				digest.update(digest.digest());
			}
			
			byte[] sha1 = digest.digest();
			
			byte[] aes_key = new byte[16];
			System.arraycopy(sha1, 0, aes_key, 0, aes_key.length);
									
			SecretKey secret = new SecretKeySpec(aes_key, "AES");

			return secret;

		} catch (Exception e){


			this.logInfo("readKey error: " + e);
			return null;
		}

	}

	/**
	 * Initialises a <code>Cipher</code> in the mode set in the ant task
	 * (encrypt or decrypt) with the passed <code>Key</code>.
	 * 
	 * @param key
	 *            The <code>Key</code> which the <code>Cipher</code> will use
	 *            for encryption or decryption.
	 * @return The initialised <code>Cipher</code>.
	 * @throws BuildException
	 *             If an error occurs initialising the cipher.
	 */
	private Cipher initialiseCipher(Key key) throws BuildException {
		Cipher cipher = null;
		try {
			cipher = Cipher.getInstance(this.cipherTransformation);
			final byte[] iv = new byte[16];
			Arrays.fill(iv, (byte) 0);
			IvParameterSpec ivSpec = new IvParameterSpec(iv);

			if (encrypt) {
				cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
				this.logInfo("Initialised cipher to perform encryption using " + this.cipherTransformation);
				this.logInfo("key: " + hex(key.getEncoded()));
			} else {
				cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
				this.logInfo("Initialised cipher to perform decryption using " + this.cipherTransformation);
			}
		} catch (NoSuchAlgorithmException e){


			throw new BuildException("Cipher transformation algorithm [" + this.cipherTransformation
					+ "] not supported", e);
		} catch (NoSuchPaddingException e){


			throw new BuildException("Cipher padding scheme not supported", e);
		} catch (InvalidKeyException e){


			this.logInfo("Error: " + e);
			throw new BuildException("Invalid key for cipher", e);
		} catch (InvalidAlgorithmParameterException e){


			this.logInfo("Error: " + e);
			throw new BuildException("Invalid AlgorithmParameter for cipher", e);
		}
		return cipher;
	}

	private String hex(byte[] data) {
		int offset = 0;
		int length = data.length;
		final StringBuffer buf = new StringBuffer();
		for (int i = offset; i < offset + length; i++) {
			int halfbyte = (data[i] >>> 4) & 0x0F;
			int twohalfs = 0;
			do {
				if ((0 <= halfbyte) && (halfbyte <= 9)) {
					buf.append((char) ('0' + halfbyte));
				} else {
					buf.append((char) ('a' + (halfbyte - 10)));
				}
				halfbyte = data[i] & 0x0F;
			} while (twohalfs++ < 1);
		}
		return buf.toString();
	}

	/**
	 * Performs the encryption/decryption according to the state of the passed
	 * <code>Cipher</code>, using the input and output files set in the ant
	 * task.
	 * 
	 * @param cipher
	 *            An initialised <code>Cipher</code> to use for the
	 *            encryption/decryption.
	 * @throws BuildException
	 *             If the input or output files cannot be found, read, or
	 *             written to; or if an error occurs performing the
	 *             encryption/decryption.
	 */
	private void crypt(Cipher cipher) throws BuildException {
		FileInputStream in = null;
		try {
			in = new FileInputStream(this.inputFile);
		} catch (FileNotFoundException e){


			throw new BuildException("Could not find input file " + this.inputFile, e);
		}
		FileOutputStream fileout = null;
		try {
			fileout = new FileOutputStream(this.outputFile);
		} catch (FileNotFoundException e){


			throw new BuildException("Invalid output file " + this.outputFile, e);
		}

		CipherOutputStream out = new CipherOutputStream(fileout, cipher);
		byte[] buffer = new byte[8192];
		int length;
		try {
			while ((length = in.read(buffer)) != -1) {
				out.write(buffer, 0, length);
			}
			in.close();
			out.close();
		} catch (IOException e){


			throw new BuildException("Error writing output file " + this.outputFile, e);
		}
		this.logInfo("Performed cryptographic transformation on " + this.inputFile.getAbsolutePath() + " to "
				+ this.outputFile.getAbsolutePath());
	}

	/**
	 * Called by the project to perform the encryption or decryption using the
	 * parameters set in the task.
	 * 
	 * @throws BuildException
	 *             If something goes wrong executing this task.
	 */
	public void execute() throws BuildException {

		if (!(this.inputFile == null && this.outputFile == null)) { // if input
																	// or output
																	// files
																	// specified,
																	// need to
																	// attmept
																	// enc/dec
			Key key = this.readKey();
			this.logInfo("key read " + (key != null));
			Cipher cipher = this.initialiseCipher(key);
			this.crypt(cipher);
		}
	}

	/**
	 * Sets the algorithm used to generate a <code>Key</code>. If this is not
	 * set, then the default value specified by
	 * <code>DEFAULT_KEY_ALGORITHM</code> will be used.
	 * 
	 * @param keyAlgorithm
	 *            The standard name of the requested key algorithm.
	 */
	public void setKeyAlgorithm(String keyAlgorithm) {
		this.keyAlgorithm = keyAlgorithm;
	}

	/**
	 * Sets the location of the <code>File</code> containing the
	 * <code>Key</code> to be used for encryption/decryption.
	 * 
	 * @param keyFile
	 *            The location of the key file.
	 */
	public void setKeyFile(String key) {

		this.keyFile = key;
	}

	/**
	 * Sets the location of the input <code>File</code> that is to be
	 * encrypted/decrypted.
	 * 
	 * @param inputFile
	 *            The location of the input file.
	 */
	public void setInputFile(File inputFile) {
		this.inputFile = inputFile;
	}

	/**
	 * Sets the location of the output <code>File</code> that is the results of
	 * the encryption/decryption.
	 * 
	 * @param outputFile
	 *            The location of the output file.
	 */
	public void setOutputFile(File outputFile) {
		this.outputFile = outputFile;
	}
	
	public void setSalt(String salt) {
		this.salt = salt;
	}

	/**
	 * Sets the mode that is used to determine whether encryption or decryption
	 * will be performed. If the value "true" is passed to this method then
	 * encryption will be performed, if the value "false" is passed then
	 * decryption will be performed. If this value is not set, encryption will
	 * be performed by default.
	 * 
	 * @param encrypt
	 *            Whether to perform encryption or decryption.
	 */
	public void setEncrypt(boolean encrypt) {
		this.encrypt = encrypt;
	}

	/**
	 * Sets the cipher transformation that will be used to perform the
	 * encryption/decryption. If this is not set, then the default value
	 * specified by <code>DEFAULT_CIPHER_TRANSFORMATION</code> will be used.
	 * 
	 * @param transformation
	 *            The name of the transformation, for example
	 *            <i>Blowfish/ECB/PKCS5Padding</i>.
	 */
	public void setCipherTransformation(String transformation) {
		this.cipherTransformation = transformation;
	}

	/**
	 * Calcola il SHA1 del messaggio, usando la crypto api.
	 * 
	 * @param message
	 *            the message
	 * @param offset
	 *            the offset
	 * @param length
	 *            the length
	 * @return the byte[]
	 */
	public static byte[] SHA1(final byte[] message, final int offset, final int length) {
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$
			digest.update(message, offset, length);
			final byte[] sha1 = digest.digest();

			return sha1;
		} catch (final NoSuchAlgorithmException e){


			
		}
		return null;
	}

	/**
	 * SH a1.
	 * 
	 * @param message
	 *            the message
	 * @return the byte[]
	 */
	public static byte[] SHA1(final byte[] message) {
		return SHA1(message, 0, message.length);
	}

	/**
	 * Logs an informational message. This method is required so that this task
	 * can be used outside of ant.
	 * 
	 * @param message
	 *            Message to log.
	 */
	private void logInfo(String message) {
		if (this.getProject() != null) { // we are running in ant, so use ant
											// log
			this.log(message, Project.MSG_INFO);
		} else { // we are running outside of ant, log to System.out
			System.out.println(message);
		}
	}
}
