mejor práctica para generar token aleatorio para contraseña olvidada

94

Quiero generar un identificador para la contraseña olvidada. Leí que puedo hacerlo usando la marca de tiempo con mt_rand (), pero algunas personas dicen que la marca de tiempo podría no ser única cada vez. Así que estoy un poco confundido aquí. ¿Puedo hacerlo usando la marca de tiempo con esto?

Pregunta
¿Cuál es la mejor práctica para generar tokens aleatorios / únicos de longitud personalizada?

Sé que se hacen muchas preguntas por aquí, pero me siento más confundido después de leer diferentes opiniones de diferentes personas.

afilado
fuente
@AlmaDoMundo: Una computadora no puede dividir el tiempo ilimitadamente.
juergen d
@juergend - lo siento, no entiendo eso.
Alma Do
Obtendrá la misma marca de tiempo si lo llama, por ejemplo, con un nano segundo de diferencia. Algunas funciones de tiempo, por ejemplo, solo pueden devolver el tiempo en pasos de 100 ns, algunas solo en pasos de segundos.
juergen d
@juergend ah, eso. Si. Mencioné la marca de tiempo 'clásica' con solo segundos. Pero si actúa como ha dicho, sí (eso solo nos deja una opción con la máquina del tiempo para obtener una marca de tiempo no única)
Alma Do
1
Cabeza arriba, la respuesta aceptada no aprovecha un CSPRNG .
Scott Arciszewski

Respuestas:

148

En PHP, use random_bytes(). Razón: está buscando la manera de obtener un token de recordatorio de contraseña y, si se trata de una credencial de inicio de sesión única, entonces realmente tiene datos para proteger (que es - cuenta de usuario completa)

Entonces, el código será el siguiente:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

Actualización : se refería a versiones anteriores de esta respuesta uniqid()y eso es incorrecto si hay una cuestión de seguridad y no solo de unicidad. uniqid()es esencialmente solo microtime()con algo de codificación. Hay formas sencillas de obtener predicciones precisas de la microtime()en su servidor. Un atacante puede emitir una solicitud de restablecimiento de contraseña y luego probar con un par de tokens probables. Esto también es posible si se usa more_entropy, ya que la entropía adicional es igualmente débil. Gracias a @NikiC y @ScottArciszewski por señalar esto.

Para obtener más detalles, consulte

Alma Do
fuente
21
Tenga en cuenta que random_bytes()solo está disponible a partir de PHP7. Para versiones anteriores, la respuesta de @yesitsme parece ser la mejor opción.
Gerald Schneider
3
@GeraldSchneider o random_compat , que es el polyfill para estas funciones que ha recibido la mayor cantidad de revisión por pares;)
Scott Arciszewski
Hice un campo varchar (64) en mi base de datos sql para almacenar este token. Establecí $ length en 64, pero la cadena devuelta tiene 128 caracteres. ¿Cómo puedo obtener una cadena con un tamaño fijo (aquí, 64 entonces)?
gordie
2
@gordie Establezca la longitud en 32, cada byte tiene 2 caracteres hexadecimales
JohnHoulderUK
¿Qué debería ser $length? ¿La identificación del usuario? ¿O que?
pila
71

Esto responde a la solicitud 'mejor aleatoria':

La respuesta 1 de Adi de Security.StackExchange tiene una solución para esto:

Asegúrese de tener soporte para OpenSSL, y nunca se equivocará con este resumen

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, lunes 12 de noviembre de 2018, Celeritas, "Generación de un token imposible de adivinar para correos electrónicos de confirmación", 20 de septiembre de 2013 a las 7:06, https://security.stackexchange.com/a/40314/

Si, soy yo
fuente
24
openssl_random_pseudo_bytes($length)- soporte: PHP 5> = 5.3.0, ....................................... ................... (Para PHP 7 y posteriores, use random_bytes($length)) ...................... .................... (Para PHP por debajo de 5.3 - no use PHP por debajo de 5.3)
jave.web
54

La versión anterior de la respuesta aceptada ( md5(uniqid(mt_rand(), true))) es insegura y solo ofrece aproximadamente 2 ^ 60 resultados posibles, dentro del rango de una búsqueda de fuerza bruta en aproximadamente una semana para un atacante de bajo presupuesto:

Dado que una clave DES de 56 bits puede ser forzada en aproximadamente 24 horas , y un caso promedio tendría aproximadamente 59 bits de entropía, podemos calcular 2 ^ 59/2 ^ 56 = aproximadamente 8 días. Dependiendo de cómo se implemente esta verificación de token, podría ser posible prácticamente filtrar información de tiempo e inferir los primeros N bytes de un token de reinicio válido .

Dado que la pregunta es sobre "mejores prácticas" y comienza con ...

Quiero generar un identificador para la contraseña olvidada

... podemos inferir que este token tiene requisitos de seguridad implícitos. Y cuando agrega requisitos de seguridad a un generador de números aleatorios, la mejor práctica es usar siempre un generador de números pseudoaleatorios criptográficamente seguro (abreviado CSPRNG).


Usando un CSPRNG

En PHP 7, puede usar bin2hex(random_bytes($n))(donde $nes un número entero mayor que 15).

En PHP 5, puede utilizar random_compatpara exponer la misma API.

Alternativamente, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))si ha ext/mcryptinstalado. Otra buena frase es bin2hex(openssl_random_pseudo_bytes($n)).

Separar la búsqueda del validador

Extrayendo de mi trabajo anterior sobre cookies seguras "recordarme" en PHP , la única forma efectiva de mitigar la filtración de tiempo antes mencionada (normalmente introducida por la consulta de la base de datos) es separar la búsqueda de la validación.

Si su tabla se ve así (MySQL) ...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

... necesitas agregar una columna más selector, así:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

Use un CSPRNG Cuando se emite un token de restablecimiento de contraseña, envíe ambos valores al usuario, almacene el selector y un hash SHA-256 del token aleatorio en la base de datos. Use el selector para tomar el hash y la ID de usuario, calcule el hash SHA-256 del token que el usuario proporciona con el que está almacenado en la base de datos hash_equals().

Código de ejemplo

Generando un token de reinicio en PHP 7 (o 5.6 con random_compat) con PDO:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

Verificación del token de restablecimiento proporcionado por el usuario:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) {
        // The reset token is valid. Authenticate the user.
    }
    // Remove the token from the DB regardless of success or failure.
}

Estos fragmentos de código no son soluciones completas (evité la validación de entrada y las integraciones del marco), pero deberían servir como ejemplo de qué hacer.

Scott Arciszewski
fuente
Cuando verifica el token de restablecimiento proporcionado por el usuario, ¿por qué utiliza la representación binaria del token aleatorio? ¿Crees que será posible (y seguro?): 1) almacenar en la base de datos el valor hexadecimal con hash del token con hash('sha256', bin2hex($token)), 2) verificar con if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? ¡Gracias!
Guicara
Sí, comparar cadenas hexadecimales también es seguro. Realmente es una cuestión de preferencia. Prefiero hacer todas las operaciones criptográficas en binario sin formato y solo convertirlo a hex / base64 para transmisión o almacenamiento.
Scott Arciszewski
Hola Scott, es básicamente una pregunta no solo por tu respuesta, sino por todo el artículo sobre la función "Recuérdame". ¿Por qué no utilizar el único idcomo selector? Quiero decir, la clave principal de la account_recoverymesa. No necesitamos una capa adicional de seguridad para el selector, ¿verdad? ¡Gracias!
Andre Polykanine
id:secretestá bien. selector:secretestá bien. secreten sí mismo no lo es. El objetivo es separar la consulta de la base de datos (que tiene pérdidas de tiempo) del protocolo de autenticación (que debe ser de tiempo constante).
Scott Arciszewski
¿Hay algún daño en usar openssl_random_pseudo_bytesen random_bytessu lugar si está ejecutando PHP 5.6? Además, ¿no debería agregar solo el selector y no el validador en la cadena de consulta del enlace?
Greg
7

También puede usar DEV_RANDOM, donde 128 = 1/2 de la longitud del token generado. El código siguiente genera 256 tokens.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T
fuente
4
Sugeriría MCRYPT_DEV_URANDOMmás MCRYPT_DEV_RANDOM.
Scott Arciszewski
2

Esto puede ser útil siempre que necesite un token muy, muy aleatorio

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Calif
fuente