import java.io.*;
import java.security.*;
import java.security.spec.*;

import javax.crypto.*;
import javax.crypto.spec.*;

/**
 * Clase que implementa la encriptación/desencriptacion de archivos usando el algoritmo AES y la
 * llave AES la envuelve(wrapper) en encriptación de algoritmo RSA.
 * @author José Antonio Silva, Mario Ungemach, Jorge Mellings
 */
public class FileEncryption {

        /**
         * Valor del tamaño de la llave AES, seteada en 256 bits
         */
	public static final int AES_Key_Size = 256;
	/**
         * Objeto de cifrado utilizado para llave RSA
         */
        Cipher pkCipher;
        /**
         * Objeto de cifrado utilizado para llave AES
         */
        Cipher aesCipher;
	/**
         * Objeto que almacena la llave AES en bytes
         */
        byte[] aesKey;
	/**
         * Objeto que almacenará la llave AES
         */
        SecretKeySpec aeskeySpec;

	/**
	 * Constructor: creamos objetos de la clase Cipher
	 */
	public FileEncryption() throws GeneralSecurityException {
		// Creamos el objeto pkCipher de llave publica RSA
		pkCipher = Cipher.getInstance("RSA");
	    // Creamos el objeto aesCipher almacenador de la llave AES
	    aesCipher = Cipher.getInstance("AES");
	}

	/**
	 * Metodo makeKey() el cual crea una nueva llave de tipo AES
	 */
	public void makeKey() throws NoSuchAlgorithmException {

            //Creamos una objeto KeyGenerator de tipo AES
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
	    //Inicializamos el objteto kgen, con el tamaño de la llave AES que deseamos en este caso 256
            kgen.init(AES_Key_Size);
            //Generamos la llave secreta de tipo AES de 256 bits
	    SecretKey key = kgen.generateKey();
            //Codificamos la llave en bytes
	    aesKey = key.getEncoded();

            //Creamos el objeto aeskeySpec, con la llave codificada aesKey, y el tipo de encriptacion AES
	    aeskeySpec = new SecretKeySpec(aesKey, "AES");
	}

	/**
	 * Metodo que desencripta la llave AES desde el archivo en que se encuentra utilizando la llave privada RSA
         */
	public void loadKey(File in, File privateKeyFile) throws GeneralSecurityException, IOException {
		//Leemos la llave privada RSA  a ser usada para desencriptar la llave AES
		byte[] encodedKey = new byte[(int)privateKeyFile.length()];
		new FileInputStream(privateKeyFile).read(encodedKey);


                //Creamos la llave privada a partir del objeto encodeKey
                //Se usa el modo PKSCS8, en cual fueron generadas las llaves RSA
		PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedKey);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PrivateKey pk = kf.generatePrivate(privateKeySpec);


                //Leemos la llave AES, invocamos el metodo init con el modo desencriptar y llave privada RSA
		pkCipher.init(Cipher.DECRYPT_MODE, pk);
		aesKey = new byte[AES_Key_Size/8];
		//Desencriptamos el archivo que contiene la llave AES encriptada, con la llave privada RSA
                CipherInputStream is = new CipherInputStream(new FileInputStream(in), pkCipher);
		is.read(aesKey);
		aeskeySpec = new SecretKeySpec(aesKey, "AES");
	}

	/**
	 * Metodo que encripta la llave AES a un archivo usando la llave RSA publica
         */
	public void saveKey(File out, File publicKeyFile) throws IOException, GeneralSecurityException {
		//Leemos la llave publica a ser usada para encriptar la llave AES
                byte[] encodedKey = new byte[(int)publicKeyFile.length()];
		new FileInputStream(publicKeyFile).read(encodedKey);

		//Creamos una llave publica
		X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedKey);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PublicKey pk = kf.generatePublic(publicKeySpec);

		//Escribimos la llave AES
		pkCipher.init(Cipher.ENCRYPT_MODE, pk);
		CipherOutputStream os = new CipherOutputStream(new FileOutputStream(out), pkCipher);
		os.write(aesKey);
		os.close();
	}

	/**
         * Metodo que encripta y copia los contenidos de los archivos determinados (in a out)
	 */
	public void encrypt(File in, File out) throws IOException, InvalidKeyException {
	 //Inicializamos el objeto aesCipher, en el modo desencriptar y con la llave AES desencriptada

            aesCipher.init(Cipher.ENCRYPT_MODE, aeskeySpec);
	     //Creamos el objeto is
               	FileInputStream is = new FileInputStream(in);
	     //Creamos el objeto is con el archivo de salida out, y con el objeto aesCipher
	         CipherOutputStream os = new CipherOutputStream(new FileOutputStream(out), aesCipher);

                //Copiamos los contenidos del objeto is al objeto os
                //Es decir, copiamos el contenido del archivo de entrada encriptandolo con la llave AES
                //al stream de salida os

		copy(is, os);

		os.close();
	}

	/**
	 * Metodo que desencripta y copia los contentidos de los archivos determinados (in a out)
         */
	public void decrypt(File in, File out) throws IOException, InvalidKeyException {
		//Inicializamos el objeto aesCipher, en el modo desencriptar y con la llave AES desencriptada
               aesCipher.init(Cipher.DECRYPT_MODE, aeskeySpec);

                //Creamos el objeto is con el archivo de entrada in, y con la el objeto aesCipher
		CipherInputStream is = new CipherInputStream(new FileInputStream(in), aesCipher);
		//Creamos el objeto os
                FileOutputStream os = new FileOutputStream(out);

                //Copiamos los contenidos del objeto is al objeto os
                //Es decir, copiamos el contenido del archivo de entrada desencriptandolo con la llave AES
                //al stream de salida os

		copy(is, os);

                //Cerramos los streams
		is.close();
		os.close();
	}

	/**
	 * Metodo que copia dos streams el de entrada (is) al de salida (os)
	 */
	private void copy(InputStream is, OutputStream os) throws IOException {
		int i;
		byte[] b = new byte[1024];
		while((i=is.read(b))!=-1) {
			os.write(b, 0, i);
		}
	}
}
