Back to flin
flin

Hashing de contrasenas con Argon2 integrado en FLIN

Como FLIN incluye el hashing de contrasenas con Argon2id como funcion nativa -- sin debates sobre bcrypt, sin configuracion, solo hash_password() y verify_password() con valores seguros por defecto.

Thales & Claude | March 30, 2026 8 min flin
EN/ FR/ ES
flinrust

Toda aplicacion web que almacena contrasenas debe aplicarles hash. Esto no es opcional. Almacenar contrasenas en texto plano es la falla de seguridad mas basica, y sigue ocurriendo -- LinkedIn perdio 117 millones de contrasenas hasheadas en 2012, y muchas fueron descifradas porque usaban SHA-1 sin sal.

El problema no es que los desarrolladores desconozcan el hashing de contrasenas. El problema es que elegir y configurar un algoritmo de hashing requiere una experiencia que la mayoria de los desarrolladores no posee. ¿Deberia usar bcrypt o scrypt? ¿Que factor de costo? ¿Cuantas iteraciones? ¿Que parametro de memoria? La respuesta depende de su hardware, su modelo de amenazas y el ano actual.

FLIN toma esta decision por usted. La funcion hash_password() usa Argon2id con parametros ajustados. La funcion verify_password() realiza una comparacion segura contra ataques de temporizado. No hay configuracion, no hay seleccion de algoritmo y no hay forma de usar MD5 accidentalmente.

Por que Argon2id

Argon2 gano la Password Hashing Competition (PHC) en 2015. Fue disenado especificamente para el hashing de contrasenas, a diferencia de bcrypt (1999, disenado para hashing de proposito general) y scrypt (2009, disenado para derivacion de claves).

Argon2 viene en tres variantes:

  • Argon2d -- optimizado contra ataques GPU, vulnerable a ataques de canal lateral
  • Argon2i -- optimizado contra ataques de canal lateral, mas debil contra ataques GPU
  • Argon2id -- hibrido, combina las fortalezas de ambos

FLIN usa Argon2id porque proporciona la mejor proteccion general. OWASP lo recomienda como primera opcion para el hashing de contrasenas. El IETF lo estandarizo en el RFC 9106.

Dos funciones, cero configuracion

La interfaz para el desarrollador FLIN son dos funciones:

flin// Hashear una contrasena
hash = hash_password("correct horse battery staple")
// Retorna: "$argon2id$v=19$m=65536,t=3,p=4$..."

// Verificar una contrasena
is_valid = verify_password("correct horse battery staple", hash)
// Retorna: true o false

Esa es toda la API. Sin seleccion de algoritmo. Sin ajuste de parametros. Sin generacion de sal. Sin decisiones de formato de codificacion. La funcion maneja todo internamente.

Los parametros por defecto

La implementacion de Argon2id en FLIN usa estos parametros:

rustconst ARGON2_MEMORY: u32 = 65536;     // 64 MB
const ARGON2_ITERATIONS: u32 = 3;      // 3 pasadas
const ARGON2_PARALLELISM: u32 = 4;     // 4 hilos paralelos
const ARGON2_OUTPUT_LEN: usize = 32;   // Hash de 256 bits
const ARGON2_SALT_LEN: usize = 16;     // Sal de 128 bits

Estos parametros se basan en las recomendaciones de OWASP 2024 para Argon2id:

ParametroValorProposito
Memoria64 MBHace costosos los ataques GPU/ASIC
Iteraciones3Aumenta el tiempo de computo
Paralelismo4Usa multiples nucleos de CPU
Longitud de salida32 bytesHash de 256 bits
Longitud de sal16 bytesSal aleatoria de 128 bits

Con estos parametros, hashear una contrasena toma aproximadamente 200 milisegundos en un servidor moderno. Esto es lo suficientemente rapido para que los usuarios no noten el retraso durante el inicio de sesion, pero lo suficientemente lento para que un atacante que intente descifrar una base de datos robada necesite aproximadamente 57 anos de tiempo GPU por contrasena.

