Legacy passwords
Check passwords against Django, Flask/Werkzeug and WordPress hashes, to sign in users moved from those apps and rehash them with Argon2.
Version 1.0.0 · 3 functions · 8 crates
Functions
What use legacy_passwords.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use legacy_kind, legacy_verify from legacy_passwords (Quillo 0.6).
legacy_kind(hash: str) → str?What kind of hash hash is: "django_pbkdf2_sha256", "django_pbkdf2_sha1", "werkzeug_pbkdf2", "werkzeug_scrypt", "wordpress_phpass" ($P$ and phpBB's $H$), "wordpress_bcrypt" ($wp$2y$, WordPress 6.8 and later), "bcrypt", or null.
legacy_verify(password: str, hash: str) → boolor an errorWhether password matches hash, of any kind legacy_kind knows. False for a wrong password; an error for a hash of another kind or a broken one (Django's argon2 and bcrypt_sha256 hashes are not read). Each check takes as long as the old app took: Django's 600,000 rounds, about a quarter of a second.
legacy_django_hash(password: str, salt: str, iterations: int) → stror an errorA Django-style pbkdf2_sha256$rounds$salt$hash for password with salt (letters and digits) and iterations (Django 5 uses 870,000): to make test users, or to keep writing hashes a Django app beside yours can read.
Get it
$ quillo install legacy_passwords # legacy_passwords.qwer into this folder$ quillo install legacy_passwords --example # and its example, legacy_passwords.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/legacy_passwords.qwer$ curl -O https://quillolang.org/plugins/files/legacy_passwords.qweThen, in your program, all of it or only what you call:
use legacy_passwords.qweruse legacy_kind, legacy_verify from legacy_passwords // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }sha2 = "0.10"sha1 = "0.10"md-5 = "0.10"hmac = "0.12"scrypt = { version = "0.11", default-features = false }bcrypt = "0.15"base64 = "0.22"Example
// Signing in users moved from Django, Flask and WordPress, and moving each to Argon2 at their first sign-in.//// quillo run legacy_passwords.qwe then open http://localhost:8080/loginuse "legacy_passwords.qwer"// Users imported from the old apps; every one of them has the password "hunter22".let imported = { "ada@example.com": "pbkdf2_sha256$600000$Qx7rM2pLk9sTz4wB$lFgxcYiyCVJqMgHrH6ehgm+JAuCPG6MOEBJDeQwz6eo=", "grace@example.com": "pbkdf2:sha256:600000$n8VbXq2Lr5Tz$0c772f8236ccfd7d2cd4fcb43ce9f973c2f9cba33acf80aa69be88bc950f1be9", "linus@example.com": "scrypt:32768:8:1$Hk3sPq9WmZ1aLx7c$b1893f17c3be43489677c72e05b1eaf0da5184000b94a7dbe9c558c5c4b1f29b94ad2713a5db6ab5c6b6d439fef6996a791ab0c628b89f84190cec6cb011b48b", "margaret@example.com": "$P$BaB3dE5gH.mccMVJmlyp7r9PZLhjXx1", "alan@example.com": "$wp$2y$10$kyAYiLv5VJOlGNaouYMg5.//TjnpCDzR9BdWyv2j8NYFSWidfXCiG",}fn sign_in(email: str, password: str) { let stored = imported[email] ?? null if stored == null { return { ok: false } } if starts_with(stored, "$argon2id$") { return { ok: crypto.verify_password(password, stored) } } if !legacy_verify(password, stored)? { return { ok: false, from: legacy_kind(stored) } } // Right: from now on keep an Argon2 hash (here only returned; a real app writes it to the users table). { ok: true, from: legacy_kind(stored), rehashed: starts_with(crypto.hash_password(password), "$argon2id$") }}route GET /login = keys(imported) | map((email) => { email, ...sign_in(email, "hunter22") })route GET /wrong = keys(imported) | map((email) => sign_in(email, "hunter2").ok)route GET /make = { hash: legacy_django_hash("hunter22", "Qx7rM2pLk9sTz4wB", 600000)? }$ quillo run legacy_passwords.qwe$ curl 'localhost:8080/login'$ curl 'localhost:8080/wrong'Source
//! Check passwords against hashes from Django, Flask/Werkzeug and WordPress, to sign in users moved from those apps and rehash them with Quillo's Argon2.//! version: 1.0.0//!//! [dependencies]//! pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }//! sha2 = "0.10"//! sha1 = "0.10"//! md-5 = "0.10"//! hmac = "0.12"//! scrypt = { version = "0.11", default-features = false }//! bcrypt = "0.15"//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use hmac::{Hmac, Mac};use md5::{Digest, Md5};const ITOA64: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";/// At most this many PBKDF2 rounds are run for one check, so a hash planted in the database cannot hold a core for minutes.const MAX_ROUNDS: u32 = 5_000_000;fn same(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0}fn unhex(s: &str) -> Option<Vec<u8>> { if s.len() % 2 != 0 { return None; } (0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()}fn rounds(s: &str) -> Result<u32, String> { let n: u32 = s.parse().map_err(|_| format!("`{}` is not a number of rounds", s))?; if n == 0 || n > MAX_ROUNDS { return Err(format!("{} rounds is outside 1 to {}", n, MAX_ROUNDS)); } Ok(n)}fn pbkdf2(alg: &str, password: &[u8], salt: &[u8], n: u32, len: usize) -> Result<Vec<u8>, String> { let mut out = vec![0u8; len]; match alg { "sha256" => pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password, salt, n, &mut out), "sha512" => pbkdf2::pbkdf2_hmac::<sha2::Sha512>(password, salt, n, &mut out), "sha1" => pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password, salt, n, &mut out), other => return Err(format!("PBKDF2 with {} is not known here", other)), } Ok(out)}fn encode64(input: &[u8]) -> String { let count = input.len(); let mut out = String::new(); let mut i = 0; loop { let mut v = input[i] as u32; i += 1; out.push(ITOA64[(v & 0x3f) as usize] as char); if i < count { v |= (input[i] as u32) << 8; } out.push(ITOA64[((v >> 6) & 0x3f) as usize] as char); if i >= count { break; } i += 1; if i < count { v |= (input[i] as u32) << 16; } out.push(ITOA64[((v >> 12) & 0x3f) as usize] as char); if i >= count { break; } i += 1; out.push(ITOA64[((v >> 18) & 0x3f) as usize] as char); if i >= count { break; } } out}fn phpass(password: &str, hash: &str) -> Result<bool, String> { if hash.len() != 34 || !hash.is_ascii() { return Err("a phpass hash is 34 characters".into()); } let log2 = ITOA64.iter().position(|&c| c == hash.as_bytes()[3]).ok_or("not a phpass hash")?; if !(7..=20).contains(&log2) { return Err(format!("phpass with 2^{} rounds is outside 2^7 to 2^20", log2)); } let salt = &hash.as_bytes()[4..12]; let mut h = Md5::new().chain_update(salt).chain_update(password).finalize(); for _ in 0..(1u32 << log2) { h = Md5::new().chain_update(h).chain_update(password).finalize(); } Ok(same(format!("{}{}", &hash[..12], encode64(&h)).as_bytes(), hash.as_bytes()))}/// What kind of hash `hash` is: "django_pbkdf2_sha256", "django_pbkdf2_sha1", "werkzeug_pbkdf2", "werkzeug_scrypt",/// "wordpress_phpass" (`$P$` and phpBB's `$H$`), "wordpress_bcrypt" (`$wp$2y$`, WordPress 6.8 and later), "bcrypt", or null.pub fn legacy_kind(hash: &str) -> Option<String> { let h = hash.trim(); let k = if h.starts_with("pbkdf2_sha256$") { "django_pbkdf2_sha256" } else if h.starts_with("pbkdf2_sha1$") { "django_pbkdf2_sha1" } else if h.starts_with("pbkdf2:") { "werkzeug_pbkdf2" } else if h.starts_with("scrypt:") { "werkzeug_scrypt" } else if h.starts_with("$P$") || h.starts_with("$H$") { "wordpress_phpass" } else if h.starts_with("$wp$2") { "wordpress_bcrypt" } else if h.starts_with("$2a$") || h.starts_with("$2b$") || h.starts_with("$2y$") { "bcrypt" } else { return None; }; Some(k.to_string())}/// Whether `password` matches `hash`, of any kind legacy_kind knows. False for a wrong password; an error for a hash of/// another kind or a broken one (Django's argon2 and bcrypt_sha256 hashes are not read). Each check takes as long as the/// old app took: Django's 600,000 rounds, about a quarter of a second.pub fn legacy_verify(password: &str, hash: &str) -> Result<bool, String> { let h = hash.trim(); let p = password.as_bytes(); match legacy_kind(h).as_deref() { Some(k @ ("django_pbkdf2_sha256" | "django_pbkdf2_sha1")) => { let parts: Vec<&str> = h.split('$').collect(); let [_, n, salt, expected] = parts[..] else { return Err("a Django hash is algorithm$rounds$salt$hash".into()) }; let expected = STANDARD.decode(expected).map_err(|_| "the Django hash's last part is not base64".to_string())?; let alg = if k.ends_with("sha1") { "sha1" } else { "sha256" }; Ok(same(&pbkdf2(alg, p, salt.as_bytes(), rounds(n)?, expected.len().clamp(1, 64))?, &expected)) } Some("werkzeug_pbkdf2") => { let parts: Vec<&str> = h.split('$').collect(); let [method, salt, expected] = parts[..] else { return Err("a Werkzeug hash is method$salt$hash".into()) }; let m: Vec<&str> = method.split(':').collect(); let (alg, n) = match m[..] { ["pbkdf2", alg, n] => (alg, rounds(n)?), ["pbkdf2", alg] => (alg, 150_000), _ => return Err(format!("`{}` is not a Werkzeug method", method)), }; let expected = unhex(expected).ok_or("the Werkzeug hash's last part is not hex")?; Ok(same(&pbkdf2(alg, p, salt.as_bytes(), n, expected.len().clamp(1, 64))?, &expected)) } Some("werkzeug_scrypt") => { let parts: Vec<&str> = h.split('$').collect(); let [method, salt, expected] = parts[..] else { return Err("a Werkzeug hash is method$salt$hash".into()) }; let m: Vec<&str> = method.split(':').collect(); let ["scrypt", n, r, pp] = m[..] else { return Err(format!("`{}` is not scrypt:n:r:p", method)) }; let (n, r, pp): (u64, u32, u32) = (n.parse().map_err(|_| "bad n")?, r.parse().map_err(|_| "bad r")?, pp.parse().map_err(|_| "bad p")?); if !n.is_power_of_two() || n > (1 << 20) || r > 32 || pp > 16 || n * r as u64 * pp as u64 > (1 << 22) { return Err("scrypt parameters too large for one check".into()); } let expected = unhex(expected).ok_or("the Werkzeug hash's last part is not hex")?; let params = scrypt::Params::new(n.trailing_zeros() as u8, r, pp, expected.len().clamp(10, 128)).map_err(|e| e.to_string())?; let mut out = vec![0u8; expected.len().clamp(10, 128)]; scrypt::scrypt(p, salt.as_bytes(), ¶ms, &mut out).map_err(|e| e.to_string())?; Ok(same(&out, &expected)) } Some("wordpress_phpass") => phpass(password, h), Some("wordpress_bcrypt") => { let mut m = Hmac::<sha2::Sha384>::new_from_slice(b"wp-sha384").expect("any key length"); m.update(p); let pre = STANDARD.encode(m.finalize().into_bytes()); bcrypt::verify(pre, &h[3..]).map_err(|e| e.to_string()) } Some("bcrypt") => bcrypt::verify(password, h).map_err(|e| e.to_string()), _ => Err("not a hash kind this plugin reads".into()), }}/// A Django-style `pbkdf2_sha256$rounds$salt$hash` for `password` with `salt` (letters and digits) and `iterations` (Django 5/// uses 870,000): to make test users, or to keep writing hashes a Django app beside yours can read.pub fn legacy_django_hash(password: &str, salt: &str, iterations: i64) -> Result<String, String> { if salt.is_empty() || salt.contains('$') { return Err("the salt is letters and digits, without `$`".into()); } let n = rounds(&iterations.to_string())?; Ok(format!("pbkdf2_sha256${}${}${}", n, salt, STANDARD.encode(pbkdf2("sha256", password.as_bytes(), salt.as_bytes(), n, 32)?)))}Related
argon2.qwer · 1.0.0Argon2id 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.7 views · 6 downloadsBcrypt bcrypt.qwer · 1.0.0Sign in users whose bcrypt hashes came from PHP, Rails, Django or Laravel, then move them to Quillo's Argon2.8 views · 9 downloadsScrypt passwords scrypt.qwer · 1.0.0Scrypt password hashes made, checked and marked for a rehash, the raw key Node's crypto.scrypt makes, and Firebase Authentication's modified scrypt, to sign in users exported from Firebase.6 views · 7 downloadsPassword strength password_strength.qwer · 1.0.0How guessable a password is (zxcvbn): a score from 0 to 4, a warning and suggestions, and how long it would take to crack.6 views · 8 downloadsPwned passwords pwned.qwer · 1.0.0Have I Been Pwned's Pwned Passwords by k-anonymity: only 5 characters of the SHA-1 leave the server, the answer is checked here.8 views · 9 downloadsRecovery codes mfa_codes.qwer · 1.0.0Two-factor recovery codes: a set made and shown once, stored only as hashes, each one usable a single time.7 views · 6 downloadsThread 0 posts
On Legacy passwords: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.