Los algoritmos de hashing criptográficos, cálculo de hashes con comandos de GNU/Linux y Java

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Los algoritmos de hashing criptográficos son fundamentales en la firma digital y criptografía, pero también tienen su utilidad por sí mismos para la comprobación de la integridad. Se basan en un algoritmo y funciones matemáticas que transforman un conjunto de bytes en un número binario de longitud fija que constituye el hash digital del contenido. Hay varios algoritmos de hashing criptográficos y en GNU/Linux varios comandos que permiten calcular y comprobar el hash de un archivo. En los lenguajes de programación como en el caso de Java se ofrecen clases y métodos para la generación y cálculo de hashes en los algoritmos soportados.

Java

El cifrado o encriptación permite que solo el receptor del mensaje pueda leer los datos, la firma digital permite conocer que los datos han sido generados por quien los firma y al recibirlos no han sido modificados por una tercera persona. El cifrado y la firma digital utilizan claves para realizar el cifrado y para la firma digital.

El cifrado y la firma digital son operaciones muy importantes para garantizar la seguridad en multitud de ámbitos. Sin embargo, la utilización de claves añaden algo de complejidad que a veces no es necesaria, a veces utilizar simplemente un algoritmo de hashing es suficiente y más sencillo ya que no requiere la utilización de claves.

Los algoritmos de hashing criptográficos

Los algoritmos de hashing transforman un conjunto arbitrario de bytes a una cadena binaria de bytes de longitud fija, dependiendo del algoritmo la longitud fija resultante es diferente, algunos generan como resultado un número de 160 bits y los más seguros llegando a 512 bits.

Una aplicación práctica de los algoritmos de hashing es para evitar guardar las contraseñas de los usuarios en la base de datos en texto plano en la que interviene un algoritmo de hashing, utilizando la técnica conocida como salted password hashing.

Aunque las redes actuales ofrecen un gran ancho de banda y una fiabilidad notable siguen considerándose un medio poco fiable, a veces fallan y generan errores en la transmisión como pérdida de paquetes o alteración del contenido aún con las medida implementadas en el algoritmo de transmisión de paquetes TCP utilizado de forma mayoritaria en internet.

Al descargar un archivo grande es útil que el creador adjunte el hash del contenido para comprobar su integridad una vez descargado. Comprobar la integridad proporciona dos garantías: que el contenido es el original y no ha sido modificado por un intermediario y que el archivo descargado no se ha corrompido en la descarga.

La seguridad de los algoritmos de cifrado se basan en que sea muy difícil encontrar la cadena original que produzca un determinado hash, que dada una cadena sea difícil encontrar otra cadena que produzca el mismo hash y que sea difícil que dos cadenas distintas produzcan el mismo hash. Otra de sus propiedades es que aún con el cambio de un bit en el contenido hasheado el hash resultante es muy diferente. También por seguridad son algoritmos que distribuyen de forma uniforme los hashes en todo el rango de posibles resultados.

Hay diferentes algoritmos de hashing criptográfico:

  • MD5: genera números de 160 bits de longitud. Ya se considera un algoritmo de hashing poco seguro ya que con la capacidad de computación actual es posible encontrar un cadena de bytes que produzca el mismo hash.
  • SHA-1: genera números de 160 bits de longitud. También se considera un algoritmo de hashing vulnerable pudiendo encontrar colisiones, encontrar un contenido que genere un determinado hash.
  • SHA-2: es la segunda versión de SHA utilizando algoritmos y funciones criptográficas más seguras. También se conoce como SHA-224, SHA-256, SHA-384 o SHA-512 donde el número indica el número de bits de hash de resultado.
  • SHA-3: es la última adición a los algoritmos SHA pero que es diferente de forma significativa en su estructuras internas a MD5, SHA-1 y SHA-2. También soporta diferentes longitudes de hash denominándose SHA3-224, SHA3-256, SHA3-384 o SHA3-512.

Comandos de GNU/Linux para el cálculo de hashes

El sistema operativo GNU/Linux ofrece varios comandos, uno para cada algoritmo, para utilizar y calcular los hashes de una cadena de texto o un archivo. Basta indicar el algoritmo y el contenido del que calcular su hash. En los siguientes comandos se calcula el hash de la siguiente supuesta contraseña generada con el gestor de contraseñas KeePassXC.

