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:
To see the protocol working:
```bash
cd simulator
./launch_gsm_simulator.sh
cd DryBox/UI
python3 simple_integrated_ui.py
```
2. Run the DryBox UI:
```bash
python3 UI/main.py
```
Then click through the buttons in order:
1. Connect
2. Send PING
3. Send Handshake
4. Derive Keys
5. Send Encrypted Message
3. Click "Run Automatic Test" button for full protocol testing
This demonstrates the full protocol flow without the complexity of the phone UI.
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()
```
## Next Steps
## Key Features
- **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
# 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:
# 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
try:
except Exception as e:
try:
except:
self.data_received.emit(plaintext, self.client_id)
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}")
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
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")
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)