Cifrar y descifrar datos usando algoritmos de clave simétrica con Java

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Algunos datos son sensibles y necesitan especial protección como los datos personales, bancarios o relacionados con la seguridad como contraseñas. Para minimizar los riesgos de seguridad en caso de un fallo se suele cifrar los datos al persistirlos en la base de datos de modo que en caso de la base de datos sea filtrada los datos sigan protegidos siempre y cuando la clave que permite descifrarlos no se ha filtrado también. Java ofrece clases en su JDK que implementan los principales algoritmos para cifrar y descifrar datos.

Java

El cifrado de datos consiste en hacer ilegibles los datos originales para cualquiera que no tenga la clave, el descifrado es el proceso contrario que transforma los datos cifrados en los datos originales usando la clave que permite descifrarlos.

Los algoritmos de cifrado se dividen en dos grupos, los algoritmos de clave simétrica en los que se emplea una única clave para hacer el cifrado como el descifrado y los algoritmos de clave asimétrica en los que una clave pública permite cifrar datos que solo la clave privada es capaz de descifrarlos.

El cifrado de datos es muy útil desde el punto de vista de la seguridad y para proteger información personal al guardarla en un sistema de almacenamiento ya sea el el sistema de archivos o en una base de datos. También es muy útil y usado para proteger las comunicaciones en internet, en la navegación web el cifrado se usa con el protocolo https.

De hecho en el protocolo https se usan ambos tipos de algoritmos en un primer momento un algoritmo de clave asimétrica para compartir la clave secreta entre los sistemas que se desean comunicar, la clave secreata es la que se utiliza para el cifrado en los datos transmitidos. Se utiliza el algoritmo asimétrico para compartir la clave secreta de forma segura y posteriormente un algoritmo simétrico para el cifrado por ser más rápido.

Además de usar cifrado para algunas operaciones es igual de importante para la seguridad utilizar últimas versiones de las dependencias que se usen, analizar las dependencias en busca de fallos de seguridad descubiertos y en el caso de Java utilizar una versión del JDK con mantenimiento, ya que las últimas versiones suelen incluir correcciones de seguridad.

El cifrado de datos es una parte esencial e importante en algunas aplicaciones y para algunos datos, realizarlo correctamente es igual de importante. Java implementa los algoritmos de cifrado utilizados actualmente y proporciona clases para utilizarlas desde una aplicación en este lenguaje. El cifrado y descifrado es una operación que hay que implementar correctamente con lo que no conviene tratar de implementar un algoritmo de cifrado u ofuscación, lo correcto es utilizar un algoritmo estándar aún considerado seguro.

El producto de software Vault de Hashicorp ofrece como servicio las operaciones de cifrado y descifrado, en vez de que cada aplicación implementa el cifrado y descifrado las aplicaciones pueden delegar en Vault utilizando la API que ofrece como un servicio.

Algoritmos de cifrado con clave simétrica

En los algoritmos simétricos se utiliza la misma clave tanto para cifrar como para descifrar los datos. Las claves de los algoritmos simétricos son más pequeñas, proporcionan mayor seguridad y se siguen utilizando ya que son más rápidos en el cifrado y descifrado. El inconveniente de estos algoritmos es que es necesario compartir la clave secreta de alguna forma para evitar el ataque man-in-the-middle para lo que se utiliza un algoritmo de clave asimétrica.

En general, un algoritmo con claves de mayor cantidad de bits para el tamaño de la clave proporciona una mayor seguridad. De esta forma AES con una clave de 256 bits es más seguro que AES con una clave de 128 bits. Los algoritmos de clave simétrica cifran los datos en bloques, algunos algoritmos utilizan bloques de 64 bits y otros de 128 bits. Entre los algoritmos de cifrado simétricos están los siguientes algunos han sido reemplazados por AES.

Symetric encryption

Symetric encryption

AES

