Security

Argon2 passwords

Argon2id password hashes with the memory and passes you choose, any Argon2 hash verified, its parameters read, old ones marked for a rehash, and the cost timed on this server.

Version 1.0.0 · 5 functions · 2 crates

#passwords#auth#security#migration

Functions

What use argon2.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use argon2_hash, argon2_verify from argon2 (Quillo 0.6).

argon2_hash(password: str, memory_kib: int, iterations: int, parallelism: int) → stror an error

password hashed with Argon2id and a random salt, as a PHC string ($argon2id$v=19$m=65536,t=3,p=1$...). memory_kib is the memory in KiB (19456 is OWASP's floor, 65536 is 64 MiB), iterations the passes, parallelism the lanes.

argon2_verify(password: str, hash: str) → bool

Whether password matches hash, an Argon2id, Argon2i or Argon2d PHC string with any parameters (Quillo's own crypto.hash_password, PHP's password_hash, Django's, libsodium's). False for a wrong password or a hash that is not Argon2.

argon2_params(hash: str) → anyor an error

What a hash was made with: { algorithm, version, memory_kib, iterations, parallelism, salt_bytes, hash_bytes }.

argon2_needs_rehash(hash: str, memory_kib: int, iterations: int, parallelism: int) → bool

Whether a stored hash should be replaced, at the next sign-in, by one made with these parameters: true when it is not Argon2id version 19, or uses less memory, fewer passes or other lanes than asked. True also for a hash that is not Argon2 at all.

argon2_tune(target_ms: int, memory_kib: int, parallelism: int) → anyor an error

The passes that make one hash take about target_ms on this server with memory_kib and parallelism, measured: { iterations, ms, memory_kib, parallelism }. Run it once on the production machine, then hash with what it says.

Get it

$ quillo install argon2               # argon2.qwer into this folder$ quillo install argon2 --example     # and its example, argon2.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/argon2.qwer$ curl -O https://quillolang.org/plugins/files/argon2.qwe

Then, in your program, all of it or only what you call:

main.qwe
use argon2.qweruse argon2_hash, argon2_verify from argon2      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
argon2 = "0.5"rand_core = { version = "0.6", features = ["getrandom"] }

Example

argon2.qwe
// Passwords hashed with a stronger Argon2id than the default, and users moved to it as they sign in.////   quillo run argon2.qwe      then open http://localhost:8080/hash and http://localhost:8080/rehashuse "argon2.qwer"// 64 MiB, 3 passes, 1 lane: RFC 9106's second choice, for servers with memory to spare.let MEMORY = 65536let PASSES = 3let LANES = 1route GET /hash {    let hash = argon2_hash("correct horse battery staple", MEMORY, PASSES, LANES)?    {        starts: slice(hash, 0, 30),        verified: argon2_verify("correct horse battery staple", hash),        wrong: argon2_verify("Tr0ub4dor&3", hash),        params: argon2_params(hash)?,    }}// A hash from Quillo's own crypto.hash_password still verifies, and is marked for a rehash// with the stronger cost; one made with it is not.route GET /rehash {    let old = crypto.hash_password("hunter2")    let fresh = argon2_hash("hunter2", MEMORY, PASSES, LANES)?    {        builtin: argon2_params(old)?,        builtin_verified: argon2_verify("hunter2", old),        builtin_needs_rehash: argon2_needs_rehash(old, MEMORY, PASSES, LANES),        fresh_needs_rehash: argon2_needs_rehash(fresh, MEMORY, PASSES, LANES),        bcrypt_needs_rehash: argon2_needs_rehash("$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW", MEMORY, PASSES, LANES),    }}// How many passes make a hash take about 250 ms here, with 19 MiB.route GET /tune = argon2_tune(250, 19456, 1)?
$ quillo run argon2.qwe$ curl 'localhost:8080/hash'$ curl 'localhost:8080/rehash'

Source

argon2.qwer
//! Argon2 password hashes with the cost you choose: hash with tuned memory and passes, verify any Argon2 hash (id, i or d), read a hash's parameters, tell when one should be rehashed, and time the cost on this server.//! version: 1.0.0//!//! [dependencies]//! argon2 = "0.5"//! rand_core = { version = "0.6", features = ["getrandom"] }use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};use argon2::{Algorithm, Argon2, Params, Version};use rand_core::OsRng;use serde_json::{json, Value};fn params(memory_kib: i64, iterations: i64, parallelism: i64) -> Result<Params, String> {    if memory_kib < 8 || iterations < 1 || parallelism < 1 {        return Err("memory_kib is at least 8, iterations and parallelism at least 1".into());    }    if memory_kib > 4 * 1024 * 1024 {        return Err("memory_kib above 4 GiB is surely a mistake".into());    }    Params::new(memory_kib as u32, iterations as u32, parallelism as u32, None).map_err(|e| e.to_string())}fn hasher(memory_kib: i64, iterations: i64, parallelism: i64) -> Result<Argon2<'static>, String> {    Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params(memory_kib, iterations, parallelism)?))}fn parsed(hash: &str) -> Result<PasswordHash<'_>, String> {    let h = PasswordHash::new(hash.trim()).map_err(|e| format!("not a PHC hash string ($argon2id$v=19$m=...,t=...,p=...$salt$hash): {}", e))?;    if !h.algorithm.as_str().starts_with("argon2") {        return Err(format!("a {} hash, not Argon2", h.algorithm));    }    Ok(h)}fn param(h: &PasswordHash, name: &str) -> i64 {    h.params.get_decimal(name).map(|v| v as i64).unwrap_or(0)}/// `password` hashed with Argon2id and a random salt, as a PHC string (`$argon2id$v=19$m=65536,t=3,p=1$...`)./// `memory_kib` is the memory in KiB (19456 is OWASP's floor, 65536 is 64 MiB), `iterations` the passes, `parallelism` the lanes.pub fn argon2_hash(password: &str, memory_kib: i64, iterations: i64, parallelism: i64) -> Result<String, String> {    let salt = SaltString::generate(&mut OsRng);    let a = hasher(memory_kib, iterations, parallelism)?;    a.hash_password(password.as_bytes(), &salt).map(|h| h.to_string()).map_err(|e| e.to_string())}/// Whether `password` matches `hash`, an Argon2id, Argon2i or Argon2d PHC string with any parameters/// (Quillo's own `crypto.hash_password`, PHP's `password_hash`, Django's, libsodium's). False for a wrong password or a hash that is not Argon2.pub fn argon2_verify(password: &str, hash: &str) -> bool {    match parsed(hash) {        Ok(h) => Argon2::default().verify_password(password.as_bytes(), &h).is_ok(),        Err(_) => false,    }}/// What a hash was made with: `{ algorithm, version, memory_kib, iterations, parallelism, salt_bytes, hash_bytes }`.pub fn argon2_params(hash: &str) -> Result<Value, String> {    let h = parsed(hash)?;    let salt_bytes = h.salt.map(|s| {        let mut buf = [0u8; 64];        s.decode_b64(&mut buf).map(|b| b.len()).unwrap_or(0)    });    Ok(json!({        "algorithm": h.algorithm.as_str(),        "version": h.version.unwrap_or(0x10),        "memory_kib": param(&h, "m"),        "iterations": param(&h, "t"),        "parallelism": param(&h, "p"),        "salt_bytes": salt_bytes,        "hash_bytes": h.hash.map(|o| o.len()),    }))}/// Whether a stored hash should be replaced, at the next sign-in, by one made with these parameters:/// true when it is not Argon2id version 19, or uses less memory, fewer passes or other lanes than asked. True also for a hash that is not Argon2 at all.pub fn argon2_needs_rehash(hash: &str, memory_kib: i64, iterations: i64, parallelism: i64) -> bool {    let Ok(h) = parsed(hash) else { return true };    h.algorithm.as_str() != "argon2id"        || h.version != Some(0x13)        || param(&h, "m") < memory_kib        || param(&h, "t") < iterations        || param(&h, "p") != parallelism}/// The passes that make one hash take about `target_ms` on this server with `memory_kib` and `parallelism`, measured:/// `{ iterations, ms, memory_kib, parallelism }`. Run it once on the production machine, then hash with what it says.pub fn argon2_tune(target_ms: i64, memory_kib: i64, parallelism: i64) -> Result<Value, String> {    let target = target_ms.clamp(1, 10_000) as u128;    let mut t = 1i64;    loop {        let a = hasher(memory_kib, t, parallelism)?;        let mut out = [0u8; 32];        let start = std::time::Instant::now();        a.hash_password_into(b"tuning the cost", b"a salt of 16 b..", &mut out).map_err(|e| e.to_string())?;        let ms = start.elapsed().as_millis();        if ms >= target || t >= 64 {            return Ok(json!({ "iterations": t, "ms": ms as i64, "memory_kib": memory_kib, "parallelism": parallelism }));        }        // Time grows with the passes, so jump close to the target, then step.        let next = ((t as u128 * target) / ms.max(1)) as i64;        t = next.clamp(t + 1, t * 4);    }}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On Argon2 passwords: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.