Así debes guardar tus contraseñas en una base de datos

Si quieres almacenar las contraseñas de tus usuarios de forma segura, debes utilizar la herramienta adecuada, ya que si no, estarán en peligro

Si alguna vez has programado una aplicación con usuarios, probablemente has pensado en cómo debes autenticar a esos usuarios: ¿cuenta de Google? ¿GitHub? ¿Una solución como Firebase Authentication? ¿Mi propio sistema de contraseñas? En el caso de este último, la autenticación puede llegar a convertirse en un proceso complejo, y es que la seguridad es MUY complicada. Un error como una inyección SQL puede significar que se filtren miles de contraseñas, así como los usuarios a quienes pertenecen. En el mejor de los casos, se detecta inmediatamente y se fuerza un reinicio de contraseña para todos los usuarios. En el peor de los casos, estas contraseñas son reutilizadas por el usuario en todas partes (mucha gente todavía tiene una contraseña para todo) y puede tener resultados catastróficos.

La solución "fácil": cifrado simétrico

Por esto, es importante cifrar las contraseñas. Por ejemplo, podríamos utilizar un algoritmo como AES para cifrar las contraseñas. AES funciona (de forma sencilla, no a nivel técnico) tomando una cadena de texto como ABCD y una "clave" 1234567812345678 (para AES-128) y devolviéndote una cadena cifrada como WXYZ (pero más larga y fea). Si luego descifras la cadena WXYZ con la clave que usaste previamente para cifrar, obtendrás de vuelta ABCD.

Ahora bien, esto es MUY malo, porque ¿qué pasa si se filtra la "clave maestra"? Que todas las contraseñas se quedan al descubierto, como ocurría almacenándolas directamente. Por tanto, el cifrado no nos vale, ya que con la clave correspondiente podemos recuperar la contraseña original. Aunque usásemos un par de claves pública/privada (como hacen SSH y TLS), estaríamos en las mismas si tenemos la clave privada en algún sitio.

Usemos los "hash"

¿Alguna vez has descargado un archivo y ponía debajo "SHA-256 UNACADENAMUYLARGA" o "SHA-1 OTROCHORIZO"? Eso es un "hash", o un "digest", y en resumen se tratan de aplicar unas funciones matemáticas sobre una secuencia de bytes (un archivo, una cadena de texto...), que nos devuelve un resultado. Ahora bien ¿cómo puede ser que un archivo de 5MB se guarde en una cadena de 32 caracteres? Pues no se guarda, precisamente, es el resultado de una aplicación de un solo sentido.

Con SHA-256 no utilizamos ninguna "clave" adicional ni nada del estilo. Algunos sitios web, como este te permiten generar un SHA-256 de una cadena de texto que le introduzcas. Por ejemplo, si introduzco 1234, me devuelve su hash: 03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4.

Ahora bien ¿qué pasa si tú también introduces 1234? Que nos devuelve el mismo chorizo de letras y números. Esto tiene varios problemas ¿qué pasa si me pongo a calcular el SHA de todas las palabras del diccionario o de todas las combinaciones de letras posibles? Que tardaré bastante, pero encontraré la combinación que devuelve el hash correcto, y esa será la contraseña.

Estamos cerca de tener una forma totalmente segura, pero aún no hemos llegado.

¿Y si evitamos que la misma entrada nos de la misma salida?

La solución de hacer un hash es evidentemente la mejor, pero si para una misma entrada siempre tenemos la misma salida, y siendo SHA-256 relativamente rápido de calcular en masa, es poco seguro para nuestro caso de uso (contraseñas).

Entonces, lo que se puede hacer es generar una "sal" aleatoria y pegarla a cada contraseña que se genere. De esta forma, la primera vez que generes un hash para 1234 tendrás una salt como ABC; pero la segunda vez dicha salt podrá ser XYZ, con lo que el resultado será totalmente distinto.

De esta forma, podemos almacenar en la base de datos el resultado de cifrar la contraseña y su salt. Para comprobar si la contraseña es correcta, basta con volver a calcular el hash utilizando la misma "salt", que habíamos almacenado previamente. De forma que 1234 con la sal ABC que ya teníamos nos devolverá el hash correcto, que teníamos almacenado en la base de datos.

Y ahora me preguntarás ¿y si se filtra el "hash" con la salt? Pues seguirá faltando adivinar la contraseña original, igual que en pasos anteriores. Pero aquí hay una diferencia: bcrypt hace el hash con varias rondas. Es decir, en lugar de tomar 1234ABC (números contraseña, letras salt), calcula el resultado de dicha operación y luego vuelve a generar otro hash de dicho hash.

1234ABC => wasdABC => qwerABC => zxcvABC

De esta forma, se vuelve bastante más caro calcular cada hash. De forma que es lento, pero a propósito, y cuantas más "rondas" le añadas más costoso (en recursos y tiempo) es "echar la quiniela" y calcular el hash para una contraseña. Si pruebas con 1233, tardarás muchos más milisegundos en calcular el equivalente con varias rondas y salt que si solo usases SHA-256.

El estándar más popular que utiliza estos principios es bcrypt, que funciona tal cual te he contado en esta última parte. Existen muchas implementaciones de bcrypt para distintos lenguajes de programación, siendo password4j bastante popular en java, bcrypt en nodeJS y paquetes con nombres similares en otros lenguajes.