Cifrando y comprobando contraseñas en PHP

Introducción

Es una buena práctica (y algo que debemos hacer todos) cifrar las contraseñas si las vamos a guardar en una base de datos. También es importante hacerlo con un algoritmo de un solo camino; es decir, que se pueda cifrar pero que nunca se pueda hacer el proceso inverso.

¿Por qué cifrarlas, si mi base de datos está protegida?

Recordemos que la seguridad absoluta no existe y que siempre estamos propensos a un ataque de cualquier tipo, así que hay que proteger cada cosa individualmente.

Supongamos que nuestro servidor FTP sí está bien protegido, y que ningún usuario malicioso puede acceder a él. Ahora imaginemos que alguien logra hacer un ataque a la base de datos, y consigue todos los nombres de usuario y contraseñas. Lo que conseguirá serán contraseñas cifradas que no le servirán para nada. Pero si no las ciframos, podrán hacer lo que se les antoje.

Cifrando contraseñas

Debilidad de SHA1 y MD5

PHP ya viene con una opción para cifrar las contraseñas. No debemos usar nunca SHA1 ni MD5, porque aunque no son reversibles sí son predecibles con ataques de diccionario.

Aquí pongo un ejemplo:

Si yo uso SHA1 para cifrar “123” obtendré “40bd001563085fc35165329ea1ff5c5ecbdbbeef“. Esto nunca cambiará; siempre que yo cifre “123” obtendré el mismo resultado. Si alguien obtiene la contraseña cifrada que será “40bd001563085fc35165329ea1ff5c5ecbdbbeef”, no sabrá cuál contraseña plana es.

Pero comenzará a probar diferentes contraseñas y las irá cifrando, una por una. Puede comenzar con las más comunes, como 12345, 0000, etcétera. Por lo que conseguirá los siguientes resultados:

  • 12345 se convierte en 8cb2237d0679ca88db6464eac60da96345513964. No es esta contraseña porque no coincide con 40bd001563085fc35165329ea1ff5c5ecbdbbeef
  • 0000 se convierte en 39dfa55283318d31afe5a3ff4a0e3253e2045e43. Tampoco es esta porque no coincide con 40bd001563085fc35165329ea1ff5c5ecbdbbeef

E irá probando hasta que llegue a 123. Se verá así:

  • 123 se convierte en 40bd001563085fc35165329ea1ff5c5ecbdbbeef. Sí es, porque coincide con 40bd001563085fc35165329ea1ff5c5ecbdbbeef.

Lo peor de esto es que ya hay diccionarios online que tienen la mayoría de contraseñas más comunes y no tan comunes. Por eso nunca se debe usar esta función.

Con MD5 es lo mismo, así que no lo explicaré

Usando sal

Cuando usamos la función que PHP recomienda, se le añade sal a la contraseña cifrada. Es una cadena aleatoria que siempre producirá resultados diferentes.

La primera vez que cifre 123 puede que el resultado sea $2y$10$I4tJZFA.mxRSaxiy/uLUWOAB8CCoBI5UJUJZ/pkJ278oYJm6nstJ. La segunda $2y$10$uxHsXhUq90CzCbPNeoCL/enuYtJVmChl2Bc9biUJnq/LYtDDusk5K, la tercera $2y$10$8lVMtOjKf6nsorwQp4cWW.hzfuVqk.MyuKzesEPZu2r7fKt0X2V4K y así sucesivamente. Por lo que siempre dará un resultado distinto, y nunca se le podrá hacer un ataque como el que menciono arriba.

Algoritmos

Así que para guardar estas contraseñas el algoritmo es:

  1. Tener la contraseña plana del usuario
  2. Cifrarla con password_hash()
  3. Guardar el resultado del paso anterior (la contraseña ya cifrada) en la base de datos

Y para comprobar si esta contraseña es correcta cuando el usuario intente acceder es:

  1. Obtener contraseña cifrada de la base de datos
  2. Utilizar password_verify()
  3. Si el resultado del paso anterior es true, entonces la contraseña es correcta. De otro modo, no.

Ejemplo

Estructura de la tabla

El manual oficial recomienda que si vamos a usar password_hash declaremos nuestro campo en la base de datos con una longitud de 255.

Registrar usuario

Voy a abstraer y omitir  la conexión a la base de datos porque explicarlo no es el propósito de este post. Así que supondré que el usuario sabe manejar bases de datos y sus conexiones.

if(isset($_POST["nombre_usuario"]) && isset($_POST["password"])){
  $usuario = $_POST["nombre_usuario"];
  $pass = $_POST["password"];
  $passCifrada = password_hash($pass, PASSWORD_DEFAULT);
  $baseDeDatos->prepare("INSERT INTO usuarios (nombre, pass) VALUES (?,?);");
  $baseDeDatos->execute([$usuario, $passCifrada]);
}

En el código de arriba tomamos el usuario y la contraseña del usuario, que pudieron ser enviadas por un formulario. Preparamos la consulta SQL para evitar inyecciones y guardamos.

Es importante notar que usamos password_hash() y le pasamos como primer argumento la contraseña plana, y como segundo una constante que es el algoritmo a ser utilizado. Recomiendo usar siempre esta constante.

Ese fue el proceso para guardar el usuario y la contraseña.

Comprobando credenciales

 

Ahora imaginemos que el usuario intenta acceder, y que igualmente manda el nombre de usuario y la contraseña por un formulario. El código quedaría así:

if(isset($_POST["nombre_usuario"]) && isset($_POST["password"])){
  $nombreUsuario = $_POST["nombre_usuario"];
  $pass = $_POST["password"];
  $baseDeDatos->prepare("SELECT pass FROM usuarios WHERE nombre = ? LIMIT 1;");
  $baseDeDatos->execute([$nombreUsuario]);
  $passObtenida = $baseDeDatos->fetch()->pass;
  //Aquí comprobamos:
  if(password_verify($pass, $passObtenida) === TRUE){
    //Datos correctos
  }else{
    //Datos incorrectos
  }
}

Conclusión

Así es como, a grosso modo, podemos comprobar si las credenciales de determinado usuario son válidas, al mismo tiempo que protegemos su contraseña.

Es muy importante mencionar que no debemos intentar crear nuestros propios algoritmos, por más que pensemos que somos listos nunca debemos de intentar hacerlo por nuestra cuenta.

Encantado de ayudarte


Estoy disponible para trabajar en tu proyecto, modificar el programa del post o realizar tu tarea pendiente, no dudes en ponerte en contacto conmigo.

No te pierdas ninguno de mis posts

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.