Tema 1: Servicios criptográficos en Java

1.1. Overview: De qué trata este módulo

En este módulo veremos una serie de APIs Java (JCA, JCE, JAAS, JSSE) cuya implementación conlleva la programación de seguridad, esto es, el desarrollo de programas que proporcionen servicios de encriptación y desencriptación, autentificación, autorización y comunicación segura.

Encriptación/desencriptación. En primer lugar estudiaremos los protocolos más sencillos, esto es los esquemas simétricos, que están basados en usar la misma clave secreta, usualmente de 128 bits, tanto para encriptar como para desencriptar. Los algoritmos más conocidos son TripleDES (versión de tres pasadas de DES) y Blowfish. Estos algoritmos se aplican a través de ciphers o cifradores de dos tipos: de bloque (permiten encriptar bloques de datos de 64 o 128 bits) y de de stream (encriptan bit-a-bit o byte a byte). Los primeros sirven para encriptar mensajes puntuales mientras que los segundos son más apropiados para flujos de datos. Estos aspectos se cubrirán en el Tema 2.

Dado que los algoritmos simétricos son sensibles a la interceptación de la clave secreta, (ataques de fisgones), en los esquemas asimétricos (de clave pública) se opta por dividir la clave secreta en una parte pública, para encriptar, y otra privada, para desencriptar. Podemos guardarnos nuestra clave privada y difundir la clave pública sin problemas ya que con esa clave otros podrán encriptar mensajes que solamente nosotros podremos desencriptar. Los algoritmos más usuales son RSA y ElGamal. Las claves necesarias son más largas (1024 bits) y la encriptación es más lenta. Estos aspectos se cubrirán, entre otros, en el Tema 3.

Autentificación. Se aplica tanto a datos como a usuarios. En el caso de los datos, p.e. un mensaje, nos permite saber si éste ha sido alterado o no, mientras que en el caso de los usuarios se trata de verificar su identidad. En este sentido, el mecanismo más básico aplicado a datos es el digest que consiste en obtener un código que siempre es el mismo si se aplica sobre los mismos datos (una alteración de los datos implica un código distinto). Los algoritmos clásicos son MD5 y SHA-1. Si al digest se le aplica una clave entonces tenemos un MAC (message authentication code). Si se protege con un esquema asimétrico entoces obtenemos una firma digital, cuyo objeto es asegurar la procedencia de los datos (puesto que se se calcula con una clave privada y se verifica con la clave pública correspodiente) y también que los datos no han sido alterados (ya que lo que se encripta/desencripta es un digest). Los algoritmos más conocidos son RSA y DSA. No obstante, el uso de las claves públicas como mecanismo de autentificación tiene el problema de garantizar que la clave pertenece a una determinada persona. Este es el origen de los certificados digitales que básicamente se obtienen firmando la clave pública con la clave privada de alguien con la clave privada de un tercero que asegura o certifica la identidad. Las firmas digitales y los certificados se tratan en el Tema 4.

Los servicios de encriptación y autentificación los proporcionan los APIs JCA (Java Cryptography Architecture) y JCE (Java Cryptography Extension).

Autorización. Sea cual sea el mecanismo de autentificación (p.e. un simple login-password) se suele hacer corresponder la identidad del usuario con el acceso a determinados recursos o incluso la ejecución de determinado código. El API Java JAAS (Java Authentication and Authorization Service) integra autentificación y autorización. Desarrollaremos este API en el Tema 5 y su aplicación en el contexto del servidor de aplicaciones BEA WebLogic.

