parent
5c274817df
commit
a14084ce68
@ -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
|
@ -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)
|
Loading…
Reference in New Issue
Block a user