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
//! **The security key's wrap v2** (CRYPT-PQ-SK, owner 2026-10-30): the person's
//! encryption key is sealed SYMMETRICALLY under a key HKDF-SHA256 derives from
//! the security key's PRF / hmac-secret output (a random salt per wrap, info
//! `gunnar-crypt user-key wrap v2`), with XChaCha20-Poly1305. No X25519 step,
//! so the wrap is quantum-safe. A v1 wrap (age X25519) still opens and is
//! re-wrapped to v2. Runs native and wasm32.

#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;

use gunnar_crypt::members::Level;
use gunnar_crypt::skwrap;
use gunnar_crypt::userkey::{self, security_key, SecurityKeyWrap, UserKey, UserKeyError};

const CRED: &[u8] = b"credential v2";

fn prf(fill: u8) -> [u8; 32] {
    [fill; 32]
}

/// ★ Round trip: wrapped to the PRF output, opened with it, for a quantum-safe
/// and a classic key; every wrap has its own salt and nonce; no age, no X25519.
#[test]
fn a_v2_wrap_round_trips_and_names_no_x25519() {
    for level in [Level::QuantumSafe, Level::Classic] {
        let user = UserKey::generate(level).unwrap();
        let wrap = user.wrap_to_security_key_v2(&prf(7), CRED).unwrap();
        assert!(wrap.starts_with(skwrap::TAG), "{wrap}");
        assert!(skwrap::is_v2(&wrap));
        let parts = skwrap::parse(&wrap).expect("a well-formed v2 wrap");
        assert_eq!((parts.salt.len(), parts.nonce.len()), (32, 24));
        assert!(
            gunnar_crypt::policy::age_stanzas(wrap.as_bytes()).is_none(),
            "not an age file: no X25519 stanza"
        );
        let (opened, version) =
            userkey::open_with_security_key(wrap.as_bytes(), &prf(7), CRED, &user.public())
                .unwrap();
        assert_eq!(
            (opened.public(), version),
            (user.public(), SecurityKeyWrap::V2)
        );
        let again = user.wrap_to_security_key_v2(&prf(7), CRED).unwrap();
        assert_ne!(again, wrap, "a fresh salt and nonce per wrap");
        assert!(userkey::wrap_is_quantum_safe("security-key", &wrap));
    }
    assert_eq!(skwrap::INFO, b"gunnar-crypt user-key wrap v2");
}

/// ★ A wrong PRF output, another credential, a flipped byte: refused, never a key.
#[test]
fn a_wrong_prf_output_is_refused() {
    let user = UserKey::generate(Level::NEW).unwrap();
    let wrap = user.wrap_to_security_key_v2(&prf(7), CRED).unwrap();
    let wrong = userkey::open_with_security_key(wrap.as_bytes(), &prf(8), CRED, &user.public())
        .unwrap_err();
    assert_eq!(wrong, UserKeyError::WrongKey);
    let other = userkey::open_with_security_key(
        wrap.as_bytes(),
        &prf(7),
        b"another credential",
        &user.public(),
    )
    .unwrap_err();
    assert_eq!(
        other,
        UserKeyError::WrongKey,
        "the credential id is bound in"
    );
    let mut parts = skwrap::parse(&wrap).unwrap();
    parts.sealed[3] ^= 1;
    let flipped = skwrap::format(&parts);
    assert_eq!(
        userkey::open_with_security_key(flipped.as_bytes(), &prf(7), CRED, &user.public())
            .unwrap_err(),
        UserKeyError::WrongKey
    );
    let someone = UserKey::generate(Level::NEW).unwrap();
    let swapped = someone.wrap_to_security_key_v2(&prf(7), CRED).unwrap();
    assert_eq!(
        userkey::open_with_security_key(swapped.as_bytes(), &prf(7), CRED, &user.public())
            .unwrap_err(),
        UserKeyError::NotThisAccountsKey,
        "a substituted wrap is refused"
    );
    for bad in [
        "",
        "gunnar-crypt user-key wrap v2",
        "gunnar-crypt user-key wrap v2 AAAA AAAA AAAA",
        "gunnar-crypt user-key wrap v3 x y z",
    ] {
        assert!(skwrap::parse(bad).is_none(), "{bad:?}");
    }
}

/// ★ v1 → v2: an X25519 (v1) wrap still opens, says it is v1, and is not
/// quantum-safe; its v2 re-wrap opens with the same touch and is.
#[test]
fn a_v1_wrap_opens_and_is_rewrapped_to_v2() {
    let user = UserKey::generate(Level::NEW).unwrap();
    let v1 = user
        .wrap_to_security_key(&security_key::recipient(&prf(5), CRED))
        .unwrap();
    assert!(!skwrap::is_v2(&v1));
    assert!(!userkey::wrap_is_quantum_safe("security-key", &v1));
    let (opened, version) =
        userkey::open_with_security_key(v1.as_bytes(), &prf(5), CRED, &user.public()).unwrap();
    assert_eq!(version, SecurityKeyWrap::V1);
    let v2 = opened.wrap_to_security_key_v2(&prf(5), CRED).unwrap();
    let (again, version) =
        userkey::open_with_security_key(v2.as_bytes(), &prf(5), CRED, &user.public()).unwrap();
    assert_eq!(
        (again.public(), version),
        (user.public(), SecurityKeyWrap::V2)
    );
    assert!(userkey::wrap_is_quantum_safe("security-key", &v2));
    assert!(
        !userkey::wrap_is_quantum_safe("ssh", &v2),
        "the SSH method stays not quantum-safe"
    );
    assert!(userkey::wrap_is_quantum_safe("recovery-code", ""));
}