nonce-auth 0.3.0

A secure nonce-based authentication library for preventing replay attacks
Documentation

Nonce Auth

CI codecov Crates.io Documentation License

A Rust-based secure nonce authentication library that provides one-time token (nonce) generation, signing, and verification functionality to effectively prevent replay attacks.

Features

  • 🔐 HMAC-SHA256 Signing - Cryptographic signing of requests using shared secrets
  • Timestamp Window Validation - Prevents replay attacks from expired requests
  • 🔑 One-time Nonce - Ensures each nonce can only be used once
  • 💾 SQLite Persistence - Automatic nonce storage and cleanup management
  • 🎯 Context Isolation - Support for nonce isolation across different business scenarios
  • 🚀 Async Support - Fully asynchronous API design
  • 🛡️ Security Protection - Constant-time comparison to prevent timing attacks
  • 📦 Client-Server Separation - Clean separation of client and server responsibilities
  • 🔧 Flexible Signature Algorithm - Customizable signature data construction

Architecture

Client-Server Separation Design

The library provides two independent managers for clear separation of responsibilities:

NonceClient - Client-side Manager

  • Responsible for generating signed authentication data
  • No database dependencies required
  • Lightweight design suitable for client-side use
  • Flexible signature algorithm through closures

NonceServer - Server-side Manager

  • Responsible for verifying signed authentication data
  • Manages nonce storage and cleanup
  • Includes timestamp validation and replay attack prevention
  • Supports context isolation for different business scenarios

Parameter Explanation

  • default_ttl: Nonce time-to-live, representing the duration from generation to expiration, defaults to 5 minutes
  • time_window: Timestamp validation window, defaults to 1 minute

Both work together to prevent replay attacks.

Important Notes

  • The server uses local SQLite for nonce persistence, please consider using with connection sticky policies
  • Signature algorithms are fully customizable through closures for maximum flexibility

Quick Start

Add Dependencies

[dependencies]
nonce-auth = "0.2.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
warp = "0.3"
hmac = "0.12"

Simple Usage Example

use hmac::Mac;
use nonce_auth::{NonceClient, NonceServer};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Pre-shared key between client and server
    let psk = b"my-secret-key";

    // Initialize server
    NonceServer::init().await?;
    let server = NonceServer::new(
        psk,
        Some(Duration::from_secs(300)), // 5 minutes TTL for nonce storage
        Some(Duration::from_secs(60)),  // 1 minute time window for timestamp validation
    );

    // Initialize client
    let client = NonceClient::new(psk);

    // Client generates authentication data with custom signature (timestamp + nonce)
    let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
        mac.update(timestamp.as_bytes());
        mac.update(nonce.as_bytes());
    })?;
    println!("Generated authentication data: {protection_data:?}");

    // Server verifies the authentication data with matching signature algorithm
    match server
        .verify_protection_data(&protection_data, None, |mac| {
            mac.update(protection_data.timestamp.to_string().as_bytes());
            mac.update(protection_data.nonce.as_bytes());
        })
        .await
    {
        Ok(()) => println!("✅ Authentication verified successfully"),
        Err(e) => println!("❌ Authentication verification failed: {e:?}"),
    }

    // Try to use the same nonce again (should fail)
    match server
        .verify_protection_data(&protection_data, None, |mac| {
            mac.update(protection_data.timestamp.to_string().as_bytes());
            mac.update(protection_data.nonce.as_bytes());
        })
        .await
    {
        Ok(()) => println!("❌ This should not happen - nonce reuse detected"),
        Err(e) => println!("✅ Correctly rejected duplicate nonce: {e:?}"),
    }

    Ok(())
}

Complete Web Application Example

JavaScript Client

// client.js
class NonceClient {
    constructor(psk) {
        this.psk = new TextEncoder().encode(psk);
        this.lastRequest = null;
    }

    async createSignedRequest(message) {
        const timestamp = Math.floor(Date.now() / 1000);
        const nonce = this.generateUUID();
        const signature = await this.sign(timestamp.toString(), nonce, message);
        
        const request = {
            timestamp,
            nonce,
            signature
        };
        
        // Save the last request for repeating
        this.lastRequest = { message, auth: {...request} };
        
        return {
            payload: message,
            session_id: sessionId, // From server-embedded variable
            auth: request
        };
    }

    async sign(timestamp, nonce, message) {
        try {
            const key = await crypto.subtle.importKey(
                'raw',
                this.psk,
                { name: 'HMAC', hash: 'SHA-256' },
                false,
                ['sign']
            );
            
            const data = new TextEncoder().encode(timestamp + nonce + message);
            const signature = await crypto.subtle.sign('HMAC', key, data);
            
            return Array.from(new Uint8Array(signature))
                .map(b => b.toString(16).padStart(2, '0'))
                .join('');
        } catch (error) {
            console.error('Signing failed:', error);
            throw error;
        }
    }

    generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0;
            const v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }
}

// Usage example
async function makeAuthenticatedRequest() {
    const client = new NonceClient(currentPsk); // PSK embedded from server
    const message = "Hello, secure world!";
    
    try {
        const requestData = await client.createSignedRequest(message);
        
        const response = await fetch('/api/protected', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(requestData)
        });
        
        const result = await response.json();
        console.log('Server response:', result);
    } catch (error) {
        console.error('Request failed:', error);
    }
}

Rust Server

// server.rs
use hmac::Mac;
use nonce_auth::NonceServer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use warp::Filter;

#[derive(Deserialize)]
struct AuthenticatedRequest {
    payload: String,
    session_id: String,
    auth: nonce_auth::ProtectionData,
}

#[derive(Serialize)]
struct ApiResponse {
    success: bool,
    message: String,
    data: Option<String>,
}

// Store PSKs for each session
type PskStore = Arc<std::sync::Mutex<HashMap<String, String>>>;

#[tokio::main]
async fn main() {
    // Initialize the nonce server database
    NonceServer::init()
        .await
        .expect("Failed to initialize database");

    // Create PSK store
    let psk_store: PskStore = Arc::new(std::sync::Mutex::new(HashMap::new()));

    // Serve index.html at the root path with embedded PSK
    let psk_store_filter = warp::any().map(move || psk_store.clone());
    let index_route = warp::path::end()
        .and(psk_store_filter.clone())
        .and_then(handle_index_request);

    // Protected API route
    let protected_route = warp::path("api")
        .and(warp::path("protected"))
        .and(warp::post())
        .and(warp::body::json())
        .and(psk_store_filter)
        .and_then(handle_protected_request);

    // Combine routes
    let routes = index_route.or(protected_route).with(
        warp::cors()
            .allow_any_origin()
            .allow_headers(vec!["content-type"])
            .allow_methods(vec!["GET", "POST"]),
    );

    println!("Server running on http://localhost:3000");
    println!("Open this URL in your browser to test the authentication");
    println!("Each page refresh will generate a new PSK");

    warp::serve(routes).run(([127, 0, 0, 1], 3000)).await;
}

async fn handle_protected_request(
    req: AuthenticatedRequest,
    psk_store: PskStore,
) -> Result<impl warp::Reply, warp::Rejection> {
    // Get the PSK from store using session ID
    let psk = {
        let store = psk_store.lock().unwrap();
        println!("Looking for session ID: {}", req.session_id);
        store.get(&req.session_id).cloned()
    };

    let psk = match psk {
        Some(psk) => psk,
        None => {
            let response = ApiResponse {
                success: false,
                message: "Invalid session ID. Please refresh the page.".to_string(),
                data: None,
            };
            return Ok(warp::reply::json(&response));
        }
    };

    // Create server with PSK
    let server = NonceServer::new(
        psk.as_bytes(),
        Some(Duration::from_secs(60)), // 1 minute TTL
        Some(Duration::from_secs(15)), // 15 seconds time window
    );

    // Verify the request with custom signature including payload
    match server
        .verify_protection_data(&req.auth, None, |mac| {
            mac.update(req.auth.timestamp.to_string().as_bytes());
            mac.update(req.auth.nonce.as_bytes());
            mac.update(req.payload.as_bytes());
        })
        .await
    {
        Ok(()) => {
            let response = ApiResponse {
                success: true,
                message: "Request authenticated successfully".to_string(),
                data: Some(format!("Processed: {}", req.payload)),
            };
            Ok(warp::reply::json(&response))
        }
        Err(e) => {
            let response = ApiResponse {
                success: false,
                message: format!("Authentication failed: {e:?}"),
                data: None,
            };
            Ok(warp::reply::json(&response))
        }
    }
}

Example Authentication Flow Sequence Diagram

sequenceDiagram
    participant Browser as Web Browser
    participant RustServer as Rust Server
    participant DB as SQLite Database

    Note over Browser, DB: Session-based Authentication Flow

    Browser->>RustServer: 1. GET / (page request)
    RustServer->>RustServer: 2. Generate random PSK and session ID
    RustServer->>RustServer: 3. Store PSK with session ID
    RustServer->>Browser: 4. HTML with embedded PSK and session ID
    
    Browser->>Browser: 5. User enters payload
    Browser->>Browser: 6. Generate UUID nonce
    Browser->>Browser: 7. Create timestamp
    Browser->>Browser: 8. Sign (timestamp + nonce + payload) with HMAC-SHA256
    
    Browser->>RustServer: 9. POST /api/protected<br/>{payload, session_id, auth: {timestamp, nonce, signature}}
    
    RustServer->>RustServer: 10. Lookup PSK by session_id
    
    alt Invalid session ID
        RustServer-->>Browser: 401 Invalid session ID
    end
    
    RustServer->>RustServer: 11. Create NonceServer with PSK
    RustServer->>RustServer: 12. Verify timestamp within window
    
    alt Timestamp out of window
        RustServer-->>Browser: 401 Timestamp expired
    end
    
    RustServer->>RustServer: 13. Verify HMAC signature
    
    alt Invalid signature
        RustServer-->>Browser: 401 Invalid signature
    end
    
    RustServer->>DB: 14. Check if nonce exists
    
    alt Nonce already used
        RustServer-->>Browser: 401 Duplicate nonce
    end
    
    RustServer->>DB: 15. Store nonce
    RustServer->>RustServer: 16. Process business logic
    RustServer-->>Browser: 200 Success response
    
    Note over RustServer, DB: Background cleanup
    RustServer->>DB: Cleanup expired nonces as needed

API Documentation

NonceClient

Constructor

pub fn new(secret: &[u8]) -> Self
  • secret: Secret key used for signing

Methods

Create Authentication Data
pub fn create_protection_data<F>(&self, signature_builder: F) -> Result<ProtectionData, NonceError>
where
    F: FnOnce(&mut hmac::Hmac<sha2::Sha256>, &str, &str),

Generates authentication data with custom signature algorithm. The closure receives the MAC instance, timestamp string, and nonce string.

Generate Signature
pub fn generate_signature<F>(&self, data_builder: F) -> Result<String, NonceError>
where
    F: FnOnce(&mut hmac::Hmac<sha2::Sha256>),

Generates HMAC-SHA256 signature with custom data builder.

NonceServer

Constructor

pub fn new(
    secret: &[u8], 
    default_ttl: Option<Duration>, 
    time_window: Option<Duration>
) -> Self
  • secret: Secret key used for verification
  • default_ttl: Default nonce expiration time (default: 5 minutes)
  • time_window: Allowed time window for timestamp validation (default: 1 minute)

Methods

Verify Authentication Data
pub async fn verify_protection_data<F>(
    &self, 
    protection_data: &ProtectionData, 
    context: Option<&str>,
    signature_builder: F,
) -> Result<(), NonceError>
where
    F: FnOnce(&mut hmac::Hmac<sha2::Sha256>),

Verifies authentication data with custom signature algorithm. The closure should match the one used on the client side.

Initialize Database
pub async fn init() -> Result<(), NonceError>

Creates necessary database tables and indexes.

ProtectionData

pub struct ProtectionData {
    pub timestamp: u64,    // Unix timestamp
    pub nonce: String,     // UUID format one-time token
    pub signature: String, // HMAC-SHA256 signature
}

Error Types

pub enum NonceError {
    DuplicateNonce,         // Nonce already used
    ExpiredNonce,           // Nonce expired
    InvalidSignature,       // Invalid signature
    TimestampOutOfWindow,   // Timestamp outside allowed window
    DatabaseError(String),  // Database error
    CryptoError(String),    // Cryptographic error
}

Typical Use Cases

1. API Authentication

  • Client generates authentication data for each request
  • Server verifies each request independently
  • Each authentication data can only be used once

2. Form Submission Protection

  • Generate authentication data when rendering form
  • Verify authentication data when submitting
  • Prevents duplicate form submissions

3. Microservice Authentication

  • Service A generates authentication data for requests
  • Service B verifies requests from Service A
  • Ensures request uniqueness and authenticity

4. Session-based Authentication

  • Server generates random PSK per session
  • Client uses session PSK for all requests
  • Provides stateless authentication with session isolation

Security Features

Replay Attack Prevention

  1. Time Window Limitation: Only accepts requests within specified time window
  2. One-time Nonce: Each nonce is deleted after verification, ensuring no reuse
  3. Context Isolation: Nonces from different business scenarios are isolated

Timing Attack Prevention

  • Uses constant-time comparison algorithms for signature verification

Cryptographic Strength

  • Uses HMAC-SHA256 algorithm to ensure signature integrity and authenticity
  • Supports custom secret key lengths
  • Flexible signature algorithms through closures

Performance Optimization

  • Automatic background cleanup of expired nonce records
  • Database index optimization for query performance
  • Asynchronous design supports high-concurrency scenarios

Dependencies

  • hmac - HMAC signing
  • sha2 - SHA256 hashing
  • rusqlite - SQLite database library
  • uuid - UUID generation
  • serde - Serialization support
  • tokio - Async runtime
  • thiserror - Error handling

License

MIT OR Apache-2.0

Contributing

Issues and Pull Requests are welcome!