Todo aquel que ocupe utilizar contraseñas de usuarios para propósitos de autenticación requiere almacenarlas en alguna parte. Normalmente, esto es en una base de datos como MySQL, Postgres o MongoDB. Sin embargo, la historia nos ha mostrado lo común que es llegar a ser vícitmas de ataques cibernéticos que culminan en nuestros preciados datos siendo expuestos a atacantes o al mundo entero.

En un principio era normal almacenar contraseñas en texto plano. Después de ser claro que esto ocasionaba un serio problema de privacidad, se comenzaron usar algoritmos de reducción criptográfica (el nombre español de Hashing Functions) las cuales creaban una firma de longitud fija para representar la contraseña y de esa forma no tener que almacenarla en texto plano.

Función Hash ( Contraseña ) => Valor Hash

Entre los requerimientos esenciales que se esperan de una función Hash se encuentran:

  • Velocidad de reducción
  • Evitar colisiones; Por ningún motivo dos o más passwords diferentes deben de tener el mismo valor Hash

Conforme fué avanzando el uso de tecnología y el escudriño de la comunidad, fue siendo aparente que el uso de algoritmos de Hashing tenían propiedades que resultaban vulnerables ante ciertos ataques.

La mayoría de éstos Hashes Criptográficos como MD5, la familia SHA-1/2/3 entre otros son extremadamente rápidos, por lo que un equipo personal puede calcular desde millones a billones de hashes por segundo dependiendo las especificaciones de CPU/GPU entre otras cosas.

Muchos desarrolladores utilizan funciones Hash como SHA256 o SHA512 combinado con Salts para combatir ataques como Rainbow Tables los cuales hacen un trade off de almacenamiento por tiempo.

Una mejor alternativa...

Sin embargo, las llamadas KDFs (Key Derivation Functions o Funciones de Derivación de Claves) son mejores alternativas para almacenar contraseñas ya que no solo cumplen con los requerimientos de los algoritmos de Hash Criptográficos, sino que agregan una propiedad vital a la ecuación: Factor de Lentitud.

Esta nueva propiedad no hace imposible un ataque de fuerza bruta o de diccionario, sino mas bien lo hace impráctico debido al tiempo que le toma a cada KDF generar un resultado.

Aquí podemos ver un ejemplo usando el módulo timeit de Python donde podemos apreciar la diferencia entre usar una función de Hash Criptográfico vs una Función de Derivación de Claves (Bcrypt).

# Usando SHA512
>>> NUMERO_DE_ITERACIONES = 1000000
>>> timeit("hashlib.sha512('My awesome password')",
        setup="import hashlib",
        number=NUMERO_DE_ITERACIONES)
0.7003529071807861

# Usando SHA512 + Salt aleatorio de 32 bytes
>>> timeit("hashlib.sha512('My awesome password' + os.urandom(32))", 
        setup="import hashlib, os",
        number=NUMERO_DE_ITERACIONES)
4.473568916320801

Eso es lo que tomó generar 1 millón de operaciones usando SHA512 y SHA512 + Salt en Python.

Ahora veamos lo que toma utilizando Bcrypt, un KDF que tiene casi los 20 años desde que se lanzó y sigue vigente al día de hoy (2018/08/09).

>>> NUMERO_DE_ITERACIONES = 3
>>> timeit("bcrypt.hashpw('My awesome password', bcrypt.gensalt())",
        setup="import bcrypt",
        number=NUMERO_DE_ITERACIONES)
0.8256781101226807

>>> NUMERO_DE_ITERACIONES = 17
>>> timeit("bcrypt.hashpw('My awesome password', bcrypt.gensalt())",
        setup="import bcrypt",
        number=NUMERO_DE_ITERACIONES)
4.739080905914307
Algoritmo Iteraciones Segundos
SHA512 1,000,000 0.7003529071807861
Bcrypt 3 0.8256781101226807
SHA512 + 32-byte Salt 1,000,000 4.473568916320801
Bcrypt 17 4.739080905914307

Un hash de Bcrypt es claramente más costoso de generar, lo cual para fines de seguridad, es excelente.

