Guardando contraseñas de forma segura
En esta bitácora no es necesario, ni para el único usuario que se autentica (el administrador), porque he usado uno de los módulos de Tornado (concretamente Google OAuth), así que entro en la administración usando mi Google Account. Pero en mi último intento
con Dancer y Perl, sí implementé gestión de usuarios (y sus contraseñas).
Recuerdo que en el año 2001 me miraron raro en la empresa donde trabajaba porque decía cosas como cifrar contraseñas usando MD5. Es decir, no guardar las contraseñas en texto plano en la aplicación, para reducir el impacto en caso de que las mismas se vieran comprometidas.
Además poco después implementé contraseñas de un uso con Javascript, para asegurar la autenticación en los casos en los que no disponíamos de SSL y HTTPS, que se basa en la misma idea que voy a explicar hoy.
En aquel momento fue una mejora, pero definitivamente estaba mal implementado, y la demostración es la siguiente:
21232f297a57a5a743894a0e4a801fc3
¿Está claro no? Por si acaso: es el hash MD5 de la palabra admin (una de las contraseñas temporales más usadas, aún reconozco su MD5 cuando lo veo :P).
Almacenar un hash de la contraseña no es seguro, si existe la posibilidad de que haya un DVD circulando por ahí con tablas precalculadas y listas para comparar con nuestra contraseñas cifradas.
No es que MD5 sea vulnerable, de hecho es un hash bastante lento de calcular, lo que lo hace adecuado para entorpecer ataques con fuerza bruta (que hayan n colisiones cada m millones de generaciones, no es tan grave porque no estamos firmando), sino que los usuarios no usan buenas contraseñas, así que podemos preparar descargar una rainbow table que nos facilite el ataque.
Si lo estamos haciendo mal, ¿cuál es la forma adecuada de guardar contraseñas?
En primer lugar, trabajar con cifrado sin tener suficientes conocimientos suele ser el primer paso para dispararse en un pie:
Es más fácil no usar mal un framework bien implementado, que implementar bien nuestra propia rutina criptográfica.
Mi propuesta es aplicar la idea de salt, que ya se empleaba en las versiones iniciales de UNIX para cifrar las contraseñas (¡oh, novedad!).
Ese salt es una serie aletoria de bits que se utiliza para derivar una clave en otra, de forma que el hash obtenido tras cifrar dos veces la misma contraseña, sea diferente.
Mi implementación en Perl está integrada en el ORM, pero refactorizando un poco tenemos:
- La contraseña almacenada: consiste en
6 bytesde salt, concatenados alhashde la contraseña derivada. A un salt más grande y aleatorio, la fórmula es más robusta. Además es buena idea usar una función de hash lo más lenta posible (entonces:MD5mejor queSHA). - La función para nuevas contraseñas: dada la contraseña de usuario, genera un salt, deriva la contraseña y cifra la contraseña para almacenar.
- Una función para comparar: toma como entrada una contraseña en plano (la que nos pasa el usuario al autenticarse), y una contraseña almacenada. Su tarea es derivar la contraseña de usuario y generar el
hashcorrespondiente para comparar con lo que tenemos almacenado.
De esta forma conseguimos que los ataques con tablas rainbow sean inviables.
Veamos código de ejemplo:
use strict; use warnings; use Digest::MD5 qw(md5_hex); # salt size in bytes my $SALT_SIZE = 6; # params: plain password # returns: hashed password sub newPassword { my $plain = shift; # we get characters with substr, so mult x 2 the number # of bytes (1 byte is 2 chars in ascii) my $salt = substr(md5_hex(rand()), 0, $SALT_SIZE*2); return $salt .md5_hex($salt . $plain); } # params: hashed password, plain password # returns: 1 if are equal sub cmpHashPasswd { my $hashed = shift; my $plain = shift; # we get characters with substr, so mult x 2 the number # of bytes (1 byte is 2 chars in ascii) my $salt = substr($hashed, 0, $SALT_SIZE*2); my $plainHashed = $salt .md5_hex($salt . $plain); return $hashed eq $plainHashed; } # simple test: admin my @hashed = (); for (my $i=0; $i<10; $i++) { push(@hashed, newPassword("admin")); } foreach(@hashed) { print "admin hashed as: $_, is admin? " .cmpHashPasswd($_, "admin") ."\n"; }
En el ejemplo vemos como generamos 10 contraseñas listas para almacenar, que son distintas (lo que imposibilita un ataque rainbow), y que podemos compararlas con la contraseña en texto plano sin ningún problema.
Pero si volvemos a mi consejo inicial: ¿nos podemos fiar de esta implementación?
En este caso es sencillo, y además me tomé la molestia de documentarme y revisar el código de la implementación de OpenBSD en C, así que a mi me parece de fiar :D. Sino, siempre hay frameworks que lo hacen bien: buscamos uno, lo evaluamos, y listo; pero bajo ningún concepto usaremos un hash sin más para almacenar las contraseñas porque no es seguro.
Hay 3 comentarios
![]()
Estoy bastante de acuerdo contigo :)
El tema es más bien tener claro cuáles son los mínimos (con coste asumible).
Lo que me lleva a esto:
apache.org incident report for 04/09/2010
Cito: “JIRA and Confluence both use a SHA-512 hash, but without a random salt. We believe the risk to simple passwords based on dictionary words is quite high, and most users should rotate their passwords“.
A mi me parece un buen FAIL por parte de esos proyectos, teniendo en cuenta que es fácil implementar un salt mínimo.
![]()
El caso es que últimamente estoy bastante liado, pero reconozco que siento una gran curiosidad por coger una base de datos de usuarios y hacer un test a ver en 24, 48 y 72 horas, cuantos passwords podrían salir con un simple ataque de diccionario.
En fin… lo dejaré como tarea pendiente para un futuro indefinido ;)
por Pau Sanchez, en 2010-06-15 23:14:01 ∞
Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 15 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.



por Pau Sanchez, en 2010-06-13 19:27:20 ∞