prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::{
    derive_identity_hash, DecryptError, IdentityEncryptionPublicKey, IdentityHash, IdentitySigner,
    IdentitySigningPublicKey, IDENTITY_SECRET_KEY_LEN,
};
use crate::crypto::{
    ed25519_public_key, ed25519_sign, x25519_diffie_hellman, x25519_public_key, Ed25519SecretKey,
    Ed25519Signature, X25519PublicKey, X25519SecretKey, X25519SharedSecret,
};

pub struct InMemoryNodeIdentity {
    encryption_secret: X25519SecretKey,
    signing_secret: Ed25519SecretKey,
    encryption_public: IdentityEncryptionPublicKey,
    signing_public: IdentitySigningPublicKey,
    hash: IdentityHash,
}

pub struct IdentityParts {
    pub encryption_secret: X25519SecretKey,
    pub signing_secret: Ed25519SecretKey,
    pub encryption_public: IdentityEncryptionPublicKey,
    pub signing_public: IdentitySigningPublicKey,
    pub hash: IdentityHash,
}

impl InMemoryNodeIdentity {
    pub fn from_secret_key_bytes(bytes: &[u8; IDENTITY_SECRET_KEY_LEN]) -> Self {
        let mut encryption_secret_bytes = [0u8; X25519SecretKey::LEN];
        encryption_secret_bytes.copy_from_slice(&bytes[..X25519SecretKey::LEN]);
        let mut signing_secret_bytes = [0u8; Ed25519SecretKey::LEN];
        signing_secret_bytes.copy_from_slice(&bytes[X25519SecretKey::LEN..]);

        let encryption_secret = X25519SecretKey::new(encryption_secret_bytes);
        let signing_secret = Ed25519SecretKey::new(signing_secret_bytes);
        let encryption_public =
            IdentityEncryptionPublicKey::new(x25519_public_key(&encryption_secret));
        let signing_public = IdentitySigningPublicKey::new(ed25519_public_key(&signing_secret));
        let hash = derive_identity_hash(&encryption_public, &signing_public);

        Self {
            encryption_secret,
            signing_secret,
            encryption_public,
            signing_public,
            hash,
        }
    }

    pub fn shared_secret_with(
        &self,
        peer_encryption_public: &X25519PublicKey,
    ) -> X25519SharedSecret {
        x25519_diffie_hellman(&self.encryption_secret, peer_encryption_public)
    }

    pub fn decrypt_in_place<'t>(
        &self,
        ciphertext_token: &'t mut [u8],
    ) -> Result<&'t [u8], DecryptError> {
        super::decrypt_token_in_place(&self.encryption_secret, &self.hash, ciphertext_token)
    }

    pub fn decrypt(&self, ciphertext_token: &[u8], out: &mut [u8]) -> Result<usize, DecryptError> {
        super::decrypt_token(&self.encryption_secret, &self.hash, ciphertext_token, out)
    }

    pub fn into_parts(self) -> IdentityParts {
        let Self {
            encryption_secret,
            signing_secret,
            encryption_public,
            signing_public,
            hash,
        } = self;
        IdentityParts {
            encryption_secret,
            signing_secret,
            encryption_public,
            signing_public,
            hash,
        }
    }
}

impl IdentitySigner for InMemoryNodeIdentity {
    fn encryption_public_key(&self) -> IdentityEncryptionPublicKey {
        self.encryption_public
    }

    fn signing_public_key(&self) -> IdentitySigningPublicKey {
        self.signing_public
    }

    fn identity_hash(&self) -> IdentityHash {
        self.hash
    }

    fn sign(&self, message: &[u8]) -> Ed25519Signature {
        ed25519_sign(&self.signing_secret, message)
    }
}

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

    fn fixed_secret_key_bytes() -> [u8; IDENTITY_SECRET_KEY_LEN] {
        let mut bytes = [0u8; IDENTITY_SECRET_KEY_LEN];
        bytes[..32].fill(0x22);
        bytes[32..].fill(0x11);
        bytes
    }

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

    const RNS_SEALED_TOKEN: &str =
        "81359acd4801e770d203b5b8f8500cd30830045a31616ff3167cac747c3c9072\
         93c11f98685deef2b25d3f6514d10a3c17c1bb903f6531e5499ef38dd7536fad\
         65701dad8b651b60ed993be65e1433a7f49cdb641b314b1ebeac3930058deea3";

    const RUST_SEALED_TOKEN: &str =
        "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14\
         44444444444444444444444444444444f3fc0f35b7a182440fd9efc1ed35ae58\
         99108742c09abbdf0d496ff0e0461e0b9959bc4e968a39f8934dc9b071066050";

    fn token_hex(s: &str) -> std::vec::Vec<u8> {
        let cleaned: std::string::String = s.split_whitespace().collect();
        (0..cleaned.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&cleaned[i..i + 2], 16).expect("valid hex"))
            .collect()
    }

    fn remote_for(identity: &InMemoryNodeIdentity) -> super::super::RemoteIdentity {
        super::super::RemoteIdentity::from_public_keys(
            identity.encryption_public_key(),
            identity.signing_public_key(),
        )
    }

    #[test]
    fn decrypt_in_place_opens_the_rns_1_4_2_token_without_a_copy() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let mut token = token_hex(RNS_SEALED_TOKEN);
        let plaintext = identity.decrypt_in_place(&mut token).unwrap();
        assert_eq!(plaintext, b"hello-single");
    }

    #[test]
    fn decrypt_in_place_rejects_tampering_and_short_tokens() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());

        let mut tampered = token_hex(RNS_SEALED_TOKEN);
        tampered[50] ^= 0x01;
        assert_eq!(
            identity.decrypt_in_place(&mut tampered),
            Err(DecryptError::InvalidToken),
        );

        let mut short = [0xAA; ENCRYPTION_EPHEMERAL_PUBLIC_KEY_LEN];
        assert_eq!(
            identity.decrypt_in_place(&mut short),
            Err(DecryptError::TokenTooShort),
        );
    }

    #[test]
    fn decrypt_opens_a_token_sealed_by_rns_1_4_2() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let token = token_hex(RNS_SEALED_TOKEN);
        let mut out = [0u8; 64];
        let n = identity.decrypt(&token, &mut out).unwrap();
        assert_eq!(&out[..n], b"hello-single");
    }

    #[test]
    fn encrypt_seals_the_token_rns_1_4_2_opens() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let mut out = [0u8; 128];
        let n = remote_for(&identity)
            .encrypt(
                &X25519SecretKey::new([0x33; 32]),
                &[0x44; 16],
                b"hello-single",
                &mut out,
            )
            .unwrap();
        assert_eq!(out[..n].to_vec(), token_hex(RUST_SEALED_TOKEN));
    }

    const RATCHET_SEALED_TOKEN: &str =
        "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14\
         44444444444444444444444444444444f0c0d10df07782f3a9a89a271b84960b\
         c9d2525bfcfd385954b4ebda6c6702dd9b82ca630f3b45c1c57457ad70aa14e6";

    #[test]
    fn encrypt_to_ratchet_reproduces_the_rns_1_4_2_minted_token() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let ratchet_public = x25519_public_key(&X25519SecretKey::new([0x55; 32]));
        let mut out = [0u8; 128];
        let n = remote_for(&identity)
            .encrypt_to_ratchet(
                &ratchet_public,
                &X25519SecretKey::new([0x33; 32]),
                &[0x44; 16],
                b"ratchet-parity",
                &mut out,
            )
            .unwrap();
        assert_eq!(out[..n].to_vec(), token_hex(RATCHET_SEALED_TOKEN));
    }

    #[test]
    fn a_ratchet_sealed_token_does_not_open_with_the_identity_key_alone() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let mut token = token_hex(RATCHET_SEALED_TOKEN);
        assert_eq!(
            identity.decrypt_in_place(&mut token),
            Err(DecryptError::InvalidToken),
        );
    }

    #[test]
    fn encrypt_round_trips_through_decrypt() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let plaintext = [0xC7; 211];
        let mut sealed = [0u8; 512];
        let n = remote_for(&identity)
            .encrypt(
                &X25519SecretKey::new([0x55; 32]),
                &[0x66; 16],
                &plaintext,
                &mut sealed,
            )
            .unwrap();

        let mut opened = [0u8; 512];
        let opened_len = identity.decrypt(&sealed[..n], &mut opened).unwrap();
        assert_eq!(&opened[..opened_len], &plaintext);
    }

    #[test]
    fn a_tampered_token_is_rejected() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let mut out = [0u8; 64];

        let mut tampered_ciphertext = token_hex(RNS_SEALED_TOKEN);
        tampered_ciphertext[50] ^= 0x01;
        assert_eq!(
            identity.decrypt(&tampered_ciphertext, &mut out),
            Err(DecryptError::InvalidToken),
        );

        let mut tampered_mac = token_hex(RNS_SEALED_TOKEN);
        let last = tampered_mac.len() - 1;
        tampered_mac[last] ^= 0x01;
        assert_eq!(
            identity.decrypt(&tampered_mac, &mut out),
            Err(DecryptError::InvalidToken),
        );
    }

    #[test]
    fn a_token_no_longer_than_the_ephemeral_key_is_rejected() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let mut out = [0u8; 64];
        assert_eq!(
            identity.decrypt(&[0xAA; ENCRYPTION_EPHEMERAL_PUBLIC_KEY_LEN], &mut out),
            Err(DecryptError::TokenTooShort),
        );
    }

    #[test]
    fn another_identity_cannot_open_the_token() {
        let other = InMemoryNodeIdentity::from_secret_key_bytes(&[0x05; IDENTITY_SECRET_KEY_LEN]);
        let token = token_hex(RNS_SEALED_TOKEN);
        let mut out = [0u8; 64];
        assert_eq!(
            other.decrypt(&token, &mut out),
            Err(DecryptError::InvalidToken),
        );
    }

    #[test]
    fn undersized_buffers_are_reported_not_panicked() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let token = token_hex(RNS_SEALED_TOKEN);
        let mut tiny = [0u8; 4];
        assert_eq!(
            identity.decrypt(&token, &mut tiny),
            Err(DecryptError::BufferTooShort),
        );

        let mut tiny_out = [0u8; 16];
        assert_eq!(
            remote_for(&identity).encrypt(
                &X25519SecretKey::new([0x33; 32]),
                &[0x44; 16],
                b"hello-single",
                &mut tiny_out,
            ),
            Err(super::super::EncryptError::BufferTooShort),
        );
    }

    #[test]
    fn from_secret_key_bytes_derives_rns_public_keys_and_hash() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());

        assert_eq!(
            identity.encryption_public_key().as_bytes(),
            &bytes_from_hex::<32>(
                "0faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20"
            ),
        );
        assert_eq!(
            identity.signing_public_key().as_bytes(),
            &bytes_from_hex::<32>(
                "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737"
            ),
        );

        assert_eq!(
            identity.identity_hash(),
            IdentityHash::new(bytes_from_hex::<16>("4cd0cc45a7405dbd5cf9b5be1ef92f10")),
        );
    }

    #[test]
    fn same_seed_is_deterministic() {
        let a = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let b = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        assert_eq!(a.identity_hash(), b.identity_hash());
    }

    #[test]
    fn signatures_verify_under_the_identitys_public_key() {
        use crate::crypto::ed25519_verify;

        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let message = b"announce-ourselves";
        let signature = identity.sign(message);

        assert!(ed25519_verify(
            identity.signing_public_key().as_ed25519(),
            message,
            &signature
        )
        .is_ok());
        assert!(ed25519_verify(
            identity.signing_public_key().as_ed25519(),
            b"tampered",
            &signature
        )
        .is_err());
    }

    #[test]
    fn key_agreement_matches_the_rns_shared_secret() {
        let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let peer = X25519PublicKey(bytes_from_hex::<32>(
            "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14",
        ));
        assert_eq!(
            identity.shared_secret_with(&peer).as_bytes(),
            &bytes_from_hex::<32>(
                "1fdc192faa0212a9aae7bb4f41b580227fd5ad3e5d777faae230dfe973f3e805"
            ),
        );
    }

    #[test]
    fn distinct_seeds_yield_distinct_identities() {
        let a = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key_bytes());
        let b = InMemoryNodeIdentity::from_secret_key_bytes(&[0x05; IDENTITY_SECRET_KEY_LEN]);
        assert_ne!(a.identity_hash(), b.identity_hash());
    }
}