You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.7 KiB

+++ title = "Implement 4-word passphrase auth_code generator (must work in WASM/workers-rs)" priority = 7 status = "done" ticket_type = "task" dependencies = ["1f5bb5", "6ed325"] +++

The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.

Shared types and utilities are in src/lib.rs — code placed there must compile for both the host target and wasm32-unknown-unknown.

Auth codes are 4-word passphrases (e.g. ocean-table-purple-storm) assigned to quotes on creation. They are stored plaintext and used to authorise updates and deletes.

TRIAGE 6ed325 resolved: Use a custom embedded word list (EFF Short Word List 1) with rand::rngs::OsRng from rand 0.10. OsRng does not use thread-local storage and is safe on wasm32. Entropy on WASM comes from getrandom 0.4 with the wasm_js feature, which calls crypto.getRandomValues() — available in both browsers and Cloudflare Workers.

Implement a `generate_auth_code() -> String` function in `src/lib.rs` that produces a random 4-word passphrase. Place it in shared lib code so both the API (generation) and UI (display) can reference it.

1. Cargo.toml changes (covered by ticket 1f5bb5, listed here for reference)

[dependencies]
rand = "0.10"

[target.'cfg(target_arch = "wasm32")'.dependencies]
# Enables OsRng entropy via Web Crypto API (crypto.getRandomValues())
# Required by both rand (OsRng) and uuid (v4) on wasm32 targets
getrandom = { version = "0.4", features = ["wasm_js"] }

2. Embed the EFF Short Word List 1 in src/lib.rs

The EFF Short Word List 1 contains 1296 common English words designed for memorable passphrases. Source: https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt

Generate the Rust const array (run from shell, paste output into src/lib.rs):

curl -s 'https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt' \
  | awk '{print $2}' \
  | awk 'NR % 8 == 1 {printf "    "} {printf "\"%s\", ", $0} NR % 8 == 0 {print ""}' \
  | sed '$a\' | tr -d '\n' | sed 's/, $//'

Place the word list as a module-level constant:

/// EFF Short Word List 1 — 1296 common English words designed for memorable passphrases.
/// Source: <https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt>
const WORDS: &[&str] = &[
    "acid", "acorn", "acre", "acts", "afar", "affix", "aged", "agent",
    "agile", "aging", "agony", "ahead", "aide", "aids", "aim", "ajar",
    // ... (full 1296-word list, generated via shell command above)
];

3. Implement generate_auth_code() in src/lib.rs

use rand::rngs::OsRng;
use rand::seq::SliceRandom;

/// Generates a random 4-word passphrase in the format `word-word-word-word`.
///
/// Words are drawn from the EFF Short Word List 1 (1296 common English words).
/// The passphrase is used as an `auth_code` to authorize quote edits and deletes.
///
/// Uses `rand::rngs::OsRng` for entropy, which is safe on both native and
/// `wasm32-unknown-unknown` targets. On WASM (Cloudflare Workers), entropy
/// is sourced via `crypto.getRandomValues()` through `getrandom`'s `wasm_js` feature.
///
/// # Examples
///
/// ```
/// let code = quotesdb::generate_auth_code();
/// let words: Vec<&str> = code.split('-').collect();
/// assert_eq!(words.len(), 4);
/// assert!(words.iter().all(|w| !w.is_empty()));
/// ```
pub fn generate_auth_code() -> String {
    WORDS
        .choose_multiple(&mut OsRng, 4)
        .copied()
        .collect::<Vec<_>>()
        .join("-")
}

4. Unit tests (src/lib.rs tests module or src/tests.rs)

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    /// Verify format: exactly 4 non-empty words from WORDS, joined by hyphens.
    #[test]
    fn auth_code_has_four_valid_words() {
        for _ in 0..100 {
            let code = generate_auth_code();
            let parts: Vec<&str> = code.split('-').collect();
            assert_eq!(parts.len(), 4, "expected 4 words, got: {code}");
            for word in &parts {
                assert!(!word.is_empty(), "empty word in code: {code}");
                assert!(
                    WORDS.contains(word),
                    "word '{word}' not in word list, code: {code}"
                );
            }
        }
    }

    /// Verify randomness: 20 samples should produce at least 10 distinct codes.
    #[test]
    fn auth_codes_are_varied() {
        let codes: HashSet<String> = (0..20).map(|_| generate_auth_code()).collect();
        assert!(
            codes.len() > 10,
            "expected >10 unique codes in 20 samples, got {}",
            codes.len()
        );
    }
}
- `generate_auth_code()` must live in `src/lib.rs` (shared code, not bin-specific) - Use `rand::rngs::OsRng` — do NOT use `rand::thread_rng()` (thread-local, unsafe on WASM) - Do not use `std::fs`, thread-based RNG, or any crate that requires file-system access - All public items must have rustdoc comments with doc-examples (per project style) - `getrandom = { version = "0.4", features = ["wasm_js"] }` must be in the wasm32 cfg section only, not in `[dependencies]`, to avoid pulling wasm-bindgen into native builds Use `superpowers:test-driven-development` — write the unit tests (step 4) before implementing (step 3). Use `superpowers:verification-before-completion` before closing. Run in order from the `quotesdb/` directory:
cargo fmt
cargo check
cargo clippy
cargo test
`feat(quotesdb): implement WASM-compatible 4-word passphrase auth_code generator`