¿Qué es la función de hashing?
Una función de hashing es una función unidireccional que toma alguna entrada y devuelve una salida determinante. La salida se denomina a menudo un resumen, un código hash o simplemente un hash.
Las funciones de Hashing tienen muchos usos en el software. En esencia, son una herramienta para tomar entradas de longitudes arbitrarias y producir una salida de longitud fija. Se utilizan en las tablas hash para proporcionar una búsqueda constante de tiempo. En el almacenamiento en caché, a menudo se utilizan para detectar cambios en los datos. En criptografía estamos más interesados en algunas propiedades:
- Determinante – la misma entrada siempre devuelve la misma salida. Esto es importante porque necesitamos saber que podemos confiar en la salida de la función.
- Rápido – pueden ser computados con relativa rapidez (aunque, en muchas aplicaciones, como el hash de las contraseñas, la lentitud puede ser un rasgo deseable).
- Unidireccional – es inviable reproducir la entrada dada la salida. Esto mantiene la privacidad de la entrada original, haciendo imposible recuperar, por ejemplo, la clave privada que se pasó a la función.
- Caótico – un pequeño cambio en la entrada producirá una salida tan diferente que es imposible correlacionar las dos mirando sólo las salidas. Esto hace difícil – si no imposible – hacer deducciones sobre la entrada en base a una serie de salidas.
- Único – es inviable encontrar dos entradas que produzcan la misma salida.
¿Por qué necesito uno?
En criptografía, las funciones de hash se utilizan a menudo como código de autenticación de mensajes (MAC). Un MAC se utiliza para comprobar la autenticidad del mensaje (es decir, de dónde procede el mensaje). Los MAC producidos por una función de hashing se suelen denominar HMAC.
Veamos un ejemplo usando un MAC. Enviaremos el siguiente mensaje.
{ "mensaje": "Dale 10 dólares a Bob" }
Si un atacante interceptara el mensaje, podrían cambiarlo en vuelo.
{ "mensaje": "Dale 1000 dólares a Bob" }// o{ "mensaje": "Dale 10 dólares a Eve" }
Para prevenir esto podemos intentar añadir un MAC que calculamos usando alguna función de hashing.
mac(mensaje) = hash(mensaje)
Tomamos la salida de la función MAC y la añadimos al mensaje.
{ "mensaje": "Dale 10 dólares a Bob", mac: "12345" }
El receptor puede calcular el MAC para el mensaje dado usando la misma función de hashing, y confirmar que coincide con lo enviado. Pero ahora, el atacante sólo tiene que modificar el MAC y el mensaje parecerá legítimo.
{ "mensaje": "Dale 10 dólares a Eve", mac: "6789" }
Para evitarlo, podemos añadir un secreto cuando calculamos el MAC. La adición de un secreto compartido hace de esta función un HMAC.
hmac(secreto, mensaje) = hash(secreto + mensaje)
El remitente hará un hash del mensaje junto con el secreto para producir un HMAC, y luego el receptor verificará que puede calcular el mismo HMAC usando la misma clave cuando reciba el mensaje.
// secreto = "cachorros"{ "mensaje": "Dale 10 dólares a Bob", hmac: "28573" }
A menos que el atacante conozca el secreto, será increíblemente difícil conseguir un mensaje que coincida con el MAC dado (ver propiedad 5).
// secreto = ??? - Idk, intentemos con la contraseña: "Give $100 to Eve", hmac: "56184" }
Esta práctica de autentificar un mensaje mediante un HMAC se utiliza en lugares donde es difícil determinar si una solicitud entrante debe ser confiable. En particular, OAuth 1 utiliza un algoritmo de hash y claves precompartidas para generar un hash en cada petición.
Tenga en cuenta que, si bien esto puede parecer un simple esquema de seguridad para implementar, hay múltiples formas sutiles en las que se pueden explotar las implementaciones ingenuas. Siempre use código criptográfico que haya sido construido y revisado por expertos en seguridad.
Las bibliotecas estándar de muchos idiomas contienen implementaciones de algoritmos HMAC. Por ejemplo, el marco de trabajo .Net tiene una clase llamada HMACSHA256 en el espacio de nombres System.Security.Cryptography. Este algoritmo utiliza el algoritmo SHA-2 256 para calcular un HMAC.
Otro uso de los algoritmos hash es en la verificación de contraseñas, pero eso es otra entrada de blog.
¿Cómo funcionan?
Imaginemos una función de hashing simplista.
LENGTH_HASH(x) = LENGTH(x)
Es decir, dada alguna entrada x, vamos a devolver la longitud de la entrada en bytes. Consideremos la propiedad 5: singularidad. Con esta simple función de hashing, muchas entradas producen la misma salida.
LENGTH_HASH($0027cachorros$0027) = 7LENGTH_HASH($0027gatitos$0027) = 7
Dependiendo de nuestro caso de uso, esto podría ser suficiente, pero no es realmente una función de hash. Consideremos algo un poco más complejo.
SUM_HASH(x) = LET suma = 0 PARA c EN x suma += c DEVOLVER suma
Digamos que nuestra entrada está restringida a caracteres del alfabeto en mayúsculas, con caracteres a los que se les asigna valores que comienzan en 1 (es decir, A=1, B=2, …). Usando SUM_HASH obtenemos una salida más variada que la que obtuvimos anteriormente.
SUM_HASH($0027ABC$0027) = 1 + 2 + 3 = 6SUM_HASH($0027BCD$0027) = 2 + 3 + 4 = 9
Hemos mejorado un poco nuestra producción. ¿Pero qué pasa si transponemos algunos de los personajes?
SUM_HASH($0027ABC$0027) = 6SUM_HASH($0027CBA$0027) = 6
Oops, tenemos colisiones de nuevo. Recuerden, vamos por una salida única, así que esta función de hashing ligeramente menos ingenua tampoco va a cortarla.
También queremos dificultar la determinación de la entrada dada la salida. Aunque no es trivial, podemos deducir mucho sobre la entrada dada su suma. Si suponemos que todas las letras tienen la misma probabilidad, entonces esperamos que el valor del carácter medio sea 13 (mediana del rango 1-26). Si tenemos un valor alrededor de 1300, podemos deducir que la entrada original tenía unos 100 caracteres de longitud. Eso podría no parecer mucha información si lo haces a mano, pero si puedes priorizar qué cadenas va a probar primero tu algoritmo de craqueo en función de la longitud, podrías ahorrar mucho tiempo.
Esta solución tampoco es muy caótica. Si cambio mi entrada de ABC a ABD, la entrada cambia de 6 a 7. Si hago un hash de ABE, obtengo 8. Estoy viendo una tendencia aquí…
Así que en resumen, nuestro algoritmo SUM_HASH obtiene un 2⁄5. Es determinístico, y rápido. Desafortunadamente, las tres propiedades restantes son tan importantes que no podemos usarlo para la criptografía.
Claramente, esta es un área en la que es mejor apoyarse en los expertos. El diseño de una función de hashing criptográfica es un proceso difícil que involucra a las matemáticas mucho más allá de las habilidades del ingeniero de software promedio. Les recuerdo de nuevo, siempre usen código criptográfico que haya sido construido y revisado por expertos en seguridad.
¿Qué algoritmo debería usar?
En caso de duda, ve a la familia de algoritmos hashing SHA-2. SHA-2 viene en seis variantes diferentes, pero la elección de una se reduce a la duración del hachís. El SHA-2 256 produce un resumen de 256 bits, mientras que el SHA-2 512 produce – lo adivinaron – un resumen de 512 bits. También hay variantes de 224 y 384 bits. El SHA-2 tiene la ventaja de haber existido durante un tiempo (publicado por primera vez en 2001), por lo que hay un amplio apoyo para él, y esa edad implica un cierto nivel de endurecimiento en la batalla.
SHA-3 es el algoritmo de hashing que está en alza. Se publicó en 2015, así que no tiene un apoyo tan amplio. Sin embargo, el SHA-3 es un estándar del NIST, por lo que sus características de seguridad son bien conocidas y deberían ser seguras para su uso en la producción.
Debe evitar el uso de MD5 y SHA-1 para usos criptográficos. Estos algoritmos tienen vulnerabilidades bien conocidas que los descalifican para su uso en escenarios seguros.
Categorías: technicalTags: security, getting started