# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 # client.py (relay + crittografia ibrida + keep-alive + sender ID + connect/disconnect notify) import socket import struct import os import threading import time from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.fernet import Fernet # Aggiunto per crittografia simmetrica # --- Configurazione --- MULTICAST_GROUP = '224.1.1.1' MULTICAST_PORT = 5007 FRAGMENT_SIZE = 1024 # Deve corrispondere a quello del server per l'invio MAX_PACKET_SIZE = 65507 KEEPALIVE_INTERVAL = 20 # Secondi # --- Costanti TLV (devono corrispondere al server) --- TLV_FRAGMENT = 1 TLV_SERVER_PUBLIC_KEY_RESPONSE = 5 TLV_REQUEST_SERVER_PUBLIC_KEY = 7 TLV_DISCONNECT_REQUEST = 8 # Client invia questo (payload crittografato simmetricamente) TLV_KEEPALIVE = 9 TLV_SENDER_ID = 10 TLV_NOTIFY_CONNECT = 11 # Client riceve questo TLV_NOTIFY_DISCONNECT = 12 # Client riceve questo TLV_ENCRYPTED_SYMMETRIC_KEY = 13 # Client invia questo TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE = 14 # Messaggio crittografato simmetricamente # --- Funzioni di utilità --- def create_tlv(type, value): type_bytes = struct.pack('!H', type) length_bytes = struct.pack('!H', len(value)) return type_bytes + length_bytes + value def parse_tlv(data): if len(data) < 4: raise ValueError("Dati TLV insufficienti.") type, length = struct.unpack('!HH', data[:4]) if len(data) < 4 + length: raise ValueError("Lunghezza TLV non corrisponde ai dati.") value = data[4:4 + length] remaining_data = data[4 + length:] # Corretto slicing return type, value, remaining_data def encrypt_data_asymmetric(data, public_key): # Usato solo dal client per inviare la chiave simmetrica return public_key.encrypt(data, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) def encrypt_data_symmetric(data, fernet_key): f = Fernet(fernet_key) return f.encrypt(data) def decrypt_data_symmetric(token, fernet_key): f = Fernet(fernet_key) try: return f.decrypt(token) except Exception as e: # print(f"Errore durante la decrittazione simmetrica: {e}") # Può essere verboso return None # Questa funzione non è più usata dal client per decrittare messaggi dal server # def decrypt_data_asymmetric(ciphertext, private_key): # try: return private_key.decrypt(ciphertext, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) # except Exception: print(f"Errore decrittazione asimmetrica"); return None def fragment_data(data, fragment_size): return [data[i:i+fragment_size] for i in range(0, len(data), fragment_size)] # def defragment_data(fragments): return b''.join(fragments) # Non serve al client def initial_connect_and_key_exchange(sock, own_fernet_key): """ Richiede la chiave pubblica del server e invia la propria chiave simmetrica crittografata. """ # 1. Richiedi chiave pubblica del server tlv_request = create_tlv(TLV_REQUEST_SERVER_PUBLIC_KEY, b"") # Tipo 7 sock.sendto(tlv_request, (MULTICAST_GROUP, MULTICAST_PORT)) print("Richiesta chiave pubblica server inviata...") sock.settimeout(10.0) # Timeout più lungo per la prima risposta server_public_key = None try: data, server_addr = sock.recvfrom(MAX_PACKET_SIZE) # print(f"Risposta ricevuta da {server_addr}") # Debug type, value, _ = parse_tlv(data) if type == TLV_SERVER_PUBLIC_KEY_RESPONSE: # Tipo 5 server_public_key = serialization.load_pem_public_key(value, backend=default_backend()) print("Chiave pubblica server ricevuta.") else: print(f"Errore: Ricevuto tipo TLV {type} invece della chiave pubblica ({TLV_SERVER_PUBLIC_KEY_RESPONSE}).") return None except socket.timeout: print("Errore: Timeout durante l'attesa della chiave pubblica del server.") return None except (ValueError, struct.error) as e: print(f"Errore parsing risposta chiave pubblica: {e}") return None except Exception as e: print(f"Errore imprevisto durante ricezione chiave pubblica: {e}") return None # 2. Crittografa e invia la chiave simmetrica del client if server_public_key: try: encrypted_fernet_key = encrypt_data_asymmetric(own_fernet_key, server_public_key) tlv_send_key = create_tlv(TLV_ENCRYPTED_SYMMETRIC_KEY, encrypted_fernet_key) # Tipo 13 sock.sendto(tlv_send_key, (MULTICAST_GROUP, MULTICAST_PORT)) print("Chiave simmetrica crittografata inviata al server.") return server_public_key # Restituisce la chiave pubblica del server se tutto ok except Exception as e: print(f"Errore durante l'invio della chiave simmetrica crittografata: {e}") return None else: return None # Fallito il recupero della chiave pubblica def send_encrypted_message(message, own_fernet_key, sock): """ Crittografa simmetricamente il messaggio, lo wrappa in TLV 14, lo frammenta se necessario (con TLV 1) e lo invia.""" try: message_bytes = message.encode('utf-8') if isinstance(message, str) else message encrypted_payload = encrypt_data_symmetric(message_bytes, own_fernet_key) tlv_inner_message = create_tlv(TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE, encrypted_payload) # Tipo 14 msg_id = os.urandom(4) # ID univoco per i frammenti di questo messaggio # Controlla se la frammentazione è necessaria if len(tlv_inner_message) > FRAGMENT_SIZE: udp_fragments_payloads = fragment_data(tlv_inner_message, FRAGMENT_SIZE) num_fragments = len(udp_fragments_payloads) print(f"Invio messaggio frammentato in {num_fragments} parti...") for seq_num, frag_payload in enumerate(udp_fragments_payloads): # Costruisce il payload del frammento: msg_id, seq_num, total_fragments, data fragment_header = struct.pack('!I', int.from_bytes(msg_id, byteorder='big')) + \ struct.pack('!HH', seq_num, num_fragments) tlv_fragment_data = fragment_header + frag_payload # Wrappa il frammento in un TLV di tipo FRAGMENT tlv_outer_fragment = create_tlv(TLV_FRAGMENT, tlv_fragment_data) # Tipo 1 sock.sendto(tlv_outer_fragment, (MULTICAST_GROUP, MULTICAST_PORT)) # print(f" Inviato frammento {seq_num + 1}/{num_fragments}") # Debug - verboso time.sleep(0.005) # Piccola pausa per evitare di sovrasaturare il buffer di rete/ricezione server else: # Invia come singolo pacchetto (sempre wrappato in TLV 1 per coerenza col server) print("Invio messaggio non frammentato...") fragment_header = struct.pack('!I', int.from_bytes(msg_id, byteorder='big')) + \ struct.pack('!HH', 0, 1) # Seq 0, Total 1 tlv_fragment_data = fragment_header + tlv_inner_message tlv_outer_fragment = create_tlv(TLV_FRAGMENT, tlv_fragment_data) # Tipo 1 sock.sendto(tlv_outer_fragment, (MULTICAST_GROUP, MULTICAST_PORT)) # print(f"Messaggio '{message[:20]}...' inviato.") except Exception as e: print(f"Errore durante l'invio del messaggio: {e}") def receive_message_thread(sock, own_fernet_key, stop_event): """ Thread per ricevere e processare messaggi dal server. """ print("Thread ricezione avviato.") while not stop_event.is_set(): try: sock.settimeout(1.0) # Timeout per permettere controllo stop_event data, sender_server_addr = sock.recvfrom(MAX_PACKET_SIZE) # print(f"Dati ricevuti da {sender_server_addr}, len={len(data)}") # Debug # I messaggi relayati dovrebbero iniziare con TLV SENDER_ID (10) # Le notifiche (CONNECT/DISCONNECT) arrivano come TLV singolo (11 o 12) offset = 0 sender_info_address = None # Parsing TLV multipli nel pacchetto ricevuto while offset < len(data): original_offset = offset try: type, value, _ = parse_tlv(data[offset:]) offset += 4 + len(value) # Avanza correttamente #print(f"Processing TLV type {type} from server") # Debug if type == TLV_SENDER_ID: # Tipo 10: Info sul mittente originale try: sender_ip = value[:-2].decode('utf-8') sender_port = struct.unpack("!H", value[-2:])[0] sender_info_address = (sender_ip, sender_port) except (UnicodeDecodeError, struct.error, IndexError): print("Errore parsing TLV SENDER_ID.") sender_info_address = "Sconosciuto (errore parsing)" elif type == TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE: # Tipo 14: Messaggio crittografato decrypted_message = decrypt_data_symmetric(value, own_fernet_key) if decrypted_message: try: message_text = decrypted_message.decode('utf-8') sender_display = f"Da {sender_info_address}:" if sender_info_address else "Da Sconosciuto:" # Stampa a capo per non interferire con input() print(f"\n{sender_display} {message_text}") # Ripristina prompt input utente se necessario (dipende da come gestisci l'input) # print("Messaggio (Ctrl+C per uscire): ", end='', flush=True) except UnicodeDecodeError: sender_display = f"Dati binari da {sender_info_address}:" if sender_info_address else "Dati binari da Sconosciuto:" print(f"\n{sender_display} <{len(decrypted_message)} bytes>") else: print("\nErrore: Impossibile decrittare messaggio ricevuto.") # Resetta sender_info dopo aver processato il messaggio sender_info_address = None elif type == TLV_NOTIFY_CONNECT: # Tipo 11: Nuovo client connesso try: addr_ip = value[:-2].decode('utf-8') addr_port = struct.unpack("!H", value[-2:])[0] print(f"\n*** Nuovo client connesso: {addr_ip}:{addr_port} ***") except (UnicodeDecodeError, struct.error, IndexError): print("\n*** Nuovo client connesso (errore parsing indirizzo) ***") elif type == TLV_NOTIFY_DISCONNECT: # Tipo 12: Client disconnesso try: addr_ip = value[:-2].decode('utf-8') addr_port = struct.unpack("!H", value[-2:])[0] print(f"\n*** Client disconnesso: {addr_ip}:{addr_port} ***") except (UnicodeDecodeError, struct.error, IndexError): print("\n*** Client disconnesso (errore parsing indirizzo) ***") else: print(f"\nWARN: Ricevuto tipo TLV non gestito ({type}) dal server.") except (ValueError, struct.error) as e: print(f"\nErrore parsing TLV ricevuto: {e}") # Prova a recuperare avanzando di un byte? Rischioso. Meglio scartare il resto del pacchetto. print(" Pacchetto scartato.") break # Esci dal while interno per questo pacchetto except socket.timeout: continue # Normale, riprova il ciclo except OSError as e: if stop_event.is_set(): print("Thread ricezione: Uscita richiesta, errore socket ignorato.") break # Esci dal loop principale se l'evento di stop è settato print(f"\nErrore socket in ricezione (OSError): {e}") # Potrebbe essere un errore grave, forse uscire? O attendere e riprovare? time.sleep(1) except Exception as e: print(f"\nErrore generico in ricezione: {e}") # Stampa traceback per debug se necessario # import traceback # traceback.print_exc() time.sleep(1) # Pausa prima di riprovare print("Thread ricezione terminato.") def send_keepalive(sock): """ Invia un messaggio di keep-alive (TLV 9) non crittografato. """ tlv_keepalive = create_tlv(TLV_KEEPALIVE, b"") # Tipo 9 try: sock.sendto(tlv_keepalive, (MULTICAST_GROUP, MULTICAST_PORT)) #print("Keep-alive inviato.") # Debug - verboso except Exception as e: print(f"Errore invio keep-alive: {e}") def keepalive_thread_function(sock, stop_event): """ Thread per inviare periodicamente keep-alive. """ print("Thread keep-alive avviato.") while not stop_event.is_set(): send_keepalive(sock) # Aspetta KEEPALIVE_INTERVAL o finché stop_event non è settato stop_event.wait(KEEPALIVE_INTERVAL) print("Thread keep-alive terminato.") def run_client(): # Crea socket UDP sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # Imposta Time-to-live (TTL) per pacchetti multicast ttl = struct.pack('b', 1) # 1=stessa subnet, aumenta per routing sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) # Opzionale: Abilita ricezione dei propri messaggi multicast (loopback) # sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) # 1 per abilitare # Genera chiave simmetrica Fernet per questo client own_fernet_key = Fernet.generate_key() print("Chiave simmetrica Fernet generata.") # Esegui connessione iniziale e scambio chiavi server_public_key = initial_connect_and_key_exchange(sock, own_fernet_key) if server_public_key is None: print("Impossibile stabilire connessione sicura con il server.") sock.close() return # Connessione riuscita, avvia thread ricezione e keep-alive stop_event = threading.Event() receive_thread = threading.Thread(target=receive_message_thread, args=(sock, own_fernet_key, stop_event), daemon=True) keepalive_thread = threading.Thread(target=keepalive_thread_function, args=(sock, stop_event), daemon=True) receive_thread.start() keepalive_thread.start() print("\nConnesso! Puoi iniziare a inviare messaggi.") # Non inviare "CONNECT", la registrazione avviene con l'invio della chiave simmetrica try: while True: message = input("Messaggio (o 'quit' per uscire): ") if message.lower() == 'quit': break if message: # Non inviare messaggi vuoti send_encrypted_message(message, own_fernet_key, sock) except KeyboardInterrupt: print("\nChiusura richiesta (Ctrl+C)...") finally: print("Invio messaggio di disconnessione...") try: # Invia messaggio "DISCONNECT" crittografato simmetricamente send_encrypted_message("DISCONNECT", own_fernet_key, sock) time.sleep(0.5) # Dai tempo al messaggio di partire except Exception as e: print(f"Errore invio messaggio di disconnessione: {e}") print("Arresto thread...") stop_event.set() # Segnala ai thread di terminare # Attendi la terminazione dei thread keepalive_thread.join(timeout=2.0) receive_thread.join(timeout=2.0) if keepalive_thread.is_alive(): print("WARN: Thread Keepalive non terminato.") if receive_thread.is_alive(): print("WARN: Thread Ricezione non terminato.") sock.close() print("Socket client chiuso.") print("Client terminato.") if __name__ == "__main__": run_client()