29 de abril de 2015

Login PHP con password_hash()

Primero una aclaración…como no guardar contraseñas No guardar contraseñas en texto plano, esto debería ser obvio ya que si alguien tiene acceso a tu base de datos y a las contraseñas de tus usuarios, si un ataque externo…o interno(en quien confias?)…expone las contraseñas de tus usuarios, tu trabajo como programador-analista-diseñador-DBA-conserje –prepara café se verá comprometido, ni hablar de las represalias directas de la compañía que se lavaran las manos en tu incapacidad. Si a esto le sumamos que muchos usuarios, como yo, utilizan la misma contraseña para muchos sitios pues el pecado habrá sido mayor, recuerda que no solo te expones a ataques desde fuera de tu empresa ya que hasta un shared server puede fácilmente entrar a tu base de datos. No inventes, a lo mejor no eres un expert en seguridad, lo mejor es probar con soluciones que han sido aceptadas por la comunidad de programadores que como tu están buscando proteger las valiosas contraseñas de sus usuarios. No encriptar contraseñas Había un profesor de la universidad que quería llamar la atención haciéndonos ver que la pablara encriptar no existe más nunca explico el problema de encriptar contraseñas…bueno, el gran problema es que el encriptar una frase es un proceso reversible, esto es, que al encriptar pabletoreto podría obtener algo como esto: hjb32 y pareciera que estaría seguro pero qué tal si desencripto ese texto y obtengo pablo de nuevo? Utilizar Hash pero no MD5 Si no sabes que es un hash te lo contamos. No es más que una secuencia de caracteres única que representa una entidad ( en nuestro caso una contraseña), esta secuencia se obtiene mediante algoritmos hash (generalmente md5 o sha1), las características de estos algoritmos que nos resulta útil para gestionar las contraseñas son: Algoritmos de una sola vía: No hay forma de obtener la cadena original partiendo de la secuencia obtenida. Son deterministas: Es decir, la misma entrada siempre producirá la misma secuencia de salida. Son “económicas” de calcular: Esto quiere decir que no se necesitan grandes cantidades de tiempo y memoria para calcularlas, haciendo factible su implementación en nuestras aplicaciones. Y es precisamente esta última cualidad que actualmente ha comprometido la efectividad de estos algoritmos hash, debido al incremento de la capacidad computacional de los equipos con los que trabajamos y el empleo conjunto con técnicas de “crack” de hash como las tablas arcoiris ( Rainbow tables), las que nos obligan a ir un paso más allá e implementar técnicas que le dificulten o por lo menos no hagan atractivo computacionalmente hablando el tratar de romper nuestras contraseñas. Utilizar hash para las contraseñas es ir en el camino correcto pero no te vayas por las opciones MD5 o SHA-1 porque son propensas a ataques como fuerza bruta o rainbow tables, para más información acerca de estos ataques pregúntale a Yahoo. LinkedIn o a la ultra segura portentosa y divina Apple. Como sugerencia utiliza Bcrypt Utiliza SALT Para aumentarle la dificultad a los crackers, hay 2 métodos muy comunes que son salting y peppering. En el caso de un salt, La idea es crear un string aleatoreo y añadirlo al password. Ese string nunca lo debe saber el usuario, de hecho el nisiquiera debería saber que existe. El objetivo de este método es aumentar la longitud y complejidad del password original. Por otro lado un pepper sería otro string aleatoreo, que se guarda generalmente por fuera de la base de datos y se le añade a un salt. Su función principal es generar distintos hashes para distintos dominios/páginas. Personalmente pienso que un pepper no añade beneficios gigantes a la seguridad de una aplicación, asi que no hablaré mucho al respecto. Las Tablas Arcoiris y las Lookup Tables, solo funcionan cuando cada password ha sido hasheado de la misma forma. Si 2 usuarios tienen el mismo password, su hash sería el mismo. Esa situación se previene, creando un salt para cada password, cosa que si 2 usuarios usan el mismo password, el resultado serían 2 hashes distintos! De preferencia no utilices una salt estatica sino una que se genere aleatoriamente, la clave en todo este asunto es nunca reusar un mismo salt, siempre generar uno nuevo y preferiblemente, que tenga una longitúd decente (pongale unos 20 o 32 caracteres). Para generar salts aleatoreos en PHP, lo mejor es usar las funciónes mcrypt_create_iv o openssl_random_pseudo_bytes - nada de mt_rand, rand, uniqid, ni algo inventado por nosotros mismos, entonces en vez de tener esto
<?php
$salt = '34a@$#aA9823$';
$password = 'dinvaders1234';
$password = hash('sha256', $salt . $password);
?>
opta por cualquiera de estas dos opciones para generar datos aleatorios, primero considera usar mcrypt_create_iv que crea un vector de inicialización (IV) desde una fuente aleatoria, el primer parametro representa el tamaño del vector y el segundo parametro representa la fuente del IV. El parámetro fuente puede ser MCRYPT_RAND (generador de números aleatorios del sistema), MCRYPT_DEV_RANDOM (lee datos de /dev/random) y MCRYPT_DEV_URANDOM (lee datos de /dev/urandom). Antes de 5.3.0, MCRYPT_RAND era la única soportada en Windows. Obsérvese que el valor predeterminado de este parámetro era MCRYPT_DEV_RANDOM antes de PHP 5.6.0.
<?php
// A Crypt no le gustan los '+' así que los vamos a reemplazar por puntos.
$salt = strtr(base64_encode(mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)), '+', '.');
?>
o bien esta segunda opcion que utiliza openssl_random_pseudo_bytes que genera una cadena de bytes pseudo-aleatoria, con el número para crear aleatoriamente 22 numeros:
<?php
// Generamos un salt aleatorio, de 22 caracteres para Bcrypt
$salt = substr(base64_encode(openssl_random_pseudo_bytes('30')), 0, 22);

// A Crypt no le gustan los '+' así que los vamos a reemplazar por puntos.
$salt = strtr($salt, array('+' => '.')); 
?>
crypt — Hash de cadenas de un sólo sentido(PHP 4, PHP 5) Vamos a lo importante, primero mostrare como utilizar la función crypt sobre una contraseña y como validarla, no ingresare la contraseña a la base de datos
<?php
$username = 'Admin';
$password = 'pabletoreto';
// costo base 2
$cost = 10;
// Creamos una salt aleatoria de 16 elementos
$salt = strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');
// $2a$ significa Blowfish algorithm y %02d$ es el costo 
// deben ir antes de la salt para ser un formato aceptado
// por la funcion crypt
$salt = sprintf("$2a$%02d$", $cost) . $salt;

$hash = crypt($password, $salt);
echo "
"; echo "
"; echo "se obtuvo este valor encryptado" .$hash; echo "
"; echo "
"; echo "Ahora probaremos la validacion con pabletoreto de contraseña"; echo "
"; $password2="pabletoreto"; //Vamos a verificar if(password_verify($password2, $hash)){ echo "OK, validacion correcta"; } else { echo "validacion incorrecta"; } ?>
Podríamos haber verificado la contraseña con if ( hash_equals($hash, crypt($password2, $hash))) pero debes verificar que tu versión de PHP lo permita. crypt() devolverá el hash de un string utilizando el algoritmo estándar basado en DES de Unix o algoritmos alternativos que puedan estar disponibles en el sistema. El parámetro salt es opcional. Sin embargo, crypt() crea una contraseña débil sin salt. PHP 5.6 o posterior emiten un error de nivel E_NOTICE sin él. Asegúrese de especificar una sal lo suficientemente fuerte para mayor seguridad. password_hash — Crea un nuevo hash de contraseña (PHP 5 >= 5.5.0) password_hash() utiliza un hash fuerte, genera una sal fuerte, y aplica los redondeos necesarios automáticamente. password_hash() es una envoltura simple de crypt() compatible con los hash de contraseñas existentes. Se aconseja el uso de password_hash(). Lo bueno de password_hash() es que la misma función nos crea una salt segura, esta será aleatoria y diferente para cada password, el coste por defecto es 10 el cual es suficiente, estos dos valores ya están presentes en el hash que password_hash entrega, además que si utilizamos PASSWORD_DEFAULT como segundo parámetro es como que estuviéramos utilizando el algoritmo de hash BCRYPT, pero si queremos utilizar el algoritmo recomendado CRYPT_BLOWFISH se debe especificar como segundo parametro PASSWORD_BCRYPT. password_hash() crea un nuevo hash de contraseña usando un algoritmo de hash fuerte de único sentido. password_hash() es compatible con crypt(). Por lo tanto, los hash de contraseñas creados con crypt() se pueden usar con password_hash(), significa que podemos utilizar password_verify() para ambos casos. Actualmente, se tiene soporte para los siguientes algoritmos: PASSWORD_DEFAULT - Usar el algoritmo bcrypt (predeterminado a partir de PHP 5.5.0). Observe que esta constante está diseñada para cambiar siempre que se añada un algoritmo nuevo y más fuerte a PHP. Por esta razión, la longitud del resultado de usar este identificador puede cambiar en el tiempo. Por lo tanto, se recomienda almacenar el resultado en una columna de una base de datos que pueda apliarse a más de 60 caracteres (255 caracteres sería una buena elección). PASSWORD_BCRYPT - Usar el algoritmo CRYPT_BLOWFISH para crear el hash. Producirá un hash estándar compatible con crypt() utilizando el identificador "$2y$". El resultado siempre será un string de 60 caracteres, o FALSE en caso de error. Opciones admitias: salt - para proporcionar manualmente una sal a usar cuando se realiza el hash de la contraseña. Observe que esto sobrescribirá y prevendrá que una sal sea generada automáticamente. Si se omite, se generará una sal aleatoria mediante password_hash() para cada contraseña con hash. Este es el modo de operación previsto. cost - que denota el coste del algoritmo que debería usarse. Se pueden encontrar ejemplo de estos valores en la página de crypt(). Si se omite, se usará el valor predeterminado 10. Este es un buen coste de referencia, pero se podría considerar aumentarlo dependiendo del hardware. Entonces utilicemos password_hash, en la pagina de PHP se muestra este ejemplo de password_hash()
<?php
/**
 * Queremos realizar un hash a nuestra contraseña cuando el algoritmo DEFAULT actual.
 * Actualmente es BCRYPT, y producirá un resultado de 60 caracteres.
 *
 * A que tener en cuenta que DEFAULT puede cambiar con el tiempo, por lo que debería prepararse 
 * para permitir que el almacenamento se amplíe a más de 60 caracteres (255 estaría bien)
 */
