evidence 0.1.0

Type-level tags for cryptographic primitives
Documentation
//! Property-based tests for Encrypted.

#![cfg(feature = "chacha20poly1305")]

use bolero::check;
use evidence::{
    codec::Identity,
    encrypted::{Encrypted, EncryptedUnchecked},
    encryption::chacha20poly1305::ChaCha20Poly1305,
};

/// Helper to create a key from seed bytes.
fn key_from_seed(seed: [u8; 32]) -> chacha20poly1305::Key {
    chacha20poly1305::Key::from(seed)
}

#[test]
#[allow(clippy::expect_used)] // test should panic if decryption fails
fn encrypted_decrypt_roundtrip() {
    check!()
        .with_type::<([u8; 32], Vec<u8>)>()
        .for_each(|(seed, plaintext)| {
            let key = key_from_seed(*seed);

            let encrypted: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt(&key, plaintext);

            let decrypted = encrypted
                .try_decrypt(&key)
                .expect("decryption should succeed for just-encrypted payload");

            assert_eq!(
                &decrypted, plaintext,
                "decrypted payload must match original"
            );
        });
}

#[test]
fn encrypted_different_nonces() {
    check!()
        .with_type::<([u8; 32], Vec<u8>)>()
        .for_each(|(seed, plaintext)| {
            let key = key_from_seed(*seed);

            let encrypted1: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt(&key, plaintext);
            let encrypted2: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt(&key, plaintext);

            assert_ne!(
                encrypted1.nonce(),
                encrypted2.nonce(),
                "different encryptions should use different nonces"
            );
        });
}

#[test]
fn wrong_key_fails_decryption() {
    check!()
        .with_type::<([u8; 32], [u8; 32], Vec<u8>)>()
        .filter(|(a, b, _)| a != b)
        .for_each(|(seed_correct, seed_wrong, plaintext)| {
            let correct_key = key_from_seed(*seed_correct);
            let wrong_key = key_from_seed(*seed_wrong);

            let encrypted: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt(&correct_key, plaintext);

            assert!(
                encrypted.try_decrypt(&wrong_key).is_err(),
                "decryption must fail with wrong key"
            );
        });
}

#[test]
#[allow(clippy::indexing_slicing)] // ciphertext is non-empty (filter guarantees)
fn tampered_ciphertext_fails_decryption() {
    check!()
        .with_type::<([u8; 32], Vec<u8>)>()
        .cloned()
        .filter(|(_, plaintext)| !plaintext.is_empty())
        .for_each(|(seed, plaintext)| {
            let key = key_from_seed(seed);

            let encrypted: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt(&key, &plaintext);

            let mut tampered_ciphertext = encrypted.ciphertext().to_vec();
            tampered_ciphertext[0] = tampered_ciphertext[0].wrapping_add(1);

            let tampered: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::from_unchecked_parts(tampered_ciphertext, *encrypted.nonce());

            assert!(
                tampered.try_decrypt(&key).is_err(),
                "decryption must fail with tampered ciphertext"
            );
        });
}

#[test]
fn deterministic_with_same_nonce() {
    check!()
        .with_type::<([u8; 32], [u8; 12], Vec<u8>)>()
        .for_each(|(seed, nonce_bytes, plaintext)| {
            let key = key_from_seed(*seed);
            let nonce = hybrid_array::Array::from(*nonce_bytes);

            let encrypted1: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt_with_nonce(&key, &nonce, plaintext);
            let encrypted2: Encrypted<Vec<u8>, ChaCha20Poly1305, Identity> =
                Encrypted::encrypt_with_nonce(&key, &nonce, plaintext);

            assert_eq!(
                encrypted1.ciphertext(),
                encrypted2.ciphertext(),
                "same key + nonce + plaintext must produce same ciphertext"
            );
        });
}