anubis-wormhole 1.0.0

A post-quantum secure file transfer tool based on the Magic Wormhole protocol.
Documentation
use sha2::{Digest, Sha384};

pub struct SasWords<'a> {
    pub w1: &'a str,
    pub w2: &'a str,
}

// Minimal pluggable dictionary. Default uses synthetic words w0000..w1023
pub struct Dictionary<'a> { words: &'a [&'a str] }

impl<'a> Dictionary<'a> {
    pub fn builtin() -> Self {
        // Build synthetic words w0000..w1023 on the heap and leak for 'static lifetime
        let mut v: Vec<String> = Vec::with_capacity(1024);
        for i in 0..1024 { v.push(format!("w{:04}", i)); }
        let leaked: Vec<&'static str> = v.into_iter().map(|s| Box::leak(s.into_boxed_str()) as &str).collect();
        let leaked_ref: &'static [&'static str] = Box::leak(leaked.into_boxed_slice());
        Dictionary { words: leaked_ref }
    }

    pub fn from_slice(words: &'a [&'a str]) -> Self { Self { words } }
    pub fn get(&self, idx: usize) -> &'a str { self.words[idx % self.words.len()] }
}

pub fn two_word_sas<'a>(dict: &'a Dictionary<'a>, k_verify: &[u8]) -> SasWords<'a> {
    // Take 20 bits from H(K_verify) => two 10-bit indices
    let mut h = Sha384::new();
    h.update(k_verify);
    let out = h.finalize();
    let b0 = out[0] as u32;
    let b1 = out[1] as u32;
    let b2 = out[2] as u32;
    let idx1 = ((b0 << 2) | (b1 >> 6)) & 0x3ff; // top 10 bits
    let idx2 = (b1 & 0x3f) << 4 | (b2 >> 4);    // next 10 bits
    let w1 = dict.get(idx1 as usize);
    let w2 = dict.get(idx2 as usize);
    SasWords { w1, w2 }
}

pub fn load_dictionary_file(path: &std::path::Path) -> Option<Dictionary<'static>> {
    use std::fs;
    let data = fs::read_to_string(path).ok()?;
    let mut words: Vec<&str> = data.lines().map(|l| l.trim()).filter(|l| !l.is_empty() && !l.starts_with('#')).collect();
    if words.len() < 1024 { return None; }
    words.truncate(1024);
    let leaked: Vec<&'static str> = words.into_iter().map(|s| Box::leak(s.to_string().into_boxed_str()) as &str).collect();
    let leaked_slice: &'static [&'static str] = Box::leak(leaked.into_boxed_slice());
    Some(Dictionary::from_slice(leaked_slice))
}