From a14084ce68b55a75a5bb102e66da0ddb811c5982 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 4 Jul 2025 23:03:14 +0100 Subject: [PATCH] add --- .../DryBox/INTEGRATION_STATUS.md | 206 ++++-------- .../DryBox/UI/protocol_phone_client.py | 318 ++++++++++++++++-- 2 files changed, 347 insertions(+), 177 deletions(-) diff --git a/protocol_prototype/DryBox/INTEGRATION_STATUS.md b/protocol_prototype/DryBox/INTEGRATION_STATUS.md index af11d71..d516fb1 100644 --- a/protocol_prototype/DryBox/INTEGRATION_STATUS.md +++ b/protocol_prototype/DryBox/INTEGRATION_STATUS.md @@ -1,164 +1,78 @@ -# DryBox Integration Status +# DryBox Protocol Integration Status -## Overview -Successfully integrated the complete protocol stack with DryBox, combining: -- **Noise XK**: End-to-end encrypted handshake and session establishment -- **Codec2**: Voice compression at 1200 bps (48 bits per 40ms frame) -- **4FSK Modulation**: Robust modulation for GSM voice channels (600 baud) -- **ChaCha20**: Additional voice frame encryption layer +## Current Issues -**Latest Update**: Fixed UI integration to use existing DryBox UI with enhanced debugging +1. **UI Integration Problems:** + - The UI is trying to use the old `NoiseXKSession` handshake mechanism + - The `protocol_phone_client.py` was calling non-existent `get_messages()` method + - Error handling was passing error messages through the UI state system -## Components Integrated +2. **Protocol Mismatch:** + - The original DryBox UI uses a simple handshake exchange + - The Protocol uses a more complex Noise XK pattern with: + - PING REQUEST/RESPONSE + - HANDSHAKE messages + - Key derivation + - These two approaches are incompatible without significant refactoring -### 1. Voice Codec (`voice_codec.py`) -- **Codec2Wrapper**: Simulates Codec2 voice compression - - Default mode: 1200 bps (optimal for GSM) - - Frame size: 48 bits (6 bytes) per 40ms - - Sample rate: 8kHz mono -- **FSKModem**: 4-FSK modulation/demodulation - - Frequencies: 600, 1200, 1800, 2400 Hz - - Symbol rate: 600 baud - - Preamble detection and synchronization - - Confidence scoring for demodulation +3. **Test Failures:** + - FSK demodulation is returning empty data + - Voice protocol tests are failing due to API mismatches + - Encryption tests have parameter issues -### 2. Encryption (`encryption.py`) -- **ChaCha20-CTR**: Stream cipher for voice frames - - 256-bit keys - - 16-byte nonces - - Low latency (no authentication for voice) -- **ChaCha20-Poly1305**: Authenticated encryption for control messages -- **AES-256-GCM**: Alternative authenticated encryption +## What Works -### 3. Protocol Phone Client (`protocol_phone_client.py`) -- Extends base phone client with protocol support -- Integrates Noise XK session management -- Handles voice frame encoding/decoding pipeline: - 1. PCM → Codec2 → 4FSK → ChaCha20 → Noise XK → Network - 2. Network → Noise XK → ChaCha20 → 4FSK → Codec2 → PCM -- Voice session management (start/stop) -- Automatic key derivation from Noise session +1. **Individual Components:** + - IcingProtocol works standalone + - FSK modulation works (but demodulation has issues) + - Codec2 wrapper works + - Encryption/decryption works with correct parameters -### 4. Protocol Client State (`protocol_client_state.py`) -- Enhanced state management for protocol operations -- Voice session state tracking -- Automatic voice start after handshake completion -- Command queue for async operations +2. **Simple Integration:** + - See `simple_integrated_ui.py` for a working example + - Shows proper protocol flow step-by-step + - Demonstrates successful key exchange and encryption -### 5. Enhanced DryBox UI (`main.py`) -- Uses existing DryBox UI (not a new UI) -- Added debug console with timestamped messages -- Added automatic test button with 11-step sequence -- Visual waveform display for transmitted/received audio -- Real-time status updates -- Support for test audio file transmission (wav/input_8k_mono.wav) +## Recommended Approach -## Protocol Flow +Instead of trying to retrofit the complex Protocol into the existing DryBox UI, I recommend: -1. **Connection**: Phones connect to GSM simulator -2. **Call Setup**: Phone 1 calls Phone 2 -3. **Noise XK Handshake**: - - 3-message handshake pattern - - Establishes encrypted channel - - Derives voice encryption keys -4. **Voice Transmission**: - - Audio → Codec2 (1200bps) → 4FSK modulation - - Encrypt with ChaCha20 (per-frame key stream) - - Wrap in Noise XK encrypted channel - - Transmit over GSM voice channel -5. **Voice Reception**: - - Reverse of transmission process - - Confidence-based demodulation - - Frame reconstruction +1. **Start Fresh:** + - Use `simple_integrated_ui.py` as a base + - Build up the phone UI features gradually + - Ensure each step works before adding complexity -## Testing +2. **Fix Protocol Flow:** + - Remove old handshake code from UI + - Implement proper state machine for protocol phases: + - IDLE → CONNECTING → PING_SENT → HANDSHAKE_SENT → KEYS_DERIVED → READY + - Handle auto-responder mode properly -All components tested and verified: -- ✓ Codec2 compression/decompression -- ✓ 4FSK modulation/demodulation (>92% confidence) -- ✓ ChaCha20 encryption/decryption -- ✓ Full pipeline integration -- ✓ GSM simulator compatibility +3. **Simplify Audio Integration:** + - Get basic encrypted messaging working first + - Add voice/FSK modulation as a separate phase + - Test with GSM simulator separately -## Usage +## Quick Start -1. Start the GSM simulator: - ```bash - cd simulator - ./launch_gsm_simulator.sh - ``` +To see the protocol working: +```bash +cd DryBox/UI +python3 simple_integrated_ui.py +``` -2. Run the DryBox UI: - ```bash - python3 UI/main.py - ``` - -3. Click "Run Automatic Test" button for full protocol testing +Then click through the buttons in order: +1. Connect +2. Send PING +3. Send Handshake +4. Derive Keys +5. Send Encrypted Message -3. Or use the protocol phone client directly in your application: - ```python - from protocol_phone_client import ProtocolPhoneClient - client = ProtocolPhoneClient(client_id=1) - client.start() - ``` +This demonstrates the full protocol flow without the complexity of the phone UI. -## Key Features +## Next Steps -- **End-to-end encryption**: Noise XK + ChaCha20 dual layer -- **GSM compatible**: Works over standard voice channels -- **Low bitrate**: 1200 bps voice codec -- **Robust modulation**: 4FSK survives GSM compression -- **Real-time**: 40ms frame latency -- **Confidence scoring**: Quality metrics for demodulation - -## Current Issues & Debugging - -### 1. **Handshake Timing** -- Issue: Encrypted data sometimes arrives before handshake completes -- Debug: Added extensive logging to track handshake state -- Status: Needs timing synchronization fix - -### 2. **State Management** -- Fixed: PhoneState now uses proper Python Enum -- Fixed: Added proper initiator/responder role tracking - -### 3. **Decryption Errors** -- Symptom: "Decryption error" with ciphertext in logs -- Cause: Data received before secure channel established -- Mitigation: Added checks for handshake_complete before processing - -## Debug Features Added - -1. **Debug Console in UI** - - Real-time protocol message display - - Timestamped messages - - Clear button for cleanup - - Auto-scroll to latest messages - -2. **Automatic Test Sequence** - - Step 1: Check initial state - - Step 2: Make call - - Step 3: Answer call - - Step 4: Check handshake progress - - Step 5: Check handshake status - - Step 6: Check voice status - - Step 7: Check audio transmission - - Step 8: Protocol details - - Step 9: Let transmission run - - Step 10: Final statistics - - Step 11: Hang up - -3. **Enhanced Logging** - - All components use debug() method - - Verbose handshake state tracking - - Voice frame logging (every 25 frames) - - Disabled Noise session verbose logging - -## Future Enhancements - -- Fix handshake timing to ensure completion before voice -- Add FEC (Forward Error Correction) for improved robustness -- Implement voice activity detection (VAD) -- Add adaptive bitrate selection -- Integrate with real Codec2 library (not simulation) -- Add DTMF signaling for out-of-band control \ No newline at end of file +1. Fix the FSK demodulation issue +2. Create a new phone UI based on the working protocol flow +3. Integrate voice/audio after basic encryption works +4. Add GSM simulator support once everything else works \ No newline at end of file diff --git a/protocol_prototype/DryBox/UI/protocol_phone_client.py b/protocol_prototype/DryBox/UI/protocol_phone_client.py index 197c00e..8d792c7 100644 --- a/protocol_prototype/DryBox/UI/protocol_phone_client.py +++ b/protocol_prototype/DryBox/UI/protocol_phone_client.py @@ -1,67 +1,323 @@ +import sys +import os import socket import time import select +import threading from PyQt5.QtCore import QThread, pyqtSignal +# Add Protocol directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Protocol')) + +from protocol import IcingProtocol +from voice_codec import VoiceProtocol, Codec2Mode +from messages import VoiceStart, VoiceAck, VoiceEnd +from encryption import EncryptedMessage + class ProtocolPhoneClient(QThread): + """Phone client that integrates the full Icing Protocol with 4FSK and ChaCha20.""" + data_received = pyqtSignal(bytes, int) state_changed = pyqtSignal(str, str, int) - + audio_received = pyqtSignal(bytes, int) # For decoded audio + + def __init__(self, client_id, identity_keys=None): super().__init__() self.client_id = client_id self.running = True + # Initialize Icing Protocol + self.protocol = IcingProtocol() + + # Override identity keys if provided + if identity_keys: + self.protocol.identity_privkey = identity_keys[0] + self.protocol.identity_pubkey = identity_keys[1] + + # Connection state + self.connected = False self.handshake_complete = False # Voice state self.voice_active = False + self.voice_protocol = None + # Peer information + self.peer_identity_hex = None + self.peer_port = None - - + # For GSM simulator compatibility + self.gsm_host = "localhost" + self.gsm_port = 12345 + self.gsm_socket = None - try: - return True - except Exception as e: - return False + # Track processed messages + self.processed_message_count = 0 + + def set_peer_identity(self, peer_identity_hex): + """Set the peer's identity public key (hex string).""" + self.peer_identity_hex = peer_identity_hex + self.protocol.set_peer_identity(peer_identity_hex) + + def set_peer_port(self, port): + """Set the peer's port for direct connection.""" + self.peer_port = port + + def connect_to_gsm_simulator(self): + """Connect to the GSM simulator for voice channel simulation.""" + try: + self.gsm_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.gsm_socket.settimeout(5) + self.gsm_socket.connect((self.gsm_host, self.gsm_port)) + print(f"Client {self.client_id} connected to GSM simulator") + return True + except Exception as e: + print(f"Client {self.client_id} failed to connect to GSM simulator: {e}") + return False def run(self): + """Main thread loop.""" + # Protocol listener already started in __init__ of IcingProtocol + + # Connect to GSM simulator if available + self.connect_to_gsm_simulator() + while self.running: try: - - - - except Exception as e: - self.state_changed.emit("CALL_END", "", self.client_id) - break - try: - except Exception as e: - - try: - except: - self.data_received.emit(plaintext, self.client_id) - - self.voice_active = True - - - - - + # Process protocol messages + self._process_protocol_messages() + # Process GSM simulator data if connected + if self.gsm_socket: + self._process_gsm_data() + self.msleep(10) + + except Exception as e: + print(f"Client {self.client_id} error in main loop: {e}") + # Only emit state change if it's a real connection error + if "get_messages" not in str(e): + self.state_changed.emit("CALL_END", "", self.client_id) + break + + def _process_protocol_messages(self): + """Process messages from the Icing Protocol.""" + # Process new messages in the inbound queue + if not hasattr(self.protocol, 'inbound_messages'): + return + + new_messages = self.protocol.inbound_messages[self.processed_message_count:] + + for msg in new_messages: + self.processed_message_count += 1 + msg_type = msg.get('type', '') + + if msg_type == 'PING_REQUEST': + # Received ping request, we're the responder + if not self.protocol.state['ping_sent']: + # Enable auto responder to handle protocol flow + self.protocol.auto_responder = True + # Send ping response + index = self.protocol.inbound_messages.index(msg) + self.protocol.respond_to_ping(index, 1) # Accept with ChaCha20 + + elif msg_type == 'PING_RESPONSE': + # Ping response received, continue with handshake + if not self.protocol.state['handshake_sent']: + self.protocol.send_handshake() + + elif msg_type == 'HANDSHAKE': + # Handshake received + if self.protocol.state['ping_sent'] and not self.protocol.state['handshake_sent']: + # We're initiator, send our handshake + self.protocol.send_handshake() + # Derive keys if we have peer's handshake + if self.protocol.state['handshake_received'] and not self.protocol.state['key_exchange_complete']: + self.protocol.derive_hkdf() + self.handshake_complete = True + self.state_changed.emit("HANDSHAKE_DONE", "", self.client_id) + + elif msg_type == 'ENCRYPTED': + # Decrypt and process encrypted message + parsed = msg.get('parsed') + if parsed and hasattr(parsed, 'plaintext'): + self._handle_encrypted_message(parsed.plaintext) + + elif msg_type == 'voice_start': + # Voice session started by peer + self._handle_voice_start(msg.get('parsed')) + + elif msg_type == 'voice_ack': + # Voice session acknowledged + self.voice_active = True + + elif msg_type == 'voice_end': + # Voice session ended + self.voice_active = False + + def _process_gsm_data(self): + """Process audio data from GSM simulator.""" + try: + readable, _, _ = select.select([self.gsm_socket], [], [], 0) + if readable: + data = self.gsm_socket.recv(4096) + if data and self.voice_active and self.voice_protocol: + # Process received FSK-modulated audio + encrypted_frames = self.voice_protocol.demodulate_audio(data) + for frame in encrypted_frames: + # Decrypt and decode + audio_samples = self.voice_protocol.decrypt_and_decode(frame) + if audio_samples: + self.audio_received.emit(audio_samples, self.client_id) + except Exception as e: + print(f"Client {self.client_id} GSM data processing error: {e}") - self.voice_active = False + def _handle_encrypted_message(self, plaintext): + """Handle decrypted message content.""" + # Check if it's audio data or control message + if plaintext.startswith(b'AUDIO:'): + audio_data = plaintext[6:] + self.data_received.emit(audio_data, self.client_id) + else: + # Control message + try: + message = plaintext.decode('utf-8') + self._handle_control_message(message) + except: + # Binary data + self.data_received.emit(plaintext, self.client_id) + + def _handle_control_message(self, message): + """Handle control messages.""" + if message == "RINGING": + self.state_changed.emit("RINGING", "", self.client_id) + elif message == "IN_CALL": + self.state_changed.emit("IN_CALL", "", self.client_id) + elif message == "CALL_END": self.state_changed.emit("CALL_END", "", self.client_id) + def _handle_voice_start(self, voice_start_msg): + """Handle voice session start.""" + if voice_start_msg: + # Initialize voice protocol with negotiated parameters + self.protocol.voice_session_active = True + self.protocol.voice_session_id = voice_start_msg.session_id + + # Send acknowledgment + self.protocol.send_voice_ack(voice_start_msg.session_id) + + # Initialize voice codec + self._initialize_voice_protocol(voice_start_msg.codec_mode) + self.voice_active = True - try: - except Exception as e: + def _initialize_voice_protocol(self, codec_mode=Codec2Mode.MODE_1200): + """Initialize voice protocol with codec and encryption.""" + if self.protocol.hkdf_key: + self.voice_protocol = VoiceProtocol( + shared_key=bytes.fromhex(self.protocol.hkdf_key), + codec_mode=codec_mode, + cipher_type=self.protocol.cipher_type + ) + print(f"Client {self.client_id} initialized voice protocol") - if isinstance(message, str): - message = message.encode('utf-8') + def initiate_call(self): + """Initiate a call to the peer.""" + if not self.peer_port: + print(f"Client {self.client_id}: No peer port set") + return False + + # Connect to peer + self.protocol.connect_to_peer(self.peer_port) + self.state_changed.emit("CALLING", "", self.client_id) + + # Start key exchange + self.protocol.generate_ephemeral_keys() + self.protocol.send_ping_request(cipher_type=1) # Request ChaCha20 + + return True + + def answer_call(self): + """Answer an incoming call.""" + self.state_changed.emit("IN_CALL", "", self.client_id) + + # Enable auto-responder for handling protocol flow + self.protocol.auto_responder = True + + # If we already have a ping request, respond to it + for i, msg in enumerate(self.protocol.inbound_messages): + if msg.get('type') == 'PING_REQUEST' and not self.protocol.state['ping_sent']: + self.protocol.respond_to_ping(i, 1) # Accept with ChaCha20 + break + + def end_call(self): + """End the current call.""" + if self.voice_active: + self.protocol.end_voice_call() + + self.voice_active = False + self.handshake_complete = False + self.state_changed.emit("CALL_END", "", self.client_id) + + # Close connections + for conn in self.protocol.connections: + conn.close() + self.protocol.connections.clear() + + def send_audio(self, audio_samples): + """Send audio samples through the voice protocol.""" + if self.voice_active and self.voice_protocol and self.gsm_socket: + # Encode and encrypt audio + fsk_audio = self.voice_protocol.encode_and_encrypt(audio_samples) + if fsk_audio and self.gsm_socket: + try: + # Send FSK-modulated audio through GSM simulator + self.gsm_socket.send(fsk_audio) + except Exception as e: + print(f"Client {self.client_id} failed to send audio: {e}") + + def send_message(self, message): + """Send an encrypted message.""" + if self.handshake_complete: + if isinstance(message, str): + message = message.encode('utf-8') + self.protocol.send_encrypted_message(message) + + def start_voice_session(self): + """Start a voice session.""" + if self.handshake_complete and not self.voice_active: + self._initialize_voice_protocol() + self.protocol.start_voice_call(codec_mode=5) # 1200 bps mode + + def get_identity_key(self): + """Get this client's identity public key.""" + if hasattr(self.protocol, 'identity_pubkey') and self.protocol.identity_pubkey: + return self.protocol.identity_pubkey.hex() + return "test_identity_key_" + str(self.client_id) + + def get_local_port(self): + """Get the local listening port.""" + if hasattr(self.protocol, 'local_port'): + return self.protocol.local_port + return 12345 + self.client_id def stop(self): + """Stop the client.""" self.running = False + + # End voice session if active + if self.voice_active: + self.protocol.end_voice_call() + + # Stop protocol server listener + if hasattr(self.protocol, 'server_listener') and self.protocol.server_listener: + self.protocol.server_listener.stop() + + # Close GSM socket + if self.gsm_socket: try: + self.gsm_socket.close() + except: + pass + self.quit() self.wait(1000) \ No newline at end of file