El resultado de los algoritmos de hashing es un número de un determinado número de bits, los hashes se representan como un número en base 16, lo que para un número de 512 bits da una cadena de 128 números hexadecimales.

1
2
3
4
5
6
7
8
9
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | md5sum -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha1sum -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha256sum -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha512sum -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha3-256sum -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha3-512sum -

$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | shasum -a 512 -
$ echo -n "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT" | sha3sum -a 512 -
command-hash.sh
1
2
3
4
5
6
7
8
9
d46c344b3b827a7bbb7bde1c9d8243d7  -
3a678ee72466ff435593ccbfb023d237aef5d65b  -
314682878adeb719b0a33100bc44603f9d2d468f4f7a699d312c141cf0675c80  -
b96f93dde9d05f788ed6298eee5ddc90e0633ec2ebaae4f3c14f09dd0ca630bb99cd4b19e4a4c86400a04871a28e11df4e06502233be2878a997959de6a1a40b  -
a6dd40b8235861ba55dd41f15829245f1ec0e394af6ceb8de260aca93dc0b3d7  -
055daf0be26ba0e6d521eaf2ed0b54459da83e295287b09b30d835d6b55bda6ca9088756d83e6c51185e993f1465c2e65d48adeaf98e48c10147458965d0d4cd  -

b96f93dde9d05f788ed6298eee5ddc90e0633ec2ebaae4f3c14f09dd0ca630bb99cd4b19e4a4c86400a04871a28e11df4e06502233be2878a997959de6a1a40b  -
055daf0be26ba0e6d521eaf2ed0b54459da83e295287b09b30d835d6b55bda6ca9088756d83e6c51185e993f1465c2e65d48adeaf98e48c10147458965d0d4cd  -
command-hash.out

El paquete sha3sum hay que instalarlo manualmente si no está instalado, los otros comandos generalmente están instalados por ser dependencia de otro.

Usar algoritmos de hashing con Java

Desde un lenguaje de programación de alto nivel como Java también puede ser útil utilizar algoritmos de hashing. Java incorpora una API e implementa de forma eficiente los estándares de hashing criptográficos para ser utilizados de forma fácil por los programas.

Listar algoritmos de hashing soportados por el JDK

Los algoritmos de hashing soportados dependen de la versión del JDK, en el JDK 9 se añadió el soporte para SHA-3. Es posible listar qué algoritmos de hashing soporta el JDK con el que se ejecuta un programa con el siguiente código a través de la clase Security.

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

...

public class Main {

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

        ...
    }

    private static void hashing() throws Exception {
        Set<String> messageDigest = Security.getAlgorithms("MessageDigest");
        System.out.println("Supported algorithms: " + messageDigest.stream().sorted().collect(Collectors.joining(",")));

        ...
    }

    ...
}

Main-1.java
1
2
Supported algorithms: MD2,MD5,SHA-1,SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256,SHA3-224,SHA3-256,SHA3-384,SHA3-512

Main-1.out

Estos son los algoritmos de hashing soportados por el JDK 17.

1
2
MD2,MD5,SHA-1,SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256,SHA3-224,SHA3-256,SHA3-384,SHA3-512

java-17-hash-algorithms.out

Por el JDK 11.

1
2
MD2,MD5,SHA,SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256,SHA3-224,SHA3-256,SHA3-384,SHA3-512

java-11-hash-algorithms.out

Y por el JDK 8.

1
2
MD2,MD5,SHA,SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256

java-8-hash-algorithms.out

Calcular hashes con las clases del JDK de Java

La clase principal para calcular hashes criptográficos en Java es MessageDigest, se obtiene una instancia de la clase a partir del algoritmo que se desea utilizar para calcular el hash. La clase MessageDigest ofrece los métodos update para proporcionar los bytes del contenido sobre él que calcular el hash y para terminar el cálculo se utiliza el método digest.

 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 hashing() throws Exception {
        ...

        String password = "rw@wbnaq2R@DS#u3o7hxWckqhfkzbT";

        System.out.println("Plain text: " + password);
        System.out.println("MD5: " + calculateHash("MD5", password));
        System.out.println("SHA-1: " + calculateHash("SHA-1", password));
        System.out.println("SHA-256: " + calculateHash("SHA-256", password));
        System.out.println("SHA-512: " + calculateHash("SHA-512", password));
        System.out.println("SHA3-256: " + calculateHash("SHA3-256", password));
        System.out.println("SHA3-512: " + calculateHash("SHA3-512", password));

        ...
    }
    
    private static String calculateHash(String algorithm, String content) throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        messageDigest.update(content.getBytes(StandardCharsets.UTF_8));
        byte[] hash = messageDigest.digest();
        return HexFormat.of().formatHex(hash);
    }

    ...
}

