Recovery codes
Two-factor recovery codes: a set made and shown once, stored only as hashes, each one usable a single time.
Version 1.0.0 · 3 functions · 2 crates
Functions
What use mfa_codes.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use mfa_codes, mfa_hash from mfa_codes (Quillo 0.6).
mfa_codes(count: int) → listcount new codes (1 to 50; 10 is usual), each 10 characters as xxxxx-xxxxx (about 49 bits): show them to the user once, then store mfa_hash of each and forget the codes.
mfa_hash(code: str) → strThe hash to store for a code (SHA-256, hex). Spaces, dashes and capitals do not count, so ABCDE FGHJK is abcde-fghjk.
mfa_check(code: str, hashes: list) → intWhich stored hash code matches: its position in hashes, or -1 for none. Remove that hash once it is used: each code works one time. Every hash is compared, in constant time, so the answer's timing tells nothing.
Get it
$ quillo install mfa_codes # mfa_codes.qwer into this folder$ quillo install mfa_codes --example # and its example, mfa_codes.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/mfa_codes.qwer$ curl -O https://quillolang.org/plugins/files/mfa_codes.qweThen, in your program, all of it or only what you call:
use mfa_codes.qweruse mfa_codes, mfa_hash from mfa_codes // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
rand = "0.8"sha2 = "0.10"Example
// Two-factor recovery codes: shown once, kept as hashes, each used one time.//// quillo run mfa_codes.qwe then open http://localhost:8080/setupuse "mfa_codes.qwer"route GET /setup { let codes = mfa_codes(10) // Store only the hashes (db.exec("insert into recovery_codes ...") in a real app). write_file("data/recovery.json", json.stringify(codes | map((c) => mfa_hash(c)))) { show_once: codes, count: len(codes) }}route GET /use { let codes = mfa_codes(10) let stored = codes | map((c) => mfa_hash(c)) let typed = upper(replace(codes[3], "-", " ")) // as a user types it from paper let at = mfa_check(typed, stored) let left = [...slice(stored, 0, at), ...slice(stored, at + 1, len(stored))] { matched: at, second_time: mfa_check(typed, left), wrong: mfa_check("aaaaa-bbbbb", stored), left: len(left) }}$ quillo run mfa_codes.qwe$ curl 'localhost:8080/use'$ curl 'localhost:8080/setup'Source
//! Recovery codes for two-factor sign-in: a set made once, shown once, stored only as hashes, and each usable one time.//! version: 1.0.0//!//! [dependencies]//! rand = "0.8"//! sha2 = "0.10"use rand::Rng;use sha2::{Digest, Sha256};/// No 0, o, 1, i or l, so a code copied from paper is read right.const ALPHABET: &[u8] = b"23456789abcdefghjkmnpqrstuvwxyz";fn normal(code: &str) -> String { code.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_lowercase()}fn hash(code: &str) -> String { let n = normal(code); Sha256::digest(format!("mfa-recovery:{}", n).as_bytes()).iter().map(|b| format!("{:02x}", b)).collect()}/// `count` new codes (1 to 50; 10 is usual), each 10 characters as `xxxxx-xxxxx` (about 49 bits): show them to the user once,/// then store mfa_hash of each and forget the codes.pub fn mfa_codes(count: i64) -> Vec<String> { let mut rng = rand::rngs::OsRng; (0..count.clamp(1, 50)).map(|_| { let s: String = (0..10).map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char).collect(); format!("{}-{}", &s[..5], &s[5..]) }).collect()}/// The hash to store for a code (SHA-256, hex). Spaces, dashes and capitals do not count, so `ABCDE FGHJK` is `abcde-fghjk`.pub fn mfa_hash(code: &str) -> String { hash(code)}/// Which stored hash `code` matches: its position in `hashes`, or -1 for none. Remove that hash once it is used: each code/// works one time. Every hash is compared, in constant time, so the answer's timing tells nothing.pub fn mfa_check(code: &str, hashes: Vec<String>) -> i64 { if normal(code).len() < 8 { return -1; } let h = hash(code); let mut found = -1i64; for (i, s) in hashes.iter().enumerate() { let same = s.len() == h.len() && s.bytes().zip(h.bytes()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0; if same && found < 0 { found = i as i64; } } found}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 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 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 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 Recovery codes: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.