La implementacion

La implementacion en Rust en el runtime de FLIN usa el crate argon2 con la siguiente estructura:

rustuse argon2::{Argon2, PasswordHasher, PasswordVerifier};
use argon2::password_hash::{SaltString, rand_core::OsRng};

pub fn hash_password(password: &str) -> Result<String, SecurityError> {
    let salt = SaltString::generate(&mut OsRng);

    let argon2 = Argon2::new(
        argon2::Algorithm::Argon2id,
        argon2::Version::V0x13,
        argon2::Params::new(
            ARGON2_MEMORY,
            ARGON2_ITERATIONS,
            ARGON2_PARALLELISM,
            Some(ARGON2_OUTPUT_LEN),
        )?,
    );

    let hash = argon2
        .hash_password(password.as_bytes(), &salt)?
        .to_string();

    Ok(hash)
}

pub fn verify_password(password: &str, hash: &str) -> Result<bool, SecurityError> {
    let parsed_hash = argon2::PasswordHash::new(hash)
        .map_err(|_| SecurityError::InvalidHash)?;

    let argon2 = Argon2::default();

    Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}

Varios detalles de seguridad merecen destacarse:

Generacion de sal aleatoria. Cada contrasena recibe una sal unica de 16 bytes del generador de numeros aleatorios criptograficos del sistema operativo (OsRng). Dos usuarios con la misma contrasena obtienen hashes diferentes. Esto derrota los ataques de tabla arcoiris.

Comparacion segura contra temporizado. La funcion verify_password usa internamente una comparacion de tiempo constante. Un atacante no puede determinar cuantos caracteres de la contrasena son correctos midiendo el tiempo de respuesta.

Formato de hash autodescriptivo. La cadena de salida $argon2id$v=19$m=65536,t=3,p=4$... codifica el algoritmo, la version y todos los parametros. Si alguna vez actualizamos los parametros por defecto, verify_password aun puede verificar hashes creados con los parametros anteriores porque los parametros estan almacenados en el propio hash.

Parametros personalizados

Para aplicaciones con requisitos especificos, FLIN permite la personalizacion de parametros:

flin// Mayor seguridad (mas lento, mas memoria)
hash = hash_password(password, {
    algorithm: "argon2id",
    memory: 131072,        // 128 MB
    iterations: 4,
    parallelism: 8
})

// Menor uso de recursos (para entornos restringidos)
hash = hash_password(password, {
    algorithm: "argon2id",
    memory: 32768,         // 32 MB
    iterations: 3,
    parallelism: 2
})

// Compatibilidad con bcrypt (para migrar sistemas legados)
hash = hash_password(password, {
    algorithm: "bcrypt",
    cost: 12
})

Los parametros personalizados son intencionalmente mas dificiles de usar que los predeterminados. El valor por defecto es una llamada a funcion con un argumento. Los parametros personalizados requieren un objeto de opciones. Este diseno de API orienta a los desarrolladores hacia el valor seguro por defecto.

Migracion de contrasenas

Al migrar desde otro framework, los hashes de contrasenas existentes pueden usar bcrypt, scrypt o incluso MD5 simple. FLIN soporta la migracion transparente:

flinfn login(email, password) {
    user = User.where(email == email).first
    if user == none {
        return error(401, "Invalid credentials")
    }

    // verify_password detecta automaticamente el algoritmo del formato del hash
    if !verify_password(password, user.password) {
        return error(401, "Invalid credentials")
    }

    // Si el hash usa un algoritmo antiguo, re-hashear con Argon2id
    if !user.password.starts_with("$argon2id$") {
        user.password = hash_password(password)
        save user
    }

    create_session(user)
}

La funcion verify_password detecta el algoritmo del prefijo del formato del hash ($2b$ para bcrypt, $argon2id$ para Argon2id) y usa el algoritmo de verificacion apropiado. La funcion de inicio de sesion luego re-hashea con Argon2id en una autenticacion exitosa, migrando gradualmente a todos los usuarios activos al algoritmo mas fuerte.

