Pwned passwords
Have I Been Pwned's Pwned Passwords by k-anonymity: only 5 characters of the SHA-1 leave the server, the answer is checked here.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use pwned.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use pwned_range, pwned_count from pwned (Quillo 0.6).
pwned_range(password: str) → mapWhat to ask the API for password: { prefix, suffix, url }. url is https://api.pwnedpasswords.com/range/<prefix>; fetch it (with the Add-Padding: true header, so the answer's size says nothing) and give its text to pwned_count. Neither the password nor its whole hash is sent.
pwned_count(password: str, range_text: str) → intor an errorHow many times password appears in breaches, read from range_text (the API's answer for its prefix: lines of SUFFIX:COUNT). 0 when it is not there; padding lines (count 0) are ignored. An error when the text is not such an answer.
pwned_sha1(password: str) → strThe SHA-1 of password in upper-case hex, as Pwned Passwords writes it: for the downloadable list, or a cache of ranges.
Get it
$ quillo install pwned # pwned.qwer into this folder$ quillo install pwned --example # and its example, pwned.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/pwned.qwer$ curl -O https://quillolang.org/plugins/files/pwned.qwe$ curl -O https://quillolang.org/plugins/files/pwned_sample.txtThen, in your program, all of it or only what you call:
use pwned.qweruse pwned_range, pwned_count from pwned // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
sha1 = "0.10"Example
// A password-change form that refuses passwords seen in data breaches.//// quillo run pwned.qwe then open http://localhost:8080/check?password=password123//// A real app asks the API for the range (only five characters of the hash are sent):// let range = http.get(pwned_range(password).url, { "add-padding": "true" })?.body// This example reads a sample answer for the prefix CBFDA instead, so it runs offline.use "pwned.qwer"fn range_for(password: str) { let r = pwned_range(password) if r.prefix == "CBFDA" { return read_file("pwned_sample.txt")? } "00000000000000000000000000000000000:0" // any other prefix: no breach in this sample}route GET /check ? { password: str } { let seen = pwned_count(password, range_for(password))? { prefix_sent: pwned_range(password).prefix, seen_in_breaches: seen, accepted: seen == 0 }}route GET /sample = { password123: pwned_count("password123", read_file("pwned_sample.txt")?)?, other: pwned_count("correct horse battery staple", read_file("pwned_sample.txt")?)? }$ quillo run pwned.qwe$ curl 'localhost:8080/check?password=password123'$ curl 'localhost:8080/check?password=correct%20horse'Source
//! Have I Been Pwned's Pwned Passwords, by k-anonymity: only the first 5 characters of the password's SHA-1 leave the server, and the answer is checked here; for sign-up and password-change forms.//! version: 1.0.0//!//! [dependencies]//! sha1 = "0.10"use sha1::{Digest, Sha1};use std::collections::HashMap;fn sha1_upper(text: &str) -> String { Sha1::digest(text.as_bytes()).iter().map(|b| format!("{:02X}", b)).collect()}/// What to ask the API for `password`: `{ prefix, suffix, url }`. `url` is `https://api.pwnedpasswords.com/range/<prefix>`;/// fetch it (with the `Add-Padding: true` header, so the answer's size says nothing) and give its text to pwned_count./// Neither the password nor its whole hash is sent.pub fn pwned_range(password: &str) -> HashMap<String, String> { let h = sha1_upper(password); HashMap::from([ ("prefix".to_string(), h[..5].to_string()), ("suffix".to_string(), h[5..].to_string()), ("url".to_string(), format!("https://api.pwnedpasswords.com/range/{}", &h[..5])), ])}/// How many times `password` appears in breaches, read from `range_text` (the API's answer for its prefix: lines of/// `SUFFIX:COUNT`). 0 when it is not there; padding lines (count 0) are ignored. An error when the text is not such an answer.pub fn pwned_count(password: &str, range_text: &str) -> Result<i64, String> { let suffix = &sha1_upper(password)[5..]; let mut lines = 0; for line in range_text.lines().take(100_000) { let line = line.trim(); if line.is_empty() { continue; } let (s, n) = line.split_once(':').ok_or_else(|| format!("`{}` is not SUFFIX:COUNT: is this the range answer?", line.chars().take(60).collect::<String>()))?; lines += 1; if s.trim().eq_ignore_ascii_case(suffix) { return n.trim().parse::<i64>().map_err(|_| format!("`{}` is not a count", n)); } } if lines == 0 { return Err("the range answer is empty".into()); } Ok(0)}/// The SHA-1 of `password` in upper-case hex, as Pwned Passwords writes it: for the downloadable list, or a cache of ranges.pub fn pwned_sha1(password: &str) -> String { sha1_upper(password)}Related
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 downloadsArgon2 passwords 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 downloadsLegacy passwords legacy_passwords.qwer · 1.0.0Check passwords against Django, Flask/Werkzeug and WordPress hashes, to sign in users moved from those apps and rehash them with Argon2.7 views · 7 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 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 downloadsThread 0 posts
On Pwned passwords: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.