nonce-auth 0.6.3

A secure nonce-based authentication library with pluggable storage backends
Documentation
//! A lightweight, secure nonce-based authentication library for Rust.
//!
//! This library provides a simple, robust solution for preventing replay attacks
//! in APIs and other network services. It uses a combination of nonces, timestamps,
//! and HMAC signatures to ensure that each request is unique and authentic.
//!
//! # Core Components
//!
//! - [`CredentialBuilder`] - Creates cryptographic credentials
//! - [`CredentialVerifier`] - Verifies cryptographic credentials
//! - [`NonceStorage`] - Pluggable storage backends
//!
//! # Quick Example
//!
//! ```rust
//! use nonce_auth::{CredentialBuilder, CredentialVerifier, storage::MemoryStorage};
//! use std::sync::Arc;
//!
//! # async fn example() -> Result<(), nonce_auth::NonceError> {
//! // Create a credential
//! let credential = CredentialBuilder::new(b"shared_secret")
//!     .sign(b"important_data")?;
//!
//! // Verify the credential
//! let storage = Arc::new(MemoryStorage::new());
//! CredentialVerifier::new(storage)
//!     .with_secret(b"shared_secret")
//!     .verify(&credential, b"important_data")
//!     .await?;
//! # Ok(())
//! # }
//! ```
//!
//! For detailed configuration options, see [CONFIGURATION.md](https://github.com/kookyleo/nonce-auth/blob/main/docs/CONFIGURATION.md).

use serde::{Deserialize, Serialize};

pub mod nonce;
pub mod signature {
    //! Pluggable signature algorithms for cryptographic operations.
    pub use crate::nonce::signature::*;
}
pub mod storage {
    //! Pluggable storage backends for nonce persistence.
    pub use crate::nonce::storage::*;
}

// Re-export key types for easy access.
pub use nonce::{
    BoxedCleanupStrategy,
    CleanupStrategy,
    // Configuration
    ConfigPreset,
    // Core architecture
    CredentialBuilder,
    CredentialVerifier,
    CustomCleanupStrategy,

    HybridCleanupStrategy,
    MacLike,
    // Storage and cleanup systems
    MemoryStorage,
    NonceConfig,

    NonceEntry,
    NonceError,
    NonceGeneratorFn,
    NonceStorage,
    // Signature algorithms
    SignatureAlgorithm,
    StorageStats,
    TimeProviderFn,
};

// Conditional exports based on features
#[cfg(feature = "algo-hmac-sha256")]
pub use nonce::{DefaultSignatureAlgorithm, create_default_algorithm};

#[cfg(feature = "metrics")]
pub use nonce::{
    ErrorMetrics, InMemoryMetricsCollector, MetricEvent, MetricsCollector, MetricsTimer,
    NoOpMetricsCollector, NonceMetrics, PerformanceMetrics,
};

/// A self-contained cryptographic credential used to authenticate a request.
///
/// This structure is generated by a [`CredentialBuilder`] and verified by a [`CredentialVerifier`].
/// It is designed to be serialized and sent alongside your application's request data.
///
/// # Fields
///
/// - `timestamp`: Unix timestamp indicating when the credential was created.
/// - `nonce`: A unique, single-use value to prevent replay attacks.
/// - `signature`: An HMAC-SHA256 signature covering the timestamp, nonce, and user-defined payload.
///
/// # Example
///
/// ```rust
/// use nonce_auth::CredentialBuilder;
///
/// let credential = CredentialBuilder::new(b"secret")
///     .sign(b"data")?;
///
/// // Serialize for transmission
/// let json = serde_json::to_string(&credential)?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NonceCredential {
    pub timestamp: u64,
    pub nonce: String,
    pub signature: String,
}

#[cfg(test)]
mod tests {
    use crate::{CredentialBuilder, CredentialVerifier, NonceError, storage::MemoryStorage};
    use hmac::Mac;
    use std::sync::Arc;

    #[tokio::test]
    async fn test_basic_credential_workflow() {
        let secret = b"test_secret";
        let payload = b"test_payload";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create a credential
        let credential = CredentialBuilder::new(secret).sign(payload).unwrap();

        // Verify the credential
        let result = CredentialVerifier::new(storage)
            .with_secret(secret)
            .verify(&credential, payload)
            .await;

        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_nonce_replay_protection() {
        let secret = b"test_secret";
        let payload = b"test_payload";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create a credential
        let credential = CredentialBuilder::new(secret).sign(payload).unwrap();

        // First verification should succeed
        CredentialVerifier::new(Arc::clone(&storage))
            .with_secret(secret)
            .verify(&credential, payload)
            .await
            .unwrap();

        // Second verification should fail (replay attack)
        let result = CredentialVerifier::new(storage)
            .with_secret(secret)
            .verify(&credential, payload)
            .await;

        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
    }

    #[tokio::test]
    async fn test_context_isolation() {
        let secret = b"test_secret";
        let payload = b"test_payload";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create a credential
        let credential = CredentialBuilder::new(secret).sign(payload).unwrap();

        // Use in context1
        CredentialVerifier::new(Arc::clone(&storage))
            .with_secret(secret)
            .with_context(Some("context1"))
            .verify(&credential, payload)
            .await
            .unwrap();

        // Should work in context2 (different context)
        let result = CredentialVerifier::new(Arc::clone(&storage))
            .with_secret(secret)
            .with_context(Some("context2"))
            .verify(&credential, payload)
            .await;

        assert!(result.is_ok());

        // Should fail in context1 again (same context)
        let result = CredentialVerifier::new(storage)
            .with_secret(secret)
            .with_context(Some("context1"))
            .verify(&credential, payload)
            .await;

        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
    }

    #[tokio::test]
    async fn test_invalid_signature() {
        let payload = b"test_payload";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create credential with one secret
        let credential = CredentialBuilder::new(b"secret1").sign(payload).unwrap();

        // Verify with different secret
        let result = CredentialVerifier::new(storage)
            .with_secret(b"secret2")
            .verify(&credential, payload)
            .await;

        assert!(matches!(result, Err(NonceError::InvalidSignature)));
    }

    #[tokio::test]
    async fn test_structured_signing() {
        let secret = b"test_secret";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());
        let components = [b"part1".as_slice(), b"part2", b"part3"];

        // Create credential with structured signing
        let credential = CredentialBuilder::new(secret)
            .sign_structured(&components)
            .unwrap();

        // Verify with structured verification
        let result = CredentialVerifier::new(storage)
            .with_secret(secret)
            .verify_structured(&credential, &components)
            .await;

        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_custom_signing() {
        let secret = b"test_secret";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create credential with custom signing
        let credential = CredentialBuilder::new(secret)
            .sign_with(|mac, timestamp, nonce| {
                mac.update(b"prefix:");
                mac.update(timestamp.as_bytes());
                mac.update(b":nonce:");
                mac.update(nonce.as_bytes());
                mac.update(b":custom");
            })
            .unwrap();

        // Verify with matching custom verification
        let result = CredentialVerifier::new(storage)
            .with_secret(secret)
            .verify_with(&credential, |mac| {
                mac.update(b"prefix:");
                mac.update(credential.timestamp.to_string().as_bytes());
                mac.update(b":nonce:");
                mac.update(credential.nonce.as_bytes());
                mac.update(b":custom");
            })
            .await;

        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_serialization() {
        let secret = b"test_secret";
        let payload = b"test_payload";

        // Create credential
        let credential = CredentialBuilder::new(secret).sign(payload).unwrap();

        // Test JSON serialization
        let json = serde_json::to_string(&credential).unwrap();
        let deserialized: super::NonceCredential = serde_json::from_str(&json).unwrap();

        assert_eq!(credential.timestamp, deserialized.timestamp);
        assert_eq!(credential.nonce, deserialized.nonce);
        assert_eq!(credential.signature, deserialized.signature);
    }

    #[tokio::test]
    async fn test_secret_provider() {
        let payload = b"test_payload";
        let storage: Arc<dyn crate::storage::NonceStorage> = Arc::new(MemoryStorage::new());

        // Create credential with known secret
        let credential = CredentialBuilder::new(b"user123_secret")
            .sign(payload)
            .unwrap();

        // Verify using secret provider
        let result = CredentialVerifier::new(storage)
            .with_context(Some("user123"))
            .with_secret_provider(|context| {
                let owned_context = context.map(|s| s.to_owned());
                async move {
                    match owned_context.as_deref() {
                        Some("user123") => Ok(b"user123_secret".to_vec()),
                        Some("user456") => Ok(b"user456_secret".to_vec()),
                        _ => Err(NonceError::CryptoError("Unknown user".to_string())),
                    }
                }
            })
            .verify(&credential, payload)
            .await;

        assert!(result.is_ok());
    }
}