Bcrypt tiene mas de 20 años... ¿Existe algo más reciente?

Argon2 es una Función de Derivación de Clave (Key Derivation Function) ganadora de la última Password Hashing Competition efectuado entre el 2013 y el 2015. Esto le da el privilegio de pertenecer a un selecto grupo de herramientas como Scrypt, Bcrypt y PBKDF2 para el almacenamiento seguro de contraseñas.

Este es un extracto del Password Hashing Competition:

"PHC ran from 2013 to 2015 as an open competition—the same kind of process as NIST's AES and SHA-3 competitions, and the most effective way to develop a crypto standard. We received 24 candidates, including many excellent designs, and selected one winner, Argon2, an algorithm designed by Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich from University of Luxembourg."

Espera un momento...

>>> NUMERO_DE_ITERACIONES = 7000
>>> timeit("argon2.PasswordHasher().hash('My awesome password')",
        setup="import argon2",
        number=NUMERO_DE_ITERACIONES)
5.41093897819519

waitasec

Mmmmm ¿7,000 operaciones en 5.4 segundos?.

bcrypt toma considerablemente mas tiempo en generar que argon2, ¿Cómo es posible que argon2 sea considerado mejor?.

Esa es una excelente pregunta. Lo cual se responde en el siguiente texto:

"...(Argon2) has a simple design aimed at the highest memory filling rate and effective use of multiple computing units, while still providing defense against tradeoff attacks (by exploiting the cache and memory organization of the recent processors).
We recommend Argon2 for the applications that aim for high performance. Both versions of Argon2 allow to fill 1GB of RAM in a fraction of a second, and smaller amounts even faster. It scales easily to the arbitrary number of parallel computing of units."

En pocas palabras, Argon2 no limita su costo meramente en el uso de CPU, el cual puede ser combatido mediante el uso de GPUs modernos. Argon2 junto con otros KDFs como Scrypt optan por un enfoque no solo de computo sino de uso de memoria, el cual ocasiona un problema para los atacantes que usan GPU en sus equipos de crackeo.

Pueden leer mas acerca de ésto en el paper oficial https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf

Implementando Argon2 en Python 3

La librería argon2_cffi ofrece una API bastante sencilla además de tener muy buena documentación al respecto.

Para instalar solo ocupamos usar easy_install, pip o pipenv.

$ pip install argon2_cffi

A continuación pueden ver una implementación bastante sencilla utilizando esta librería.

import argon2

ph = argon2.PasswordHasher()

password = "My secure password!"
hashed = ph.hash(password)

try:
  ph.verify(hashed, password)
except argon2.exceptions.VerifyMismatchError:
  print("La verificación con el password `{}` ha fallado".format(password))

Implementando Argon2 en JavaScript

https://github.com/ranisalt/node-argon2

Para instalar usamos npm.

$ npm install argon2
 const argon2 = require('argon2');

// Helper function para verificar el password en cuestión
function verifyHash(hash, password) {
    argon2.verify(hash, password).then(verified => {
        if (verified) {
            console.log(`✔ '${password}' es el valor que buscamos!`);
        } else {
            console.log(`✘ '${password}' no es el password correcto!`);
        }
    }).catch(err => console.log(`Error en la verificación: (${err})`));
}
 
// Aquí creamos el Hash de un String
argon2.hash("foobar123").then(hash => {
    console.log(`Argon2 Hash: '${hash}'`);
    // Vamos a intentar verificarlo con el password incorrecto
    verifyHash(hash, "nofoobar");

    // Ahora con el password original
    verifyHash(hash, "foobar123");

}).catch(err => {console.log(`Un error interno ha ocurrido (${err})`)});

En resumen

Cuando ocupes almacenar contraseñas, opta por un KDF como argon2 para protegerlas en caso de exfiltración de tus datos. En caso de no disponer de una librería en tu lenguaje o plataforma, aun es válido utilizar otros KDFs como Bcrypt, Scrypt o PBKDF2 con un factor de operación razonable.

Para más información acerca de este tema