add
All checks were successful
/ mirror (push) Successful in 5s

This commit is contained in:
Bartosz 2025-07-04 23:03:14 +01:00
parent 5c274817df
commit a14084ce68
2 changed files with 347 additions and 177 deletions

View File

@ -1,164 +1,78 @@
# DryBox Integration Status # DryBox Protocol Integration Status
## Overview ## Current Issues
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
**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`) 3. **Test Failures:**
- **Codec2Wrapper**: Simulates Codec2 voice compression - FSK demodulation is returning empty data
- Default mode: 1200 bps (optimal for GSM) - Voice protocol tests are failing due to API mismatches
- Frame size: 48 bits (6 bytes) per 40ms - Encryption tests have parameter issues
- 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
### 2. Encryption (`encryption.py`) ## What Works
- **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
### 3. Protocol Phone Client (`protocol_phone_client.py`) 1. **Individual Components:**
- Extends base phone client with protocol support - IcingProtocol works standalone
- Integrates Noise XK session management - FSK modulation works (but demodulation has issues)
- Handles voice frame encoding/decoding pipeline: - Codec2 wrapper works
1. PCM → Codec2 → 4FSK → ChaCha20 → Noise XK → Network - Encryption/decryption works with correct parameters
2. Network → Noise XK → ChaCha20 → 4FSK → Codec2 → PCM
- Voice session management (start/stop)
- Automatic key derivation from Noise session
### 4. Protocol Client State (`protocol_client_state.py`) 2. **Simple Integration:**
- Enhanced state management for protocol operations - See `simple_integrated_ui.py` for a working example
- Voice session state tracking - Shows proper protocol flow step-by-step
- Automatic voice start after handshake completion - Demonstrates successful key exchange and encryption
- Command queue for async operations
### 5. Enhanced DryBox UI (`main.py`) ## Recommended Approach
- 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)
## Protocol Flow Instead of trying to retrofit the complex Protocol into the existing DryBox UI, I recommend:
1. **Connection**: Phones connect to GSM simulator 1. **Start Fresh:**
2. **Call Setup**: Phone 1 calls Phone 2 - Use `simple_integrated_ui.py` as a base
3. **Noise XK Handshake**: - Build up the phone UI features gradually
- 3-message handshake pattern - Ensure each step works before adding complexity
- 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
## 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: 3. **Simplify Audio Integration:**
- ✓ Codec2 compression/decompression - Get basic encrypted messaging working first
- ✓ 4FSK modulation/demodulation (>92% confidence) - Add voice/FSK modulation as a separate phase
- ✓ ChaCha20 encryption/decryption - Test with GSM simulator separately
- ✓ Full pipeline integration
- ✓ GSM simulator compatibility
## Usage ## Quick Start
1. Start the GSM simulator: To see the protocol working:
```bash ```bash
cd simulator cd DryBox/UI
./launch_gsm_simulator.sh python3 simple_integrated_ui.py
``` ```
2. Run the DryBox UI: Then click through the buttons in order:
```bash 1. Connect
python3 UI/main.py 2. Send PING
``` 3. Send Handshake
4. Derive Keys
3. Click "Run Automatic Test" button for full protocol testing 5. Send Encrypted Message
3. Or use the protocol phone client directly in your application: This demonstrates the full protocol flow without the complexity of the phone UI.
```python
from protocol_phone_client import ProtocolPhoneClient
client = ProtocolPhoneClient(client_id=1)
client.start()
```
## Key Features ## Next Steps
- **End-to-end encryption**: Noise XK + ChaCha20 dual layer 1. Fix the FSK demodulation issue
- **GSM compatible**: Works over standard voice channels 2. Create a new phone UI based on the working protocol flow
- **Low bitrate**: 1200 bps voice codec 3. Integrate voice/audio after basic encryption works
- **Robust modulation**: 4FSK survives GSM compression 4. Add GSM simulator support once everything else works
- **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

View File

@ -1,67 +1,323 @@
import sys
import os
import socket import socket
import time import time
import select import select
import threading
from PyQt5.QtCore import QThread, pyqtSignal 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): class ProtocolPhoneClient(QThread):
"""Phone client that integrates the full Icing Protocol with 4FSK and ChaCha20."""
data_received = pyqtSignal(bytes, int) data_received = pyqtSignal(bytes, int)
state_changed = pyqtSignal(str, str, 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__() super().__init__()
self.client_id = client_id self.client_id = client_id
self.running = True 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 self.handshake_complete = False
# Voice state # Voice state
self.voice_active = False 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: # Track processed messages
return True self.processed_message_count = 0
except Exception as e:
return False 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): 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: while self.running:
try: try:
# Process protocol messages
self._process_protocol_messages()
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 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) 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: def _initialize_voice_protocol(self, codec_mode=Codec2Mode.MODE_1200):
except Exception as e: """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): def initiate_call(self):
message = message.encode('utf-8') """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): def stop(self):
"""Stop the client."""
self.running = False 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: try:
self.gsm_socket.close()
except:
pass
self.quit() self.quit()
self.wait(1000) self.wait(1000)