gunnar-crypt 1.6.0

Encrypted repositories: the content envelope, the per-repo key ring, its members, the server's plaintext check, each person's one encryption key and its unlock methods, and the ring's wrap to members' encryption keys
Documentation
//! **How much slower is the quantum-safe level?** Plain timings, release
//! build, median of several runs:
//!
//! ```text
//! cargo run --release -p gunnar-crypt --example pq_bench
//! ```
//!
//! keygen; then for 1, 10, 100 and 500 members, Classic (members' ssh-ed25519
//! keys, made with `ssh-keygen`) against QuantumSafe (age1pq1… keys): wrapping
//! the ring, unwrapping it as the FIRST and as the LAST member (age tries
//! anonymous hybrid stanzas one by one; SSH stanzas are tagged), the server's
//! ring check (`policy::check_ring`, no key), and the key file's size.

#[cfg(target_arch = "wasm32")]
fn main() {}

#[cfg(not(target_arch = "wasm32"))]
fn main() {
    use std::process::Command;
    use std::time::{Duration, Instant};

    use gunnar_crypt::keyring::Keyring;
    use gunnar_crypt::members::{Level, Members};
    use gunnar_crypt::wrap::{self, age, NoPassphrase};
    use gunnar_crypt::{policy, pq};

    fn time<T>(runs: usize, mut f: impl FnMut() -> T) -> f64 {
        let mut v: Vec<Duration> = Vec::with_capacity(runs);
        for _ in 0..runs {
            let t = Instant::now();
            std::hint::black_box(f());
            v.push(t.elapsed());
        }
        v.sort();
        v[v.len() / 2].as_secs_f64() * 1e3
    }

    let dir = tempfile::tempdir().unwrap();
    println!(
        "keygen        pq (age1pq, ML-KEM-768+X25519): {:.3} ms   x25519 (age1): {:.3} ms",
        time(200, pq::Identity::generate),
        time(200, age::x25519::Identity::generate)
    );

    // Classic members: real ssh-ed25519 keys.
    let ssh: Vec<(Vec<u8>, String)> = (0..500)
        .map(|i| {
            let path = dir.path().join(format!("k{i}"));
            let ok = Command::new("ssh-keygen")
                .args(["-q", "-t", "ed25519", "-N", "", "-C", "", "-f"])
                .arg(&path)
                .status()
                .expect("ssh-keygen runs");
            assert!(ok.success());
            (
                std::fs::read(&path).unwrap(),
                std::fs::read_to_string(path.with_extension("pub"))
                    .unwrap()
                    .trim()
                    .to_string(),
            )
        })
        .collect();
    let pqs: Vec<pq::Identity> = (0..500).map(|_| pq::Identity::generate()).collect();
    let ring = Keyring::generate();

    println!(
        "{:>7}  {:<12} {:>9} {:>14} {:>13} {:>13} {:>9}",
        "members", "level", "wrap ms", "unwrap 1st ms", "unwrap last", "check_ring", "keys KB"
    );
    for n in [1usize, 10, 100, 500] {
        let runs = if n >= 100 { 5 } else { 30 };
        for level in [Level::Classic, Level::QuantumSafe] {
            let mut m = Members::new();
            m.set_level(level).unwrap();
            for i in 0..n {
                let key = match level {
                    Level::Classic => ssh[i].1.clone(),
                    Level::QuantumSafe => pqs[i].to_public().to_string(),
                };
                m.add(&format!("m{i}"), &key).unwrap();
            }
            let file = wrap::wrap(&ring, &m).unwrap();
            let id = |i: usize| -> Box<dyn age::Identity> {
                match level {
                    Level::Classic => wrap::ssh_identity(&ssh[i].0, None, NoPassphrase).unwrap(),
                    Level::QuantumSafe => Box::new(pqs[i].clone()),
                }
            };
            let (first, last) = (id(0), id(n - 1));
            let text = m.to_text();
            assert_eq!(policy::check_ring(&text, file.as_bytes()), Ok(()));
            println!(
                "{:>7}  {:<12} {:>9.2} {:>14.2} {:>13.2} {:>13.3} {:>9.1}",
                n,
                level.word(),
                time(runs, || wrap::wrap(&ring, &m).unwrap()),
                time(runs, || wrap::unwrap(file.as_bytes(), &[first.as_ref()])
                    .unwrap()),
                time(runs, || wrap::unwrap(file.as_bytes(), &[last.as_ref()])
                    .unwrap()),
                time(runs, || policy::check_ring(&text, file.as_bytes())),
                file.len() as f64 / 1024.0
            );
        }
    }
}