Contraseñas y secretos
Guarda las contraseñas de modo que una base de datos robada no las revele, y mantén las claves que usa tu servicio fuera del código, de los logs y del alcance de quien no las necesita.
AL TERMINAR, PODRÁS
- Explicar por qué las contraseñas necesitan un hash lento y con sal, y no uno rápido
- Verificar una contraseña contra un registro guardado y subir su costo sin riesgo
- Decidir dónde vive un secreto y qué hacer cuando se filtra
Descripción del diagrama: Contraseña → Hash lento; Sal → Hash lento; Hash lento → Registro guardado; Registro guardado → Inicio de sesión (se lee); Registro guardado → Una copia robada
Guarda una contraseña para que una filtración no la revele
Un servicio nunca necesita leer una contraseña de vuelta, solo comprobarla, así que guarda un hash de un solo sentido en lugar de la contraseña.
No un cifrado: quien obtenga la clave recuperaría todas las contraseñas de una vez. Tampoco un hash rápido como SHA-256: le permite a un atacante que copie la tabla probar intentos muy deprisa. Las funciones de hash de contraseñas son lentas a propósito, con un costo que puedes subir a medida que el hardware se vuelve más rápido.
Cada contraseña lleva además su propia sal (salt) aleatoria, guardada junto al hash, para que las tablas precalculadas no sirvan y dos personas con la misma contraseña obtengan hashes distintos. La guía de OWASP sobre almacenamiento de contraseñas recomienda primero Argon2id (al menos 19 MiB de memoria, 2 iteraciones y paralelismo 1), luego scrypt (costo 2^17, tamaño de bloque 8 y paralelismo 1), bcrypt solo para sistemas antiguos (factor de trabajo 10 o más, con la entrada limitada a 72 bytes) y PBKDF2 con HMAC-SHA-256 y al menos 600.000 iteraciones donde se exija cumplir FIPS-140. Guarda juntos el algoritmo, sus parámetros, la sal y el hash, para poder verificar el registro y mejorarlo más adelante.
import hashlib, secrets
COST = 17 # n = 2**17, the minimum OWASP gives for scrypt
def hash_password(password):
salt = secrets.token_bytes(16)
digest = hashlib.scrypt(password.encode(), salt=salt, n=2**COST, r=8, p=1,
maxmem=2**28)
return f"scrypt${COST}${salt.hex()}${digest.hex()}"
Verifica, compara y actualiza
Para comprobar un inicio de sesión, lee el registro guardado, vuelve a calcular el hash de la contraseña enviada con la sal y los parámetros del propio registro, y compara los dos.
Compara con hmac.compare_digest, que según la documentación de Python resiste los ataques de tiempo (timing attacks): un == normal puede detenerse en el primer byte distinto, y lo que tarda revela cuánto de un intento era correcto. hashlib.scrypt de Python necesita un maxmem mayor que el predeterminado para el costo recomendado; sin él, la llamada falla con un error de límite de memoria en lugar de usar en silencio un ajuste más débil. Cuando subes el costo, los registros viejos siguen verificándose, porque cada uno lleva sus parámetros. Vuelve a calcular el hash con el costo nuevo después del siguiente inicio de sesión correcto, el único momento en que tienes la contraseña en texto plano.
Las cuentas que nunca vuelven a entrar conservan el costo viejo, así que OWASP sugiere hacer caducar los hashes muy antiguos y pedir una contraseña nueva.
import hmac
def verify_password(password, record):
_, cost, salt, digest = record.split("$")
candidate = hashlib.scrypt(password.encode(), salt=bytes.fromhex(salt),
n=2 ** int(cost), r=8, p=1, maxmem=2**28)
return hmac.compare_digest(candidate, bytes.fromhex(digest))
Mantén los secretos fuera del código
Relay tiene sus propios secretos: la contraseña de la base de datos, la clave de API del proveedor de correo y la clave que firma los tokens de sesión.
Un secreto que se sube a un repositorio queda en su historial y en cada copia, así que borrar el archivo después no lo recupera. Cuando uno se filtra, primero revócalo en el proveedor y emite uno nuevo; limpiar el historial viene después y tiene sus propios problemas. Guarda los secretos en un gestor de secretos (secret manager) y dale a cada servicio solo los que necesita.
Las variables de entorno son comunes, pero otros procesos pueden leerlas y pueden terminar en logs o volcados de memoria, así que la guía de OWASP sobre gestión de secretos las trata como último recurso. Nunca registres un secreto en un log; enmascáralo. Rota los secretos con regularidad, para que uno robado funcione solo por un tiempo, y prefiere credenciales de corta vida emitidas a pedido donde la plataforma lo permita.
Reiniciar un servicio no revoca una clave robada: solo el proveedor puede hacerlo.
ERROR FRECUENTE
Cifrar las contraseñas en lugar de aplicarles un hash. Quien consiga la clave recupera todas las contraseñas, mientras que un hash de contraseña no tiene ninguna clave que robar.
INTÉNTALO TÚ
Enumera cada secreto que usa Relay. Para cada uno, escribe dónde vive, qué servicios pueden leerlo, cómo lo rotarías y qué harías en la primera hora después de que se filtre.
Decide en el laboratorio.
El checkpoint de dos preguntas, tus notas y tu progreso guardado están en el laboratorio. Gratis y sin cuenta.
Abrir en el laboratorio ↗CONÉCTALO CON RELAY
Agrega cuentas a Relay: guarda las contraseñas como registros de scrypt con una sal por persona, verifícalas con hmac.compare_digest y vuelve a calcular el hash después del inicio de sesión cuando suba el costo. Saca del código la contraseña de la base de datos y la clave de correo de Relay, y escribe cómo se rota cada una.