prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
//! Entropy is always an *input*. Nothing here generates randomness, so the engine stays deterministic.

mod exchange;
mod hash;
mod kdf;
mod mac;
pub mod ratchets;
mod sign;
mod token;

pub use exchange::{
    x25519_diffie_hellman, x25519_keys_for_seal, x25519_public_key, X25519PublicKey,
    X25519SecretKey, X25519SharedSecret,
};
pub use hash::{
    sha256, sha256_chunks, sha256_prefix_and_digest_suffix, Sha256PrefixState, SharedPrefixDigests,
    SHA256_OUTPUT_LEN,
};
pub use kdf::{hkdf_sha256, hkdf_sha256_into, HkdfOutputTooLong};
pub use mac::{hmac_sha256, hmac_sha256_chunks, hmac_sha256_verify, HmacSha256Stream, InvalidMac};
pub use sign::{
    ed25519_public_key, ed25519_sign, ed25519_verify, Ed25519PublicKey, Ed25519SecretKey,
    Ed25519Signature, Ed25519Verifier, InvalidPublicKey, InvalidSignature,
};
pub use token::{
    sealed_len, token_is_authentic, token_open, token_open_in_place, token_seal, token_seal_chunks,
    token_seal_in_place, BadKeyLength, BufferTooShort, TokenKey, TokenOpenError, TokenOpenStream,
    TOKEN_OVERHEAD,
};

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

    fn bytes_from_hex<const N: usize>(s: &str) -> [u8; N] {
        let mut bytes = [0u8; N];
        assert_eq!(s.len(), N * 2, "the vector spells exactly {N} bytes");
        for (i, byte) in bytes.iter_mut().enumerate() {
            *byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("valid hex");
        }
        bytes
    }

    #[test]
    fn sha256_matches_rns_1_4_2() {
        assert_eq!(
            sha256(b"personal-reticulum-suite"),
            bytes_from_hex("fbf93abb74e7a87e0bb67364e3eddf7718e5f1d38eedf1b21b806a8e612e89d2"),
        );
    }

    #[test]
    fn hmac_sha256_matches_rns_1_4_2_and_verifies_constant_time() {
        let key: [u8; 32] = core::array::from_fn(|i| i as u8);
        let tag: [u8; 32] =
            bytes_from_hex("aa868925242368f32a02fef52ecf6fcdb07222647c9476e300e848ca886efe2e");
        assert_eq!(hmac_sha256(&key, b"announce-test"), tag);
        assert!(hmac_sha256_verify(&key, b"announce-test", &tag).is_ok());

        let mut bad = tag;
        bad[0] ^= 1;
        assert_eq!(
            hmac_sha256_verify(&key, b"announce-test", &bad),
            Err(InvalidMac),
        );
    }

    #[test]
    fn hkdf_sha256_matches_rns_1_4_2() {
        let ikm = [0x42u8; 32];
        let salt = [0x01u8; 16];
        assert_eq!(
            hkdf_sha256::<32>(&ikm, &salt, b"context"),
            bytes_from_hex("d3a68f6569700c188c5a7c2bcd22c37e9757d022658f06b59753f7c079dcdb3a"),
        );
        assert_eq!(
            hkdf_sha256::<64>(&ikm, &salt, b"context"),
            bytes_from_hex(
                "d3a68f6569700c188c5a7c2bcd22c37e9757d022658f06b59753f7c079dcdb3a\
                 82958b17892dbd30978719b5ba66787152ad0a0c7aeb4df49bce91d36c8915dd"
            ),
        );
    }

    #[test]
    fn ed25519_sign_verify_and_pubkey_match_rns_1_4_2() {
        let secret = Ed25519SecretKey::new([0x11u8; 32]);
        let public = Ed25519PublicKey(bytes_from_hex(
            "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737",
        ));
        let sig = Ed25519Signature(bytes_from_hex(
            "ee646fb3251af01efbe35f4b03905b3ec2b90ea4acd9a51a46cb795f76575b4a\
             36e2893c356db8b2135417f6001a99ecd81de04dde2f2b3428fd4f8ea46e1107",
        ));

        assert_eq!(ed25519_public_key(&secret), public);
        assert_eq!(ed25519_sign(&secret, b"sign-this"), sig);
        assert!(ed25519_verify(&public, b"sign-this", &sig).is_ok());

        assert_eq!(
            ed25519_verify(&public, b"sign-thus", &sig),
            Err(InvalidSignature),
        );
        let mut bad = sig;
        bad.0[0] ^= 1;
        assert_eq!(
            ed25519_verify(&public, b"sign-this", &bad),
            Err(InvalidSignature),
        );
    }

    #[test]
    fn ed25519_verifier_matches_the_one_shot_verify() {
        let public = Ed25519PublicKey(bytes_from_hex(
            "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737",
        ));
        let sig = Ed25519Signature(bytes_from_hex(
            "ee646fb3251af01efbe35f4b03905b3ec2b90ea4acd9a51a46cb795f76575b4a\
             36e2893c356db8b2135417f6001a99ecd81de04dde2f2b3428fd4f8ea46e1107",
        ));

        let verifier = Ed25519Verifier::new(&public).expect("canonical key");
        assert_eq!(verifier.public_key(), &public);
        assert!(verifier.verify(b"sign-this", &sig).is_ok());
        assert_eq!(verifier.verify(b"sign-thus", &sig), Err(InvalidSignature));
    }

    #[test]
    fn x25519_dh_and_pubkey_match_rns_1_4_2() {
        let a = X25519SecretKey::new([0x22u8; 32]);
        let a_pub = X25519PublicKey(bytes_from_hex(
            "0faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20",
        ));
        let b_pub = X25519PublicKey(bytes_from_hex(
            "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14",
        ));
        assert_eq!(x25519_public_key(&a), a_pub);
        assert_eq!(
            x25519_diffie_hellman(&a, &b_pub).as_bytes(),
            &bytes_from_hex("1fdc192faa0212a9aae7bb4f41b580227fd5ad3e5d777faae230dfe973f3e805"),
        );
    }

    #[test]
    fn token_round_trips_and_matches_rns_1_4_2_aes128() {
        let key: [u8; 32] = core::array::from_fn(|i| i as u8);
        let plaintext = b"secret payload one-twenty-eight";
        let rns_token = bytes_from_hex::<80>(
            "b0f6eebcf00a7c913d7ea7800390e775afb12b483b2379380d1b6fb5631e1add\
             75ce22bfbc301038bcd42dc15aac9f4be06264c618186e381dfe74e49ef4cb99\
             e861b0c85026daa336876e4b44410d32",
        );

        let mut out = [0u8; 256];
        let n = token_open(&TokenKey::from_aes128(&key), &rns_token, &mut out).unwrap();
        assert_eq!(&out[..n], plaintext);

        let iv = bytes_from_hex("b0f6eebcf00a7c913d7ea7800390e775");
        let mut sealed = [0u8; 256];
        let m = token_seal(&TokenKey::from_aes128(&key), &iv, plaintext, &mut sealed).unwrap();
        assert_eq!(&sealed[..m], &rns_token[..]);
    }

    #[test]
    fn token_matches_rns_1_4_2_aes256() {
        let key: [u8; 64] = core::array::from_fn(|i| i as u8);
        let plaintext = b"secret payload two-fifty-six!!";
        let rns_token = bytes_from_hex::<80>(
            "392b4018bb3fb568466bb35fbfede968eef72be093395e687c3e61c0df992093\
             06c92ab94e39cef8644c44b863cd1582e8bd0178939547a414c1669ee2fa3237\
             a4312dcc3fd9c1177597c454819ce6e7",
        );

        let mut out = [0u8; 256];
        let n = token_open(&TokenKey::from_aes256(&key), &rns_token, &mut out).unwrap();
        assert_eq!(&out[..n], plaintext);

        let iv = bytes_from_hex("392b4018bb3fb568466bb35fbfede968");
        let mut sealed = [0u8; 256];
        let m = token_seal(&TokenKey::from_aes256(&key), &iv, plaintext, &mut sealed).unwrap();
        assert_eq!(&sealed[..m], &rns_token[..]);
    }

    #[test]
    fn token_open_rejects_tampered_mac() {
        let key: [u8; 32] = core::array::from_fn(|i| i as u8);
        let mut token = bytes_from_hex::<80>(
            "b0f6eebcf00a7c913d7ea7800390e775afb12b483b2379380d1b6fb5631e1add\
             75ce22bfbc301038bcd42dc15aac9f4be06264c618186e381dfe74e49ef4cb99\
             e861b0c85026daa336876e4b44410d32",
        );
        *token.last_mut().unwrap() ^= 1;
        let mut out = [0u8; 256];
        assert_eq!(
            token_open(&TokenKey::from_derived(&key).unwrap(), &token, &mut out),
            Err(TokenOpenError::InvalidMac),
        );
    }

    #[test]
    fn a_derived_key_of_unsupported_length_is_refused() {
        assert!(matches!(
            TokenKey::from_derived(&[0u8; 33]),
            Err(BadKeyLength)
        ));
    }
}