MMORPG Safe: MD5 y HMAC (1/2)

Juan Mellado, 9 Mayo, 2008 - 16:11

Hace unos cuantos posts comenté la necesidad de guardar en la base de datos la clave del usuario codificada utilizando algún sistema de encriptación. Para romper un poco la monotonía de tanta teoría acerca del diseño de estas últimas semanas decidí intentar escribir mi propia clase para realizar la encriptación. No obstante, habida cuenta de que mi única experiencia práctica previa al respecto había sido el uso de la función MD5 que implementa PHP, casi me decantaba a priori por utilizar alguna librería gratuita disponible en C++, el lenguaje con el que voy a trabajar. Sin embargo, buscando información por Internet me he llevado la grata sorpresa de comprobar que los sistemas más populares de encriptación en realidad se basan en algoritmos que resultan sencillos de implementar.

El algoritmo del MD5 (Message-Digest algorithm 5) por ejemplo, descrito en el RFC 1321, consiste en la ejecución de cinco simples pasos. Admite como entrada un bloque de información a codificar de cualquier longitud dada, y devuelve un código hash de 128 bits (16 bytes) que normalmente suele representarse en hexadecimal como una cadena de texto legible de 32 bytes. Su uso más común, además de la codificación de claves de usuario, ha sido tradicionalmente la firma de aplicaciones, es decir, proporcionar un código único (el hash) que garantice que un fichero es realmente el fichero que dice ser. Desgraciadamente este último uso ha dejado de ser práctico hoy en día, ya que existen métodos que añadiendo bytes extras a un archivo cualquiera consiguen que genere un código hash concreto. Al parecer el algoritmo es susceptible a las colisiones y han conseguido sacar partido de esa debilidad. No obstante, aún sigue siendo una alternativa válida para la codificación de claves.

Los cinco pasos del algoritmo son los siguientes:

1) Añadir bits de relleno al bloque de información a codificar. Con este paso se busca hacer la longitud del bloque en bits (no bytes) congruente con 448 módulo 512. Es decir, que le falten 64 bits para ser múltiplo exacto de 512 bits (448 = 512 – 64). Esto debe hacerse siempre, incluso aunque la longitud inicial ya cumpla esta condición. Para extender el bloque se añadirá primero un bit a 1, y luego el resto a 0 hasta completar el tamaño que sea necesario.

Pensando en bytes, que es como normalmente se trabaja, lo que pide este paso es que el tamaño del bloque sea congruente con 56 módulo 64. Es decir, que le falten 8 bytes para ser múltiplo entero de 64 bytes.

El hecho de que este paso tenga que aplicarse siempre es lo que hace que incluso las cadenas vacías tengan un hash.

2) Añadir la longitud del bloque de información a codificar al propio bloque de información. En este paso lo que se hace es añadir la longitud inicial del bloque en bits (no bytes) al final del resultado del paso anterior. La longitud se agrega en formato little-endian (primero los bytes menos significativos) con un tamaño máximo de 8 bytes. Si la longitud ocupa más de 8 bytes entonces los más significativos simplemente se pierden sin que ello suponga ningún problema para el algoritmo.

En este paso, al añadir los 8 bytes que se reservaron en el paso anterior, se consigue que el bloque sea múltiplo entero de 64 bytes.

3) Inicializar las variables intermedias de trabajo. Para el cálculo del hash se utilizan cuatro variables intermedias enteras de 32 bits denotadas como A, B, C y D. Cada una de ellas toma un valor inicial constante predefinido:

A = 0x67452301
B = 0xefcdab89
C = 0x98badcfe
D = 0x10325476

4) Procesar la información en bloques de 16 enteros de 32 bits. A resultas de los dos primeros pasos, el bloque de información acaba teniendo una longitud que es un múltiplo entero de 512 bits (64 bytes), por lo que se puede trabajar con él tomando bloques de 16 palabras enteras sin signo de 32 bits (4 bytes) cada una (4 * 16 = 64).

En este punto el algoritmo define cuatro funciones denotadas como F, G, H e I, que admiten tres enteros sin signo de 32 bits y generan otro entero sin signo de 32 bits:

F(x, y, z) = (x AND y) OR ( (NOT x) AND z )
G(x, y, z) = (x AND z) OR ( y AND (NOT z) )
H(x, y, z) = x XOR y XOR z
I(x, y, z) = y XOR ( x OR (NOT z) )

El propósito de estas funciones es extraer las características diferenciales del bloque de información, operando a nivel de bit, con el objetivo de generar el hash que lo identifique de forma unívoca. El propósito concreto de cada una, así como la base matemática en la que se sustenta todo el proceso queda fuera de mi comprensión.

