Code source de iziproxy.secure_config

"""
Module de gestion sécurisée des mots de passe et des configurations de proxy
"""

import re
from urllib.parse import unquote, quote

from cryptography.fernet import Fernet


[docs] class SecurePassword: """ Classe qui encapsule un mot de passe avec chiffrement en mémoire pour éviter l'exposition en clair dans la mémoire ou les logs. Cette classe permet de manipuler des mots de passe de manière sécurisée en: - Chiffrant le mot de passe en mémoire - Masquant le mot de passe dans les représentations string/repr - Permettant un accès contrôlé au mot de passe en clair """
[docs] def __init__(self, password): """ Initialise un mot de passe sécurisé en le chiffrant en mémoire Args: password (str): Le mot de passe en clair à sécuriser """ self._SecurePassword__encrypted_password = None self._SecurePassword__cipher = None self._SecurePassword__key = None if isinstance(password, SecurePassword): # Si on passe déjà un SecurePassword, on récupère ses attributs self.__key = password._SecurePassword__key self.__cipher = password._SecurePassword__cipher self.__encrypted_password = password._SecurePassword__encrypted_password else: # Sinon, on chiffre le nouveau mot de passe self.__key = Fernet.generate_key() self.__cipher = Fernet(self.__key) self.__encrypted_password = self.__cipher.encrypt(str(password).encode())
[docs] def __str__(self): """Retourne une version masquée du mot de passe.""" return "***********"
[docs] def __repr__(self): """Retourne une version masquée du mot de passe pour le débogage.""" return f"SecurePassword('***********')"
[docs] def get_password(self): """ Retourne le mot de passe non masqué. Returns: str: Le mot de passe en clair """ return self.__cipher.decrypt(self.__encrypted_password).decode()
[docs] class SecureProxyConfig(dict): """ Classe de configuration de proxy sécurisée qui utilise SecurePassword pour masquer les mots de passe dans les URLs de proxy. Cette classe permet de: - Stocker des configurations de proxy avec authentification - Masquer les mots de passe dans les représentations et logs - Récupérer les configurations réelles pour les requêtes HTTP """
[docs] def __init__(self, proxy_dict=None): """ Initialise une configuration de proxy sécurisée Args: proxy_dict (dict, optional): Dictionnaire de configuration de proxy (ex: {'http': 'http://user:pass@proxy:8080'}) """ super().__init__() self._secure_passwords = {} # Pour stocker les mots de passe sécurisés if proxy_dict: # Remplace les mots de passe dans les URLs par des objets SecurePassword for key, url in proxy_dict.items(): secured_url, secure_password = self._secure_url(url) self[key] = secured_url if secure_password: # Stocker les mots de passe sécurisés séparément self._secure_passwords[(key, url)] = secure_password
[docs] def __str__(self): """Masque les mots de passe dans la représentation string.""" return self._mask_passwords(super().__str__())
[docs] def __repr__(self): """Masque les mots de passe dans la représentation repr.""" return f"SecureProxyConfig({self._mask_passwords(dict(self))})"
def _parse_url_with_auth(self, url): """ Parse une URL en gérant correctement les caractères spéciaux dans le mot de passe, y compris lorsque le mot de passe contient '@' ou ':'. Cette méthode ne se fie pas à urlparse qui s'arrête au premier '@', mais utilise une approche plus robuste. Args: url (str): URL à analyser Returns: tuple: (scheme, username, password, host_with_path) """ if not url or '@' not in url: return None, None, None, url # Récupérer le schéma (http, https, etc.) match = re.match(r'^([a-z]+)://(.*)', url) if not match: return None, None, None, url scheme = match.group(1) remainder = match.group(2) # Trouver le dernier '@' qui sépare les identifiants du reste de l'URL last_at_index = remainder.rindex('@') auth_part = remainder[:last_at_index] host_with_path = remainder[last_at_index + 1:] # Trouver le premier ':' qui sépare le username du password first_colon_index = auth_part.find(':') if first_colon_index == -1: # Pas de mot de passe, juste un nom d'utilisateur return scheme, auth_part, None, host_with_path # Extraire le nom d'utilisateur et le mot de passe username = auth_part[:first_colon_index] password = auth_part[first_colon_index + 1:] # Décoder le mot de passe encodé s'il y en a un try: password = unquote(password) except Exception: # Si le décodage échoue, garder le mot de passe tel quel pass return scheme, username, password, host_with_path def _secure_url(self, url): """ Convertit les mots de passe dans les URLs en objets SecurePassword. Gère correctement les mots de passe contenant des caractères spéciaux comme '@' ou ':'. Args: url (str): URL de proxy, potentiellement avec authentification Returns: tuple: (URL avec mot de passe masqué, objet SecurePassword ou None) """ if not url or not isinstance(url, str): return url, None # Utiliser notre parser personnalisé au lieu de urlparse scheme, username, password, host_with_path = self._parse_url_with_auth(url) if not username or not password: return url, None # Créer un objet SecurePassword pour le mot de passe secure_password = SecurePassword(password) # Reconstruire l'URL avec le mot de passe masqué masked_url = f"{scheme}://{username}:***********@{host_with_path}" return masked_url, secure_password @staticmethod def _mask_passwords(obj): """ Masque les mots de passe dans les objets pour l'affichage Args: obj: Objet à masquer (dict ou str) Returns: Objet avec mots de passe masqués """ if isinstance(obj, dict): return {k: SecureProxyConfig._mask_url_password(v) if isinstance(v, str) else v for k, v in obj.items()} elif isinstance(obj, str): return SecureProxyConfig._mask_url_password(obj) return obj @staticmethod def _mask_url_password(url): """ Masque les mots de passe dans les URLs pour l'affichage. Gère correctement les cas où le mot de passe contient des caractères spéciaux. Args: url (str): URL à masquer Returns: str: URL avec mot de passe masqué """ if not url or not isinstance(url, str) or '@' not in url: return url # Trouver le schéma (http://, https://, etc.) match = re.match(r'^([a-z]+)://(.*)', url) if not match: return url scheme = match.group(1) remainder = match.group(2) # Trouver le dernier '@' qui sépare les identifiants du reste de l'URL try: last_at_index = remainder.rindex('@') except ValueError: return url auth_part = remainder[:last_at_index] host_with_path = remainder[last_at_index + 1:] # Trouver le premier ':' qui sépare le username du password first_colon_index = auth_part.find(':') if first_colon_index == -1: return url # Masquer le mot de passe username = auth_part[:first_colon_index] masked_url = f"{scheme}://{username}:***********@{host_with_path}" return masked_url
[docs] def get_real_config(self): """ Retourne la configuration réelle (non masquée) à utiliser dans les requêtes Returns: dict: Configuration de proxy avec mots de passe en clair et encodés """ real_config = {} for key, url in self.items(): try: if not url: real_config[key] = url continue # Utiliser notre parser personnalisé au lieu de urlparse scheme, username, _, host_with_path = self._parse_url_with_auth(url) if not username: # Pas d'authentification dans l'URL real_config[key] = url continue # Chercher s'il existe un mot de passe sécurisé pour cette clé secure_password = None for (stored_key, _), stored_password in self._secure_passwords.items(): if key == stored_key: secure_password = stored_password break if secure_password: # Récupérer le mot de passe en clair password = secure_password.get_password() # Encoder les caractères spéciaux du mot de passe pour l'URL encoded_password = quote(password, safe='') # Reconstruire l'URL avec le mot de passe encodé real_config[key] = f"{scheme}://{username}:{encoded_password}@{host_with_path}" else: # Pas de mot de passe sécurisé trouvé, utiliser l'URL telle quelle real_config[key] = url except Exception as e: # En cas d'erreur, utiliser l'URL telle quelle real_config[key] = url return real_config
[docs] def get_credentials(self, proxy_type='http'): """ Récupère les identifiants (username, SecurePassword) pour un type de proxy Args: proxy_type (str): Type de proxy ('http', 'https', etc.) Returns: tuple: (username, SecurePassword) ou (None, None) si pas d'authentification """ url = self.get(proxy_type) if not url: return None, None # Utiliser notre parser personnalisé _, username, _, _ = self._parse_url_with_auth(url) if not username: return None, None # Chercher s'il existe un mot de passe sécurisé pour ce type de proxy secure_password = None for (stored_key, _), stored_password in self._secure_passwords.items(): if proxy_type == stored_key: secure_password = stored_password break return username, secure_password