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
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
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

View File

@ -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)