Comunicación segura. El protocolo SSL (Secure Sockets Layer) es el protocolo habitualmente usado para cifrar la comunicación cliente servidor, p.e. para garantizar seguridad a HTTP (HTTPS). Como veremos en el Tema 6 el protocolo se basa en un proceso de intercambio de claves y certtificados. No obstante, el API Java JSSE (Java Secure Sockets Extension permite hacer transparente este proceso para el programador. También estudiaremos este tipo de servicios en el contexto de BEA WebLogic.

Aplicaciones. Aunque a lo largo de todos los temas realizaremos pequeños programas de aplicación para demostrar el uso de los diversos APIs, dedicaremos el Tema 7 a estudiar, a través de un ejemplo completo, la aplicación de mecanismos de encriptación sobre bases de datos.

1.2. JCA: Java Cryptography Architecture

Este es uno de los dos API que proporcionan facilidades criptográficas en Java (firmas, certificados, etc). Se trata del conjunto de clases que forma el package java.security. Las clases principales son

Provider Interfaz al package de la implementación de un determinado proveedor.
Security Manejo de proveedores instalados en la MV y de propiedades de seguridad (permisos).
MessageDigest Manejo de digests con SHA-1 o con MD5.
Signature Manejo de firma digital con DSA o RSA (encriptación) y MD5 (digests).
AlgorithmParameter Manejo de representaciones opacas (sin acceso directo) de los parámetros criptográficos.
AlgorithmParameterGenerator Obtiene un conjunto de parámetros apropiados para un algoritmo.
Key Interfaz para manejo de claves representadas de forma opaca.
KeySpec Manejo de representaciones transparentes de las claves y (en este caso) comunes a todas ellas.
KeyFactory Convierte representaciones de claves opacas (claves) en transparentes (especificaciones). Es bidireccional.
CertificateFactory Genera certificados y listas de revocación siguiendo una deterinada codificación (p.e. X.509).
KeyPair Par (público, privado) para un esquema asimétrico.
KeyPairGenerator Obtiene un par (público, privado).
KeyStore Interfaz para el manejo de almacenes de claves.
SecureRandom Manejo de números aleatorios.

Por ejemplo, como ya hemos dicho, una de estas facilidades es un message digest o hash, esto es una huella para comprobar, más tarde, que un determinado documento no ha sido alterado. Para crear un digest primero se crea una instancia de un algoritmo de hashing (MD5 o SHA-1), especificando opcionalmente el proveedor de la implementación:

MessageDigest md = MessageDigest.getInstance("MD5", "Sun");

A continuación proporcionamos los datos sobre los que se debe crear el digest, suponiendo que estos se encuentran en un array de datos de tipo byte:

md.update(datos);

Finalmente podemos almacenar el digest propiamente dicho en otro array:

byte[] d = md.digest();

1.2. JCE: Java Cryptography Extension

Este segundo API añade facilidades de encriptación y desencriptación de datos a JCA y se corresponde con las clases del package javax.crypto. Las más importantes son:

Cipher Cifrador de un determinado algoritmo en modo encriptar/desencriptar.
CipherInput{Output}Stream Facilita el uso de cifradores de stream de salida (entrada).
KeyGenerator Generador de claves secretas para esquemas simétricos.
SecretKeyFactory Convertir claves (opacas) en especificaciones (transparentes) y viceversa.
KeyAgreement Funcionalidad para el protocolo del mismo nombre para intercambiar mensajes de forma segura sin intercambiar una clave secreta.
MAC Manejo de Message Authentication Codes.

En general, lo primero que hay que hacer es crear un generador de claves, especificando el algoritmo de encriptación (p.e. Blowfish):

KeyGenerator kg = KeyGenerator.getInstance("Blowfish");

A partir de este generador podemos obtener una clave haciendo

Key k = KeyGenerator.generateKey();

Una vez tenemos la clave, la encriptación se hace creando un cifrador (cipher) especificando algoritmo, modo y padding, y después se inicializa dicho cifrador con la clave que acabamos de crear:

Cipher c = Cipher.getInstance("Blowfish/ECB/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, k);

Finalmente realizaremos la encriptación propiamente dicha de los datos contenidos en un array de tipo byte y los devolveremos en otro array:

byte[] cf = c.doFinal(datos);

Para poder usar JCE hay que instalar previamente una implementación. Una de las mas completas (en términos de la cantidad de algoritmos que incorpora) es la de BouncyCastle, disponible en http://www.bouncycastle.org (descargar JCE con proveedor y API). Una vez descargado lo más recomendable es instalarlo como una extensión, es decir copiar el fichero JAR correspondiente en el directorio $JAVA_HOME/jre/lib/ext. Una vez colocado el JAR como extensión se ha de modificar el fichero $JAVA_HOME/jre/lib/security/java.security en donde se definen los distintos proveedores criptográficos para añadir el nuevo proveedor. En JDK 1.4.2 haríamos lo siguiente:

security.provider.1=sun.security.provider.Sun
security.provider.2=com.sun.net.ssl.internal.ssl.Provider
security.provider.3=com.sun.rsajca.Provider
security.provider.4=com.sun.crypto.provider.SunJCE
security.provider.5=sun.security.jgss.SunProvider
#Nuevo proveedor de BouncyCastle
security.provider.6=org.bouncycastle.jce.provider.BouncyCastleProvider

Para testar la instalación podemos utilizar uno de los ejemplos que vienen con Blowfish o bien compilar y ejecutar el fichero JCETestInstalacion.java cuyo código mostramos a continuación:

import javax.crypto.*;

public class JCETestInstalacion {

  // Definimos cadena a encriptar
  public static final String cadena = "Esto es un test.";

  public static void main(String[] args) throws Exception {

    System.out.println("El texto original es: " + cadena);
    
    // Generar una clave Blowfish
    System.out.print("Intentando coger una clave Blowfish....");
    KeyGenerator generador = KeyGenerator.getInstance("Blowfish");
    generador.init(128);
    SecretKey clave = generador.generateKey();
    System.out.println("OK");

    // Intentar encriptar texto
    System.out.print("Intentando coger un cifrado y encriptar...");
    Cipher cifrador = Cipher.getInstance("Blowfish/ECB/PKCS5Padding");
    cifrador.init(Cipher.ENCRYPT_MODE, clave);
    byte[] textoCifrado = cifrador.doFinal(cadena.getBytes("UTF8"));
    System.out.println("Ok");
    System.out.println("El texto cifrado es: " + textoCifrado);

    System.out.println("Test completado con exito");
    }
  }


No obstante, el test funciona incluso si no se ha instalado el proveedor, ya que la máquina virtual incorpora por defecto el SunJCE. No obstante no se recomienda usarlo por dos motivos: (1) es muy limitado y (2) crea incompatibilidades con otras implementaciones.

Tema 2: Encriptación Simétrica

2.1. Estrategias de clave secreta

La forma más sencilla de realizar encriptación-desencriptación de datos es utilizar la misma clave en ambos procesos. Como acabamos de ver, podemos definir un cifrador para encriptar y luego utilizar el mismo cifrador para desencriptar. La encriptación simétrica es rápida y por ello se recomienda en aquellas aplicaciones en donde se requiere proteger un gran segmento de datos. Sin embargo, al exigir que tanto emisor como receptor se pongan de acuerdo es muy sensible a ataques-a-mitad, por lo que la clave que ha de enviarse suele encriptarse de forma asimétrica como veremos más adelante. En todo caso cuanto mayor sea la longitud de la clave más segura será la encriptación (lo habitual es manejar claves de entre 40 y 128 bits, recomendándose la longitud de 128 bits).

2.2. DESede y Blowfish en JCE

Los algoritmos de encriptación simétrica más conocidos son TripleDES y Blowfish. El primero, también llamado DESede, es una versión evolucionada (y más segura) del algoritmo DES que utilizaba claves de solo 56 bits. En DESede producen tres rondas de DES (encriptación, desencriptación y otra vez encriptación) en las que se utilizan 2 o 3 claves distintas de 56 bits, obteniéndose una clave combinada de 112 o de 168 bits. Por otro lado, Blowfish es otro algoritmo que permite claves de longitud mayor (448 bits). El ejemplo de test que vimos al final del capítulo anterior utilizaba Blowfish a la hora de generar la clave y el cifrador. Si queremos modificar dicho ejemplo para utilizar DESede simplemente tendremos que sustituir las líneas que hacen referencia a Blowfish por

...
KeyGenerator generador = KeyGenerator.getInstance("DESede");
...
Cipher cifrador = Cipher.getInstance("DESede/ECB/PKCS5Padding");

La expresión "DESede/ECB/PCKCS5Padding" contiene el nombre del algoritmo, el tipo de padding (según estándard PCKS5) y el modo de encriptación (ECB, CBC, etc).

2.3. Encriptación Basada en Paswords (PBE)

Supogamos que queremos utilizar un password como clave en un esquema privado. Dado que la longitud de los passwords es de 6-8 caractéres, el espacio de claves a consultar es mucho menor a pesar de tener muchas alternativas para cada carácter. Por ello, lo que se suele hacer es aplicarle un digest al password y construir con el resultado una clave binaria. No obstante, un hacker puede construir una lista de passwords, aplicarles hashing y tener una lista precompilada de claves. Esto se puede contrarrestar con dos técnicas: saltos aleatorios (añadir datos aleatorios antes del hashing) y cuentas de iteración (número de veces que hacemos el hashing sobre el password y el salto).

Para aplicar PBE, crearemos en primer lugar una clave adeacuada. Esto se hace creando un objeto PBEKeySpec seguido de un SecretKeyFactory en donde especificamos el algoritmo PBE.

char[] password = "mipassword".toCharArray();
PBEKeySpec especificacion = new PBEKeySpec(password);
SecretKeyFactory factoria = SecretKeyFactory.getInstance("PBEWithSHAAndTwofish-CBC");
SecretKey clave = factoria.generateSecret(especificacion);

Una vez que tenemos la clave deberemos crear un cifrador pasándole el salto y el número de iteraciones a un objeto PBEParameterSpec que los envuelve.

PBEParameterSpec parametros = new PBEParameterSpec(salto, iteraciones);
Cipher cifrador = Cipher.getInstance("PBEWithSHAAndTwofish-CBC");
cifrador.init(Cipher.ENCRYPT_MODE, clave, parametros);

Veamos el ejemplo PBE.java. En la parte de encriptación se toma como entrada un password y un texto a cifrar. En primer lugar se construye un salto aleatorio de 8 bytes. Seguidamente se crea, inicializa y aplica un cifrador para PBE. El resultado del cifrado se completa con una codificación en BASE64 para poder los datos tipo byte formados por el salto y el texto cifrado. Esta codificación es necesaria porque los byte tienen 8 bits pero los caracteres ASCII tienen 7 bits. Así, aplicando esta codificación devolvemos la concatenación del salto y el texto cifrado.

private static String encriptar(char[] password, String textoPlano)
        throws Exception
    {
                // Creamos un salto de 64 bits (8 bytes)
        byte[] salto = new byte[8];
        Random random = new Random();
        random.nextBytes(salto);

                // PBEKeySpec, SecretKeyFactory, SecretKey, PBEParameterSpec, Cipher
        ...

        byte[] textoCifrado = cifrador.doFinal(textoPlano.getBytes());

                // Codificación BASE64
        BASE64Encoder codificador = new BASE64Encoder();

        String cadenaSalto = codificador.encode(salto);
        String cadenaCifrado = codificador.encode(textoCifrado);

        return cadenaSalto+cadenaCifrado;
    }

Para desencriptar tomaremos como entrada el password y el texto BASE64 que contiene el salto seguido del texto cifrado. Lo primero, entonces, es decodificar salto y texto cifrado. A continuación utilizamos el salto y el password para inicializar el cifrador y se lo aplicamos al texto cifrado para desencriptarlo.

private static String desencriptar(char[] password, String texto)
        throws Exception
    {
                // Partimos el texto en salto y texto. El salto lo dan los
                // 12 primeros caracteres codificados en BASE64 a partir de 8 bytes.
        String salto = texto.substring(0,12);
        String textoCifrado = texto.substring(12,texto.length());

                // Descodificar en BASE64 los bytes a partir del salto y
                //del texto cifrado.
        BASE64Decoder decodificador = new BASE64Decoder();
        byte[] arraySalto = decodificador.decodeBuffer(salto);
        byte[] arrayCifrado = decodificador.decodeBuffer(textoCifrado);

        // PBEKeySpec, SecretKeyFactory, SecretKey, PBEParameterSpec, Cipher
        ...

        // Desencriptar
        byte[] arrayTextoPlano = cifrador.doFinal(arrayCifrado);

        return new String(arrayTextoPlano);
    }

2.4. Codificación de Claves Secretas

Si en algún momento hemos de almacenar la clave en el sistema de ficheros es conveniente codificarla de alguna forma para que sea más difícil descifrarla. Por ello, PBE se complementa con mecanismos adicionales. El primero de ellos es utilizar los métodos de javax.crypto.Cipher wrap()/unwrap() para codificar/decodificar la clave. Para ello inicializaremos un cifrador PBE en modo WRAP_MODE/UNWRAP_MODE:

...
cifrador.init(Cipher.WRAP_MODE, clavePassword, parametros);
byte[] bytesClaveCodificada = cifrador.wrap(clave);
...
cifrador.init(Cipher.UNWRAP_MODE, clavePassword, parametros);
Key clave = cifrador.unwrap(bytesClaveCodificada, "Blowfish", Cipher.SECRET_KEY);

Si nuestro proveedor no incluye la posibilidad de wrapping entonces trendremos que recurrir al método java.security.Key.getEncoded() y a javax.crypto.SecretKeySpec. Por un lado getEncoded() realiza la codificación y por otro SecretKeySpec recoge la clave desencriptada del cifrador y la decodifica:

...
byte[] bytesClave = clave.getEncoded();
cifrador.init(Cipher.ENCRYPT_MODE, clave, parametros);
byte[] bytesClaveEnciptada = cifrador.doFinal(bytesClave);
...
cifrador.init(Cipher.DECRYPT_MODE, password, parametros);
byte[] bytesClave = cifrador.doFinal(bytesClaveEncriptada);
SecretKeySpec clave = new SecretKeySpec(bytesClave, "Blowfish");

2.5. Cifradores de Stream

Los cifradores que hemos visto hasta ahora son de bloque, es decir que se aplican sobre bloque de datos (usualmente 64 bits o incluso 128 bits) cada vez. Estos cifradores son adecuados para datos concretos como claves o mensajes concretos. Por el contrario, cuando se trata de encriptar flujos constantes de datos es más conveniente utilizar cifradores de stream, que encriptan bit a bit o byte a byte.

Los cifradores de stream suelen funcionar bajo modo CBC. Este modo se diferencia de ECB en que a cada byte no se le asocia necesariamente el mismo código sino que éste código depende de los anteriores y la inicialización es aleatoria realizándose con un IV o Initialization Vector. Dicho vector se obtiene a partir de un byte aleatorio de un determinado tamaño usando la clase java.security.SecureRandom:

byte[] bytesAleatorios = new byte[8];
SecureRandom aleatorio = new SecureRandom();
aleatorio.nextBytes(bytesAleatorios);

A continuación se crea el IV propiamente dicho, usando la clase IVParameterSpec:

IVParameterSpec iv = new IVParameterSpec(aleatorios);

Con el IV ya podemos crear e inicializar un cifrador CBC:

Cipher cifrador = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
cifrador.init(Cipher.ENCRYPT_MODE, clave, iv);

En cuanto al uso de este tipo de cifradores, el hecho de disponer de las clases CipherInputStream y CipherOutputStream en javax.crypto facilita su aplicación a cualquier tipo de flujo de datos como un fichero. Por ejemplo, podemos encriptar un fichero con el siguiente fragmento de código:

FileInputStream entrada = new FileInputStream("fichero.txt");
FileOutputStream salida = new FileOutputStream("fichero.enc");
...
CipherOutputStream salidaCifrador = new CipherOutputStream(salida, cifrador);
int r = 0;
while (r = entrada.read() != -1) {
   salidaCifrador.write(r);
}
salidaCifrador.close();
salida.close();
entrada.close();

Un aspecto importante a tener en cuenta con los IV es que hay que utilizar para desencriptar el mismo IV que se usó para encriptar por lo que será necesario almacenarlo, por ejemplo al principio del fichero encriptado.

2.6. Ejemplo de Encriptación de Ficheros

Como ejemplo de aplicación de todo lo que hemos visto hasta ahora vamos a ver un ejemplo en el que se encripta/desencripta un fichero haciendo uso de PBE. El código completo se encuentra en Ficheros.java. Aquí podemos encontrar el método crearClave() que genera una clave para el algoritmo AES (Rijndael) la encripta con un password (PBE) y la guarda en un fichero. Por otro lado, el método cargarClave(), lee la clave del fichero y la devuelve desencriptada. Esta clave la utiliza el método encriptar() que utiliza el password para cargar la clave y luego utiliza un cifrador de stream en modo CBC para encriptar un fichero. Simétricamente, el método desencriptar() utiliza el password para cargar la clave y utiliza otro cifrador CBC para desencriptar el fichero generando un nuevo fichero de salida.