Lo que los desarrolladores no tienen que pensar

La lista de decisiones de seguridad que FLIN toma por el desarrollador es extensa:

Generacion de sal. Cada hash recibe una sal criptograficamente aleatoria. No existe una funcion hash_password(password, "mi-sal"). No se pueden reutilizar sales. No se pueden usar sales predecibles.

Seleccion de algoritmo. Argon2id es el predeterminado. No se puede usar accidentalmente MD5, SHA-256 o texto plano. La opcion bcrypt existe solo para migracion de sistemas legados, no para nuevas aplicaciones.

Ajuste de parametros. Los parametros por defecto estan ajustados para hardware de 2024-2026. Seran actualizados en futuras versiones de FLIN a medida que aumente la potencia de computo.

Formato de codificacion. El formato de cadena PHC se usa automaticamente. No hay confusion entre bytes crudos, codificacion hexadecimal y codificacion Base64.

Ataques de temporizado. La comparacion es siempre de tiempo constante. No hay un hash == stored_hash que filtre informacion a traves del temporizado.

Manejo de errores. Si el hashing falla (por ejemplo, por falta de memoria debido a la asignacion de 64 MB), el error es explicito y la funcion nunca retorna un hash parcial o vacio.

Benchmarks

En un servidor estandar (4 nucleos, 8 GB de RAM), la implementacion de Argon2id en FLIN produce estos numeros:

OperacionTiempoMemoria
Hash (parametros por defecto)190 ms64 MB pico
Verificacion (parametros por defecto)180 ms64 MB pico
Hash (compatibilidad bcrypt, costo 12)250 ms< 1 MB

Para un endpoint de inicio de sesion, 190 ms de verificacion de contrasena son invisibles para el usuario. El tiempo total de la solicitud (incluyendo busqueda en base de datos, creacion de sesion y respuesta) es tipicamente inferior a 250 ms.

Para un atacante de fuerza bruta, 190 ms por intento significa aproximadamente 5 intentos por segundo por nucleo de CPU. Con una contrasena de 10 caracteres que contenga letras minusculas, mayusculas y digitos, el espacio de busqueda es 62^10 (aproximadamente 8.4 x 10^17). A 5 intentos por segundo, la busqueda exhaustiva tomaria 5.3 mil millones de anos.

El patron del mundo real

Asi es como aparece el hashing de contrasenas en un flujo completo de autenticacion FLIN:

flin// Registro
route POST {
    validate {
        email: text @required @email
        password: text @required @minLength(8)
        name: text @required @minLength(2)
    }

    existing = User.where(email == body.email).first
    if existing != none {
        return error(409, "Email already registered")
    }

    user = User {
        email: body.email,
        password: hash_password(body.password),
        name: body.name,
        role: "user"
    }
    save user

    create_session(user)
    response { status: 201, body: user }
}

Una llamada a funcion. Una linea. La contrasena se hashea con Argon2id, se le aplica sal con aleatoriedad criptografica y se almacena en un formato autodescriptivo que puede verificarse indefinidamente. El desarrollador no necesita saber nada sobre criptografia para hacerlo correctamente.

Ese es el punto. La seguridad no deberia requerir experiencia. Deberia ser el valor por defecto.

En el proximo articulo, cubrimos la autenticacion JWT -- como FLIN crea y verifica JSON Web Tokens en tres lineas de codigo.


Esta es la Parte 107 de la serie "Como construimos FLIN", que documenta como un CEO en Abidjan y un CTO de IA disenaron y construyeron un lenguaje de programacion desde cero.

Navegacion de la serie: - [106] Seguridad por diseno: OWASP Top 10 en el lenguaje - [107] Hashing de contrasenas con Argon2 integrado en FLIN (estas aqui) - [108] Autenticacion JWT en 3 lineas de FLIN - [109] Limitacion de tasa y cabeceras de seguridad

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles