Hashing y salado de contraseñas


Qué hay en este artículo ⌄
  • Cómo implementar hashing y aplicar sal a contraseñas
  • Mejores prácticas para el almacenamiento seguro de contraseñas
  • Por qué usar sal en el hashing de contraseñas
  • Cómo prevenir ataques con tablas arcoíris
  • Qué es el salado de contraseñas
  • Cómo mejorar la seguridad de contraseñas con pepper
  • Qué son sal y pepper en la protección de contraseñas

Hashing simple

Al gestionar PINs o contraseñas de usuarios, almacenarlos como texto plano genera riesgos significativos de seguridad.

Podríamos pensar en aplicar un hashing y almacenar la versión hasheada de la contraseña:

  1. El cliente envía el PIN plano al servidor (a través de conexión segura HTTPS)
  2. El servidor hashea el PIN y lo almacena en la base de datos (DB)
  3. Cuando un usuario verifica su PIN más tarde, lo envía nuevamente como PIN plano por HTTPS
  4. El servidor hashea el PIN recibido y lo compara con el almacenado en la DB
  5. El servidor devuelve el resultado: sí/no
  6. No nos preocupamos por posibles filtraciones de la DB, ya que no almacenamos contraseñas en texto plano (¿o sí?).

Sin embargo, existe algo llamado “tablas arcoíris”. Son tablas precomputadas con el siguiente formato:

pass       | hash
----------------------
"1234"     | "a1b2c3"
"changeit" | "x7y8z9"
"qwerty"   | "m4n5p6"
...millones más...

En caso de que se filtre una base de datos con contraseñas hasheadas, un atacante podría comparar los hashes filtrados con estas tablas arcoíris y así descubrir el PIN o contraseña.

Podrías pensar: “espera, puede haber colisiones en los hashes, de modo que diferentes datos generen el mismo hash”. El problema es que las colisiones de hash son extremadamente raras con funciones hash modernas como SHA-256, SHA-3 o BLAKE2 (esa es la razón por la que se utilizan para verificar la integridad de archivos y firmas digitales).

Esto significa que si vemos el hash x7y8z9 en la base de datos, podemos estar casi seguros de que la contraseña es changeit.


Hashing + salado

El truco para aumentar la fortaleza criptográfica es agregar sal de la siguiente manera:

  1. El cliente envía el PIN plano al servidor (a través de conexión segura HTTPS)
  2. El servidor genera una sal única para este usuario en particular
  3. El servidor hashea la combinación de PIN + sal y almacena el hash resultante en la base de datos Y TAMBIÉN la sal (en su representación original)
  4. Cuando un usuario verifica su PIN más tarde, lo envía nuevamente como PIN plano por HTTPS
  5. El servidor lee la sal para este usuario desde la DB, añade la sal al PIN recibido, hashea esa combinación y la compara con la almacenada en la DB
  6. El servidor devuelve el resultado: sí/no

Presta atención:

  • El PIN plano nunca se almacena
  • Solo se almacenan el hash (generado a partir de PIN+sal) y la sal en la base de datos
  • El hashing ocurre en el lado del servidor
  • El cliente siempre envía el PIN plano (pero a través de HTTPS)
  • Cada usuario tiene una sal única

¿Por qué esto funciona?

Sin una sal única, un atacante podría precomputar hashes para todos los PINs posibles. Luego simplemente los compara contra la base de datos.

Con una sal única – los mismos PINs generan hashes diferentes para usuarios diferentes:

User1: PIN "1234" + sal "abc" = hash "x7y8z9"
User2: PIN "1234" + sal "def" = hash "a1b2c3"

Por lo tanto, el atacante necesita calcular una nueva tabla arcoíris para CADA sal. Esto hace que el ataque sea computacionalmente costoso.

El código de implementación de ejemplo sería algo así:

# Creación de PIN / contraseña
def store_pin(plain_pin):
    salt = generate_random_salt()
    hash = hash_function(plain_pin + salt)
    database.save(user_id, hash, salt)

# Verificación de PIN / contraseña
def verify_pin(received_pin):
    stored_salt = database.get_salt(user_id)
    stored_hash = database.get_hash(user_id)
    calculated_hash = hash_function(received_pin + stored_salt)
    return calculated_hash == stored_hash

Bonus: Pepper

  • Pepper es un valor igual para todos los usuarios.
  • Se almacena de manera separada (no en la base de datos).
  • Es un valor secreto (como una clave de la aplicación).
hash = hash(contraseña + sal + pepper)

En la base de datos solo se almacena: hash + sal.

Es útil cuando la base de datos está comprometida, pero la configuración del servidor no lo está.

Ejemplo de implementación:

# Pepper almacenado en la configuración del servidor/entorno
PEPPER = "secret_key_123"

def store_password(password):
    salt = generate_salt()
    hash = hash(password + salt + PEPPER)
    db.save(hash, salt)  # pepper no se guarda!

Es una capa de seguridad adicional: incluso con acceso a la base de datos, el atacante también necesitaría acceso al servidor para obtener el valor de pepper.