//! Build script for soliton. //! //! Embeds the EFF large wordlist for verification phrase generation. //! The wordlist is checked into the repo at `src/eff_large_wordlist.txt` //! and SHA-256 verified at build time. //! //! All cryptographic dependencies are pure Rust — no C compilation or linking needed. use sha2::{Digest, Sha256}; use std::env; use std::fs; use std::io::Write; use std::path::PathBuf; /// SHA-256 hash of the canonical EFF large wordlist (7776 lines). /// const EFF_WORDLIST_SHA256: &str = "addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e"; fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/eff_large_wordlist.txt"); generate_wordlist(); } /// Read the EFF large wordlist from the in-repo cached copy and generate /// a Rust source file with the words as a static array. The file is /// written to OUT_DIR and included via `include!` in the verification /// module. /// /// The EFF large wordlist contains 7776 words (6^5), one per line, prefixed /// with a dice roll number and a tab. We strip the prefix and keep only the /// words. fn generate_wordlist() { // Cargo guarantees OUT_DIR and CARGO_MANIFEST_DIR for build scripts. let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let wordlist_rs = out_dir.join("eff_wordlist.rs"); let cache_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) .join("src") .join("eff_large_wordlist.txt"); // Skip regeneration if the output already exists AND the cached source // passes SHA-256 verification. Safe because the output is deterministically // generated from the source — same input always produces identical output. // The SHA-256 check protects against tampered build caches. if wordlist_rs.exists() { if let Ok(cached) = fs::read_to_string(&cache_path) { verify_wordlist_hash(&cached); return; } } let raw = fs::read_to_string(&cache_path) .expect("EFF wordlist not available: src/eff_large_wordlist.txt missing from repo"); verify_wordlist_hash(&raw); // Parse: each line is "DDDDD\tword\n" (5-digit dice roll, tab, word). let words: Vec<&str> = raw .lines() .filter_map(|line| { let line = line.trim(); if line.is_empty() { return None; } line.split('\t').nth(1) }) .collect(); // Panic: build scripts communicate errors by panicking. A count mismatch // indicates a corrupted or non-canonical wordlist that passed SHA-256 // (hash collision or wrong file). assert_eq!( words.len(), 7776, "EFF wordlist should have 7776 entries, got {}", words.len() ); // Panic on I/O failure: build scripts have no recovery path — Cargo // treats a panic as a build error and reports the message to the user. let mut out = fs::File::create(&wordlist_rs).unwrap(); writeln!(out, "/// EFF large wordlist (7776 words).").unwrap(); writeln!( out, "/// Source: , checked into repo at src/eff_large_wordlist.txt." ) .unwrap(); writeln!(out, "pub(crate) static EFF_WORDLIST: [&str; 7776] = [").unwrap(); for word in &words { writeln!(out, " \"{word}\",").unwrap(); } writeln!(out, "];").unwrap(); } /// Verify the SHA-256 hash of the EFF wordlist content. /// /// # Security /// /// Panics with a clear message if the hash doesn't match, preventing /// use of a tampered or corrupted wordlist in verification phrases. /// This is the sole integrity gate for the in-repo wordlist. fn verify_wordlist_hash(content: &str) { let computed = format!("{:x}", Sha256::digest(content.as_bytes())); assert_eq!( computed, EFF_WORDLIST_SHA256, "EFF wordlist SHA-256 mismatch!\n expected: {}\n computed: {}\n\ The wordlist may be corrupted or tampered with.", EFF_WORDLIST_SHA256, computed ); }