echo password_hash("rasmuslerdorf", PASSWORD_DEFAULT)."\n";
?>
Ejemplo de password_hash() estableciendo el coste manualmente
<?php
/**
 * En este caso, queremos aumentar el coste predeterminado de BCRYPT a 12.
 * Observe que también cambiamos a BCRYPT, que tendrá siempre 60 caracteres.
 */
$opciones = [
    'cost' => 12,
];
echo password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $opciones)."\n";
?>
Ejemplo más completo con mysqli, aquí la conexión a la base de datos se encuentra en conexión.php pero esa la haces tu solo la utilizo emplicitamente en este código, además se dan las opciones de ingresar un nuevo password y de verificar password existentes, fíjate en elos $_POST[‘registrar’] y $_POST[‘ingresar’] y para verificar la contraseña contra el hash generado se utiliza password_verify($password_a_verificar, $hash_generado))
<?php
session_start();
ob_start();
require_once "conexion.php";
error_reporting(E_ALL);

if((isset($_POST['ingresar'])) or (isset($_POST['registrar']))){
  
    if(!($mysqli = conectarse())){
 echo "Error al conectarse a la base de datos";
 echo "
"; echo $mysqli->connect_errno. " - " .$mysqli->connect_error; exit(); } $errores = array(); $username = filter_var($username, FILTER_SANITIZE_STRING); $password = filter_var($password, FILTER_SANITIZE_STRING); if( empty($username) ) $errores[] = "Debe especificar username"; if( empty($password) ) $errores[] = "Debe especificar password"; if( count($errores) > 0 ) { echo "

ERRORES ENCONTRADOS:

"; for( $contador=0; $contador < count($errores); $contador++ ) echo $errores[$contador]."
"; exit(); } if(isset($_POST['ingresar'])) { if(!($stmt = $mysqli->prepare("SELECT username, password FROM login_php WHERE username = ?"))){ echo "Prepare failed: (" . $mysqli->errno . ")" . $mysqli->error; exit(); } if(!$stmt->bind_param('s', $username)){ echo "Bind failed: (" . $stmt->errno . ")" . $stmt->error; exit(); } if(!$stmt->execute()){ echo "Execute failed: (" . $stmt->errno .")" . $stmt->error; exit(); } $userdata = $stmt->get_result(); $row = $userdata->fetch_array(MYSQLI_ASSOC); $stmt->store_result(); if(password_verify($password, $row['password'])){ $_SESSION['user'] = $username; header('Location: formulario.html'); exit(); }else{ echo "Login Failed: (" . $stmt->errno .")" . $stmt->error; exit(); } } if (isset($_POST['registrar'])){ $password_hash = password_hash($password, PASSWORD_DEFAULT); if(!($stmt = $mysqli->prepare("INSERT INTO login_php (username, password) VALUES (?,?)"))){ echo "Prepare failed: (" . $mysqli->errno . ")" . $mysqli->error; } if(!$stmt->bind_param('ss', $username, $password_hash)){ echo "Binding paramaters failed:(" . $stmt->errno . ")" . $stmt->error; } if(!$stmt->execute()){ echo "Execute failed: (" . $stmt->errno .")" . $stmt->error; } if($stmt) { header('Location: form_login.html'); } else{ echo "Registration failed"; } } }else{ header('location:form_login.html'); } $contenido = ob_get_contents(); ob_end_clean(); $stmt->close(); $mysqli->close(); ?>
El algoritmo, coste y sal usados son devueltos como parte del hash. Por lo tanto, toda la información que es necesaria para verificar el hash, está incluida en él. Esto permite que la función password_verify() verifique el hash sin tener que almacenar por separado la información de la sal o del algoritmo. Uso alternativo
<?php
/**
 * Observe que la sal se genera aleatoriamente aquí.
 * No use nunca una sal estática o una que no se genere aleatoriamente.
 *
 * Para la GRAN mayoría de los casos de uso, dejar que password_hash genere la sal aleatoriamente
 */
$opciones = [
    'cost' => 11,
    'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM),
];
echo password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $opciones)."\n";
?>
Ejemplo de password_hash() buscando un buen coste
<?php
/**
 * Este código evaluará el servidor para determinar el coste permitido.
 * Se establecerá el mayor coste posible sin disminuir demasiando la velocidad
 * del servidor. 8-10 es una buena referencia, y más es bueno si los servidores
 * son suficientemente rápidos. El código que sigue tiene como objetivo un tramo de
 * ≤ 50 milisegundos, que es una buena referencia para sistemas con registros interactivos.
 */
$timeTarget = 0.05; // 50 milisegundos 

$coste = 8;
do {
    $coste++;
    $inicio = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $coste]);
    $fin = microtime(true);
} while (($fin - $inicio) < $timeTarget);

echo "Coste conveniente encontrado: " . $coste . "\n";
?>