diff --git a/protocol_prototype/auto_mode.py b/protocol_prototype/auto_mode.py new file mode 100644 index 0000000..690ba9c --- /dev/null +++ b/protocol_prototype/auto_mode.py @@ -0,0 +1,430 @@ +import time +import threading +import queue +from typing import Optional, Dict, Any, List, Callable, Tuple + +# ANSI colors for logging +RED = "\033[91m" +GREEN = "\033[92m" +YELLOW = "\033[93m" +BLUE = "\033[94m" +RESET = "\033[0m" + +class AutoModeConfig: + """Configuration parameters for the automatic mode behavior.""" + def __init__(self): + # Ping behavior + self.ping_response_accept = True # Whether to accept incoming pings + self.ping_auto_initiate = False # Whether to initiate pings when connected + self.ping_retry_count = 3 # Number of ping retries + self.ping_retry_delay = 5.0 # Seconds between ping retries + self.ping_timeout = 10.0 # Seconds to wait for ping response + self.preferred_cipher = 0 # 0=AES-GCM, 1=ChaCha20-Poly1305 + + # Handshake behavior + self.handshake_retry_count = 3 # Number of handshake retries + self.handshake_retry_delay = 5.0 # Seconds between handshake retries + self.handshake_timeout = 10.0 # Seconds to wait for handshake + + # Messaging behavior + self.auto_message_enabled = False # Whether to auto-send messages + self.message_interval = 10.0 # Seconds between auto messages + self.message_content = "Hello, secure world!" # Default message + + # General behavior + self.active_mode = False # If true, initiates protocol instead of waiting + + +class AutoMode: + """ + Manages automated behavior for the Icing protocol. + Handles automatic progression through the protocol stages: + 1. Connection setup + 2. Ping/discovery + 3. Key exchange + 4. Encrypted communication + """ + + def __init__(self, protocol_interface): + """ + Initialize the AutoMode manager. + + Args: + protocol_interface: An object implementing the required protocol methods + """ + self.protocol = protocol_interface + self.config = AutoModeConfig() + self.active = False + self.state = "idle" + + # Message queue for automated sending + self.message_queue = queue.Queue() + + # Tracking variables + self.ping_attempts = 0 + self.handshake_attempts = 0 + self.last_action_time = 0 + self.timer_tasks = [] # List of active timer tasks (for cleanup) + + def start(self): + """Start the automatic mode.""" + if self.active: + return + + self.active = True + self.state = "idle" + self.ping_attempts = 0 + self.handshake_attempts = 0 + self.last_action_time = time.time() + + self._log_info("Automatic mode started") + + # Start in active mode if configured + if self.config.active_mode and self.protocol.connections: + self._start_ping_sequence() + + def stop(self): + """Stop the automatic mode and clean up any pending tasks.""" + if not self.active: + return + + # Cancel any pending timers + for timer in self.timer_tasks: + if timer.is_alive(): + timer.cancel() + self.timer_tasks = [] + + self.active = False + self.state = "idle" + self._log_info("Automatic mode stopped") + + def handle_connection_established(self): + """Called when a new connection is established.""" + if not self.active: + return + + self._log_info("Connection established") + + # If in active mode, start pinging + if self.config.active_mode: + self._start_ping_sequence() + + def handle_ping_received(self, index: int): + """ + Handle a received ping request. + + Args: + index: Index of the ping request in the protocol's inbound message queue + """ + if not self.active or not self._is_valid_message_index(index): + return + + self._log_info(f"Ping request received (index={index})") + + # Automatically respond to ping if configured to accept + if self.config.ping_response_accept: + self._log_info(f"Auto-responding to ping with accept={self.config.ping_response_accept}") + try: + # Schedule the response with a small delay to simulate real behavior + timer = threading.Timer(0.5, self._respond_to_ping, args=[index]) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + except Exception as e: + self._log_error(f"Failed to auto-respond to ping: {e}") + + def handle_ping_response_received(self, accepted: bool): + """ + Handle a received ping response. + + Args: + accepted: Whether the ping was accepted + """ + if not self.active: + return + + self.ping_attempts = 0 # Reset ping attempts counter + + if accepted: + self._log_info("Ping accepted! Proceeding with handshake") + # Send handshake if not already done + if self.state != "handshake_sent": + self._ensure_ephemeral_keys() + self._start_handshake_sequence() + else: + self._log_info("Ping rejected by peer. Stopping auto-protocol sequence.") + self.state = "idle" + + def handle_handshake_received(self, index: int): + """ + Handle a received handshake. + + Args: + index: Index of the handshake in the protocol's inbound message queue + """ + if not self.active or not self._is_valid_message_index(index): + return + + self._log_info(f"Handshake received (index={index})") + + try: + # Ensure we have ephemeral keys + self._ensure_ephemeral_keys() + + # Process the handshake (compute ECDH) + self.protocol.generate_ecdhe(index) + + # Derive HKDF key + self.protocol.derive_hkdf() + + # If we haven't sent our handshake yet, send it + if self.state != "handshake_sent": + timer = threading.Timer(0.5, self.protocol.send_handshake) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + self.state = "handshake_sent" + else: + self.state = "key_exchange_complete" + + # Start sending queued messages if auto messaging is enabled + if self.config.auto_message_enabled: + self._start_message_sequence() + + except Exception as e: + self._log_error(f"Failed to process handshake: {e}") + + def handle_encrypted_received(self, index: int): + """ + Handle a received encrypted message. + + Args: + index: Index of the encrypted message in the protocol's inbound message queue + """ + if not self.active or not self._is_valid_message_index(index): + return + + # Try to decrypt automatically + try: + plaintext = self.protocol.decrypt_received_message(index) + self._log_info(f"Auto-decrypted message: {plaintext}") + except Exception as e: + self._log_error(f"Failed to auto-decrypt message: {e}") + + def queue_message(self, message: str): + """ + Add a message to the auto-send queue. + + Args: + message: Message text to send + """ + self.message_queue.put(message) + self._log_info(f"Message queued for sending: {message}") + + # If we're in the right state, start sending messages + if self.active and self.state == "key_exchange_complete" and self.config.auto_message_enabled: + self._process_message_queue() + + def _start_ping_sequence(self): + """Start the ping sequence to discover the peer.""" + if self.ping_attempts >= self.config.ping_retry_count: + self._log_warning(f"Maximum ping attempts ({self.config.ping_retry_count}) reached") + self.state = "idle" + return + + self.state = "pinging" + self.ping_attempts += 1 + + self._log_info(f"Sending ping request (attempt {self.ping_attempts}/{self.config.ping_retry_count})") + try: + self.protocol.send_ping_request(self.config.preferred_cipher) + self.last_action_time = time.time() + + # Schedule next ping attempt if needed + timer = threading.Timer( + self.config.ping_retry_delay, + self._check_ping_response + ) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + + except Exception as e: + self._log_error(f"Failed to send ping: {e}") + + def _check_ping_response(self): + """Check if we got a ping response, retry if not.""" + if not self.active or self.state != "pinging": + return + + # If we've waited long enough for a response, retry + if time.time() - self.last_action_time >= self.config.ping_timeout: + self._log_warning("No ping response received, retrying") + self._start_ping_sequence() + + def _respond_to_ping(self, index: int): + """ + Respond to a ping request. + + Args: + index: Index of the ping request in the inbound messages + """ + if not self.active or not self._is_valid_message_index(index): + return + + try: + answer = 1 if self.config.ping_response_accept else 0 + self.protocol.respond_to_ping(index, answer) + + if answer == 1: + # If we accepted, we should expect a handshake + self.state = "accepted_ping" + self._ensure_ephemeral_keys() + + # Set a timer to send our handshake if we don't receive one + timer = threading.Timer( + self.config.handshake_timeout, + self._check_handshake_received + ) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + self.last_action_time = time.time() + + except Exception as e: + self._log_error(f"Failed to respond to ping: {e}") + + def _check_handshake_received(self): + """Check if we've received a handshake after accepting a ping.""" + if not self.active or self.state != "accepted_ping": + return + + # If we've waited long enough and haven't received a handshake, initiate one + if time.time() - self.last_action_time >= self.config.handshake_timeout: + self._log_warning("No handshake received after accepting ping, initiating handshake") + self._start_handshake_sequence() + + def _start_handshake_sequence(self): + """Start the handshake sequence.""" + if self.handshake_attempts >= self.config.handshake_retry_count: + self._log_warning(f"Maximum handshake attempts ({self.config.handshake_retry_count}) reached") + self.state = "idle" + return + + self.state = "handshake_sent" + self.handshake_attempts += 1 + + self._log_info(f"Sending handshake (attempt {self.handshake_attempts}/{self.config.handshake_retry_count})") + try: + self.protocol.send_handshake() + self.last_action_time = time.time() + + # Schedule handshake retry check + timer = threading.Timer( + self.config.handshake_retry_delay, + self._check_handshake_response + ) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + + except Exception as e: + self._log_error(f"Failed to send handshake: {e}") + + def _check_handshake_response(self): + """Check if we've completed the key exchange, retry handshake if not.""" + if not self.active or self.state != "handshake_sent": + return + + # If we've waited long enough for a response, retry + if time.time() - self.last_action_time >= self.config.handshake_timeout: + self._log_warning("No handshake response received, retrying") + self._start_handshake_sequence() + + def _start_message_sequence(self): + """Start the automated message sending sequence.""" + if not self.config.auto_message_enabled: + return + + self._log_info("Starting automated message sequence") + + # Add the default message if queue is empty + if self.message_queue.empty(): + self.message_queue.put(self.config.message_content) + + # Start processing the queue + self._process_message_queue() + + def _process_message_queue(self): + """Process messages in the queue and send them.""" + if not self.active or self.state != "key_exchange_complete" or not self.config.auto_message_enabled: + return + + if not self.message_queue.empty(): + message = self.message_queue.get() + self._log_info(f"Sending queued message: {message}") + + try: + self.protocol.send_encrypted_message(message) + + # Schedule next message send + timer = threading.Timer( + self.config.message_interval, + self._process_message_queue + ) + timer.daemon = True + timer.start() + self.timer_tasks.append(timer) + + except Exception as e: + self._log_error(f"Failed to send queued message: {e}") + # Put the message back in the queue + self.message_queue.put(message) + + def _ensure_ephemeral_keys(self): + """Ensure ephemeral keys are generated if needed.""" + if not hasattr(self.protocol, 'ephemeral_pubkey') or self.protocol.ephemeral_pubkey is None: + self._log_info("Generating ephemeral keys") + self.protocol.generate_ephemeral_keys() + + def _is_valid_message_index(self, index: int) -> bool: + """ + Check if a message index is valid in the protocol's inbound_messages queue. + + Args: + index: The index to check + + Returns: + bool: True if the index is valid, False otherwise + """ + if not hasattr(self.protocol, 'inbound_messages'): + self._log_error("Protocol has no inbound_messages attribute") + return False + + if index < 0 or index >= len(self.protocol.inbound_messages): + self._log_error(f"Invalid message index: {index}") + return False + + return True + + # Helper methods for logging + def _log_info(self, message: str): + print(f"{BLUE}[AUTO]{RESET} {message}") + if hasattr(self, 'verbose_logging') and self.verbose_logging: + state_info = f"(state={self.state})" + if 'pinging' in self.state and hasattr(self, 'ping_attempts'): + state_info += f", attempts={self.ping_attempts}/{self.config.ping_retry_count}" + elif 'handshake' in self.state and hasattr(self, 'handshake_attempts'): + state_info += f", attempts={self.handshake_attempts}/{self.config.handshake_retry_count}" + print(f"{BLUE}[AUTO-DETAIL]{RESET} {state_info}") + + def _log_warning(self, message: str): + print(f"{YELLOW}[AUTO-WARN]{RESET} {message}") + if hasattr(self, 'verbose_logging') and self.verbose_logging: + timer_info = f"Active timers: {len(self.timer_tasks)}" + print(f"{YELLOW}[AUTO-WARN-DETAIL]{RESET} {timer_info}") + + def _log_error(self, message: str): + print(f"{RED}[AUTO-ERROR]{RESET} {message}") + if hasattr(self, 'verbose_logging') and self.verbose_logging: + print(f"{RED}[AUTO-ERROR-DETAIL]{RESET} Current state: {self.state}, Active: {self.active}") \ No newline at end of file diff --git a/protocol_prototype/cli.py b/protocol_prototype/cli.py index 63c84d6..53c3f01 100644 --- a/protocol_prototype/cli.py +++ b/protocol_prototype/cli.py @@ -1,12 +1,53 @@ import sys +import argparse +import shlex from protocol import IcingProtocol RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" +MAGENTA = "\033[95m" +CYAN = "\033[96m" RESET = "\033[0m" +def print_help(): + """Display all available commands.""" + print(f"\n{YELLOW}=== Available Commands ==={RESET}") + print(f"\n{CYAN}Basic Protocol Commands:{RESET}") + print(" help - Show this help message") + print(" peer_id - Set peer identity public key") + print(" connect - Connect to a peer at the specified port") + print(" show_state - Display current protocol state") + print(" exit - Exit the program") + + print(f"\n{CYAN}Manual Protocol Operation:{RESET}") + print(" generate_ephemeral_keys - Generate ephemeral ECDH keys") + print(" send_ping [cipher] - Send PING request (cipher: 0=AES-GCM, 1=ChaCha20-Poly1305, default: 0)") + print(" respond_ping <0|1> - Respond to a PING (0=reject, 1=accept)") + print(" send_handshake - Send handshake with ephemeral keys") + print(" generate_ecdhe - Process handshake at specified index") + print(" derive_hkdf - Derive encryption key using HKDF") + print(" send_encrypted - Encrypt and send a message") + print(" decrypt <index> - Decrypt received message at index") + + print(f"\n{CYAN}Automatic Mode Commands:{RESET}") + print(" auto start - Start automatic mode") + print(" auto stop - Stop automatic mode") + print(" auto status - Show current auto mode status and configuration") + print(" auto config <param> <value> - Configure auto mode parameters") + print(" auto config list - Show all configurable parameters") + print(" auto message <text> - Queue message for automatic sending") + print(" auto passive - Configure as passive peer (responds to pings but doesn't initiate)") + print(" auto active - Configure as active peer (initiates protocol)") + print(" auto log - Toggle detailed logging for auto mode") + + print(f"\n{CYAN}Debugging Commands:{RESET}") + print(" debug_message <index> - Display detailed information about a message in the queue") + + print(f"\n{CYAN}Legacy Commands:{RESET}") + print(" auto_responder <on|off> - Enable/disable legacy auto responder (deprecated)") + def main(): protocol = IcingProtocol() @@ -16,54 +57,27 @@ def main(): print("======================================\n" + RESET) print(f"Listening on port: {protocol.local_port}") print(f"Your identity public key (hex): {protocol.identity_pubkey.hex()}") - print("\nAvailable commands:") - print(" help - Show this help message") - print(" peer_id <hex_pubkey> - Set peer identity public key") - print(" connect <port> - Connect to a peer at the specified port") - print(" generate_ephemeral_keys - Generate ephemeral ECDH keys") - print(" send_ping [cipher] - Send PING request (cipher: 0=AES-GCM, 1=ChaCha20-Poly1305, default: 0)") - print(" respond_ping <index> <0|1> - Respond to a PING (0=reject, 1=accept)") - print(" send_handshake - Send handshake with ephemeral keys") - print(" generate_ecdhe <index> - Process handshake at specified index") - print(" derive_hkdf - Derive encryption key using HKDF") - print(" send_encrypted <plaintext> - Encrypt and send a message") - print(" decrypt <index> - Decrypt received message at index") - print(" auto_responder <on|off> - Enable/disable automatic responses") - print(" show_state - Display current protocol state") - print(" exit - Exit the program\n") + print_help() while True: try: - line = input("Cmd> ").strip() + line = input(f"{MAGENTA}Cmd>{RESET} ").strip() except EOFError: break if not line: continue - parts = line.split() + parts = shlex.split(line) # Handle quoted arguments properly cmd = parts[0].lower() try: + # Basic commands if cmd == "exit": protocol.stop() break elif cmd == "help": - print("\nAvailable commands:") - print(" help - Show this help message") - print(" peer_id <hex_pubkey> - Set peer identity public key") - print(" connect <port> - Connect to a peer at the specified port") - print(" generate_ephemeral_keys - Generate ephemeral ECDH keys") - print(" send_ping [cipher] - Send PING request (cipher: 0=AES-GCM, 1=ChaCha20-Poly1305, default: 0)") - print(" respond_ping <index> <0|1> - Respond to a PING (0=reject, 1=accept)") - print(" send_handshake - Send handshake with ephemeral keys") - print(" generate_ecdhe <index> - Process handshake at specified index") - print(" derive_hkdf - Derive encryption key using HKDF") - print(" send_encrypted <plaintext> - Encrypt and send a message") - print(" decrypt <index> - Decrypt received message at index") - print(" auto_responder <on|off> - Enable/disable automatic responses") - print(" show_state - Display current protocol state") - print(" exit - Exit the program") + print_help() elif cmd == "show_state": protocol.show_state() @@ -88,7 +102,8 @@ def main(): print(f"{RED}[ERROR]{RESET} Invalid port number.") except Exception as e: print(f"{RED}[ERROR]{RESET} Connection failed: {e}") - + + # Manual protocol operation elif cmd == "generate_ephemeral_keys": protocol.generate_ephemeral_keys() @@ -103,7 +118,7 @@ def main(): cipher = 0 except ValueError: print(f"{YELLOW}[WARNING]{RESET} Invalid cipher code. Using AES-GCM (0).") - protocol.send_ping_request() + protocol.send_ping_request(cipher) elif cmd == "send_handshake": protocol.send_handshake() @@ -164,6 +179,127 @@ def main(): except Exception as e: print(f"{RED}[ERROR]{RESET} Failed to decrypt message: {e}") + # Debugging commands + elif cmd == "debug_message": + if len(parts) != 2: + print(f"{RED}[ERROR]{RESET} Usage: debug_message <index>") + continue + try: + idx = int(parts[1]) + protocol.debug_message(idx) + except ValueError: + print(f"{RED}[ERROR]{RESET} Index must be an integer.") + except Exception as e: + print(f"{RED}[ERROR]{RESET} Failed to debug message: {e}") + + # Automatic mode commands + elif cmd == "auto": + if len(parts) < 2: + print(f"{RED}[ERROR]{RESET} Usage: auto <command> [options]") + print("Available commands: start, stop, status, config, message, passive, active") + continue + + subcmd = parts[1].lower() + + if subcmd == "start": + protocol.start_auto_mode() + print(f"{GREEN}[AUTO]{RESET} Automatic mode started") + + elif subcmd == "stop": + protocol.stop_auto_mode() + print(f"{GREEN}[AUTO]{RESET} Automatic mode stopped") + + elif subcmd == "status": + config = protocol.get_auto_mode_config() + print(f"{YELLOW}=== Auto Mode Status ==={RESET}") + print(f"Active: {protocol.auto_mode.active}") + print(f"State: {protocol.auto_mode.state}") + print(f"\n{YELLOW}--- Configuration ---{RESET}") + for key, value in vars(config).items(): + print(f" {key}: {value}") + + elif subcmd == "config": + if len(parts) < 3: + print(f"{RED}[ERROR]{RESET} Usage: auto config <param> <value> or auto config list") + continue + + if parts[2].lower() == "list": + config = protocol.get_auto_mode_config() + print(f"{YELLOW}=== Auto Mode Configuration Parameters ==={RESET}") + for key, value in vars(config).items(): + print(f" {key} ({type(value).__name__}): {value}") + continue + + if len(parts) != 4: + print(f"{RED}[ERROR]{RESET} Usage: auto config <param> <value>") + continue + + param = parts[2] + value_str = parts[3] + + # Convert the string value to the appropriate type + config = protocol.get_auto_mode_config() + if not hasattr(config, param): + print(f"{RED}[ERROR]{RESET} Unknown parameter: {param}") + print("Use 'auto config list' to see all available parameters") + continue + + current_value = getattr(config, param) + try: + if isinstance(current_value, bool): + if value_str.lower() in ("true", "yes", "on", "1"): + value = True + elif value_str.lower() in ("false", "no", "off", "0"): + value = False + else: + raise ValueError(f"Boolean value must be true/false/yes/no/on/off/1/0") + elif isinstance(current_value, int): + value = int(value_str) + elif isinstance(current_value, float): + value = float(value_str) + elif isinstance(current_value, str): + value = value_str + else: + value = value_str # Default to string + + protocol.configure_auto_mode(**{param: value}) + print(f"{GREEN}[AUTO]{RESET} Set {param} = {value}") + + except ValueError as e: + print(f"{RED}[ERROR]{RESET} Invalid value for {param}: {e}") + + elif subcmd == "message": + if len(parts) < 3: + print(f"{RED}[ERROR]{RESET} Usage: auto message <text>") + continue + + message = " ".join(parts[2:]) + protocol.queue_auto_message(message) + print(f"{GREEN}[AUTO]{RESET} Message queued for sending: {message}") + + elif subcmd == "passive": + # Configure as passive peer (responds but doesn't initiate) + protocol.configure_auto_mode( + ping_response_accept=True, + ping_auto_initiate=False, + active_mode=False + ) + print(f"{GREEN}[AUTO]{RESET} Configured as passive peer") + + elif subcmd == "active": + # Configure as active peer (initiates protocol) + protocol.configure_auto_mode( + ping_response_accept=True, + ping_auto_initiate=True, + active_mode=True + ) + print(f"{GREEN}[AUTO]{RESET} Configured as active peer") + + else: + print(f"{RED}[ERROR]{RESET} Unknown auto mode command: {subcmd}") + print("Available commands: start, stop, status, config, message, passive, active") + + # Legacy commands elif cmd == "auto_responder": if len(parts) != 2: print(f"{RED}[ERROR]{RESET} Usage: auto_responder <on|off>") @@ -173,6 +309,7 @@ def main(): print(f"{RED}[ERROR]{RESET} Value must be 'on' or 'off'.") continue protocol.enable_auto_responder(val == "on") + print(f"{YELLOW}[WARNING]{RESET} Using legacy auto responder. Consider using 'auto' commands instead.") else: print(f"{RED}[ERROR]{RESET} Unknown command: {cmd}") diff --git a/protocol_prototype/protocol.py b/protocol_prototype/protocol.py index 6007ce5..1887476 100644 --- a/protocol_prototype/protocol.py +++ b/protocol_prototype/protocol.py @@ -23,6 +23,7 @@ from encryption import ( EncryptedMessage, MessageHeader, generate_iv, encrypt_message, decrypt_message ) +from auto_mode import AutoMode, AutoModeConfig # ANSI colors RED = "\033[91m" @@ -63,9 +64,13 @@ class IcingProtocol: "ping_received": False, "handshake_sent": False, "handshake_received": False, + "key_exchange_complete": False } - # Auto-responder toggle + # Auto mode for automated protocol operation + self.auto_mode = AutoMode(self) + + # Legacy auto-responder toggle (kept for backward compatibility) self.auto_responder = False # Active connections list @@ -96,6 +101,9 @@ class IcingProtocol: def on_new_connection(self, conn: transmission.PeerConnection): print(f"{GREEN}[IcingProtocol]{RESET} New incoming connection.") self.connections.append(conn) + + # Notify auto mode + self.auto_mode.handle_connection_established() def on_data_received(self, conn: transmission.PeerConnection, data: bytes): bits_count = len(data) * 8 @@ -130,8 +138,11 @@ class IcingProtocol: } self.inbound_messages.append(msg) - # Auto-respond if enabled - if self.auto_responder: + # Handle in auto mode (if active) + self.auto_mode.handle_ping_received(index) + + # Legacy auto-responder (for backward compatibility) + if self.auto_responder and not self.auto_mode.active: timer = threading.Timer(2.0, self._auto_respond_ping, args=[index]) timer.daemon = True timer.start() @@ -152,6 +163,9 @@ class IcingProtocol: "connection": conn } self.inbound_messages.append(msg) + + # Notify auto mode (if active) + self.auto_mode.handle_ping_response_received(ping_response.answer == 1) return # HANDSHAKE message (168 bytes) @@ -168,8 +182,11 @@ class IcingProtocol: } self.inbound_messages.append(msg) - # Auto-respond if enabled - if self.auto_responder: + # Notify auto mode (if active) + self.auto_mode.handle_handshake_received(index) + + # Legacy auto-responder + if self.auto_responder and not self.auto_mode.active: timer = threading.Timer(2.0, self._auto_respond_handshake, args=[index]) timer.daemon = True timer.start() @@ -198,6 +215,9 @@ class IcingProtocol: } self.inbound_messages.append(msg) print(f"{YELLOW}[NOTICE]{RESET} Stored inbound ENCRYPTED_MESSAGE at index={index}.") + + # Notify auto mode + self.auto_mode.handle_encrypted_received(index) return except Exception as e: print(f"{RED}[ERROR]{RESET} Failed to parse message header: {e}") @@ -269,10 +289,12 @@ class IcingProtocol: ) derived_key = hkdf.derive(ikm) self.hkdf_key = derived_key.hex() + self.state["key_exchange_complete"] = True print(f"{GREEN}[HKDF]{RESET} Derived HKDF key: {self.hkdf_key}") + return True # ------------------------------------------------------------------------- - # Auto-responder helpers + # Legacy Auto-responder helpers (kept for backward compatibility) # ------------------------------------------------------------------------- def _auto_respond_ping(self, index: int): @@ -306,6 +328,140 @@ class IcingProtocol: # 4) Show final state self.show_state() + # ------------------------------------------------------------------------- + # Public Methods for Auto Mode Management + # ------------------------------------------------------------------------- + + def start_auto_mode(self): + """Start the automatic protocol operation mode.""" + self.auto_mode.start() + + def stop_auto_mode(self): + """Stop the automatic protocol operation mode.""" + self.auto_mode.stop() + + def configure_auto_mode(self, **kwargs): + """ + Configure the automatic mode parameters. + + Args: + **kwargs: Configuration parameters to set. Supported parameters: + - ping_response_accept: bool, whether to accept incoming pings + - ping_auto_initiate: bool, whether to initiate pings on connection + - ping_retry_count: int, number of ping retries + - ping_retry_delay: float, seconds between ping retries + - ping_timeout: float, seconds to wait for ping response + - preferred_cipher: int, preferred cipher (0=AES-GCM, 1=ChaCha20-Poly1305) + - handshake_retry_count: int, number of handshake retries + - handshake_retry_delay: float, seconds between handshake retries + - handshake_timeout: float, seconds to wait for handshake + - auto_message_enabled: bool, whether to auto-send messages + - message_interval: float, seconds between auto messages + - message_content: str, default message content + - active_mode: bool, whether to actively initiate protocol + """ + for key, value in kwargs.items(): + if hasattr(self.auto_mode.config, key): + setattr(self.auto_mode.config, key, value) + print(f"{BLUE}[CONFIG]{RESET} Set auto mode {key} = {value}") + else: + print(f"{RED}[ERROR]{RESET} Unknown auto mode configuration parameter: {key}") + + def get_auto_mode_config(self): + """Return the current auto mode configuration.""" + return self.auto_mode.config + + def queue_auto_message(self, message: str): + """ + Add a message to the auto-send queue. + + Args: + message: Message text to send + """ + self.auto_mode.queue_message(message) + + def toggle_auto_mode_logging(self): + """ + Toggle detailed logging for auto mode. + When enabled, will show more information about state transitions and decision making. + """ + if not hasattr(self.auto_mode, 'verbose_logging'): + self.auto_mode.verbose_logging = True + else: + self.auto_mode.verbose_logging = not self.auto_mode.verbose_logging + + status = "enabled" if self.auto_mode.verbose_logging else "disabled" + print(f"{BLUE}[AUTO-LOG]{RESET} Detailed logging {status}") + + def debug_message(self, index: int): + """ + Debug a message in the inbound message queue. + Prints detailed information about the message. + + Args: + index: The index of the message in the inbound_messages queue + """ + if index < 0 or index >= len(self.inbound_messages): + print(f"{RED}[ERROR]{RESET} Invalid message index {index}") + return + + msg = self.inbound_messages[index] + print(f"\n{YELLOW}=== Message Debug [{index}] ==={RESET}") + print(f"Type: {msg['type']}") + print(f"Length: {len(msg['raw'])} bytes = {len(msg['raw'])*8} bits") + print(f"Raw data: {msg['raw'].hex()}") + + if msg['parsed'] is not None: + print(f"\n{YELLOW}--- Parsed Data ---{RESET}") + if msg['type'] == 'PING_REQUEST': + ping = msg['parsed'] + print(f"Version: {ping.version}") + print(f"Cipher: {ping.cipher} ({'AES-256-GCM' if ping.cipher == 0 else 'ChaCha20-Poly1305' if ping.cipher == 1 else 'Unknown'})") + print(f"Session nonce: {ping.session_nonce.hex()}") + print(f"CRC32: {ping.crc32:08x}") + + elif msg['type'] == 'PING_RESPONSE': + resp = msg['parsed'] + print(f"Version: {resp.version}") + print(f"Cipher: {resp.cipher} ({'AES-256-GCM' if resp.cipher == 0 else 'ChaCha20-Poly1305' if resp.cipher == 1 else 'Unknown'})") + print(f"Answer: {resp.answer} ({'Accept' if resp.answer == 1 else 'Reject'})") + print(f"CRC32: {resp.crc32:08x}") + + elif msg['type'] == 'HANDSHAKE': + hs = msg['parsed'] + print(f"Ephemeral pubkey: {hs.ephemeral_pubkey.hex()[:16]}...") + print(f"Ephemeral signature: {hs.ephemeral_signature.hex()[:16]}...") + print(f"PFS hash: {hs.pfs_hash.hex()[:16]}...") + print(f"Timestamp: {hs.timestamp}") + print(f"CRC32: {hs.crc32:08x}") + + elif msg['type'] == 'ENCRYPTED_MESSAGE': + header = msg['parsed'] + print(f"Flag: 0x{header.flag:04x}") + print(f"Data length: {header.data_len} bytes") + print(f"Retry: {header.retry}") + print(f"Connection status: {header.connection_status} ({'CRC included' if header.connection_status & 0x01 else 'No CRC'})") + print(f"IV: {header.iv.hex()}") + + # Calculate expected message size + expected_len = 18 + header.data_len + 16 # Header + payload + tag + if header.connection_status & 0x01: + expected_len += 4 # Add CRC + + print(f"Expected total length: {expected_len} bytes") + print(f"Actual length: {len(msg['raw'])} bytes") + + # If we have a key, try to decrypt + if self.hkdf_key: + print("\nAttempting decryption...") + try: + key = bytes.fromhex(self.hkdf_key) + plaintext = decrypt_message(msg['raw'], key, self.cipher_type) + print(f"Decrypted: {plaintext.decode('utf-8')}") + except Exception as e: + print(f"Decryption failed: {e}") + print() + # ------------------------------------------------------------------------- # Public Methods # ------------------------------------------------------------------------- @@ -314,6 +470,9 @@ class IcingProtocol: conn = transmission.connect_to_peer("127.0.0.1", port, self.on_data_received) self.connections.append(conn) print(f"{GREEN}[IcingProtocol]{RESET} Outgoing connection to port {port} established.") + + # Notify auto mode + self.auto_mode.handle_connection_established() def set_peer_identity(self, peer_pubkey_hex: str): pubkey_bytes = bytes.fromhex(peer_pubkey_hex) @@ -326,13 +485,24 @@ class IcingProtocol: print(f"{GREEN}[IcingProtocol]{RESET} Generated ephemeral key pair: pubkey={self.ephemeral_pubkey.hex()[:16]}...") # Send PING (session discovery and cipher negotiation) - def send_ping_request(self): + def send_ping_request(self, cipher_type=0): + """ + Send a ping request to the first connected peer. + + Args: + cipher_type: Preferred cipher type (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) + """ if not self.connections: print(f"{RED}[ERROR]{RESET} No active connections.") - return + return False + + # Validate cipher type + if cipher_type not in (0, 1): + print(f"{YELLOW}[WARNING]{RESET} Invalid cipher type {cipher_type}, defaulting to AES-256-GCM (0)") + cipher_type = 0 - # Create ping request with our default cipher preference (AES-256-GCM = 0) - ping_request = PingRequest(version=0, cipher=0) + # Create ping request with specified cipher + ping_request = PingRequest(version=0, cipher=cipher_type) # Store session nonce if not already set if self.session_nonce is None: @@ -343,6 +513,7 @@ class IcingProtocol: pkt = ping_request.serialize() self._send_packet(self.connections[0], pkt, "PING_REQUEST") self.state["ping_sent"] = True + return True def send_handshake(self): """ @@ -355,13 +526,13 @@ class IcingProtocol: """ if not self.connections: print(f"{RED}[ERROR]{RESET} No active connections.") - return + return False if not self.ephemeral_privkey or not self.ephemeral_pubkey: print(f"{RED}[ERROR]{RESET} Ephemeral keys not generated.") - return + return False if self.peer_identity_pubkey_bytes is None: print(f"{RED}[ERROR]{RESET} Peer identity not set; needed for PFS tracking.") - return + return False # 1) Sign ephemeral_pubkey using identity key sig_der = sign_data(self.identity_privkey, self.ephemeral_pubkey) @@ -383,10 +554,15 @@ class IcingProtocol: pkt = handshake.serialize() self._send_packet(self.connections[0], pkt, "HANDSHAKE") self.state["handshake_sent"] = True + return True def enable_auto_responder(self, enable: bool): + """ + Legacy method for enabling/disabling auto responder. + For new code, use start_auto_mode() and stop_auto_mode() instead. + """ self.auto_responder = enable - print(f"{BLUE}[AUTO]{RESET} Auto responder set to {enable}.") + print(f"{YELLOW}[LEGACY]{RESET} Auto responder set to {enable}. Consider using auto_mode instead.") # ------------------------------------------------------------------------- # Manual Responses @@ -399,11 +575,11 @@ class IcingProtocol: """ if index < 0 or index >= len(self.inbound_messages): print(f"{RED}[ERROR]{RESET} Invalid index {index}.") - return + return False msg = self.inbound_messages[index] if msg["type"] != "PING_REQUEST": print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a PING_REQUEST.") - return + return False ping_request = msg["parsed"] version = ping_request.version @@ -424,6 +600,7 @@ class IcingProtocol: resp = ping_response.serialize() self._send_packet(conn, resp, "PING_RESPONSE") print(f"{BLUE}[MANUAL]{RESET} Responded to ping with answer={answer}.") + return True def generate_ecdhe(self, index: int): """ @@ -434,11 +611,11 @@ class IcingProtocol: """ if index < 0 or index >= len(self.inbound_messages): print(f"{RED}[ERROR]{RESET} Invalid index {index}.") - return + return False msg = self.inbound_messages[index] if msg["type"] != "HANDSHAKE": print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a HANDSHAKE.") - return + return False handshake = msg["parsed"] @@ -450,13 +627,13 @@ class IcingProtocol: ok = verify_signature(self.peer_identity_pubkey_obj, sig_der, handshake.ephemeral_pubkey) if not ok: print(f"{RED}[ERROR]{RESET} Ephemeral signature invalid.") - return + return False print(f"{GREEN}[OK]{RESET} Ephemeral signature verified.") # Check if we have ephemeral keys if not self.ephemeral_privkey: print(f"{YELLOW}[WARN]{RESET} No ephemeral_privkey available, cannot compute shared secret.") - return + return False # Compute ECDH shared secret shared = compute_ecdh_shared_key(self.ephemeral_privkey, handshake.ephemeral_pubkey) @@ -467,6 +644,7 @@ class IcingProtocol: old_session, _ = self.pfs_history.get(self.peer_identity_pubkey_bytes, (-1, "")) new_session = 1 if old_session < 0 else old_session + 1 self.pfs_history[self.peer_identity_pubkey_bytes] = (new_session, self.shared_secret) + return True # ------------------------------------------------------------------------- # Utility @@ -516,7 +694,9 @@ class IcingProtocol: for k, v in self.state.items(): print(f" {k}: {v}") - print("\nAuto Responder:", self.auto_responder) + print("\nAuto Mode Active:", self.auto_mode.active) + print("Auto Mode State:", self.auto_mode.state) + print("Legacy Auto Responder:", self.auto_responder) print("\nActive Connections:") for i, c in enumerate(self.connections): @@ -528,14 +708,24 @@ class IcingProtocol: print() def stop(self): + """Stop the protocol and clean up resources.""" + # Stop auto mode first + self.auto_mode.stop() + + # Stop server listener self.server_listener.stop() + + # Close all connections for c in self.connections: c.close() self.connections.clear() self.inbound_messages.clear() print(f"{RED}[STOP]{RESET} Protocol stopped.") - # New method: Send an encrypted message over the first active connection. + # ------------------------------------------------------------------------- + # Encrypted Messaging + # ------------------------------------------------------------------------- + def send_encrypted_message(self, plaintext: str): """ Encrypts and sends a message using the derived HKDF key and negotiated cipher. @@ -546,10 +736,10 @@ class IcingProtocol: """ if not self.connections: print(f"{RED}[ERROR]{RESET} No active connections.") - return + return False if not self.hkdf_key: print(f"{RED}[ERROR]{RESET} No HKDF key derived. Cannot encrypt message.") - return + return False # Get the encryption key key = bytes.fromhex(self.hkdf_key) @@ -582,27 +772,27 @@ class IcingProtocol: # Send the encrypted message self._send_packet(self.connections[0], encrypted, "ENCRYPTED_MESSAGE") print(f"{GREEN}[SEND_ENCRYPTED]{RESET} Encrypted message sent.") + return True - # New method: Decrypt an encrypted message from the inbound queue. def decrypt_received_message(self, index: int): """ Decrypt a received encrypted message using the HKDF key and negotiated cipher. """ if index < 0 or index >= len(self.inbound_messages): print(f"{RED}[ERROR]{RESET} Invalid message index.") - return + return None msg = self.inbound_messages[index] if msg["type"] != "ENCRYPTED_MESSAGE": print(f"{RED}[ERROR]{RESET} Message at index {index} is not an ENCRYPTED_MESSAGE.") - return + return None # Get the encrypted message encrypted = msg["raw"] if not self.hkdf_key: print(f"{RED}[ERROR]{RESET} No HKDF key derived. Cannot decrypt message.") - return + return None # Get the encryption key key = bytes.fromhex(self.hkdf_key)