Main-2.java
1
2
3
4
5
6
7
Plain text: rw@wbnaq2R@DS#u3o7hxWckqhfkzbT
MD5: d46c344b3b827a7bbb7bde1c9d8243d7
SHA-1: 3a678ee72466ff435593ccbfb023d237aef5d65b
SHA-256: 314682878adeb719b0a33100bc44603f9d2d468f4f7a699d312c141cf0675c80
SHA-512: b96f93dde9d05f788ed6298eee5ddc90e0633ec2ebaae4f3c14f09dd0ca630bb99cd4b19e4a4c86400a04871a28e11df4e06502233be2878a997959de6a1a40b
SHA3-256: a6dd40b8235861ba55dd41f15829245f1ec0e394af6ceb8de260aca93dc0b3d7
SHA3-512: 055daf0be26ba0e6d521eaf2ed0b54459da83e295287b09b30d835d6b55bda6ca9088756d83e6c51185e993f1465c2e65d48adeaf98e48c10147458965d0d4cd
Main-2.out

El resultado es un array de bytes que hay que codificar en hexadecimal. En el JDK 17 se ha añadido la clase HexFormat que permite hacer la conversión fácilmente sin dependencias adicionales.

Si se desea calcular el hash de un flujo de InputStream u OutputStream la API de Java ofrece las clases DigestInputStream y DigestOuputStream. En este ejemplo se calcula el SHA de la imagen del medio de instalación de la distribución Arch Linux del que su hash se publica en la misma ubicación que la imagen ISO y en la página de descargas de Arch Linux.

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package io.github.picodotdev.blogbitix.javahashingencrypt;

...

public class Main {

    ...

    private static void hashing() throws Exception {
        ...

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://mirror.rackspace.com/archlinux/iso/latest/archlinux-bootstrap-x86_64.tar.gz"))
                .timeout(Duration.ofMinutes(2))
                .header("Content-Type", "application/json")
                .GET()
                .build();
        HttpResponse<InputStream> response = HttpClient.newBuilder().build().send(request, HttpResponse.BodyHandlers.ofInputStream());
        HttpRequest shasumRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://mirror.rackspace.com/archlinux/iso/latest/sha256sums.txt"))
                .timeout(Duration.ofMinutes(2))
                .header("Content-Type", "application/json")
                .GET()
                .build();
        HttpResponse<String> shasumResponse = HttpClient.newBuilder().build().send(shasumRequest, HttpResponse.BodyHandlers.ofString());

        String shasum = shasumResponse.body().lines().skip(3).findFirst().get().split(" ")[0];
        System.out.println("Arch Linux ISO (SHA-256): " + shasum);

        String shasumCalculated = calculateHash("SHA-256", response.body());
        System.out.println("SHA-256: " + shasumCalculated);
        System.out.println("sha256sum matches: " + shasum.equals(shasumCalculated));
    }

    ...

    private static String calculateHash(String algorithm, InputStream stream) throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        try (
            InputStream is = new BufferedInputStream(stream, 10 * 1024 * 1024);
            DigestInputStream digestInputStream = new DigestInputStream(is, messageDigest)
        ) {
            digestInputStream.transferTo(OutputStream.nullOutputStream());
            byte[] hash = messageDigest.digest();
            return HexFormat.of().formatHex(hash);
        }
    }

    ...
}

Main-3.java

Este es el hash calculado por con Java.

1
2
3
Arch Linux ISO (SHA-256): 68ec9fd67fba8e1cda83ef757c61a4b85613d3f7855681760dca604561148681
SHA-256: 68ec9fd67fba8e1cda83ef757c61a4b85613d3f7855681760dca604561148681
sha256sum matches: true
Main-3.out

Calcular hashes con una librería de terceros

En caso de estar obligado a utilizar un JDK antiguo que no soporte alguno de los algoritmos de hashing hay que recurrir a una librería de terceros. Hay varias, entre ellas Bouncy Castle, Guava y Apache Commons.

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: