WEB3DEV Español

Cover image for Hashing de Contraseñas con PBKDF2 en Rust utilizando Ring
Banana Labs
Banana Labs

Posted on

Hashing de Contraseñas con PBKDF2 en Rust utilizando Ring

La biblioteca de criptografía Ring proporciona una implementación de PBKDF2, una función de derivación de clave estandarizada que se puede utilizar para hashear contraseñas de manera segura. Usando el módulo ring::pbkdf2, podemos derivar un hash de contraseña y luego verificar si un hash coincide o no con una contraseña dada.

Introducción a PBKDF2

La Función de Derivación de Clave Basada en Contraseña 2 (PBKDF2) está especificada en la Sección 5.2 de la RFC 2898. PBKDF2 no es un algoritmo muy moderno, pero, todavía se considera seguro siempre que se utilice correctamente con parámetros cuidadosamente seleccionados. PBKDF2 aplica una función pseudoaleatoria (como un HMAC) a la contraseña de entrada y al valor de sal, y repite el proceso un número de veces para producir una clave derivada que, luego puede utilizarse como clave criptográfica en operaciones posteriores. El trabajo computacional añadido de las numerosas iteraciones hace que el descifrado de contraseñas sea muy difícil. PBKDF2 también es recomendado por NIST y tiene implementaciones que se conforman con FIPS-140.

El documento RFC define la función PBKDF2 de la siguiente manera:

PBKDF2 (P, S, c, dkLen) -> DK
Enter fullscreen mode Exit fullscreen mode

Estos son los parámetros de la función PBKDF2:

Options:        PRF        función pseudoaleatoria subyacente   
                           (hLen denota la longitud en octetos      
                           de la salida de la función    
                           pseudoaleatoria)


Input:          P          contraseña, una cadena de octetos
                S          sal, una cadena de octetos
                c          número de iteraciones, un número   
                           entero positivo
                dkLen      longitud prevista en octetos de la
                           clave derivada, un entero positivo,                
                           en la mayoría (2 ^ 32 - 1) * hLen

Output:         DK         clave derivada, una cadena    
                           dkLen-octet

Enter fullscreen mode Exit fullscreen mode

Directrices de Uso de PBKDF2

  • Las contraseñas débiles pueden ser vulnerables a ataques de fuerza bruta, por lo que se debe requerir a los usuarios que establezcan contraseñas fuertes que sean largas y contengan suficiente entropía.
  • El uso de una sal de baja entropía puede hacer que el hash (clave derivada) sea vulnerable a ataques de pre-cálculo utilizando tablas arco iris (rainbow tables). El parámetro de sal mejora la seguridad de PBKDF2 cambiando la salida del hash para cualquier contraseña de entrada dada. La sal debe ser impredecible y seleccionada para ser única por usuario y por base de datos (esta parte a veces se llama pimienta ‘pepper’).
  • El uso de un número bajo de iteraciones también puede hacer que el hash sea vulnerable a ataques de fuerza bruta. Cuanto mayor sea el número de iteraciones seleccionadas, más tiempo tarda en ejecutarse la función PBKDF2 y, por lo tanto, requiere más trabajo para descifrar las contraseñas con éxito. El número de iteraciones que debes seleccionar es un objetivo móvil y aumenta con el tiempo a medida que aumenta el poder de la CPU. OWASP proporciona un buen recurso sobre los parámetros recomendados aquí. Actualmente, para PBKDF2-HMAC-SHA256, el número de iteraciones recomendado es 600000 y para PBKDF2-HMAC-SHA512 es 210000.
  • La longitud de la clave de salida necesita ser suficientemente larga para prevenir ataques de fuerza bruta y, por lo tanto, se recomienda una longitud de clave mínima de al menos 128 bits. Generalmente, la longitud de salida debe establecerse igual a la longitud de la función del hash elegido.
  • Cuando PBKDF2 se utiliza con un HMAC, puede ser necesario prehashear manualmente la contraseña en algunos casos para prevenir ciertas vulnerabilidades de denegación de servicio. Esto puede ocurrir cuando la contraseña de entrada es más larga que el tamaño de bloque de la función de hash. Vea aquí para más detalles.

Resumen de Tipos

El módulo ring::pbkdf2 contiene los siguientes tipos y funciones:

struct Algorithm - El tipo de algoritmo PBKDF2. Se soportan los algoritmos PBKDF2_HMAC_SHA1, PBKDF2_HMAC_SHA256, PBKDF2_HMAC_SHA384 y PBKDF2_HMAC_SHA512.

fn derive - El algoritmo PBKDF2 utilizado para derivar un hash de contraseña a partir de una contraseña dada. Pasamos el algoritmo elegido, el número de iteraciones a utilizar, una sal y el secreto (contraseña). El valor de hash se almacena en el array de bytes dado, out. El tamaño del array out determina el tamaño del valor de hash devuelto.

pub fn derive(
    algorithm: Algorithm, 
    iterations: NonZeroU32, 
    salt: &[u8], 
    secret: &[u8], 
    out: &mut [u8]
)
Enter fullscreen mode Exit fullscreen mode

fn verify - Comprueba si un secreto dado (contraseña) coincide con un hash derivado previamente. Pasamos el algoritmo, el número de iteraciones, la sal, el secreto a comprobar y el hash que se derivó previamente utilizando la función pbkdf2::derive.

pub fn verify(
    algorithm: Algorithm, 
    iterations: NonZeroU32, 
    salt: &[u8], 
    secret: &[u8], 
    previously_derived: &[u8]
) -> Result<(), Unspecified>
Enter fullscreen mode Exit fullscreen mode

Importaciones en Rust

Comencemos importando los tipos requeridos en nuestro proyecto.

use std::num::NonZeroU32;
use ring::digest::SHA256_OUTPUT_LEN;
use ring::digest::SHA512_OUTPUT_LEN;
use ring::pbkdf2;
use ring::pbkdf2::Algorithm;
use ring::pbkdf2::PBKDF2_HMAC_SHA1;
use ring::pbkdf2::PBKDF2_HMAC_SHA256;
use ring::pbkdf2::PBKDF2_HMAC_SHA384;
use ring::pbkdf2::PBKDF2_HMAC_SHA512
Enter fullscreen mode Exit fullscreen mode

Preparar las Iteraciones, la Sal y el Secreto

En el código a continuación, declaramos los parámetros que se pasarán a la función pbkdf2::derive. En este ejemplo, estamos utilizando el algoritmo PBKDF2_HMAC_SHA256 y, por lo tanto, el número de iteraciones se establece en 600000 según lo recomendado por OWASP. La variable de iteraciones es de tipo NonZeroU32 de la biblioteca estándar de Rust, lo que evita establecer un conteo de iteraciones de cero.

const PBKDF2_HMAC_SHA256_ITERATIONS: u32 = 600_000; // número recomendado por OWASP para PBKDF2 con SHA256

let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA256_ITERATIONS).unwrap();
let salt = b"random salt"; // esto debe generarse aleatoriamente, utilizando algún componente específico del usuario y un componente específico de la base de datos
let secret = b"strong password"; // selecciona una contraseña segura
println!("Secret/password value: {}", hex::encode(secret)); // no imprimas esto en producción
Enter fullscreen mode Exit fullscreen mode

Derivar el Hash de la Contraseña y Almacenarlo

Luego, llamamos a la función pbkdf2::derive con nuestros parámetros elegidos y almacenamos el hash en un array de bytes. En este ejemplo, el tamaño del hash de salida será de la misma longitud que la salida de SHA256, pero podríamos hacerlo más largo si fuera necesario. La longitud se establece eligiendo el tamaño del array password_hash.

let mut password_hash = [0u8; SHA256_OUTPUT_LEN]; // inicializa con ceros
pbkdf2::derive(PBKDF2_HMAC_SHA256, iterations, salt, secret, &mut password_hash);
println!("Password hash: {}", hex::encode(password_hash)); // no imprimas esto en producción
Enter fullscreen mode Exit fullscreen mode

Verificar si una Contraseña Coincide con el Hash de la Contraseña

Usando los mismos parámetros de entrada que antes, podemos llamar a la función pbkdf2::verify para comprobar si una contraseña dada coincide con el hash derivado anteriormente. Si la contraseña coincide con el hash, la función verify devuelve un Result::Ok que contiene un (), de lo contrario, devuelve un Result::Err que contiene error::Unspecified (definido en el módulo de error de Ring).

pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, secret, &password_hash).unwrap(); // caso exitoso 
pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // caso fallado
Enter fullscreen mode Exit fullscreen mode

Código de Ejemplo Completo

Aquí está el código de ejemplo completo como referencia. El ejemplo del escenario 2, a continuación, utiliza PBKDF2_HMAC_SHA512 con el número recomendado de iteraciones de 210000 y utiliza un tamaño de hash de salida igual al tamaño de SHA512.

use std::num::NonZeroU32;
use ring::digest::SHA256_OUTPUT_LEN;
use ring::digest::SHA512_OUTPUT_LEN;
use ring::pbkdf2;
use ring::pbkdf2::Algorithm;
use ring::pbkdf2::PBKDF2_HMAC_SHA1;
use ring::pbkdf2::PBKDF2_HMAC_SHA256;
use ring::pbkdf2::PBKDF2_HMAC_SHA384;
use ring::pbkdf2::PBKDF2_HMAC_SHA512;

fn main() {
    // escenario 1 - PBKDF2_HMAC_SHA256
    const PBKDF2_HMAC_SHA256_ITERATIONS: u32 = 600_000; //número recomendado por OWASP para PBKDF2 con SHA256

    // Preparar iteraciones, sal y secreto.
    let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA256_ITERATIONS).unwrap();
    let salt = b"random salt"; // esto debe generarse aleatoriamente, utilizando algún componente específico del usuario y un componente específico de la base de datos
    let secret = b"strong password"; // selecciona una contraseña segura
    println!("Secret/password value: {}", hex::encode(secret)); // no imprimas esto en producción

    // Derivar el hash de la contraseña y almacenar
    let mut password_hash = [0u8; SHA256_OUTPUT_LEN]; // inicializa con ceros
    pbkdf2::derive(PBKDF2_HMAC_SHA256, iterations, salt, secret, &mut password_hash);
    println!("Password hash: {}", hex::encode(password_hash)); //no imprimas esto en producción

    // Verificar si una contraseña coincide o no con el hash de la contraseña almacenada
    pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, secret, &password_hash).unwrap(); // caso exitoso
    //pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // caso fallado


    // escenario 2 - PBKDF2_HMAC_SHA512
    const PBKDF2_HMAC_SHA512_ITERATIONS: u32 = 210_000; // número recomendado por OWASP para PBKDF2 con SHA256

    // Preparar iteraciones, sal y secreto
    let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA512_ITERATIONS).unwrap();
    let salt = b"random salt"; // esto debe generarse aleatoriamente, utilizando algún componente específico del usuario y un componente específico de la base de datos
    let secret = b"strong password"; // selecciona una contraseña segura
    println!("Secret/password value: {}", hex::encode(secret)); // no imprimas esto en producción

    // Derivar el hash de la contraseña y almacenar
    let mut password_hash = [0u8; SHA512_OUTPUT_LEN]; // inicializar con ceros
    pbkdf2::derive(PBKDF2_HMAC_SHA512, iterations, salt, secret, &mut password_hash);
    println!("Password hash: {}", hex::encode(password_hash)); //  no imprimas esto en producción

    // Verificar si una contraseña coincide o no con el hash de la contraseña almacenada
    pbkdf2::verify(PBKDF2_HMAC_SHA512, iterations, salt, secret, &password_hash).unwrap(); // caso exitoso
    //pbkdf2::verify(PBKDF2_HMAC_SHA512, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // caso fallado 
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

En esta publicación, presentamos el algoritmo PBKDF2, explicamos cómo funciona y describimos cómo elegir correctamente los parámetros para la función PBKDF2. Luego, explicamos cómo usar el módulo Ring pbkdf2 para derivar un hash de contraseña utilizando la función pbkdf2::derive y verificar que una contraseña coincide con un hash de contraseña utilizando la función pbkdf2::verify. Los ejemplos, muestran dos escenarios utilizando los algoritmos PBKDF2 más comúnmente utilizados; PBKDF2_HMAC_SHA256 y PBKDF2_HMAC_SHA512.



Este artículo es una traducción realizada por @bananlabs. Puedes encontrar el artículo original aquí

Discussion (0)