From d09a722c463173ea7d757b97a4adbd7581bfaeac Mon Sep 17 00:00:00 2001 From: Obludka Date: Wed, 7 May 2025 00:13:08 +0000 Subject: [PATCH 1/2] Implement WebClientNonceManager for secure nonce generation and validation --- src/web_client_nonce.py | 97 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/web_client_nonce.py diff --git a/src/web_client_nonce.py b/src/web_client_nonce.py new file mode 100644 index 00000000..4caab7f2 --- /dev/null +++ b/src/web_client_nonce.py @@ -0,0 +1,97 @@ +import secrets +import time +from typing import Dict, Any + +class WebClientNonceManager: + """ + A class to manage nonce generation and validation for web clients. + + This class provides methods to generate unique, time-limited nonces + that can be used for security purposes such as preventing replay attacks. + """ + + def __init__(self, nonce_expiry_seconds: int = 300): + """ + Initialize the WebClientNonceManager. + + Args: + nonce_expiry_seconds (int, optional): Time in seconds after which + a nonce becomes invalid. + Defaults to 300 seconds (5 minutes). + """ + self._nonce_store: Dict[str, Dict[str, Any]] = {} + self._nonce_expiry = nonce_expiry_seconds + + def generate_nonce(self, client_id: str) -> str: + """ + Generate a unique nonce for a given client. + + Args: + client_id (str): Unique identifier for the client. + + Returns: + str: A unique nonce string. + """ + # Generate a cryptographically secure random nonce + nonce = secrets.token_urlsafe(32) + + # Store nonce with timestamp + self._nonce_store[nonce] = { + 'client_id': client_id, + 'timestamp': time.time() + } + + # Clean up expired nonces + self._cleanup_expired_nonces() + + return nonce + + def validate_nonce(self, nonce: str, client_id: str) -> bool: + """ + Validate a nonce for a specific client. + + Args: + nonce (str): The nonce to validate. + client_id (str): The client ID associated with the nonce. + + Returns: + bool: True if nonce is valid, False otherwise. + """ + # Check if nonce exists and belongs to the client + if nonce not in self._nonce_store: + return False + + stored_nonce_data = self._nonce_store[nonce] + + # Check client ID match + if stored_nonce_data['client_id'] != client_id: + return False + + # Check nonce age + current_time = time.time() + nonce_age = current_time - stored_nonce_data['timestamp'] + + if nonce_age > self._nonce_expiry: + # Remove expired nonce + del self._nonce_store[nonce] + return False + + # Remove used nonce to prevent replay attacks + del self._nonce_store[nonce] + + return True + + def _cleanup_expired_nonces(self) -> None: + """ + Remove expired nonces from the storage. + + Nonces older than the configured expiry time are removed. + """ + current_time = time.time() + expired_nonces = [ + nonce for nonce, data in self._nonce_store.items() + if current_time - data['timestamp'] > self._nonce_expiry + ] + + for nonce in expired_nonces: + del self._nonce_store[nonce] \ No newline at end of file From f21dc082ff22fd0fd738d1c4e6257068286955fd Mon Sep 17 00:00:00 2001 From: Obludka Date: Wed, 7 May 2025 00:13:21 +0000 Subject: [PATCH 2/2] Add comprehensive tests for WebClientNonceManager --- tests/test_web_client_nonce.py | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/test_web_client_nonce.py diff --git a/tests/test_web_client_nonce.py b/tests/test_web_client_nonce.py new file mode 100644 index 00000000..5b79a180 --- /dev/null +++ b/tests/test_web_client_nonce.py @@ -0,0 +1,51 @@ +import time +import pytest +from src.web_client_nonce import WebClientNonceManager + +def test_nonce_generation(): + """Test nonce generation creates unique values.""" + nonce_manager = WebClientNonceManager() + client_id = 'test_client' + + nonce1 = nonce_manager.generate_nonce(client_id) + nonce2 = nonce_manager.generate_nonce(client_id) + + assert nonce1 != nonce2, "Nonces should be unique" + +def test_nonce_validation(): + """Test nonce validation works correctly.""" + nonce_manager = WebClientNonceManager() + client_id = 'test_client' + + nonce = nonce_manager.generate_nonce(client_id) + + assert nonce_manager.validate_nonce(nonce, client_id), "Nonce should be valid" + assert not nonce_manager.validate_nonce(nonce, client_id), "Nonce should be invalidated after first use" + +def test_nonce_expiry(): + """Test nonce expiry mechanism.""" + nonce_manager = WebClientNonceManager(nonce_expiry_seconds=1) + client_id = 'test_client' + + nonce = nonce_manager.generate_nonce(client_id) + + time.sleep(2) # Wait for nonce to expire + + assert not nonce_manager.validate_nonce(nonce, client_id), "Expired nonce should be invalid" + +def test_nonce_client_mismatch(): + """Test that nonces cannot be used with different client IDs.""" + nonce_manager = WebClientNonceManager() + client_id1 = 'client1' + client_id2 = 'client2' + + nonce = nonce_manager.generate_nonce(client_id1) + + assert not nonce_manager.validate_nonce(nonce, client_id2), "Nonce should not be valid for different client" + +def test_invalid_nonce(): + """Test that invalid nonces are rejected.""" + nonce_manager = WebClientNonceManager() + client_id = 'test_client' + + assert not nonce_manager.validate_nonce('fake_nonce', client_id), "Invalid nonce should not be validated" \ No newline at end of file