Las funciones se aplican dentro de un bucle en el que se van leyendo enteros sin signo de 32 bits del bloque, en tandas de 16 en 16, con el propósito de generar unos nuevos valores que se irán sumando a las variables A, B, C y D. Para preservar el valor original de las variables se guardan en una variables auxiliares al principio de cada iteración:

AA = A
BB = B
CC = C
DD = D

Llegado este punto el algoritmo presenta cierta dificultad por la forma en que está descrito, agravado por el hecho de que el propio RFC contiene una errata (hay una "t" en lugar de una "i" en un par de líneas). Después de leerlo un par de veces, se entiende que hay que realizar 64 operaciones dentro del bucle en cada iteración. Aplicando la función F en las 16 primeras operaciones, la G en las 16 segundas, la H en las terceras, y la I en las cuartas.

En el documento se utiliza una tupla en la forma [abcd k s i] para señalar las 16 operaciones a realizar con cada función (de izquierda a derecha, y de arriba hacia abajo):

[ABCD  0  7  1] [DABC  1 12  2] [CDAB  2 17  3] [BCDA  3 22  4]
[ABCD  4  7  5] [DABC  5 12  6] [CDAB  6 17  7] [BCDA  7 22  8]
[ABCD  8  7  9] [DABC  9 12 10] [CDAB 10 17 11] [BCDA 11 22 12]
[ABCD 12  7 13] [DABC 13 12 14] [CDAB 14 17 15] [BCDA 15 22 16]
[ABCD  1  5 17] [DABC  6  9 18] [CDAB 11 14 19] [BCDA  0 20 20]
[ABCD  5  5 21] [DABC 10  9 22] [CDAB 15 14 23] [BCDA  4 20 24]
[ABCD  9  5 25] [DABC 14  9 26] [CDAB  3 14 27] [BCDA  8 20 28]
[ABCD 13  5 29] [DABC  2  9 30] [CDAB  7 14 31] [BCDA 12 20 32]
[ABCD  5  4 33] [DABC  8 11 34] [CDAB 11 16 35] [BCDA 14 23 36]
[ABCD  1  4 37] [DABC  4 11 38] [CDAB  7 16 39] [BCDA 10 23 40]
[ABCD 13  4 41] [DABC  0 11 42] [CDAB  3 16 43] [BCDA  6 23 44]
[ABCD  9  4 45] [DABC 12 11 46] [CDAB 15 16 47] [BCDA  2 23 48]
[ABCD  0  6 49] [DABC  7 10 50] [CDAB 14 15 51] [BCDA  5 21 52]
[ABCD 12  6 53] [DABC  3 10 54] [CDAB 10 15 55] [BCDA  1 21 56]
[ABCD  8  6 57] [DABC 15 10 58] [CDAB  6 15 59] [BCDA 13 21 60]
[ABCD  4  6 61] [DABC 11 10 62] [CDAB  2 15 63] [BCDA  9 21 64]

La forma en la que ha de interpretarse las tuplas es la siguiente:

a = b + ((a + FUNCTION(b,c,d) + BUFFER[k] + SIN[i]) <<< s)

Para las primeras 16 operaciones FUNCTION sería F, para las 16 segundas G, y así sucesivamente. BUFFER[k] es simplemente el elemento k-ésimo dentro del bloque de 16 enteros sin signo que se esté procesando en ese momento dentro del bucle. SIN[i] es el valor absoluto de la parte entera del seno (la conocida función trigonométrica) de i (en radianes) multiplicado por 4.294.967.296 (2 elevado a 32). Este término mosquea un poco al principio, pero puede encontrarse los valores precalculados en un apéndice del propio RFC, que incluye un implementación completa en C. Por último, "<<<" es una rotación circular hacia la izquierda (lo que sale por la izquierda vuelve a entrar por la derecha) de tantos bits como indique "s".

Es decir, expandiendo las tuplas se obtiene:

A = B + ((A + F(B,C,D) + BUFFER[0] + 0xd76aa478) <<< 7)
D = A + ((D + F(A,B,C) + BUFFER[1] + 0xe8c7b756) <<< 12)
C = D + ((C + F(D,A,B) + BUFFER[2] + 0x242070db) <<< 17)
...

Finalmente se suman los valores obtenidos con los previos salvaguardados y se procede con la siguiente iteración:

A = AA + A
B = BB + B
C = CC + C
D = DD + D

5) Componer el resultado final. El código hash resultante es la concatenación de A, B, C y D, empezando con el byte menos significativo de A y terminando con el más significativo de D.

¿No encontró lo que buscaba?

Utilice el buscador para encontrar más páginas en esta web o en toda Internet.
 
Web www.inmensia.com