AES (Advanced Encryption Standard) es un algoritmo de cifrado de clave simétrica que utiliza bloques de 128 bits. AES es uno de los algoritmos de cifrado más seguros y eficientes disponibles. Es compatible con claves de 128, 192 y 256 bits.

3DES

3DES (Triple Data Encryption Standard) es un algoritmo de cifrado de clave simétrica que utiliza bloques de 64 bits y una clave de 168 bits.

Blowfish

Blowfish es un algoritmo de cifrado de clave simétrica que utiliza bloques de 64 bits y una clave de entre 32 y 448 bits. Fue desarrollado en 1993 y se utiliza en algunas aplicaciones.

RC5

RC5 es un algoritmo de cifrado de clave simétrica que utiliza bloques de 64 bits y una clave de entre 0 y 2040 bits. Fue desarrollado en 1994 y se utiliza en algunas aplicaciones.

IDEA

IDEA (International Data Encryption Algorithm) es un algoritmo de cifrado de clave simétrica que utiliza bloques de 64 bits y una clave de 128 bits. IDEA fue desarrollado en 1991 y es utilizado en algunas aplicaciones.

Ejemplos de código de cifrado y descifrado con Java

Listar algoritmos de cifrado soportados por Java

Java soporta varios algoritmos de cifrado simétrico dependiendo de la versión de Java que se pueden listar utilizando la API que ofrece el JDK.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

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

        symmetricEncrypt();
    }

    ...

    private static void symmetricEncrypt() throws Exception {
        Set<String> keyGenerators = Security.getAlgorithms("KeyGenerator");
        System.out.println("Supported key generators: " + keyGenerators.stream().sorted().collect(Collectors.joining(",")));

        Set<String> secretKeyFactories = Security.getAlgorithms("SecretKeyFactory");
        System.out.println("Supported key factory: " + secretKeyFactories.stream().sorted().collect(Collectors.joining(",")));

        Set<String> chipers = Security.getAlgorithms("Cipher");
        System.out.println("Supported ciphers: " + chipers.stream().sorted().collect(Collectors.joining(",")));

        Set<String> macs = Security.getAlgorithms("Mac");
        System.out.println("Supported macs: " + macs.stream().sorted().collect(Collectors.joining(",")));

        ...
    }

    ...
}

Main-1.java
1
2
3
4
Supported key generators: AES,ARCFOUR,BLOWFISH,CHACHA20,DES,DESEDE,HMACMD5,HMACSHA1,HMACSHA224,HMACSHA256,HMACSHA3-224,HMACSHA3-256,HMACSHA3-384,HMACSHA3-512,HMACSHA384,HMACSHA512,HMACSHA512/224,HMACSHA512/256,RC2,SUNTLS12PRF,SUNTLSKEYMATERIAL,SUNTLSMASTERSECRET,SUNTLSPRF,SUNTLSRSAPREMASTERSECRET
Supported key factory: DES,DESEDE,PBEWITHHMACSHA1ANDAES_128,PBEWITHHMACSHA1ANDAES_256,PBEWITHHMACSHA224ANDAES_128,PBEWITHHMACSHA224ANDAES_256,PBEWITHHMACSHA256ANDAES_128,PBEWITHHMACSHA256ANDAES_256,PBEWITHHMACSHA384ANDAES_128,PBEWITHHMACSHA384ANDAES_256,PBEWITHHMACSHA512ANDAES_128,PBEWITHHMACSHA512ANDAES_256,PBEWITHMD5ANDDES,PBEWITHMD5ANDTRIPLEDES,PBEWITHSHA1ANDDESEDE,PBEWITHSHA1ANDRC2_128,PBEWITHSHA1ANDRC2_40,PBEWITHSHA1ANDRC4_128,PBEWITHSHA1ANDRC4_40,PBKDF2WITHHMACSHA1,PBKDF2WITHHMACSHA224,PBKDF2WITHHMACSHA256,PBKDF2WITHHMACSHA384,PBKDF2WITHHMACSHA512
Supported ciphers: AES,AES/GCM/NOPADDING,AES/KW/NOPADDING,AES/KW/PKCS5PADDING,AES/KWP/NOPADDING,AES_128/CBC/NOPADDING,AES_128/CFB/NOPADDING,AES_128/ECB/NOPADDING,AES_128/GCM/NOPADDING,AES_128/KW/NOPADDING,AES_128/KW/PKCS5PADDING,AES_128/KWP/NOPADDING,AES_128/OFB/NOPADDING,AES_192/CBC/NOPADDING,AES_192/CFB/NOPADDING,AES_192/ECB/NOPADDING,AES_192/GCM/NOPADDING,AES_192/KW/NOPADDING,AES_192/KW/PKCS5PADDING,AES_192/KWP/NOPADDING,AES_192/OFB/NOPADDING,AES_256/CBC/NOPADDING,AES_256/CFB/NOPADDING,AES_256/ECB/NOPADDING,AES_256/GCM/NOPADDING,AES_256/KW/NOPADDING,AES_256/KW/PKCS5PADDING,AES_256/KWP/NOPADDING,AES_256/OFB/NOPADDING,ARCFOUR,BLOWFISH,CHACHA20,CHACHA20-POLY1305,DES,DESEDE,DESEDEWRAP,PBEWITHHMACSHA1ANDAES_128,PBEWITHHMACSHA1ANDAES_256,PBEWITHHMACSHA224ANDAES_128,PBEWITHHMACSHA224ANDAES_256,PBEWITHHMACSHA256ANDAES_128,PBEWITHHMACSHA256ANDAES_256,PBEWITHHMACSHA384ANDAES_128,PBEWITHHMACSHA384ANDAES_256,PBEWITHHMACSHA512ANDAES_128,PBEWITHHMACSHA512ANDAES_256,PBEWITHMD5ANDDES,PBEWITHMD5ANDTRIPLEDES,PBEWITHSHA1ANDDESEDE,PBEWITHSHA1ANDRC2_128,PBEWITHSHA1ANDRC2_40,PBEWITHSHA1ANDRC4_128,PBEWITHSHA1ANDRC4_40,RC2,RSA
Supported macs: HMACMD5,HMACPBESHA1,HMACPBESHA224,HMACPBESHA256,HMACPBESHA384,HMACPBESHA512,HMACPBESHA512/224,HMACPBESHA512/256,HMACSHA1,HMACSHA224,HMACSHA256,HMACSHA3-224,HMACSHA3-256,HMACSHA3-384,HMACSHA3-512,HMACSHA384,HMACSHA512,HMACSHA512/224,HMACSHA512/256,PBEWITHHMACSHA1,PBEWITHHMACSHA224,PBEWITHHMACSHA256,PBEWITHHMACSHA384,PBEWITHHMACSHA512,SSLMACMD5,SSLMACSHA1
Main-1.out

El siguiente código muestra el resultado de cifrar un texto asi como descifrar para obtener el valor original, también muestra el resultado de la operación de hasing HMAC.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static void symmetricEncrypt() throws Exception {
        String text = "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT";
        String password = "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT";
        String salt = "%@&LN4CLT95yMEHNSettCAaQAcHZbh";

        SecretKey key = generateKey();
        SecretKey passwordKey = generateKey(password, salt);

        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] keyEncrypted = encrypt(key, text);
        byte[] passwordEncrypted = encrypt(passwordKey, text);
        byte[] inputStreamEncrypted = encrypt(key, Main.class.getResourceAsStream("/text.txt"));

        System.out.println("Plain text: " + password);
        System.out.println("Key encrypted: " + HexFormat.of().formatHex(keyEncrypted));
        System.out.println("Password key encrypted: " + HexFormat.of().formatHex(passwordEncrypted));
        System.out.println("InputStream key encrypted: " + HexFormat.of().formatHex(inputStreamEncrypted));
        System.out.println("Key decrypted: " + new String(decrypt(key, keyEncrypted)));
        System.out.println("Password key decrypted: " + new String(decrypt(passwordKey, passwordEncrypted)));
        System.out.println("HMAC: " + calculateHmac(key, text));
    }

    ...
}

Main-2.java
1
2
3
4
5
6
7
Plain text: rw@wbnaq2R@DS#u3o7hxWckqhfkzbT
Key encrypted: 2325e4851c05a4e8dde8c0861750ae9b11f0a94d54792c91d0b490907bcf9602
Password key encrypted: 847d1e234170602a9905936ff26c07ef7cce2c22e77e79d8787f8a33e9c2bcdd
InputStream key encrypted: 2325e4851c05a4e8dde8c0861750ae9b11f0a94d54792c91d0b490907bcf9602
Key decrypted: rw@wbnaq2R@DS#u3o7hxWckqhfkzbT
Password key decrypted: rw@wbnaq2R@DS#u3o7hxWckqhfkzbT
HMAC: c0cf366aa240c55b18a88aa952aac1224f24588b42b4a34f02ec421ab81c3968
Main-2.out

Generar una clave simétrica

En los algoritmos de clave simétrica es necesario generar la clave para realizar el cifrado y descifrado de datos. La clave es simplemente un número binario de cierta longitud habiendo dos forma de generarlo, una generando el número de forma aleatoria pero segura al que en la jerga de criptografía se le denomina material. La clave en Java se representa por la clase SecretKey

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static SecretKey generateKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(256);
        return keyGenerator.generateKey();
    }

    ...
}

Main-3.java

La segunda forma es generar la clave como una derivada de una contraseña. Un número de 256 bits aleatorio aunque se represente en hexadecimal es complicado de recordar para un humano, derivar la clave a partir de una contraseña es más fácil de recordar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static SecretKey generateKey(String password, String salt) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
    }

    ...
}

Main-4.java

Cifrar datos

Una vez generada la contraseña se cifran los datos, los datos cifrados sólo pueden ser devueltos a su estado original aplicando la operación inversa del algoritmo con la misma clave simétrica.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static byte[] encrypt(SecretKey key, String text) throws Exception {
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(text.getBytes());
    }

    ...
}

Main-5.java

Descifrar datos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static byte[] decrypt(SecretKey key, byte[] encrypted) throws Exception {
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(encrypted);
    }

    ...
}

Main-6.java

Cifrar flujos de datos y archivos

A veces se desea cifrar un flujo de datos de tamaño no conocido de antemano, las clases CipherIntputStream e CipherOutputStream permite realizar el cifrado a un flujo de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static byte[] encrypt(SecretKey key, InputStream stream) throws Exception {
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        CipherInputStream cipherInputStream = new CipherInputStream(stream, cipher);
        return cipherInputStream.readAllBytes();
    }
    
    ...
}

Main-7.java

Calcular um hash HMAC

Los algoritmos criptográficos de hashing proporcionan una huella digital de los datos que tienen ciertas propiedades de seguridad. Estos utilizan como entrada los datos y un algoritmo de hashing.

Otra utilidad utilizada en el cifrado es un algoritmo HMAC que calcula un hash también a partir de los datos y el algoritmo de hashing y adicionalmente una clave.

En un algoritmo de hashing criptográfico el hash variará únicamente a partir de los datos de entrada. En un algoritmo de HMAC el hash varía en función de los datos y la clave secreta. Utilizando los mismos datos pero diferente clave el hash obtenido es diferente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    public static String calculateHmac(SecretKey key, String text) throws Exception {
        Mac mac = Mac.getInstance("HMACSHA256");
        mac.init(key);
        byte[] bytes = mac.doFinal(text.getBytes());
        return HexFormat.of().formatHex(bytes);
    }
}

Main-8.java
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: