diff --git a/agent-framework/prometheus_swarm/clients/web_client_nonce.py b/agent-framework/prometheus_swarm/clients/web_client_nonce.py new file mode 100644 index 00000000..91e119f1 --- /dev/null +++ b/agent-framework/prometheus_swarm/clients/web_client_nonce.py @@ -0,0 +1,60 @@ +import random +import string +from typing import Optional + +class WebClientNonceManager: + """ + A utility class for generating and managing web client nonces. + + Nonces (number used once) are random, unique tokens used to prevent + replay attacks and ensure request uniqueness. + """ + + @staticmethod + def generate_nonce(length: int = 32) -> str: + """ + Generate a cryptographically secure random nonce. + + Args: + length (int, optional): Length of the nonce. Defaults to 32. + + Returns: + str: A randomly generated nonce string. + + Raises: + ValueError: If length is less than 16. + """ + if length < 16: + raise ValueError("Nonce length must be at least 16 characters") + + # Use a combination of uppercase, lowercase, and digits + characters = string.ascii_letters + string.digits + + # Use SystemRandom for cryptographically secure random generation + secure_random = random.SystemRandom() + + nonce = ''.join(secure_random.choice(characters) for _ in range(length)) + return nonce + + @staticmethod + def validate_nonce(nonce: Optional[str], min_length: int = 16, max_length: int = 64) -> bool: + """ + Validate a nonce's format and characteristics. + + Args: + nonce (Optional[str]): The nonce to validate. + min_length (int, optional): Minimum acceptable nonce length. Defaults to 16. + max_length (int, optional): Maximum acceptable nonce length. Defaults to 64. + + Returns: + bool: True if nonce is valid, False otherwise. + """ + if nonce is None: + return False + + # Check length constraints + if not (min_length <= len(nonce) <= max_length): + return False + + # Ensure nonce contains only valid characters + return all(char in string.ascii_letters + string.digits for char in nonce) \ No newline at end of file diff --git a/agent-framework/tests/unit/test_web_client_nonce.py b/agent-framework/tests/unit/test_web_client_nonce.py new file mode 100644 index 00000000..f94c4dcf --- /dev/null +++ b/agent-framework/tests/unit/test_web_client_nonce.py @@ -0,0 +1,51 @@ +import pytest +from prometheus_swarm.clients.web_client_nonce import WebClientNonceManager + +def test_generate_nonce_default_length(): + """Test nonce generation with default length.""" + nonce = WebClientNonceManager.generate_nonce() + assert len(nonce) == 32 + assert WebClientNonceManager.validate_nonce(nonce) + +def test_generate_nonce_custom_length(): + """Test nonce generation with custom length.""" + for length in [16, 24, 48, 64]: + nonce = WebClientNonceManager.generate_nonce(length) + assert len(nonce) == length + assert WebClientNonceManager.validate_nonce(nonce) + +def test_generate_nonce_invalid_length(): + """Test nonce generation fails for too short nonces.""" + with pytest.raises(ValueError): + WebClientNonceManager.generate_nonce(15) + +def test_nonce_uniqueness(): + """Verify generated nonces are unique.""" + nonces = set(WebClientNonceManager.generate_nonce() for _ in range(100)) + assert len(nonces) == 100 + +def test_nonce_validation(): + """Test nonce validation with various inputs.""" + # Valid nonces + valid_cases = [ + WebClientNonceManager.generate_nonce(), + WebClientNonceManager.generate_nonce(16), + WebClientNonceManager.generate_nonce(64) + ] + + # Invalid nonces + invalid_cases = [ + None, + "", # Empty string + "short", # Too short + "x" * 65, # Too long + "invalid-nonce!", # Special characters + "UPPERCASE_ONLY", # Case sensitivity + " spaces " + ] + + for nonce in valid_cases: + assert WebClientNonceManager.validate_nonce(nonce), f"Failed to validate: {nonce}" + + for nonce in invalid_cases: + assert not WebClientNonceManager.validate_nonce(nonce), f"Incorrectly validated: {nonce}" \ No newline at end of file