Compare commits

..

44 Commits

Author SHA1 Message Date
354c630a2b Switching to new protocol Alpha.
All checks were successful
/ mirror (push) Successful in 5s
Currently only the Best Case Scenario diagram is available;
Logic diagram and according documentation is required.
2025-07-05 19:43:02 +02:00
8836cf9197 Working on protocol Alpha 2
Some checks failed
/ mirror (push) Failing after 4s
2025-07-05 13:27:11 +02:00
f1d7f156e1 Merge remote-tracking branch 'origin/Protocol_00' into Protocol_00
Some checks failed
/ build (push) Successful in 10m16s
/ build-stealth (push) Successful in 10m19s
/ mirror (push) Failing after 5s
2025-07-05 12:48:51 +02:00
901478ba8c add of integrated ui
Some checks failed
/ mirror (push) Failing after 4s
2025-06-01 09:55:02 +01:00
7c52ac321e add of codec chacha20 and modulation
All checks were successful
/ mirror (push) Successful in 5s
2025-05-24 08:16:49 +01:00
da60b32dc7 Merge branch 'dev' into Protocol_00
All checks were successful
/ mirror (push) Successful in 8s
/ build (push) Successful in 14m25s
/ build-stealth (push) Successful in 9m45s
# Conflicts:
#	dialer/android/app/src/main/AndroidManifest.xml
#	dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt
#	dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt
#	dialer/lib/features/call/call_page.dart
#	dialer/lib/features/contacts/widgets/contact_modal.dart
#	dialer/lib/features/history/history_page.dart
#	dialer/lib/features/home/home_page.dart
#	dialer/lib/main.dart
#	dialer/lib/services/call_service.dart
2025-05-14 14:49:03 +03:00
41aff9848a add drybox ui
All checks were successful
/ mirror (push) Successful in 5s
2025-05-11 17:25:51 +01:00
9e2daa7f53 Merge branch 'Protocol_00' of git.gmoker.com:icing/monorepo into Protocol_00
All checks were successful
/ mirror (push) Successful in 4s
2025-04-13 09:07:29 +01:00
ae26af0f99 add 2025-04-13 09:07:21 +01:00
71b9cc787b Better CLI & auto mode, enhancements
All checks were successful
/ mirror (push) Successful in 5s
2025-04-05 10:19:39 +03:00
0b52d602ef Fix + enhancement
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 22:59:15 +02:00
c3b0e94666 Clean
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 22:27:28 +02:00
3baf3f142b Trying to fix PING and session Nonce
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 21:50:14 +02:00
394143b4df Add encryption.py NO DECRYPTION yet
All checks were successful
/ mirror (push) Successful in 5s
2025-03-29 20:55:09 +02:00
79b0491a75 Update diagram, add HKDF derivation
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 11:17:26 +02:00
6155955cca Merge branch 'Protocol_00' of git.gmoker.com:icing/monorepo into Protocol_00
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 18:06:17 +00:00
0e619309ea add of DryBox 2025-03-28 18:06:07 +00:00
8d45d2e745 Signature fix
All checks were successful
/ mirror (push) Successful in 5s
2025-03-28 19:41:55 +02:00
045d9ad417 Merge remote-tracking branch 'origin/Protocol_00' into Protocol_00
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 19:35:54 +02:00
803ec3712b Signature NOT fix, bad attempt 2025-03-28 19:35:43 +02:00
f9e64a73d9 merge
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 17:31:38 +00:00
f5930eef82 Need fix signatures
All checks were successful
/ mirror (push) Successful in 4s
2025-03-24 13:04:56 +02:00
1b5bda2eb4 Semi-auto, bad sizes and need adjustments
All checks were successful
/ mirror (push) Successful in 6s
/ build-stealth (push) Successful in 8m30s
/ build (push) Successful in 8m37s
2025-03-23 21:32:00 +02:00
b139e36921 Update flow 2025-03-17 10:51:49 +02:00
e8bbe447c8 Drawio Handshake logic 2025-03-17 10:51:49 +02:00
d56dc33181 Init 2025-03-17 10:51:49 +02:00
0d6322a714 fix: not showing call UI on long press
All checks were successful
/ mirror (push) Successful in 4s
/ build-stealth (push) Successful in 8m21s
/ build (push) Successful in 8m30s
2025-03-14 15:59:54 +02:00
da0c5d1991 feat: call page UI
Some checks failed
/ mirror (push) Waiting to run
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-03-14 15:57:47 +02:00
3129e51eb4 Add CallPage for initiating calls with contact details (#37)
Demo call page avec les features de base:
- Haut parleur
- Couper/activer micro
- keypad
- raccrocher
- Display Icing state (toucher pour switch l'état)

S'active en faisant un appui long sur le bouton d'appel depuis les détails du contact.
Compatible avec l'obfuscation des contacts.

Co-authored-by: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com>
Reviewed-on: #37
Co-authored-by: alexis <alexis.danlos@epitech.eu>
Co-committed-by: alexis <alexis.danlos@epitech.eu>
2025-03-14 15:54:26 +02:00
2894dce1bc feat: can call and receive call
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m45s
/ build (push) Successful in 8m47s
2025-03-14 15:40:28 +02:00
5704fa1607 feat: APP IS DEFAULT DIALER
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m46s
/ build (push) Successful in 8m39s
2025-03-14 00:21:21 +02:00
e4ad9726ae rebase
All checks were successful
/ mirror (push) Successful in 5s
/ build (push) Successful in 8m33s
/ build-stealth (push) Successful in 8m35s
2025-03-13 22:45:14 +02:00
26316cf971 feat: call page UI
Some checks failed
/ mirror (push) Successful in 5s
/ build (push) Failing after 4m3s
/ build-stealth (push) Failing after 4m3s
2025-03-13 22:35:40 +02:00
98f199f450 Add CallPage for initiating calls with contact details (#37)
Demo call page avec les features de base:
- Haut parleur
- Couper/activer micro
- keypad
- raccrocher
- Display Icing state (toucher pour switch l'état)

S'active en faisant un appui long sur le bouton d'appel depuis les détails du contact.
Compatible avec l'obfuscation des contacts.

Co-authored-by: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com>
Reviewed-on: #37
Co-authored-by: alexis <alexis.danlos@epitech.eu>
Co-committed-by: alexis <alexis.danlos@epitech.eu>
2025-03-13 22:29:39 +02:00
5529a6e038 feat: request perm in flutter, wait for perm before trying to become main dialer
All checks were successful
/ build (push) Successful in 8m35s
/ build-stealth (push) Successful in 8m33s
/ mirror (push) Successful in 5s
2025-03-05 16:04:05 +01:00
c886e29d75 feat: perms & UI methodchannel
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m24s
/ build (push) Successful in 8m24s
2025-03-04 18:44:55 +01:00
24dc5a9bbe feat: update flutter UI via methodchannel, permissions via flutter at startup 2025-03-04 18:43:48 +01:00
b042a68a8e fix: makeGsmCall in historypage
All checks were successful
/ mirror (push) Successful in 7s
/ build-stealth (push) Successful in 8m29s
/ build (push) Successful in 8m30s
2025-03-04 14:23:02 +01:00
9bfb55821d fix: search bar upgrade (#42)
Some checks failed
/ mirror (push) Successful in 4s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
Reviewed-on: #42
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-03-04 14:22:06 +01:00
b7ebacec85 cicd-stealth (#40)
Reviewed-on: #40
Co-authored-by: ange <ange@yw5n.com>
Co-committed-by: ange <ange@yw5n.com>
2025-03-04 14:22:06 +01:00
ef78e4c17d fix: call correctly in history page (#41)
Reviewed-on: #41
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-03-04 14:22:06 +01:00
7c7a4f28f4 feat: call page UI
Some checks failed
/ build (push) Failing after 5s
/ mirror (push) Successful in 5s
2025-03-04 14:20:56 +01:00
b1f95a85e9 Drawio Handshake logic
All checks were successful
/ mirror (push) Successful in 4s
2025-03-02 23:20:59 +02:00
ee2eade791 Init
All checks were successful
/ mirror (push) Successful in 4s
2025-03-02 22:29:02 +02:00
99 changed files with 9191 additions and 2821 deletions

1
.gitignore vendored
View File

@ -9,7 +9,6 @@
.history .history
.svn/ .svn/
migrate_working_dir/ migrate_working_dir/
protocol_prototype/venv
# IntelliJ related # IntelliJ related
*.iml *.iml

View File

@ -0,0 +1,28 @@
package com.icing.dialer
import java.security.KeyStore
object KeyDeleterHelper {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
/**
* Deletes the key pair associated with the given alias from the Android Keystore.
*
* @param alias The alias of the key pair to delete.
* @throws Exception if deletion fails.
*/
fun deleteKeyPair(alias: String) {
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
if (!keyStore.containsAlias(alias)) {
throw Exception("No key found with alias \"$alias\" to delete.")
}
keyStore.deleteEntry(alias)
} catch (e: Exception) {
throw Exception("Failed to delete key pair: ${e.message}", e)
}
}
}

View File

@ -0,0 +1,47 @@
package com.icing.dialer
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
import java.security.KeyStore
object KeyGeneratorHelper {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
/**
* Generates an ECDSA P-256 key pair and stores it in the Android Keystore.
*
* @param alias Unique identifier for the key pair.
* @throws Exception if key generation fails.
*/
fun generateECKeyPair(alias: String) {
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
// Check if the key already exists
if (keyStore.containsAlias(alias)) {
throw Exception("Key with alias \"$alias\" already exists.")
}
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
ANDROID_KEYSTORE
)
val parameterSpec = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
)
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
.setUserAuthenticationRequired(false) // Set to true if you require user authentication
.build()
keyPairGenerator.initialize(parameterSpec)
keyPairGenerator.generateKeyPair()
} catch (e: Exception) {
throw Exception("Failed to generate EC key pair: ${e.message}", e)
}
}
}

View File

@ -1,6 +1,6 @@
package com.icing.dialer package com.icing.dialer
import android.os.Build import java.security.PrivateKey
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
@ -8,21 +8,15 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.PrivateKey
import java.security.Signature import java.security.Signature
import java.security.spec.ECGenParameterSpec
class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) { class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) {
private val ANDROID_KEYSTORE = "AndroidKeyStore" private val ANDROID_KEYSTORE = "AndroidKeyStore"
fun handleMethodCall() { fun handleMethodCall() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
result.error("UNSUPPORTED_API", "ED25519 requires Android 11 (API 30) or higher", null)
return
}
when (call.method) { when (call.method) {
"generateKeyPair" -> generateEDKeyPair() "generateKeyPair" -> generateECKeyPair()
"signData" -> signData() "signData" -> signData()
"getPublicKey" -> getPublicKey() "getPublicKey" -> getPublicKey()
"deleteKeyPair" -> deleteKeyPair() "deleteKeyPair" -> deleteKeyPair()
@ -31,7 +25,7 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
} }
} }
private fun generateEDKeyPair() { private fun generateECKeyPair() {
val alias = call.argument<String>("alias") val alias = call.argument<String>("alias")
if (alias == null) { if (alias == null) {
result.error("INVALID_ARGUMENT", "Alias is required", null) result.error("INVALID_ARGUMENT", "Alias is required", null)
@ -50,14 +44,16 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
KeyProperties.KEY_ALGORITHM_EC, KeyProperties.KEY_ALGORITHM_EC,
ANDROID_KEYSTORE ANDROID_KEYSTORE
) )
val parameterSpec = KeyGenParameterSpec.Builder( val parameterSpec = KeyGenParameterSpec.Builder(
alias, alias,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
) )
.setAlgorithmParameterSpec(ECGenParameterSpec("ed25519")) .setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
.setUserAuthenticationRequired(false) .setUserAuthenticationRequired(false)
.build() .build()
keyPairGenerator.initialize(parameterSpec) keyPairGenerator.initialize(parameterSpec)
keyPairGenerator.generateKeyPair() keyPairGenerator.generateKeyPair()
@ -77,14 +73,17 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
try { try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: run { val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: run {
result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null) result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null)
return return
} }
val signature = Signature.getInstance("Ed25519")
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey) signature.initSign(privateKey)
signature.update(data.toByteArray()) signature.update(data.toByteArray())
val signedBytes = signature.sign() val signedBytes = signature.sign()
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT) val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
result.success(signatureBase64) result.success(signatureBase64)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -0,0 +1,30 @@
package com.icing.dialer
import java.security.KeyStore
import java.security.PublicKey
import android.util.Base64
object PublicKeyHelper {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
/**
* Retrieves the public key associated with the given alias.
*
* @param alias The alias of the key pair.
* @return The public key as a Base64-encoded string.
* @throws Exception if retrieval fails.
*/
fun getPublicKey(alias: String): String {
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
val certificate = keyStore.getCertificate(alias) ?: throw Exception("Certificate not found for alias \"$alias\".")
val publicKey: PublicKey = certificate.publicKey
return Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
} catch (e: Exception) {
throw Exception("Failed to retrieve public key: ${e.message}", e)
}
}
}

View File

@ -0,0 +1,37 @@
package com.icing.dialer
import android.security.keystore.KeyProperties
import java.security.KeyStore
import java.security.Signature
import android.util.Base64
import java.security.PrivateKey
object SignerHelper {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
/**
* Signs the provided data using the private key associated with the given alias.
*
* @param alias The alias of the key pair.
* @param data The data to sign.
* @return The signature as a Base64-encoded string.
* @throws Exception if signing fails.
*/
fun signData(alias: String, data: ByteArray): String {
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
val privateKey = keyStore.getKey(alias, null) as? PrivateKey?: throw Exception("Private key not found for alias \"$alias\".")
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
signature.update(data)
val signedBytes = signature.sign()
return Base64.encodeToString(signedBytes, Base64.DEFAULT)
} catch (e: Exception) {
throw Exception("Failed to sign data: ${e.message}", e)
}
}
}

View File

@ -1,14 +0,0 @@
class AppConfig {
// Private constructor to prevent instantiation
AppConfig._();
// Global configuration
static bool isStealthMode = false;
// App initialization
static Future<void> initialize() async {
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
isStealthMode = stealthFlag.toLowerCase() == 'true';
print('Stealth mode is ${isStealthMode ? 'enabled' : 'disabled'}');
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import '../../presentation/features/call/call_page.dart';
import '../../presentation/features/call/incoming_call_page.dart';
import '../../presentation/features/home/home_page.dart';
import '../../presentation/features/settings/settings.dart'; // Updated import
import '../../presentation/features/contacts/contact_page.dart';
import '../../presentation/features/composition/composition.dart';
import 'dart:typed_data';
class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const MyHomePage());
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsPage()); // Now correctly imported
case '/composition':
return MaterialPageRoute(builder: (_) => const CompositionPage());
case '/contacts':
return MaterialPageRoute(builder: (_) => const ContactPage());
case '/call':
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
settings: settings,
builder: (_) => CallPage(
displayName: args['displayName'] as String,
phoneNumber: args['phoneNumber'] as String,
thumbnail: args['thumbnail'] as Uint8List?,
),
);
case '/incoming_call':
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
settings: settings,
builder: (_) => IncomingCallPage(
displayName: args['displayName'] as String,
phoneNumber: args['phoneNumber'] as String,
thumbnail: args['thumbnail'] as Uint8List?,
),
);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
}

View File

@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing blocked phone numbers
class BlockService {
static const String _blockedNumbersKey = 'blocked_numbers';
// Private constructor
BlockService._privateConstructor();
// Singleton instance
static final BlockService _instance = BlockService._privateConstructor();
// Factory constructor to return the same instance
factory BlockService() {
return _instance;
}
/// Block a phone number
Future<bool> blockNumber(String phoneNumber) async {
try {
final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
// Don't add if already blocked
if (blockedNumbers.contains(phoneNumber)) {
return true;
}
blockedNumbers.add(phoneNumber);
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} catch (e) {
debugPrint('Error blocking number: $e');
return false;
}
}
/// Unblock a phone number
Future<bool> unblockNumber(String phoneNumber) async {
try {
final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
if (!blockedNumbers.contains(phoneNumber)) {
return true;
}
blockedNumbers.remove(phoneNumber);
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} catch (e) {
debugPrint('Error unblocking number: $e');
return false;
}
}
/// Check if a number is blocked
Future<bool> isNumberBlocked(String phoneNumber) async {
try {
final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
return blockedNumbers.contains(phoneNumber);
} catch (e) {
debugPrint('Error checking if number is blocked: $e');
return false;
}
}
/// Get all blocked numbers
Future<List<String>> getBlockedNumbers() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList(_blockedNumbersKey) ?? [];
} catch (e) {
debugPrint('Error getting blocked numbers: $e');
return [];
}
}
}

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:qr_flutter/qr_flutter.dart';
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
List<Contact> contacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
return contacts;
} else {
// Permission denied
return [];
}
}
Future<List<Contact>> fetchFavoriteContacts() async {
if (await FlutterContacts.requestPermission()) {
// Get all contacts and filter for favorites
List<Contact> allContacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
return allContacts.where((c) => c.isStarred).toList();
} else {
// Permission denied
return [];
}
}
Future<Contact?> addNewContact(Contact contact) async {
if (await FlutterContacts.requestPermission()) {
try {
return await FlutterContacts.insertContact(contact);
} catch (e) {
debugPrint('Error adding contact: $e');
return null;
}
}
return null;
}
void showContactQRCodeDialog(BuildContext context, Contact contact) {
final String vCard = contact.toVCard();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.grey[900],
title: Text(
'QR Code for ${contact.displayName}',
style: const TextStyle(color: Colors.white),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.all(16.0),
child: QrImageView(
data: vCard,
version: QrVersions.auto,
size: 200.0,
),
),
const SizedBox(height: 16.0),
const Text(
'Scan this code to add this contact',
style: TextStyle(color: Colors.white70),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
},
);
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class QRCodeScannerScreen extends StatefulWidget {
const QRCodeScannerScreen({super.key});
@override
_QRCodeScannerScreenState createState() => _QRCodeScannerScreenState();
}
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
MobileScannerController cameraController = MobileScannerController();
bool _flashEnabled = false;
@override
void dispose() {
cameraController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scan QR Code'),
actions: [
IconButton(
icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off),
onPressed: () {
setState(() {
_flashEnabled = !_flashEnabled;
cameraController.toggleTorch();
});
},
),
IconButton(
icon: const Icon(Icons.flip_camera_ios),
onPressed: () => cameraController.switchCamera(),
),
],
),
body: MobileScanner(
controller: cameraController,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
// Return the first barcode value
final String? code = barcodes.first.rawValue;
if (code != null) {
Navigator.pop(context, code);
}
}
},
),
);
}
}

View File

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/domain/services/call_service.dart'; import 'package:dialer/services/call_service.dart';
import 'package:dialer/domain/services/obfuscate_service.dart'; import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class CallPage extends StatefulWidget { class CallPage extends StatefulWidget {

View File

@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dialer/services/call_service.dart';
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:dialer/features/call/call_page.dart';
class IncomingCallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const IncomingCallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
@override
_IncomingCallPageState createState() => _IncomingCallPageState();
}
class _IncomingCallPageState extends State<IncomingCallPage> {
static const MethodChannel _channel = MethodChannel('call_service');
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
bool icingProtocolOk = true;
void _toggleIcingProtocol() {
setState(() {
icingProtocolOk = !icingProtocolOk;
});
}
void _answerCall() async {
try {
final result = await _channel.invokeMethod('answerCall');
print('IncomingCallPage: Answer call result: $result');
if (result["status"] == "answered") {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: widget.displayName,
phoneNumber: widget.phoneNumber,
thumbnail: widget.thumbnail,
),
),
);
}
} catch (e) {
print("IncomingCallPage: Error answering call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error answering call: $e")),
);
}
}
void _declineCall() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
print("IncomingCallPage: Error declining call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error declining call: $e")),
);
}
}
@override
Widget build(BuildContext context) {
const double avatarRadius = 45.0;
const double nameFontSize = 24.0;
const double statusFontSize = 16.0;
return Scaffold(
body: Container(
color: Colors.black,
child: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 35),
ObfuscatedAvatar(
imageBytes: widget.thumbnail,
radius: avatarRadius,
backgroundColor: generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName,
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icingProtocolOk ? Icons.lock : Icons.lock_open,
color: icingProtocolOk ? Colors.green : Colors.red,
size: 16,
),
const SizedBox(width: 4),
Text(
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
style: TextStyle(
color: icingProtocolOk ? Colors.green : Colors.red,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Text(
_obfuscateService.obfuscateData(widget.displayName),
style: const TextStyle(
fontSize: nameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: const TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
const Text(
'Incoming Call...',
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
],
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: _declineCall,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call_end,
color: Colors.white,
size: 32,
),
),
),
GestureDetector(
onTap: _answerCall,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call,
color: Colors.white,
size: 32,
),
),
),
],
),
),
const SizedBox(height: 16),
],
),
),
),
);
}
}

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../domain/services/contact_service.dart'; import '../../services/contact_service.dart';
import '../../../domain/services/obfuscate_service.dart'; import '../../services/obfuscate_service.dart';
import '../../../domain/services/call_service.dart'; import '../../services/call_service.dart';
class CompositionPage extends StatefulWidget { class CompositionPage extends StatefulWidget {
const CompositionPage({super.key}); const CompositionPage({super.key});

View File

@ -0,0 +1,26 @@
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart';
import 'package:flutter/material.dart';
import 'package:dialer/widgets/loading_indicator.dart';
class ContactPage extends StatefulWidget {
const ContactPage({super.key});
@override
_ContactPageState createState() => _ContactPageState();
}
class _ContactPageState extends State<ContactPage> {
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold(
body: contactState.loading
? const LoadingIndicatorWidget()
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts: contactState.contacts, // Use all contacts here
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../domain/services/contact_service.dart'; import '../../services/contact_service.dart';
class ContactState extends StatefulWidget { class ContactState extends StatefulWidget {
final Widget child; final Widget child;
@ -23,7 +23,7 @@ class _ContactStateState extends State<ContactState> {
List<Contact> _favoriteContacts = []; List<Contact> _favoriteContacts = [];
bool _loading = true; bool _loading = true;
double _scrollOffset = 0.0; double _scrollOffset = 0.0;
Contact? _selfContact; Contact? _selfContact = Contact();
// Getters for all contacts and favorites // Getters for all contacts and favorites
List<Contact> get contacts => _allContacts; List<Contact> get contacts => _allContacts;
@ -35,23 +35,11 @@ class _ContactStateState extends State<ContactState> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeContacts(); // Rename to make it clear this is initialization fetchContacts(); // Fetch all contacts by default
FlutterContacts.addListener(_onContactChange); FlutterContacts.addListener(_onContactChange);
} }
// Private method to initialize contacts without setState during build void _onContactChange() => fetchContacts();
Future<void> _initializeContacts() async {
try {
List<Contact> contacts = await _contactService.fetchContacts();
_processContactsInitial(contacts);
} catch (e) {
debugPrint('Error fetching contacts: $e');
}
}
void _onContactChange() async {
await fetchContacts();
}
@override @override
void dispose() { void dispose() {
@ -59,92 +47,47 @@ class _ContactStateState extends State<ContactState> {
super.dispose(); super.dispose();
} }
// Fetch all contacts - public method that can be called after build // Fetch all contacts
Future<void> fetchContacts() async { Future<void> fetchContacts() async {
if (!mounted) return;
setState(() => _loading = true); setState(() => _loading = true);
try { try {
List<Contact> contacts = await _contactService.fetchContacts(); List<Contact> contacts = await _contactService.fetchContacts();
if (mounted) {
_processContacts(contacts); _processContacts(contacts);
}
} catch (e) {
debugPrint('Error fetching contacts: $e');
} finally { } finally {
if (mounted) {
setState(() => _loading = false); setState(() => _loading = false);
} }
} }
}
// Fetch only favorite contacts // Fetch only favorite contacts
Future<void> fetchFavoriteContacts() async { Future<void> fetchFavoriteContacts() async {
if (!mounted) return;
setState(() => _loading = true); setState(() => _loading = true);
try { try {
List<Contact> contacts = await _contactService.fetchFavoriteContacts(); List<Contact> contacts = await _contactService.fetchFavoriteContacts();
if (mounted) {
setState(() => _favoriteContacts = contacts); setState(() => _favoriteContacts = contacts);
}
} catch (e) {
debugPrint('Error fetching favorite contacts: $e');
} finally { } finally {
if (mounted) {
setState(() => _loading = false); setState(() => _loading = false);
} }
} }
}
// Process contacts without setState for initial loading
void _processContactsInitial(List<Contact> contacts) {
if (!mounted) return;
// Optimize by doing a single pass through contacts instead of multiple iterations
final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList()
..sort((a, b) => a.displayName.compareTo(b.displayName));
_selfContact = contacts.firstWhere(
(contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(),
);
if (_selfContact!.phones.isEmpty) {
_selfContact = null;
}
_allContacts = filteredContacts;
_favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList();
_loading = false;
// Force a rebuild after initialization is complete
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
}
}
void _processContacts(List<Contact> contacts) { void _processContacts(List<Contact> contacts) {
if (!mounted) return;
// Same optimization as above
final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList()
..sort((a, b) => a.displayName.compareTo(b.displayName));
_selfContact = contacts.firstWhere( _selfContact = contacts.firstWhere(
(contact) => contact.displayName.toLowerCase() == "user", (contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(), orElse: () => Contact(),
); );
if (_selfContact!.phones.isEmpty) { if (_selfContact!.phones.isEmpty) {
debugPrint("Self contact has no phone numbers");
_selfContact = null; _selfContact = null;
} }
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
setState(() { setState(() {
_allContacts = filteredContacts; _allContacts = contacts;
_favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList(); _favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
_selfContact = _selfContact;
}); });
} }
@ -159,6 +102,25 @@ class _ContactStateState extends State<ContactState> {
}); });
} }
bool doesContactExist(Contact contact) {
// Example: consider it "existing" if there's a matching phone number
for (final existing in _allContacts) {
if (existing.toVCard() == contact.toVCard()) {
return true;
}
// for (final phone in existing.phones) {
// for (final newPhone in contact.phones) {
// // Simple exact match; you can do more advanced logic
// if (phone.normalizedNumber == newPhone.normalizedNumber) {
// return true;
// }
// }
// } We might switch to finer and smarter logic later, ex: remove trailing spaces, capitals
}
return false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _InheritedContactState( return _InheritedContactState(
@ -168,6 +130,7 @@ class _ContactStateState extends State<ContactState> {
} }
} }
class _InheritedContactState extends InheritedWidget { class _InheritedContactState extends InheritedWidget {
final _ContactStateState data; final _ContactStateState data;

View File

@ -1,6 +1,6 @@
import 'package:dialer/widgets/qr_scanner.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../../domain/services/qr/qr_scanner.dart';
class AddContactButton extends StatelessWidget { class AddContactButton extends StatelessWidget {
const AddContactButton({super.key}); const AddContactButton({super.key});

View File

@ -0,0 +1,217 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../contact_state.dart';
import 'add_contact_button.dart';
import 'contact_modal.dart';
import 'share_own_qr.dart';
class AlphabetScrollPage extends StatefulWidget {
final double scrollOffset;
final List<Contact> contacts;
const AlphabetScrollPage({
super.key,
required this.scrollOffset,
required this.contacts,
});
@override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
}
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController;
final ObfuscateService _obfuscateService = ObfuscateService();
@override
void initState() {
super.initState();
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController.addListener(_onScroll);
}
void _onScroll() {
final contactState = ContactState.of(context);
contactState.setScrollOffset(_scrollController.offset);
}
Future<void> _refreshContacts() async {
final contactState = ContactState.of(context);
try {
await contactState.fetchContacts();
} catch (e) {
print('Error refreshing contacts: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to refresh contacts')),
);
}
}
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
}
await _refreshContacts();
} else {
print("Could not fetch contact details");
}
} catch (e) {
print("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
@override
Widget build(BuildContext context) {
final contacts = widget.contacts;
final selfContact = ContactState.of(context).selfContact;
Map<String, List<Contact>> alphabetizedContacts = {};
for (var contact in contacts) {
String firstLetter = contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '#';
if (!alphabetizedContacts.containsKey(firstLetter)) {
alphabetizedContacts[firstLetter] = [];
}
alphabetizedContacts[firstLetter]!.add(contact);
}
List<String> alphabetKeys = alphabetizedContacts.keys.toList()..sort();
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// Top buttons row
Container(
color: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AddContactButton(),
QRCodeButton(contacts: contacts, selfContact: selfContact),
],
),
),
// Contact List
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: alphabetKeys.length,
itemBuilder: (context, index) {
String letter = alphabetKeys[index];
List<Contact> contactsForLetter = alphabetizedContacts[letter]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Alphabet Letter Header
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16.0),
child: Text(
letter,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
// Contact Entries
...contactsForLetter.map((contact) {
String phoneNumber = contact.phones.isNotEmpty
? _obfuscateService.obfuscateData(contact.phones.first.number)
: 'No phone number';
Color avatarColor =
generateColorFromName(contact.displayName);
return ListTile(
leading: ObfuscatedAvatar(
imageBytes: contact.thumbnail,
radius: 25,
backgroundColor: avatarColor,
fallbackInitial: contact.displayName,
),
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
phoneNumber,
style: const TextStyle(color: Colors.white70),
),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return ContactModal(
contact: contact,
onEdit: () async {
if (await FlutterContacts.requestPermission()) {
final updatedContact =
await FlutterContacts.openExternalEdit(
contact.id);
if (updatedContact != null) {
await _refreshContacts();
Navigator.of(context).pop();
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'${contact.displayName} updated successfully!'),
),
);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content:
Text('Edit canceled or failed.'),
),
);
}
}
},
onToggleFavorite: () {
_toggleFavorite(contact);
},
isFavorite: contact.isStarred,
);
},
);
},
);
}),
],
);
},
),
),
],
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}

View File

@ -1,12 +1,11 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../common/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import '../../../common/widgets/color_darkener.dart'; import '../../../services/block_service.dart';
import '../../../../domain/services/obfuscate_service.dart'; import '../../../services/contact_service.dart';
import '../../../../domain/services/block_service.dart'; import '../../../services/call_service.dart';
import '../../../../domain/services/contact_service.dart';
import '../../../../domain/services/call_service.dart';
class ContactModal extends StatefulWidget { class ContactModal extends StatefulWidget {
final Contact contact; final Contact contact;
@ -31,7 +30,6 @@ class _ContactModalState extends State<ContactModal> {
bool isBlocked = false; bool isBlocked = false;
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService(); final CallService _callService = CallService();
final ContactService _contactService = ContactService();
@override @override
void initState() { void initState() {
@ -45,45 +43,42 @@ class _ContactModalState extends State<ContactModal> {
Future<void> _checkIfBlocked() async { Future<void> _checkIfBlocked() async {
if (phoneNumber != 'No phone number') { if (phoneNumber != 'No phone number') {
bool blocked = await BlockService().isNumberBlocked(phoneNumber); bool blocked = await BlockService().isNumberBlocked(phoneNumber);
if (mounted) {
setState(() { setState(() {
isBlocked = blocked; isBlocked = blocked;
}); });
} }
} }
}
Future<void> _toggleBlockState() async { Future<void> _toggleBlockState() async {
if (phoneNumber == 'No phone number') { if (phoneNumber == 'No phone number') {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No phone number to block or unblock')), const SnackBar(content: Text('No phone number to block or unblock')),
); );
return; } else if (isBlocked) {
}
if (isBlocked) {
await BlockService().unblockNumber(phoneNumber); await BlockService().unblockNumber(phoneNumber);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been unblocked')), SnackBar(content: Text('$phoneNumber has been unblocked')),
); );
}
} else { } else {
await BlockService().blockNumber(phoneNumber); await BlockService().blockNumber(phoneNumber);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been blocked')), SnackBar(content: Text('$phoneNumber has been blocked')),
); );
} }
}
if (phoneNumber != 'No phone number' && mounted) { if (phoneNumber != 'No phone number') {
_checkIfBlocked(); _checkIfBlocked();
} }
if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
void _launchPhoneDialer(String phoneNumber) async {
final uri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
debugPrint('Could not launch $phoneNumber');
}
} }
void _launchSms(String phoneNumber) async { void _launchSms(String phoneNumber) async {
@ -124,13 +119,12 @@ class _ContactModalState extends State<ContactModal> {
), ),
); );
if (shouldDelete && mounted) { if (shouldDelete) {
try { try {
// Delete the contact // Delete the contact
await FlutterContacts.deleteContact(widget.contact); await FlutterContacts.deleteContact(widget.contact);
// Show success message // Show success message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
@ -139,10 +133,8 @@ class _ContactModalState extends State<ContactModal> {
// Close the modal // Close the modal
Navigator.of(context).pop(); Navigator.of(context).pop();
}
} catch (e) { } catch (e) {
// Handle errors and show a failure message // Handle errors and show a failure message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content:
@ -151,11 +143,10 @@ class _ContactModalState extends State<ContactModal> {
} }
} }
} }
}
void _shareContactAsQRCode() { void _shareContactAsQRCode() {
// Use the ContactService to show the QR code for the contact's vCard // Use the ContactService to show the QR code for the contact's vCard
_contactService.showContactQRCodeDialog(context, widget.contact); ContactService().showContactQRCodeDialog(context, widget.contact);
} }
@override @override
@ -164,8 +155,6 @@ class _ContactModalState extends State<ContactModal> {
? _obfuscateService.obfuscateData(widget.contact.emails.first.address) ? _obfuscateService.obfuscateData(widget.contact.emails.first.address)
: 'No email'; : 'No email';
final avatarColor = generateColorFromName(widget.contact.displayName);
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).pop(), onTap: () => Navigator.of(context).pop(),
child: Container( child: Container(
@ -173,7 +162,6 @@ class _ContactModalState extends State<ContactModal> {
child: GestureDetector( child: GestureDetector(
onTap: () {}, onTap: () {},
child: FractionallySizedBox( child: FractionallySizedBox(
heightFactor: 0.8,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[900], color: Colors.grey[900],
@ -217,6 +205,10 @@ class _ContactModalState extends State<ContactModal> {
}, },
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return [ return [
const PopupMenuItem<String>(
value: 'show_associated_contacts',
child: Text('Show associated contacts'),
),
const PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'delete', value: 'delete',
child: Text('Delete'), child: Text('Delete'),
@ -225,6 +217,15 @@ class _ContactModalState extends State<ContactModal> {
value: 'share', value: 'share',
child: Text('Share (via QR code)'), child: Text('Share (via QR code)'),
), ),
const PopupMenuItem<String>(
value: 'create_shortcut',
child:
Text('Create shortcut (to home screen)'),
),
const PopupMenuItem<String>(
value: 'set_ringtone',
child: Text('Set ringtone'),
),
]; ];
}, },
), ),
@ -237,27 +238,12 @@ class _ContactModalState extends State<ContactModal> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
widget.contact.thumbnail != null && widget.contact.thumbnail!.isNotEmpty ObfuscatedAvatar(
? ClipOval( imageBytes: widget.contact.thumbnail,
child: Image.memory(
widget.contact.thumbnail!,
fit: BoxFit.cover,
width: 100,
height: 100,
),
)
: CircleAvatar(
backgroundColor: avatarColor,
radius: 50, radius: 50,
child: Text( backgroundColor:
widget.contact.displayName.isNotEmpty generateColorFromName(widget.contact.displayName),
? widget.contact.displayName[0].toUpperCase() fallbackInitial: widget.contact.displayName,
: '?',
style: TextStyle(
color: darken(avatarColor),
fontSize: 40,
),
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
@ -273,10 +259,6 @@ class _ContactModalState extends State<ContactModal> {
), ),
const Divider(color: Colors.grey), const Divider(color: Colors.grey),
// Contact Actions // Contact Actions
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
ListTile( ListTile(
leading: const Icon(Icons.phone, color: Colors.green), leading: const Icon(Icons.phone, color: Colors.green),
title: Text( title: Text(
@ -285,8 +267,12 @@ class _ContactModalState extends State<ContactModal> {
), ),
onTap: () async { onTap: () async {
if (widget.contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context, await _callService.makeGsmCall(
phoneNumber: phoneNumber); context,
phoneNumber: phoneNumber,
displayName: widget.contact.displayName,
thumbnail: widget.contact.thumbnail,
);
} }
}, },
), ),
@ -325,9 +311,7 @@ class _ContactModalState extends State<ContactModal> {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
// First close the modal to avoid unmounted widget issues
Navigator.of(context).pop(); Navigator.of(context).pop();
// Then toggle the favorite status
widget.onToggleFavorite(); widget.onToggleFavorite();
}, },
icon: Icon(widget.isFavorite icon: Icon(widget.isFavorite
@ -366,10 +350,6 @@ class _ContactModalState extends State<ContactModal> {
), ),
), ),
), ),
],
),
),
),
), ),
), ),
); );

View File

@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/contact.dart'; import 'package:flutter_contacts/contact.dart';
import 'package:dialer/domain/services/contact_service.dart'; import 'package:dialer/services/contact_service.dart';
class QRCodeButton extends StatelessWidget { class QRCodeButton extends StatelessWidget {
final List<Contact> contacts; final List<Contact> contacts;

View File

@ -0,0 +1,32 @@
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart';
import 'package:flutter/material.dart';
import 'package:dialer/widgets/loading_indicator.dart';
class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
@override
_FavoritesPageState createState() => _FavoritesPageState();
}
class _FavoritesPageState extends State<FavoritesPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold(
body: contactState.loading
? const LoadingIndicatorWidget()
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts:
contactState.favoriteContacts, // Use only favorites here
),
);
}
}

View File

@ -1,17 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/color_darkener.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../domain/services/obfuscate_service.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import '../../common/widgets/color_darkener.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import '../../common/widgets/username_color_generator.dart'; import '../../services/block_service.dart';
import '../../../domain/services/block_service.dart';
import '../../../domain/services/call_service.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/contact_modal.dart'; import '../contacts/widgets/contact_modal.dart';
import '../../services/call_service.dart';
class History { class History {
final Contact contact; final Contact contact;
@ -82,21 +82,16 @@ class _HistoryPageState extends State<HistoryPage>
fullContact.isStarred = !fullContact.isStarred; fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact); await FlutterContacts.updateContact(fullContact);
} }
// Check if still mounted before accessing context
if (mounted) {
await _refreshContacts(); await _refreshContacts();
}
} else { } else {
debugPrint("Could not fetch contact details"); print("Could not fetch contact details");
} }
} catch (e) { } catch (e) {
debugPrint("Error updating favorite status: $e"); print("Error updating favorite status: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status'))); SnackBar(content: Text('Failed to update favorite status')));
} }
} }
}
/// Helper: Remove all non-digit characters for simple matching. /// Helper: Remove all non-digit characters for simple matching.
String sanitizeNumber(String number) { String sanitizeNumber(String number) {

View File

@ -1,14 +1,14 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../domain/services/obfuscate_service.dart'; import 'package:dialer/features/contacts/contact_page.dart';
import '../contacts/contact_page.dart'; import 'package:dialer/features/favorites/favorites_page.dart';
import '../favorites/favorites_page.dart'; import 'package:dialer/features/history/history_page.dart';
import '../history/history_page.dart'; import 'package:dialer/features/composition/composition.dart';
import '../composition/composition.dart';
import '../settings/settings.dart';
import '../voicemail/voicemail_page.dart';
import '../contacts/widgets/contact_modal.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/domain/services/contact_service.dart'; import 'package:dialer/features/settings/settings.dart';
import '../../services/contact_service.dart';
import 'package:dialer/features/voicemail/voicemail_page.dart';
import '../contacts/widgets/contact_modal.dart';
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
late TabController _tabController; late TabController _tabController;

View File

@ -77,7 +77,7 @@ class _SettingsCallPageState extends State<SettingsCallPage> {
}, },
child: const Text('Beep'), child: const Text('Beep'),
), ),
// ...existing options... // Add more ringtone options
], ],
); );
}, },

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart'; import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
class ManageKeysPage extends StatefulWidget { class ManageKeysPage extends StatefulWidget {
const ManageKeysPage({Key? key}) : super(key: key); const ManageKeysPage({Key? key}) : super(key: key);

View File

@ -1,7 +1,10 @@
// settings.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/presentation/features/settings/call/settings_call.dart'; import 'package:dialer/features/settings/call/settingsCall.dart';
import 'package:dialer/presentation/features/settings/cryptography/key_management.dart'; // import 'package:dialer/features/settings/cryptography/';
import 'package:dialer/presentation/features/settings/blocked/settings_blocked.dart'; import 'package:dialer/features/settings/blocked/settings_blocked.dart';
import 'cryptography/key_management.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
class VoicemailPage extends StatefulWidget { class VoicemailPage extends StatefulWidget {
const VoicemailPage({super.key}); const VoicemailPage({Key? key}) : super(key: key);
@override @override
State<VoicemailPage> createState() => _VoicemailPageState(); State<VoicemailPage> createState() => _VoicemailPageState();
@ -14,7 +14,6 @@ class _VoicemailPageState extends State<VoicemailPage> {
Duration _duration = Duration.zero; Duration _duration = Duration.zero;
Duration _position = Duration.zero; Duration _position = Duration.zero;
late AudioPlayer _audioPlayer; late AudioPlayer _audioPlayer;
bool _loading = false;
@override @override
void initState() { void initState() {
@ -51,12 +50,12 @@ class _VoicemailPageState extends State<VoicemailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
// appBar: AppBar(
// // title: const Text('Voicemail'),
// backgroundColor: Colors.black,
// ),
body: ListView( body: ListView(
children: [ children: [
GestureDetector( GestureDetector(

View File

@ -1,7 +1 @@
// Global variables accessible throughout the app bool isStealthMode = false;
library globals;
import 'core/config/app_config.dart';
// Whether the app is in stealth mode (obfuscated content)
bool get isStealthMode => AppConfig.isStealthMode;

View File

@ -1,34 +1,25 @@
import 'package:dialer/presentation/features/home/home_page.dart'; import 'package:dialer/features/home/home_page.dart';
import 'package:dialer/presentation/features/home/default_dialer_prompt.dart'; import 'package:dialer/features/home/default_dialer_prompt.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/services/call_service.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'globals.dart' as globals;
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'core/config/app_config.dart';
import 'domain/services/call_service.dart';
import 'domain/services/cryptography/asymmetric_crypto_service.dart';
import 'presentation/features/contacts/contact_state.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
globals.isStealthMode = stealthFlag.toLowerCase() == 'true';
// Initialize app configuration
await AppConfig.initialize();
// Initialize cryptography service with error handling
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
try {
await cryptoService.initializeDefaultKeyPair(); await cryptoService.initializeDefaultKeyPair();
} catch (e) {
debugPrint('Error initializing cryptography: $e');
// Continue app initialization even if crypto fails
}
// Request permissions before running the app // Request permissions before running the app
await _requestPermissions(); await _requestPermissions();
// Initialize call service
CallService(); CallService();
runApp( runApp(
@ -38,7 +29,7 @@ void main() async {
create: (_) => cryptoService, create: (_) => cryptoService,
), ),
], ],
child: const DialerApp(), child: Dialer(),
), ),
); );
} }
@ -58,8 +49,8 @@ Future<void> _requestPermissions() async {
} }
} }
class DialerApp extends StatelessWidget { class Dialer extends StatelessWidget {
const DialerApp({super.key}); const Dialer({super.key});
Future<bool> _isDefaultDialer() async { Future<bool> _isDefaultDialer() async {
const channel = MethodChannel('call_service'); const channel = MethodChannel('call_service');
@ -76,7 +67,6 @@ class DialerApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContactState( return ContactState(
child: MaterialApp( child: MaterialApp(
title: 'Dialer App',
navigatorKey: CallService.navigatorKey, navigatorKey: CallService.navigatorKey,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData get darkTheme => ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.black,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
elevation: 0,
),
tabBarTheme: const TabBarThemeData(
labelColor: Colors.white,
unselectedLabelColor: Color.fromARGB(255, 158, 158, 158),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Colors.white,
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: Colors.black,
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
titleLarge: TextStyle(color: Colors.white),
),
snackBarTheme: const SnackBarThemeData(
backgroundColor: Color(0xFF303030),
contentTextStyle: TextStyle(color: Colors.white),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
filled: true,
fillColor: const Color.fromARGB(255, 30, 30, 30),
),
);
}

View File

@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
/// Darkens a color by a given percentage
Color darken(Color color, [double amount = 0.3]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(color);
final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return darkened.toColor();
}
/// Lightens a color by a given percentage
Color lighten(Color color, [double amount = 0.3]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(color);
final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
return lightened.toColor();
}

View File

@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
/// Generates a deterministic color from a string input
Color generateColorFromName(String name) {
if (name.isEmpty) return Colors.grey;
// Use the hashCode of the name to generate a consistent color
int hash = name.hashCode;
// Use the hash to generate RGB values
final r = (hash & 0xFF0000) >> 16;
final g = (hash & 0x00FF00) >> 8;
final b = hash & 0x0000FF;
// Create a color with these RGB values
return Color.fromARGB(255, r, g, b);
}

View File

@ -1,163 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:typed_data';
import '../../../domain/services/call_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import 'package:dialer/presentation/common/widgets/username_color_generator.dart';
import 'call_page.dart';
class IncomingCallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const IncomingCallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
@override
_IncomingCallPageState createState() => _IncomingCallPageState();
}
class _IncomingCallPageState extends State<IncomingCallPage> {
static const MethodChannel _channel = MethodChannel('call_service');
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
void _answerCall() async {
try {
final result = await _channel.invokeMethod('answerCall');
debugPrint('IncomingCallPage: Answer call result: $result');
if (result["status"] == "answered") {
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: widget.displayName,
phoneNumber: widget.phoneNumber,
thumbnail: widget.thumbnail,
),
),
);
}
} catch (e) {
debugPrint("IncomingCallPage: Error answering call: $e");
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error answering call: $e")),
);
}
}
void _declineCall() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
debugPrint("IncomingCallPage: Error declining call: $e");
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error declining call: $e")),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Column(
children: [
const Spacer(),
ObfuscatedAvatar(
imageBytes: widget.thumbnail,
radius: 60,
backgroundColor: generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName,
),
const SizedBox(height: 24),
Text(
_obfuscateService.obfuscateData(widget.displayName),
style: const TextStyle(
fontSize: 28,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: const TextStyle(fontSize: 18, color: Colors.white70),
),
const SizedBox(height: 16),
const Text(
'Incoming Call',
style: TextStyle(fontSize: 20, color: Colors.white70),
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(bottom: 48.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionButton(
icon: Icons.call_end,
color: Colors.red,
onPressed: _declineCall,
label: 'Decline',
),
_buildActionButton(
icon: Icons.call,
color: Colors.green,
onPressed: _answerCall,
label: 'Answer',
),
],
),
),
],
),
),
);
}
Widget _buildActionButton({
required IconData icon,
required Color color,
required VoidCallback onPressed,
required String label,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 32,
),
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(color: Colors.white),
),
],
);
}
}

View File

@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/alphabet_scroll_page.dart';
class ContactPage extends StatelessWidget {
const ContactPage({super.key});
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold(
body: contactState.loading
? const Center(child: CircularProgressIndicator())
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts: contactState.contacts,
),
);
}
}

View File

@ -1,232 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../../domain/services/obfuscate_service.dart';
import 'package:dialer/presentation/common/widgets/username_color_generator.dart';
import '../contact_state.dart';
import 'add_contact_button.dart';
import 'contact_modal.dart';
import 'share_own_qr.dart';
class AlphabetScrollPage extends StatefulWidget {
final double scrollOffset;
final List<Contact> contacts;
const AlphabetScrollPage({
super.key,
required this.scrollOffset,
required this.contacts,
});
@override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
}
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController;
final ObfuscateService _obfuscateService = ObfuscateService();
late Map<String, List<Contact>> _alphabetizedContacts;
late List<String> _alphabetKeys;
@override
void initState() {
super.initState();
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController.addListener(_onScroll);
_organizeContacts();
}
@override
void didUpdateWidget(AlphabetScrollPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.contacts != widget.contacts) {
_organizeContacts();
}
}
void _organizeContacts() {
_alphabetizedContacts = {};
for (var contact in widget.contacts) {
String firstLetter = contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '#';
(_alphabetizedContacts[firstLetter] ??= []).add(contact);
}
_alphabetKeys = _alphabetizedContacts.keys.toList()..sort();
}
void _onScroll() {
final contactState = ContactState.of(context);
contactState.setScrollOffset(_scrollController.offset);
}
Future<void> _refreshContacts() async {
final contactState = ContactState.of(context);
try {
await contactState.fetchContacts();
} catch (e) {
debugPrint('Error refreshing contacts: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to refresh contacts')),
);
}
}
Future<void> _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
}
// Check if widget is still mounted before calling functions that use context
if (mounted) {
await _refreshContacts();
}
} else {
debugPrint("Could not fetch contact details");
}
} catch (e) {
debugPrint("Error updating favorite status: $e");
// Only show snackbar if still mounted
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
}
@override
Widget build(BuildContext context) {
final selfContact = ContactState.of(context).selfContact;
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// Top buttons row
Container(
color: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AddContactButton(),
QRCodeButton(contacts: widget.contacts, selfContact: selfContact),
],
),
),
// Contact List
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _alphabetKeys.length,
itemBuilder: (context, index) {
String letter = _alphabetKeys[index];
List<Contact> contactsForLetter = _alphabetizedContacts[letter]!;
return _buildLetterSection(letter, contactsForLetter);
},
),
),
],
),
);
}
Widget _buildLetterSection(String letter, List<Contact> contactsForLetter) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Alphabet Letter Header
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Text(
letter,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
// Contact Entries
...contactsForLetter.map((contact) => _buildContactTile(contact)),
],
);
}
Widget _buildContactTile(Contact contact) {
String phoneNumber = contact.phones.isNotEmpty
? _obfuscateService.obfuscateData(contact.phones.first.number)
: 'No phone number';
Color avatarColor = generateColorFromName(contact.displayName);
return ListTile(
leading: ObfuscatedAvatar(
imageBytes: contact.thumbnail,
radius: 25,
backgroundColor: avatarColor,
fallbackInitial: contact.displayName,
),
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
phoneNumber,
style: const TextStyle(color: Colors.white70),
),
onTap: () => _showContactModal(contact),
);
}
void _showContactModal(Contact contact) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return ContactModal(
contact: contact,
onEdit: () => _onEditContact(contact),
onToggleFavorite: () => _toggleFavorite(contact),
isFavorite: contact.isStarred,
);
},
);
}
Future<void> _onEditContact(Contact contact) async {
if (await FlutterContacts.requestPermission()) {
final updatedContact = await FlutterContacts.openExternalEdit(contact.id);
if (updatedContact != null) {
await _refreshContacts();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${contact.displayName} updated successfully!'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Edit canceled or failed.'),
),
);
}
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}

View File

@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/alphabet_scroll_page.dart';
class FavoritesPage extends StatelessWidget {
const FavoritesPage({super.key});
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
if (contactState.loading) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(child: CircularProgressIndicator()),
);
}
final favorites = contactState.favoriteContacts;
return Scaffold(
backgroundColor: Colors.black,
body: favorites.isEmpty
? const Center(
child: Text(
'No favorites yet.\nStar your contacts to add them here.',
style: TextStyle(color: Colors.white60),
textAlign: TextAlign.center,
),
)
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts: favorites,
),
);
}
}

View File

@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
/// Calling settings configuration page
class SettingsCallPage extends StatelessWidget {
const SettingsCallPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Call Settings'),
),
body: ListView(
children: const [
ListTile(
title: Text('Call Forwarding', style: TextStyle(color: Colors.white)),
subtitle: Text('Manage call forwarding options', style: TextStyle(color: Colors.grey)),
),
ListTile(
title: Text('Call Waiting', style: TextStyle(color: Colors.white)),
subtitle: Text('Enable or disable call waiting', style: TextStyle(color: Colors.grey)),
),
ListTile(
title: Text('Caller ID', style: TextStyle(color: Colors.white)),
subtitle: Text('Manage your caller ID settings', style: TextStyle(color: Colors.grey)),
),
],
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:shared_preferences/shared_preferences.dart';
class BlockService {
static final BlockService _instance = BlockService._internal();
factory BlockService() {
return _instance;
}
BlockService._internal();
// Function to add a number to the blocked list
Future<void> blockNumber(String number) async {
if (number.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
if (!blockedNumbers.contains(number)) {
blockedNumbers.add(number);
await prefs.setStringList('blockedNumbers', blockedNumbers);
print('$number has been blocked');
} else {
print('$number is already blocked');
}
}
// Function to remove a number from the blocked list
Future<void> unblockNumber(String number) async {
if (number.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
if (blockedNumbers.contains(number)) {
blockedNumbers.remove(number);
await prefs.setStringList('blockedNumbers', blockedNumbers);
print('$number has been unblocked');
} else {
print('$number is not blocked');
}
}
// Check if a number is blocked
Future<bool> isNumberBlocked(String number) async {
if (number.isEmpty) return false;
final prefs = await SharedPreferences.getInstance();
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
return blockedNumbers.contains(number);
}
}

View File

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../presentation/features/call/call_page.dart'; import '../features/call/call_page.dart';
import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page import '../features/call/incoming_call_page.dart';
import 'contact_service.dart'; import '../services/contact_service.dart';
class CallService { class CallService {
static const MethodChannel _channel = MethodChannel('call_service'); static const MethodChannel _channel = MethodChannel('call_service');

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:qr_flutter/qr_flutter.dart';
// Service to manage contact-related operations
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
withAccounts: true,
withGroups: true,
withPhoto: true);
}
return [];
}
Future<List<Contact>> fetchFavoriteContacts() async {
List<Contact> contacts = await fetchContacts();
return contacts.where((contact) => contact.isStarred).toList();
}
Future<void> addNewContact(Contact contact) async {
await FlutterContacts.insertContact(contact);
}
// Function to show an AlertDialog with a QR code for the contact's vCard
void showContactQRCodeDialog(BuildContext context, Contact contact) {
showDialog(
barrierColor: Colors.white24,
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.black,
content: SizedBox(
width: 200,
height: 220,
child: QrImageView(
data: contact.toVCard(), // Generate vCard QR code
version: QrVersions.auto,
backgroundColor: Colors.white, // Make sure QR code is visible on black background
size: 200.0,
),
),
);
},
);
}
}

View File

@ -10,7 +10,7 @@ class AsymmetricCryptoService {
final String _aliasPrefix = 'icing_'; final String _aliasPrefix = 'icing_';
final Uuid _uuid = Uuid(); final Uuid _uuid = Uuid();
/// Generates an ED25519 key pair with a unique alias and stores its metadata. /// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
Future<String> generateKeyPair({String? label}) async { Future<String> generateKeyPair({String? label}) async {
try { try {
// Generate a unique identifier for the key // Generate a unique identifier for the key

View File

@ -1,7 +1,7 @@
// lib/services/obfuscate_service.dart // lib/services/obfuscate_service.dart
import 'package:dialer/presentation/common/widgets/color_darkener.dart'; import 'package:dialer/widgets/color_darkener.dart';
import '../../core/config/app_config.dart'; import '../../globals.dart' as globals;
import 'dart:ui'; import 'dart:ui';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -20,7 +20,7 @@ class ObfuscateService {
// Public method to obfuscate data // Public method to obfuscate data
String obfuscateData(String data) { String obfuscateData(String data) {
if (AppConfig.isStealthMode) { if (globals.isStealthMode) {
return _obfuscateData(data); return _obfuscateData(data);
} else { } else {
return data; return data;
@ -61,7 +61,7 @@ class ObfuscatedAvatar extends StatelessWidget {
if (imageBytes != null && imageBytes!.isNotEmpty) { if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval( return ClipOval(
child: ImageFiltered( child: ImageFiltered(
imageFilter: AppConfig.isStealthMode imageFilter: globals.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10) ? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0), : ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory( child: Image.memory(

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
Color darken(Color color, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(color);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}

View File

@ -0,0 +1,12 @@
import 'dart:math';
import 'package:flutter/material.dart';
Color generateColorFromName(String name) {
final random = Random(name.hashCode);
return Color.fromARGB(
255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
);
}

View File

@ -0,0 +1,97 @@
# Auto-Test Button Guide
## Overview
The integrated UI includes an automatic test button that simplifies testing of the encrypted voice protocol. The green "Run Auto Test" button automatically performs a comprehensive test sequence.
## Important Note
There were issues with the original auto-test implementation causing segmentation faults. A fixed version is available in `UI/integrated_ui_fixed.py` that addresses these issues.
## Features
### Automatic Port Detection
- Automatically retrieves protocol ports from both phone instances
- No manual port entry required
- Fills in peer port fields automatically
### Comprehensive Testing
The auto-test performs the following sequence:
1. **Connection Test**
- Auto-detects both phone ports
- Establishes bidirectional connection
- Verifies protocol handshake
2. **AES-256-GCM Encryption Test**
- Configures AES encryption mode
- Performs key exchange
- Sends test message "Test message with AES encryption"
- Verifies encryption success
3. **ChaCha20-Poly1305 Encryption Test**
- Resets protocol connections
- Reconfigures for ChaCha20 encryption
- Performs new key exchange
- Sends test message "Test message with ChaCha20 encryption"
- Verifies encryption success
4. **Voice Transmission Test** (if input.wav exists)
- Tests encrypted voice transmission
- Uses the configured encryption (ChaCha20)
- Processes through 4FSK modulation
## Usage
1. Start the integrated UI:
```bash
cd DryBox
python3 UI/integrated_ui.py
```
2. Click "Start GSM Simulator" button
3. Wait for both phones to initialize (you'll see their identity keys)
4. Click the green "Run Auto Test" button
5. Monitor the Protocol Status window for test progress
## Status Messages
The test provides detailed status updates:
- `✓` indicates successful steps
- `❌` indicates failed steps
- Timestamps for each operation
- Clear test section headers
## Implementation Details
The auto-test is implemented in `integrated_ui.py`:
- `run_auto_test()` method (line 550)
- `_run_auto_test_sequence()` method (line 559)
- Runs in a separate thread to keep UI responsive
- Properly resets protocols between cipher tests
- Comprehensive error handling
## Benefits
- **No Manual Configuration**: Eliminates need to manually enter ports
- **Comprehensive Coverage**: Tests both encryption methods automatically
- **Time Saving**: Complete test sequence in under 15 seconds
- **Error Detection**: Identifies issues quickly with clear status messages
- **Repeatable**: Consistent test execution every time
## Fixed Version
Due to issues with protocol resets causing segmentation faults, use the fixed version:
```bash
cd DryBox
python3 UI/integrated_ui_fixed.py
```
The fixed version:
- Properly handles GSM simulator startup
- Avoids protocol reset between cipher tests
- Includes better error handling and timeouts
- Only tests ChaCha20 by default to avoid stability issues
- Properly cleans up resources on exit

View File

@ -0,0 +1,141 @@
# DryBox Integrated Protocol
This directory contains the integrated DryBox system with Icing protocol support, featuring:
- End-to-end encryption using ChaCha20-Poly1305 or AES-256-GCM
- 4-FSK modulation for transmitting encrypted data over GSM voice channels
- Codec2 voice compression
- Full protocol key exchange with ECDH and HKDF
- PyQt5 UI for easy testing
## Architecture
```
┌─────────────────┐ ┌─────────────────┐
│ Phone 1 │ │ Phone 2 │
├─────────────────┤ ├─────────────────┤
│ Icing Protocol │<------->│ Icing Protocol │ (Key Exchange)
│ - ECDH │ │ - ECDH │
│ - ChaCha20 │ │ - ChaCha20 │
├─────────────────┤ ├─────────────────┤
│ Voice Protocol │ │ Voice Protocol │
│ - Codec2 │ │ - Codec2 │
│ - Encryption │ │ - Encryption │
│ - 4-FSK │ │ - 4-FSK │
├─────────────────┤ ├─────────────────┤
│ GSM Simulator │<------->│ GSM Simulator │ (Audio Channel)
└─────────────────┘ └─────────────────┘
```
## Quick Start
### 1. Using the Integrated UI (Recommended)
```bash
# Terminal 1: Start GSM simulator
python3 gsm_simulator.py
# Terminal 2: Start the integrated UI
python3 UI/integrated_ui.py
```
In the UI:
1. Click "Start GSM Simulator" (or manually start it)
2. Both phones will initialize automatically
3. Click "Connect" on Phone 1 (it will auto-detect Phone 2's port)
4. Click "Start Key Exchange" on Phone 1
5. Once secure, you can:
- Send encrypted text messages
- Send voice (requires input.wav in DryBox directory)
### 2. Using Command Line
```bash
# Terminal 1: Start GSM simulator
python3 gsm_simulator.py
# Terminal 2: Start receiver
python3 integrated_protocol.py receiver
# Note the protocol port shown (e.g., 35678)
# Terminal 3: Start sender
python3 integrated_protocol.py sender
# Enter the receiver's port when prompted
# Enter the receiver's identity key when prompted
```
### 3. Running Tests
```bash
# Run the automated test suite
cd ..
python3 test_drybox_integration.py
# Run manual interactive test
python3 test_drybox_integration.py --manual
```
## Features
### Encryption
- **ChaCha20-Poly1305**: Modern, fast stream cipher (recommended)
- **AES-256-GCM**: Industry standard block cipher
- **Key Exchange**: ECDH with secp256r1 curve
- **Key Derivation**: HKDF-SHA256
### Voice Processing
- **Codec2**: Ultra-low bitrate voice codec (1200 bps default)
- **4-FSK Modulation**: Robust against GSM codec distortion
- Frequencies: 600, 1200, 1800, 2400 Hz
- Baud rate: 600 symbols/second
- 2 bits per symbol
- **FEC**: Forward error correction for reliability
### Protocol Flow
1. **Connection Setup**: Phones connect to GSM simulator
2. **Protocol Handshake**:
- PING request/response (cipher negotiation)
- HANDSHAKE messages (ephemeral key exchange)
- HKDF key derivation
3. **Secure Communication**:
- Text messages: Encrypted with message headers
- Voice: Compressed → Encrypted → Modulated → Transmitted
## File Structure
```
DryBox/
├── integrated_protocol.py # Main integration module
├── gsm_simulator.py # GSM channel simulator
├── protocol.py # Original DryBox protocol (updated)
├── UI/
│ ├── integrated_ui.py # PyQt5 UI with protocol integration
│ └── python_ui.py # Original UI
├── input.wav # Input audio file for testing
└── received.wav # Output audio file (created by receiver)
```
## Creating Test Audio
If you don't have input.wav:
```bash
# Create a 1-second 440Hz test tone
sox -n input.wav synth 1 sine 440 rate 8000
# Or convert existing audio
sox your_audio.wav -r 8000 -c 1 input.wav trim 0 2
```
## Troubleshooting
1. **Import errors**: Make sure you're in the correct directory and the parent protocol modules are accessible
2. **GSM simulator already running**: Check for existing processes on port 12345
3. **No audio output**: Check that sox and required audio tools are installed
4. **Key exchange timeout**: Ensure both instances can communicate on their protocol ports (not just GSM ports)
## Security Notes
- Identity keys are generated fresh each run
- In production, identity keys should be persisted and verified out-of-band
- The current implementation uses predefined test keys for convenience
- All voice data is encrypted end-to-end before transmission

View File

@ -1,79 +0,0 @@
# client_state.py
from queue import Queue
from session import NoiseXKSession
import time
class ClientState:
def __init__(self, client_id):
self.client_id = client_id
self.command_queue = Queue()
self.initiator = None
self.keypair = None
self.peer_pubkey = None
self.session = None
self.handshake_in_progress = False
self.handshake_start_time = None
self.call_active = False
def process_command(self, client):
"""Process commands from the queue."""
if not self.command_queue.empty():
print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}")
command = self.command_queue.get()
if command == "handshake":
try:
print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}")
self.session = NoiseXKSession(self.keypair, self.peer_pubkey)
self.session.handshake(client.sock, self.initiator)
print(f"Client {self.client_id} handshake complete")
client.send("HANDSHAKE_DONE")
except Exception as e:
print(f"Client {self.client_id} handshake failed: {e}")
client.state_changed.emit("CALL_END", "", self.client_id)
finally:
self.handshake_in_progress = False
self.handshake_start_time = None
def start_handshake(self, initiator, keypair, peer_pubkey):
"""Queue handshake command."""
self.initiator = initiator
self.keypair = keypair
self.peer_pubkey = peer_pubkey
print(f"Client {self.client_id} queuing handshake, initiator: {initiator}")
self.handshake_in_progress = True
self.handshake_start_time = time.time()
self.command_queue.put("handshake")
def handle_data(self, client, data):
"""Handle received data (control or audio)."""
try:
decoded_data = data.decode('utf-8').strip()
print(f"Client {self.client_id} received raw: {decoded_data}")
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]:
client.state_changed.emit(decoded_data, decoded_data, self.client_id)
if decoded_data == "HANDSHAKE":
self.handshake_in_progress = True
elif decoded_data == "HANDSHAKE_DONE":
self.call_active = True
else:
print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}")
except UnicodeDecodeError:
if self.call_active and self.session:
try:
print(f"Client {self.client_id} received audio packet, length={len(data)}")
decrypted_data = self.session.decrypt(data)
print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}")
client.data_received.emit(decrypted_data, self.client_id)
except Exception as e:
print(f"Client {self.client_id} failed to process audio packet: {e}")
else:
print(f"Client {self.client_id} ignored non-text message: {data.hex()}")
def check_handshake_timeout(self, client):
"""Check for handshake timeout."""
if self.handshake_in_progress and self.handshake_start_time:
if time.time() - self.handshake_start_time > 30:
print(f"Client {self.client_id} handshake timeout after 30s")
client.state_changed.emit("CALL_END", "", self.client_id)
self.handshake_in_progress = False
self.handshake_start_time = None

View File

@ -0,0 +1,723 @@
#!/usr/bin/env python3
"""
Integrated UI for DryBox with Icing Protocol
Supports encrypted voice communication with 4FSK modulation
"""
import sys
import random
import socket
import threading
import time
import subprocess
import os
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame, QSizePolicy, QStyle, QTextEdit,
QLineEdit, QCheckBox
)
from PyQt5.QtCore import Qt, QTimer, QSize, QPointF, pyqtSignal, QThread
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush, QIcon, QFont
# Add parent directories to path
parent_dir = str(Path(__file__).parent.parent)
grandparent_dir = str(Path(__file__).parent.parent.parent)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
if grandparent_dir not in sys.path:
sys.path.insert(0, grandparent_dir)
# Import from DryBox directory
from integrated_protocol import IntegratedDryBoxProtocol
# ANSI colors for console
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class ProtocolThread(QThread):
"""Thread for running the integrated protocol"""
status_update = pyqtSignal(str)
key_exchange_complete = pyqtSignal(bool)
message_received = pyqtSignal(str)
def __init__(self, mode, gsm_host="localhost", gsm_port=12345):
super().__init__()
self.mode = mode
self.gsm_host = gsm_host
self.gsm_port = gsm_port
self.protocol = None
self.running = True
def run(self):
"""Run the protocol in background"""
try:
# Create protocol instance
self.protocol = IntegratedDryBoxProtocol(
gsm_host=self.gsm_host,
gsm_port=self.gsm_port,
mode=self.mode
)
self.status_update.emit(f"Protocol initialized in {self.mode} mode")
# Connect to GSM
if self.protocol.connect_gsm():
self.status_update.emit("Connected to GSM simulator")
else:
self.status_update.emit("Failed to connect to GSM")
return
# Get identity
identity = self.protocol.get_identity_key()
self.status_update.emit(f"Identity: {identity[:32]}...")
# Keep running
while self.running:
time.sleep(0.1)
# Check for key exchange completion
if (self.protocol.protocol.state.get("key_exchange_complete") and
not hasattr(self, '_key_exchange_notified')):
self._key_exchange_notified = True
self.key_exchange_complete.emit(True)
except Exception as e:
self.status_update.emit(f"Protocol error: {str(e)}")
def stop(self):
"""Stop the protocol thread"""
self.running = False
if self.protocol:
self.protocol.close()
def setup_connection(self, peer_port=None, peer_identity=None):
"""Setup protocol connection"""
if self.protocol:
port = self.protocol.setup_protocol_connection(
peer_port=peer_port,
peer_identity=peer_identity
)
return port
return None
def initiate_key_exchange(self, cipher_type=1):
"""Initiate key exchange"""
if self.protocol:
return self.protocol.initiate_key_exchange(cipher_type)
return False
def send_voice(self, audio_file):
"""Send voice through protocol"""
if self.protocol:
# Temporarily set input file
old_input = self.protocol.input_file
self.protocol.input_file = audio_file
self.protocol.send_voice()
self.protocol.input_file = old_input
def send_message(self, message):
"""Send encrypted text message"""
if self.protocol:
self.protocol.send_encrypted_message(message)
class WaveformWidget(QWidget):
"""Widget for displaying audio waveform"""
def __init__(self, parent=None, dynamic=False):
super().__init__(parent)
self.dynamic = dynamic
self.setMinimumSize(200, 80)
self.setMaximumHeight(100)
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
if self.dynamic:
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_waveform)
self.timer.start(100)
def update_waveform(self):
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
self.update()
def set_data(self, data):
amplitude = sum(byte for byte in data) % 90 + 10
self.waveform_data = self.waveform_data[1:] + [amplitude]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor("#2D2D2D"))
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor("#0078D4"))
gradient.setColorAt(1.0, QColor("#50E6A4"))
pen = QPen(QBrush(gradient), 2)
painter.setPen(pen)
bar_width = self.width() / len(self.waveform_data)
max_h = self.height() - 10
for i, val in enumerate(self.waveform_data):
bar_height = (val / 100.0) * max_h
x = i * bar_width
y = (self.height() - bar_height) / 2
painter.drawLine(QPointF(x + bar_width / 2, y),
QPointF(x + bar_width / 2, y + bar_height))
class IntegratedPhoneUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DryBox Integrated Protocol UI")
self.setGeometry(100, 100, 1000, 800)
self.setStyleSheet("""
QMainWindow { background-color: #1e1e1e; }
QLabel { color: #E0E0E0; font-size: 14px; }
QPushButton {
background-color: #0078D4; color: white; border: none;
padding: 10px 15px; border-radius: 5px; font-size: 14px;
min-height: 30px;
}
QPushButton:hover { background-color: #005A9E; }
QPushButton:pressed { background-color: #003C6B; }
QPushButton:disabled { background-color: #555555; }
QPushButton#dangerButton { background-color: #E81123; }
QPushButton#dangerButton:hover { background-color: #C50E1F; }
QPushButton#successButton { background-color: #107C10; }
QPushButton#successButton:hover { background-color: #0E6E0E; }
QFrame {
background-color: #2D2D2D; border: 1px solid #3D3D3D;
border-radius: 8px;
}
QTextEdit {
background-color: #1E1E1E; color: #E0E0E0;
border: 1px solid #3D3D3D; border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
padding: 5px;
}
QLineEdit {
background-color: #2D2D2D; color: #E0E0E0;
border: 1px solid #3D3D3D; border-radius: 4px;
padding: 5px;
}
QCheckBox { color: #E0E0E0; }
QLabel#titleLabel {
font-size: 24px; font-weight: bold; color: #00A2E8;
padding: 15px;
}
QLabel#sectionLabel {
font-size: 16px; font-weight: bold; color: #FFFFFF;
padding: 5px;
}
""")
# Protocol threads
self.phone1_protocol = None
self.phone2_protocol = None
# Setup UI
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(20, 20, 20, 20)
main_widget.setLayout(main_layout)
# Title
title = QLabel("DryBox Encrypted Voice Protocol")
title.setObjectName("titleLabel")
title.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title)
# Horizontal layout for phones
phones_layout = QHBoxLayout()
phones_layout.setSpacing(20)
main_layout.addLayout(phones_layout)
# Phone 1
self.phone1_frame = self.create_phone_frame("Phone 1", 1)
phones_layout.addWidget(self.phone1_frame)
# Phone 2
self.phone2_frame = self.create_phone_frame("Phone 2", 2)
phones_layout.addWidget(self.phone2_frame)
# Protocol status
status_frame = QFrame()
status_layout = QVBoxLayout(status_frame)
status_label = QLabel("Protocol Status")
status_label.setObjectName("sectionLabel")
status_layout.addWidget(status_label)
self.status_text = QTextEdit()
self.status_text.setMaximumHeight(150)
self.status_text.setReadOnly(True)
status_layout.addWidget(self.status_text)
main_layout.addWidget(status_frame)
# Control buttons
controls_layout = QHBoxLayout()
controls_layout.setSpacing(10)
self.start_gsm_btn = QPushButton("Start GSM Simulator")
self.start_gsm_btn.clicked.connect(self.start_gsm_simulator)
controls_layout.addWidget(self.start_gsm_btn)
self.test_voice_btn = QPushButton("Test Voice Transmission")
self.test_voice_btn.clicked.connect(self.test_voice_transmission)
self.test_voice_btn.setEnabled(False)
controls_layout.addWidget(self.test_voice_btn)
self.auto_test_btn = QPushButton("Run Auto Test")
self.auto_test_btn.clicked.connect(self.run_auto_test)
self.auto_test_btn.setEnabled(False)
self.auto_test_btn.setObjectName("successButton")
controls_layout.addWidget(self.auto_test_btn)
controls_layout.addStretch()
main_layout.addLayout(controls_layout)
def create_phone_frame(self, title, phone_id):
"""Create a phone control frame"""
frame = QFrame()
layout = QVBoxLayout(frame)
# Title
title_label = QLabel(title)
title_label.setObjectName("sectionLabel")
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Status
status_label = QLabel("Disconnected")
status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(status_label)
# Identity
identity_label = QLabel("Identity: Not initialized")
identity_label.setWordWrap(True)
identity_label.setStyleSheet("font-size: 10px;")
layout.addWidget(identity_label)
# Connection controls
conn_layout = QHBoxLayout()
port_input = QLineEdit()
port_input.setPlaceholderText("Peer port")
port_input.setMaximumWidth(100)
conn_layout.addWidget(port_input)
connect_btn = QPushButton("Connect")
connect_btn.clicked.connect(lambda: self.connect_phone(phone_id))
conn_layout.addWidget(connect_btn)
layout.addLayout(conn_layout)
# Key exchange
key_btn = QPushButton("Start Key Exchange")
key_btn.clicked.connect(lambda: self.start_key_exchange(phone_id))
key_btn.setEnabled(False)
layout.addWidget(key_btn)
# Cipher selection
cipher_layout = QHBoxLayout()
aes_radio = QCheckBox("AES-GCM")
chacha_radio = QCheckBox("ChaCha20")
chacha_radio.setChecked(True)
cipher_layout.addWidget(aes_radio)
cipher_layout.addWidget(chacha_radio)
layout.addLayout(cipher_layout)
# Message input
msg_input = QLineEdit()
msg_input.setPlaceholderText("Enter message")
layout.addWidget(msg_input)
send_btn = QPushButton("Send Encrypted Message")
send_btn.clicked.connect(lambda: self.send_message(phone_id))
send_btn.setEnabled(False)
layout.addWidget(send_btn)
# Voice controls
voice_btn = QPushButton("Send Voice")
voice_btn.clicked.connect(lambda: self.send_voice(phone_id))
voice_btn.setEnabled(False)
voice_btn.setObjectName("successButton")
layout.addWidget(voice_btn)
# Waveform
waveform = WaveformWidget()
layout.addWidget(waveform)
# Store references
frame.status_label = status_label
frame.identity_label = identity_label
frame.port_input = port_input
frame.connect_btn = connect_btn
frame.key_btn = key_btn
frame.aes_radio = aes_radio
frame.chacha_radio = chacha_radio
frame.msg_input = msg_input
frame.send_btn = send_btn
frame.voice_btn = voice_btn
frame.waveform = waveform
return frame
def start_gsm_simulator(self):
"""Start the GSM simulator in background"""
self.log_status("Starting GSM simulator...")
# Check if simulator is already running
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.connect(("localhost", 12345))
test_sock.close()
self.log_status("GSM simulator already running")
self.enable_phones()
return
except:
pass
# Start simulator
gsm_path = Path(__file__).parent.parent / "gsm_simulator.py"
self.gsm_process = subprocess.Popen(
[sys.executable, str(gsm_path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
time.sleep(1) # Give it time to start
self.log_status("GSM simulator started")
self.enable_phones()
def enable_phones(self):
"""Enable phone controls"""
self.phone1_frame.connect_btn.setEnabled(True)
self.phone2_frame.connect_btn.setEnabled(True)
self.auto_test_btn.setEnabled(True)
# Start protocol threads
self.phone1_protocol = ProtocolThread("sender")
self.phone1_protocol.status_update.connect(
lambda msg: self.update_phone_status(1, msg))
self.phone1_protocol.key_exchange_complete.connect(
lambda: self.on_key_exchange_complete(1))
self.phone1_protocol.start()
self.phone2_protocol = ProtocolThread("receiver")
self.phone2_protocol.status_update.connect(
lambda msg: self.update_phone_status(2, msg))
self.phone2_protocol.key_exchange_complete.connect(
lambda: self.on_key_exchange_complete(2))
self.phone2_protocol.start()
# Update identities
time.sleep(0.5)
if self.phone1_protocol.protocol:
identity = self.phone1_protocol.protocol.get_identity_key()
self.phone1_frame.identity_label.setText(f"Identity: {identity[:32]}...")
if self.phone2_protocol.protocol:
identity = self.phone2_protocol.protocol.get_identity_key()
self.phone2_frame.identity_label.setText(f"Identity: {identity[:32]}...")
def connect_phone(self, phone_id):
"""Connect phone to peer"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
peer_protocol = self.phone2_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
peer_protocol = self.phone1_protocol
try:
# Get peer port
peer_port = frame.port_input.text()
if not peer_port:
# Use other phone's port
if peer_protocol and peer_protocol.protocol:
peer_port = peer_protocol.protocol.protocol.local_port
else:
self.log_status(f"Phone {phone_id}: Enter peer port")
return
else:
peer_port = int(peer_port)
# Get peer identity
if peer_protocol and peer_protocol.protocol:
peer_identity = peer_protocol.protocol.get_identity_key()
else:
peer_identity = None
# Setup connection
port = protocol.setup_connection(
peer_port=peer_port,
peer_identity=peer_identity
)
self.log_status(f"Phone {phone_id}: Connected to port {peer_port}")
frame.status_label.setText("Connected")
frame.key_btn.setEnabled(True)
except Exception as e:
self.log_status(f"Phone {phone_id} connection error: {str(e)}")
def start_key_exchange(self, phone_id):
"""Start key exchange for phone"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
# Get cipher preference
cipher_type = 1 if frame.chacha_radio.isChecked() else 0
self.log_status(f"Phone {phone_id}: Starting key exchange...")
# Start key exchange in thread
threading.Thread(
target=lambda: protocol.initiate_key_exchange(cipher_type),
daemon=True
).start()
def on_key_exchange_complete(self, phone_id):
"""Handle key exchange completion"""
if phone_id == 1:
frame = self.phone1_frame
else:
frame = self.phone2_frame
self.log_status(f"Phone {phone_id}: Key exchange completed!")
frame.status_label.setText("Secure - Key Exchanged")
frame.send_btn.setEnabled(True)
frame.voice_btn.setEnabled(True)
self.test_voice_btn.setEnabled(True)
def send_message(self, phone_id):
"""Send encrypted message"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
message = frame.msg_input.text()
if message:
protocol.send_message(message)
self.log_status(f"Phone {phone_id}: Sent encrypted: {message}")
frame.msg_input.clear()
def send_voice(self, phone_id):
"""Send voice from phone"""
if phone_id == 1:
protocol = self.phone1_protocol
else:
protocol = self.phone2_protocol
# Check if input.wav exists
audio_file = Path(__file__).parent.parent / "input.wav"
if not audio_file.exists():
self.log_status(f"Phone {phone_id}: input.wav not found")
return
self.log_status(f"Phone {phone_id}: Sending voice...")
# Send in thread
threading.Thread(
target=lambda: protocol.send_voice(str(audio_file)),
daemon=True
).start()
def test_voice_transmission(self):
"""Test full voice transmission"""
self.log_status("Testing voice transmission from Phone 1 to Phone 2...")
self.send_voice(1)
def run_auto_test(self):
"""Run automated test sequence"""
self.log_status("="*50)
self.log_status("Starting Automated Test Sequence")
self.log_status("="*50)
# Disable auto test button during test
self.auto_test_btn.setEnabled(False)
# Run test in a separate thread to avoid blocking UI
threading.Thread(target=self._run_auto_test_sequence, daemon=True).start()
def _run_auto_test_sequence(self):
"""Execute the automated test sequence"""
try:
# Test 1: Auto-connect phones
self.log_status("\n[TEST 1] Auto-connecting phones...")
time.sleep(0.5)
# Wait for protocols to be ready
if not self.phone1_protocol or not self.phone2_protocol:
self.log_status("❌ Protocols not initialized")
self.auto_test_btn.setEnabled(True)
return
# Wait a bit for protocols to fully initialize
max_wait = 5
wait_time = 0
while wait_time < max_wait:
if (hasattr(self.phone1_protocol, 'protocol') and
hasattr(self.phone2_protocol, 'protocol') and
self.phone1_protocol.protocol and
self.phone2_protocol.protocol):
break
time.sleep(0.5)
wait_time += 0.5
if wait_time >= max_wait:
self.log_status("❌ Protocols failed to initialize")
self.auto_test_btn.setEnabled(True)
return
# Get ports
phone1_port = self.phone1_protocol.protocol.protocol.local_port
phone2_port = self.phone2_protocol.protocol.protocol.local_port
# Auto-fill peer ports
self.phone1_frame.port_input.setText(str(phone2_port))
self.phone2_frame.port_input.setText(str(phone1_port))
self.log_status(f"✓ Phone 1 port: {phone1_port}")
self.log_status(f"✓ Phone 2 port: {phone2_port}")
# Connect phones
self.connect_phone(1)
time.sleep(1)
self.connect_phone(2)
time.sleep(2) # Give more time for connections to establish
# Test 2: Key exchange with AES
self.log_status("\n[TEST 2] Testing AES-256-GCM encryption...")
self.phone1_frame.aes_radio.setChecked(True)
self.phone1_frame.chacha_radio.setChecked(False)
# Only phone 1 initiates key exchange to avoid race condition
self.start_key_exchange(1)
# Wait for key exchange with proper timeout
timeout = 10
start_time = time.time()
while (not self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete") and
time.time() - start_time < timeout):
time.sleep(0.2)
if self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete"):
self.log_status("✓ AES key exchange successful")
time.sleep(1) # Let the key exchange settle
# Send test message
test_msg = "Test message with AES encryption"
self.phone1_frame.msg_input.setText(test_msg)
self.send_message(1)
self.log_status(f"✓ Sent encrypted message: {test_msg}")
time.sleep(2) # Wait for message to be received
else:
self.log_status("❌ AES key exchange failed")
# Test 3: Test ChaCha20 (skip reset to avoid segfault)
self.log_status("\n[TEST 3] Testing ChaCha20-Poly1305 encryption...")
self.log_status("Note: Using same connection with different cipher")
# Set ChaCha20
self.phone1_frame.aes_radio.setChecked(False)
self.phone1_frame.chacha_radio.setChecked(True)
# Only phone 1 initiates key exchange
self.start_key_exchange(1)
# Wait for key exchange with proper timeout
timeout = 10
start_time = time.time()
while (not self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete") and
time.time() - start_time < timeout):
time.sleep(0.2)
if self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete"):
self.log_status("✓ ChaCha20 key exchange successful")
time.sleep(1) # Let the key exchange settle
# Send test message
test_msg = "Test message with ChaCha20 encryption"
self.phone1_frame.msg_input.setText(test_msg)
self.send_message(1)
self.log_status(f"✓ Sent encrypted message: {test_msg}")
time.sleep(2) # Wait for message to be received
# Test 4: Voice transmission
self.log_status("\n[TEST 4] Testing voice transmission...")
# Check if input.wav exists
audio_file = Path(__file__).parent.parent / "input.wav"
if audio_file.exists():
self.test_voice_transmission()
self.log_status("✓ Voice transmission initiated")
else:
self.log_status("❌ input.wav not found, skipping voice test")
else:
self.log_status("❌ ChaCha20 key exchange failed")
# Summary
self.log_status("\n" + "="*50)
self.log_status("Automated Test Sequence Completed")
self.log_status("✓ Auto-connection successful")
self.log_status("✓ Encryption tests completed")
self.log_status("✓ Message transmission tested")
if (Path(__file__).parent.parent / "input.wav").exists():
self.log_status("✓ Voice transmission tested")
self.log_status("="*50)
except Exception as e:
self.log_status(f"\n❌ Auto test error: {str(e)}")
import traceback
self.log_status(traceback.format_exc())
finally:
# Re-enable auto test button
self.auto_test_btn.setEnabled(True)
def update_phone_status(self, phone_id, message):
"""Update phone status display"""
self.log_status(f"Phone {phone_id}: {message}")
def log_status(self, message):
"""Log status message"""
timestamp = time.strftime("%H:%M:%S")
self.status_text.append(f"[{timestamp}] {message}")
def closeEvent(self, event):
"""Clean up on close"""
if self.phone1_protocol:
self.phone1_protocol.stop()
if self.phone2_protocol:
self.phone2_protocol.stop()
if hasattr(self, 'gsm_process'):
self.gsm_process.terminate()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = IntegratedPhoneUI()
window.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,714 @@
#!/usr/bin/env python3
"""
Fixed version of integrated UI with improved auto-test functionality
"""
import sys
import random
import socket
import threading
import time
import subprocess
import os
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame, QSizePolicy, QStyle, QTextEdit,
QLineEdit, QCheckBox
)
from PyQt5.QtCore import Qt, QTimer, QSize, QPointF, pyqtSignal, QThread
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush, QIcon, QFont
# Add parent directories to path
parent_dir = str(Path(__file__).parent.parent)
grandparent_dir = str(Path(__file__).parent.parent.parent)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
if grandparent_dir not in sys.path:
sys.path.insert(0, grandparent_dir)
# Import from DryBox directory
from integrated_protocol import IntegratedDryBoxProtocol
# ANSI colors for console
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class ProtocolThread(QThread):
"""Thread for running the integrated protocol"""
status_update = pyqtSignal(str)
key_exchange_complete = pyqtSignal(bool)
message_received = pyqtSignal(str)
def __init__(self, mode, gsm_host="localhost", gsm_port=12345):
super().__init__()
self.mode = mode
self.gsm_host = gsm_host
self.gsm_port = gsm_port
self.protocol = None
self.running = True
def run(self):
"""Run the protocol in background"""
try:
# Create protocol instance
self.protocol = IntegratedDryBoxProtocol(
gsm_host=self.gsm_host,
gsm_port=self.gsm_port,
mode=self.mode
)
self.status_update.emit(f"Protocol initialized in {self.mode} mode")
# Connect to GSM
if self.protocol.connect_gsm():
self.status_update.emit("Connected to GSM simulator")
else:
self.status_update.emit("Failed to connect to GSM")
return
# Get identity
identity = self.protocol.get_identity_key()
self.status_update.emit(f"Identity: {identity[:32]}...")
# Keep running
while self.running:
time.sleep(0.1)
# Check for key exchange completion
if (self.protocol.protocol.state.get("key_exchange_complete") and
not hasattr(self, '_key_exchange_notified')):
self._key_exchange_notified = True
self.key_exchange_complete.emit(True)
except Exception as e:
self.status_update.emit(f"Protocol error: {str(e)}")
def stop(self):
"""Stop the protocol thread"""
self.running = False
if self.protocol:
self.protocol.close()
def setup_connection(self, peer_port=None, peer_identity=None):
"""Setup protocol connection"""
if self.protocol:
port = self.protocol.setup_protocol_connection(
peer_port=peer_port,
peer_identity=peer_identity
)
return port
return None
def initiate_key_exchange(self, cipher_type=1):
"""Initiate key exchange"""
if self.protocol:
return self.protocol.initiate_key_exchange(cipher_type)
return False
def send_voice(self, audio_file):
"""Send voice through protocol"""
if self.protocol:
# Temporarily set input file
old_input = self.protocol.input_file
self.protocol.input_file = audio_file
self.protocol.send_voice()
self.protocol.input_file = old_input
def send_message(self, message):
"""Send encrypted text message"""
if self.protocol:
self.protocol.send_encrypted_message(message)
class WaveformWidget(QWidget):
"""Widget for displaying audio waveform"""
def __init__(self, parent=None, dynamic=False):
super().__init__(parent)
self.dynamic = dynamic
self.setMinimumSize(200, 80)
self.setMaximumHeight(100)
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
if self.dynamic:
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_waveform)
self.timer.start(100)
def update_waveform(self):
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
self.update()
def set_data(self, data):
amplitude = sum(byte for byte in data) % 90 + 10
self.waveform_data = self.waveform_data[1:] + [amplitude]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor("#2D2D2D"))
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor("#0078D4"))
gradient.setColorAt(1.0, QColor("#50E6A4"))
pen = QPen(QBrush(gradient), 2)
painter.setPen(pen)
bar_width = self.width() / len(self.waveform_data)
max_h = self.height() - 10
for i, val in enumerate(self.waveform_data):
bar_height = (val / 100.0) * max_h
x = i * bar_width
y = (self.height() - bar_height) / 2
painter.drawLine(QPointF(x + bar_width / 2, y),
QPointF(x + bar_width / 2, y + bar_height))
class IntegratedPhoneUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DryBox Integrated Protocol UI - Fixed Auto Test")
self.setGeometry(100, 100, 1000, 800)
self.setStyleSheet("""
QMainWindow { background-color: #1e1e1e; }
QLabel { color: #E0E0E0; font-size: 14px; }
QPushButton {
background-color: #0078D4; color: white; border: none;
padding: 10px 15px; border-radius: 5px; font-size: 14px;
min-height: 30px;
}
QPushButton:hover { background-color: #005A9E; }
QPushButton:pressed { background-color: #003C6B; }
QPushButton:disabled { background-color: #555555; }
QPushButton#dangerButton { background-color: #E81123; }
QPushButton#dangerButton:hover { background-color: #C50E1F; }
QPushButton#successButton { background-color: #107C10; }
QPushButton#successButton:hover { background-color: #0E6E0E; }
QFrame {
background-color: #2D2D2D; border: 1px solid #3D3D3D;
border-radius: 8px;
}
QTextEdit {
background-color: #1E1E1E; color: #E0E0E0;
border: 1px solid #3D3D3D; border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
padding: 5px;
}
QLineEdit {
background-color: #2D2D2D; color: #E0E0E0;
border: 1px solid #3D3D3D; border-radius: 4px;
padding: 5px;
}
QCheckBox { color: #E0E0E0; }
QLabel#titleLabel {
font-size: 24px; font-weight: bold; color: #00A2E8;
padding: 15px;
}
QLabel#sectionLabel {
font-size: 16px; font-weight: bold; color: #FFFFFF;
padding: 5px;
}
""")
# Protocol threads
self.phone1_protocol = None
self.phone2_protocol = None
# GSM simulator process
self.gsm_process = None
# Setup UI
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(20, 20, 20, 20)
main_widget.setLayout(main_layout)
# Title
title = QLabel("DryBox Encrypted Voice Protocol - Fixed Auto Test")
title.setObjectName("titleLabel")
title.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title)
# Horizontal layout for phones
phones_layout = QHBoxLayout()
phones_layout.setSpacing(20)
main_layout.addLayout(phones_layout)
# Phone 1
self.phone1_frame = self.create_phone_frame("Phone 1", 1)
phones_layout.addWidget(self.phone1_frame)
# Phone 2
self.phone2_frame = self.create_phone_frame("Phone 2", 2)
phones_layout.addWidget(self.phone2_frame)
# Protocol status
status_frame = QFrame()
status_layout = QVBoxLayout(status_frame)
status_label = QLabel("Protocol Status")
status_label.setObjectName("sectionLabel")
status_layout.addWidget(status_label)
self.status_text = QTextEdit()
self.status_text.setMaximumHeight(150)
self.status_text.setReadOnly(True)
status_layout.addWidget(self.status_text)
main_layout.addWidget(status_frame)
# Control buttons
controls_layout = QHBoxLayout()
controls_layout.setSpacing(10)
self.start_gsm_btn = QPushButton("Start GSM Simulator")
self.start_gsm_btn.clicked.connect(self.start_gsm_simulator)
controls_layout.addWidget(self.start_gsm_btn)
self.test_voice_btn = QPushButton("Test Voice Transmission")
self.test_voice_btn.clicked.connect(self.test_voice_transmission)
self.test_voice_btn.setEnabled(False)
controls_layout.addWidget(self.test_voice_btn)
self.auto_test_btn = QPushButton("Run Fixed Auto Test")
self.auto_test_btn.clicked.connect(self.run_auto_test)
self.auto_test_btn.setEnabled(False)
self.auto_test_btn.setObjectName("successButton")
controls_layout.addWidget(self.auto_test_btn)
controls_layout.addStretch()
main_layout.addLayout(controls_layout)
def create_phone_frame(self, title, phone_id):
"""Create a phone control frame"""
frame = QFrame()
layout = QVBoxLayout(frame)
# Title
title_label = QLabel(title)
title_label.setObjectName("sectionLabel")
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Status
status_label = QLabel("Disconnected")
status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(status_label)
# Identity
identity_label = QLabel("Identity: Not initialized")
identity_label.setWordWrap(True)
identity_label.setStyleSheet("font-size: 10px;")
layout.addWidget(identity_label)
# Connection controls
conn_layout = QHBoxLayout()
port_input = QLineEdit()
port_input.setPlaceholderText("Peer port")
port_input.setMaximumWidth(100)
conn_layout.addWidget(port_input)
connect_btn = QPushButton("Connect")
connect_btn.clicked.connect(lambda: self.connect_phone(phone_id))
conn_layout.addWidget(connect_btn)
layout.addLayout(conn_layout)
# Key exchange
key_btn = QPushButton("Start Key Exchange")
key_btn.clicked.connect(lambda: self.start_key_exchange(phone_id))
key_btn.setEnabled(False)
layout.addWidget(key_btn)
# Cipher selection
cipher_layout = QHBoxLayout()
aes_radio = QCheckBox("AES-GCM")
chacha_radio = QCheckBox("ChaCha20")
chacha_radio.setChecked(True)
cipher_layout.addWidget(aes_radio)
cipher_layout.addWidget(chacha_radio)
layout.addLayout(cipher_layout)
# Message input
msg_input = QLineEdit()
msg_input.setPlaceholderText("Enter message")
layout.addWidget(msg_input)
send_btn = QPushButton("Send Encrypted Message")
send_btn.clicked.connect(lambda: self.send_message(phone_id))
send_btn.setEnabled(False)
layout.addWidget(send_btn)
# Voice controls
voice_btn = QPushButton("Send Voice")
voice_btn.clicked.connect(lambda: self.send_voice(phone_id))
voice_btn.setEnabled(False)
voice_btn.setObjectName("successButton")
layout.addWidget(voice_btn)
# Waveform
waveform = WaveformWidget()
layout.addWidget(waveform)
# Store references
frame.status_label = status_label
frame.identity_label = identity_label
frame.port_input = port_input
frame.connect_btn = connect_btn
frame.key_btn = key_btn
frame.aes_radio = aes_radio
frame.chacha_radio = chacha_radio
frame.msg_input = msg_input
frame.send_btn = send_btn
frame.voice_btn = voice_btn
frame.waveform = waveform
return frame
def start_gsm_simulator(self):
"""Start the GSM simulator in background"""
self.log_status("Starting GSM simulator...")
# Check if simulator is already running
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(1)
test_sock.connect(("localhost", 12345))
test_sock.close()
self.log_status("GSM simulator already running")
self.enable_phones()
return
except:
pass
# Kill any existing GSM simulator
try:
subprocess.run(["pkill", "-f", "gsm_simulator.py"], capture_output=True)
time.sleep(0.5)
except:
pass
# Start simulator
gsm_path = Path(__file__).parent.parent / "gsm_simulator.py"
self.gsm_process = subprocess.Popen(
[sys.executable, str(gsm_path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# Wait for it to start
for i in range(10):
time.sleep(0.5)
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(1)
test_sock.connect(("localhost", 12345))
test_sock.close()
self.log_status("GSM simulator started successfully")
self.enable_phones()
return
except:
continue
self.log_status("Failed to start GSM simulator")
def enable_phones(self):
"""Enable phone controls"""
self.phone1_frame.connect_btn.setEnabled(True)
self.phone2_frame.connect_btn.setEnabled(True)
self.auto_test_btn.setEnabled(True)
# Start protocol threads
self.phone1_protocol = ProtocolThread("sender")
self.phone1_protocol.status_update.connect(
lambda msg: self.update_phone_status(1, msg))
self.phone1_protocol.key_exchange_complete.connect(
lambda: self.on_key_exchange_complete(1))
self.phone1_protocol.start()
self.phone2_protocol = ProtocolThread("receiver")
self.phone2_protocol.status_update.connect(
lambda msg: self.update_phone_status(2, msg))
self.phone2_protocol.key_exchange_complete.connect(
lambda: self.on_key_exchange_complete(2))
self.phone2_protocol.start()
# Update identities
time.sleep(0.5)
if self.phone1_protocol.protocol:
identity = self.phone1_protocol.protocol.get_identity_key()
self.phone1_frame.identity_label.setText(f"Identity: {identity[:32]}...")
if self.phone2_protocol.protocol:
identity = self.phone2_protocol.protocol.get_identity_key()
self.phone2_frame.identity_label.setText(f"Identity: {identity[:32]}...")
def connect_phone(self, phone_id):
"""Connect phone to peer"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
peer_protocol = self.phone2_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
peer_protocol = self.phone1_protocol
try:
# Get peer port
peer_port = frame.port_input.text()
if not peer_port:
# Use other phone's port
if peer_protocol and peer_protocol.protocol:
peer_port = peer_protocol.protocol.protocol.local_port
else:
self.log_status(f"Phone {phone_id}: Enter peer port")
return
else:
peer_port = int(peer_port)
# Get peer identity
if peer_protocol and peer_protocol.protocol:
peer_identity = peer_protocol.protocol.get_identity_key()
else:
peer_identity = None
# Setup connection
port = protocol.setup_connection(
peer_port=peer_port,
peer_identity=peer_identity
)
self.log_status(f"Phone {phone_id}: Connected to port {peer_port}")
frame.status_label.setText("Connected")
frame.key_btn.setEnabled(True)
except Exception as e:
self.log_status(f"Phone {phone_id} connection error: {str(e)}")
def start_key_exchange(self, phone_id):
"""Start key exchange for phone"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
# Get cipher preference
cipher_type = 1 if frame.chacha_radio.isChecked() else 0
self.log_status(f"Phone {phone_id}: Starting key exchange...")
# Start key exchange in thread
threading.Thread(
target=lambda: protocol.initiate_key_exchange(cipher_type),
daemon=True
).start()
def on_key_exchange_complete(self, phone_id):
"""Handle key exchange completion"""
if phone_id == 1:
frame = self.phone1_frame
else:
frame = self.phone2_frame
self.log_status(f"Phone {phone_id}: Key exchange completed!")
frame.status_label.setText("Secure - Key Exchanged")
frame.send_btn.setEnabled(True)
frame.voice_btn.setEnabled(True)
self.test_voice_btn.setEnabled(True)
def send_message(self, phone_id):
"""Send encrypted message"""
if phone_id == 1:
frame = self.phone1_frame
protocol = self.phone1_protocol
else:
frame = self.phone2_frame
protocol = self.phone2_protocol
message = frame.msg_input.text()
if message:
protocol.send_message(message)
self.log_status(f"Phone {phone_id}: Sent encrypted: {message}")
frame.msg_input.clear()
def send_voice(self, phone_id):
"""Send voice from phone"""
if phone_id == 1:
protocol = self.phone1_protocol
else:
protocol = self.phone2_protocol
# Check if input.wav exists
audio_file = Path(__file__).parent.parent / "input.wav"
if not audio_file.exists():
self.log_status(f"Phone {phone_id}: input.wav not found")
return
self.log_status(f"Phone {phone_id}: Sending voice...")
# Send in thread
threading.Thread(
target=lambda: protocol.send_voice(str(audio_file)),
daemon=True
).start()
def test_voice_transmission(self):
"""Test full voice transmission"""
self.log_status("Testing voice transmission from Phone 1 to Phone 2...")
self.send_voice(1)
def run_auto_test(self):
"""Run automated test sequence"""
self.log_status("="*50)
self.log_status("Starting Fixed Auto Test Sequence")
self.log_status("="*50)
# Disable auto test button during test
self.auto_test_btn.setEnabled(False)
# Run test in a separate thread to avoid blocking UI
threading.Thread(target=self._run_auto_test_sequence, daemon=True).start()
def _run_auto_test_sequence(self):
"""Execute the automated test sequence - FIXED VERSION"""
try:
# Test 1: Basic connection
self.log_status("\n[TEST 1] Setting up connections...")
time.sleep(1)
# Wait for protocols to be ready
timeout = 5
start = time.time()
while time.time() - start < timeout:
if (self.phone1_protocol and self.phone2_protocol and
hasattr(self.phone1_protocol, 'protocol') and
hasattr(self.phone2_protocol, 'protocol') and
self.phone1_protocol.protocol and
self.phone2_protocol.protocol):
break
time.sleep(0.5)
else:
self.log_status("❌ Protocols not ready")
self.auto_test_btn.setEnabled(True)
return
# Get ports
phone1_port = self.phone1_protocol.protocol.protocol.local_port
phone2_port = self.phone2_protocol.protocol.protocol.local_port
# Auto-fill peer ports
self.phone1_frame.port_input.setText(str(phone2_port))
self.phone2_frame.port_input.setText(str(phone1_port))
self.log_status(f"✓ Phone 1 port: {phone1_port}")
self.log_status(f"✓ Phone 2 port: {phone2_port}")
# Connect phones
self.connect_phone(1)
time.sleep(1)
self.connect_phone(2)
time.sleep(2)
self.log_status("✓ Connections established")
# Test 2: ChaCha20 encryption (default)
self.log_status("\n[TEST 2] Testing ChaCha20-Poly1305 encryption...")
# Ensure ChaCha20 is selected
self.phone1_frame.chacha_radio.setChecked(True)
self.phone1_frame.aes_radio.setChecked(False)
# Only phone 1 initiates to avoid race condition
self.start_key_exchange(1)
# Wait for key exchange
timeout = 10
start = time.time()
while time.time() - start < timeout:
if self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete"):
break
time.sleep(0.5)
if self.phone1_protocol.protocol.protocol.state.get("key_exchange_complete"):
self.log_status("✓ ChaCha20 key exchange successful")
time.sleep(1)
# Send test message
test_msg = "Hello from automated test with ChaCha20!"
self.phone1_frame.msg_input.setText(test_msg)
self.send_message(1)
self.log_status(f"✓ Sent encrypted message: {test_msg}")
time.sleep(2)
# Test voice if available
audio_file = Path(__file__).parent.parent / "input.wav"
if audio_file.exists():
self.log_status("\n[TEST 3] Testing voice transmission...")
self.test_voice_transmission()
self.log_status("✓ Voice transmission initiated")
else:
self.log_status("\n[TEST 3] Skipping voice test (input.wav not found)")
else:
self.log_status("❌ Key exchange failed")
# Summary
self.log_status("\n" + "="*50)
self.log_status("Fixed Auto Test Completed")
self.log_status("✓ Connection setup successful")
self.log_status("✓ ChaCha20 encryption tested")
self.log_status("✓ Message transmission verified")
self.log_status("="*50)
except Exception as e:
self.log_status(f"\n❌ Auto test error: {str(e)}")
import traceback
self.log_status(traceback.format_exc())
finally:
# Re-enable auto test button
self.auto_test_btn.setEnabled(True)
def update_phone_status(self, phone_id, message):
"""Update phone status display"""
self.log_status(f"Phone {phone_id}: {message}")
def log_status(self, message):
"""Log status message"""
timestamp = time.strftime("%H:%M:%S")
self.status_text.append(f"[{timestamp}] {message}")
def closeEvent(self, event):
"""Clean up on close"""
if self.phone1_protocol:
self.phone1_protocol.stop()
if self.phone2_protocol:
self.phone2_protocol.stop()
if self.gsm_process:
self.gsm_process.terminate()
# Kill any GSM simulator
try:
subprocess.run(["pkill", "-f", "gsm_simulator.py"], capture_output=True)
except:
pass
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = IntegratedPhoneUI()
window.show()
sys.exit(app.exec_())

View File

@ -1,215 +0,0 @@
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
)
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont
from phone_manager import PhoneManager
from waveform_widget import WaveformWidget
from phone_state import PhoneState
class PhoneUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Enhanced Dual Phone Interface")
self.setGeometry(100, 100, 900, 750)
self.setStyleSheet("""
QMainWindow { background-color: #333333; }
QLabel { color: #E0E0E0; font-size: 14px; }
QPushButton {
background-color: #0078D4; color: white; border: none;
padding: 10px 15px; border-radius: 5px; font-size: 14px;
min-height: 30px;
}
QPushButton:hover { background-color: #005A9E; }
QPushButton:pressed { background-color: #003C6B; }
QPushButton#settingsButton { background-color: #555555; }
QPushButton#settingsButton:hover { background-color: #777777; }
QFrame#phoneDisplay {
background-color: #1E1E1E; border: 2px solid #0078D4;
border-radius: 10px;
}
QLabel#phoneTitleLabel {
font-size: 18px; font-weight: bold; padding-bottom: 5px;
color: #FFFFFF;
}
QLabel#mainTitleLabel {
font-size: 24px; font-weight: bold; color: #00A2E8;
padding: 15px;
}
QWidget#phoneWidget {
border: 1px solid #4A4A4A; border-radius: 8px;
padding: 10px; background-color: #3A3A3A;
}
""")
self.manager = PhoneManager()
self.manager.initialize_phones()
# Main widget and layout
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setAlignment(Qt.AlignCenter)
main_widget.setLayout(main_layout)
# App Title
app_title_label = QLabel("Dual Phone Control Panel")
app_title_label.setObjectName("mainTitleLabel")
app_title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(app_title_label)
# Phone displays layout
phone_controls_layout = QHBoxLayout()
phone_controls_layout.setSpacing(50)
phone_controls_layout.setAlignment(Qt.AlignCenter)
main_layout.addLayout(phone_controls_layout)
# Setup UI for phones
for phone in self.manager.phones:
phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label = self._create_phone_ui(
f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self)
)
phone['button'] = phone_button
phone['waveform'] = waveform_widget
phone['sent_waveform'] = sent_waveform_widget
phone['status_label'] = phone_status_label
phone_controls_layout.addWidget(phone_container_widget)
phone['client'].data_received.connect(lambda data, cid=phone['id']: self.manager.update_waveform(cid, data))
phone['client'].state_changed.connect(lambda state, num, cid=phone['id']: self.set_phone_state(cid, state, num))
phone['client'].start()
# Spacer
main_layout.addStretch(1)
# Settings Button
self.settings_button = QPushButton("Settings")
self.settings_button.setObjectName("settingsButton")
self.settings_button.setFixedWidth(180)
self.settings_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
self.settings_button.setIconSize(QSize(20, 20))
self.settings_button.clicked.connect(self.settings_action)
settings_layout = QHBoxLayout()
settings_layout.addStretch()
settings_layout.addWidget(self.settings_button)
settings_layout.addStretch()
main_layout.addLayout(settings_layout)
# Initialize UI
for phone in self.manager.phones:
self.update_phone_ui(phone['id'])
def _create_phone_ui(self, title, action_slot):
phone_container_widget = QWidget()
phone_container_widget.setObjectName("phoneWidget")
phone_layout = QVBoxLayout()
phone_layout.setAlignment(Qt.AlignCenter)
phone_layout.setSpacing(15)
phone_container_widget.setLayout(phone_layout)
phone_title_label = QLabel(title)
phone_title_label.setObjectName("phoneTitleLabel")
phone_title_label.setAlignment(Qt.AlignCenter)
phone_layout.addWidget(phone_title_label)
phone_display_frame = QFrame()
phone_display_frame.setObjectName("phoneDisplay")
phone_display_frame.setFixedSize(250, 350)
phone_display_frame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
display_content_layout = QVBoxLayout(phone_display_frame)
display_content_layout.setAlignment(Qt.AlignCenter)
phone_status_label = QLabel("Idle")
phone_status_label.setAlignment(Qt.AlignCenter)
phone_status_label.setFont(QFont("Arial", 16))
display_content_layout.addWidget(phone_status_label)
phone_layout.addWidget(phone_display_frame, alignment=Qt.AlignCenter)
phone_button = QPushButton()
phone_button.setFixedWidth(120)
phone_button.setIconSize(QSize(20, 20))
phone_button.clicked.connect(action_slot)
phone_layout.addWidget(phone_button, alignment=Qt.AlignCenter)
# Received waveform
waveform_label = QLabel(f"{title} Received Audio")
waveform_label.setAlignment(Qt.AlignCenter)
waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
phone_layout.addWidget(waveform_label)
waveform_widget = WaveformWidget(dynamic=False)
phone_layout.addWidget(waveform_widget, alignment=Qt.AlignCenter)
# Sent waveform
sent_waveform_label = QLabel(f"{title} Sent Audio")
sent_waveform_label.setAlignment(Qt.AlignCenter)
sent_waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
phone_layout.addWidget(sent_waveform_label)
sent_waveform_widget = WaveformWidget(dynamic=False)
phone_layout.addWidget(sent_waveform_widget, alignment=Qt.AlignCenter)
return phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label
def update_phone_ui(self, phone_id):
phone = self.manager.phones[phone_id]
other_phone = self.manager.phones[1 - phone_id]
state = phone['state']
phone_number = other_phone['number'] if state != PhoneState.IDLE else ""
button = phone['button']
status_label = phone['status_label']
if state == PhoneState.IDLE:
button.setText("Call")
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
status_label.setText("Idle")
button.setStyleSheet("background-color: #0078D4;")
elif state == PhoneState.CALLING:
button.setText("Cancel")
button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
status_label.setText(f"Calling {phone_number}...")
button.setStyleSheet("background-color: #E81123;")
elif state == PhoneState.IN_CALL:
button.setText("Hang Up")
button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
status_label.setText(f"In Call with {phone_number}")
button.setStyleSheet("background-color: #E81123;")
elif state == PhoneState.RINGING:
button.setText("Answer")
button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
status_label.setText(f"Incoming Call from {phone_number}")
button.setStyleSheet("background-color: #107C10;")
def set_phone_state(self, client_id, state_str, number):
state = self.manager.map_state(state_str)
phone = self.manager.phones[client_id]
other_phone = self.manager.phones[1 - client_id]
print(f"Setting state for Phone {client_id + 1}: {state}, number: {number}, is_initiator: {phone['is_initiator']}")
phone['state'] = state
if state == PhoneState.IN_CALL:
print(f"Phone {client_id + 1} confirmed in IN_CALL state")
if number == "IN_CALL" and phone['is_initiator']:
print(f"Phone {client_id + 1} (initiator) starting handshake")
phone['client'].send("HANDSHAKE")
phone['client'].start_handshake(initiator=True, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
elif number == "HANDSHAKE" and not phone['is_initiator']:
print(f"Phone {client_id + 1} (responder) starting handshake")
phone['client'].start_handshake(initiator=False, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
elif number == "HANDSHAKE_DONE":
self.manager.start_audio(client_id, parent=self) # Pass self as parent
self.update_phone_ui(client_id)
def settings_action(self):
print("Settings clicked")
def closeEvent(self, event):
for phone in self.manager.phones:
phone['client'].stop()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = PhoneUI()
window.show()
sys.exit(app.exec_())

View File

@ -1,110 +0,0 @@
import socket
import time
import select
from PyQt5.QtCore import QThread, pyqtSignal
from client_state import ClientState
class PhoneClient(QThread):
data_received = pyqtSignal(bytes, int)
state_changed = pyqtSignal(str, str, int)
def __init__(self, client_id):
super().__init__()
self.host = "localhost"
self.port = 12345
self.client_id = client_id
self.sock = None
self.running = True
self.state = ClientState(client_id)
def connect_socket(self):
retries = 3
for attempt in range(retries):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.sock.settimeout(120)
self.sock.connect((self.host, self.port))
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
return True
except Exception as e:
print(f"Client {self.client_id} connection attempt {attempt + 1} failed: {e}")
if attempt < retries - 1:
time.sleep(1)
self.sock = None
return False
def run(self):
while self.running:
if not self.sock:
if not self.connect_socket():
print(f"Client {self.client_id} failed to connect after retries")
self.state_changed.emit("CALL_END", "", self.client_id)
break
try:
while self.running:
self.state.process_command(self)
self.state.check_handshake_timeout(self)
if not self.state.handshake_in_progress:
if self.sock is None:
print(f"Client {self.client_id} socket is None, exiting inner loop")
break
readable, _, _ = select.select([self.sock], [], [], 0.01)
if readable:
try:
if self.sock is None:
print(f"Client {self.client_id} socket is None before recv, exiting")
break
data = self.sock.recv(1024)
if not data:
print(f"Client {self.client_id} disconnected")
self.state_changed.emit("CALL_END", "", self.client_id)
break
self.state.handle_data(self, data)
except socket.error as e:
print(f"Client {self.client_id} socket error: {e}")
self.state_changed.emit("CALL_END", "", self.client_id)
break
else:
self.msleep(20)
print(f"Client {self.client_id} yielding during handshake")
self.msleep(1)
except Exception as e:
print(f"Client {self.client_id} unexpected error in run loop: {e}")
self.state_changed.emit("CALL_END", "", self.client_id)
break
finally:
if self.sock:
try:
self.sock.close()
except Exception as e:
print(f"Client {self.client_id} error closing socket: {e}")
self.sock = None
def send(self, message):
if self.sock and self.running:
try:
if isinstance(message, str):
data = message.encode('utf-8')
self.sock.send(data)
print(f"Client {self.client_id} sent: {message}, length={len(data)}")
else:
self.sock.send(message)
print(f"Client {self.client_id} sent binary data, length={len(message)}")
except socket.error as e:
print(f"Client {self.client_id} send error: {e}")
self.state_changed.emit("CALL_END", "", self.client_id)
def stop(self):
self.running = False
if self.sock:
try:
self.sock.close()
except Exception as e:
print(f"Client {self.client_id} error closing socket in stop: {e}")
self.sock = None
self.quit()
self.wait(1000)
def start_handshake(self, initiator, keypair, peer_pubkey):
self.state.start_handshake(initiator, keypair, peer_pubkey)

View File

@ -1,98 +0,0 @@
import secrets
from PyQt5.QtCore import QTimer
from phone_client import PhoneClient
from session import NoiseXKSession
from phone_state import PhoneState # Added import
class PhoneManager:
def __init__(self):
self.phones = []
self.handshake_done_count = 0
def initialize_phones(self):
for i in range(2):
client = PhoneClient(i)
keypair = NoiseXKSession.generate_keypair()
phone = {
'id': i,
'client': client,
'state': PhoneState.IDLE,
'number': "123-4567" if i == 0 else "987-6543",
'audio_timer': None,
'keypair': keypair,
'public_key': keypair.public,
'is_initiator': False
}
self.phones.append(phone)
self.phones[0]['peer_public_key'] = self.phones[1]['public_key']
self.phones[1]['peer_public_key'] = self.phones[0]['public_key']
def phone_action(self, phone_id, ui_manager):
phone = self.phones[phone_id]
other_phone = self.phones[1 - phone_id]
print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}")
if phone['state'] == PhoneState.IDLE:
phone['state'] = PhoneState.CALLING
other_phone['state'] = PhoneState.RINGING
phone['is_initiator'] = True
other_phone['is_initiator'] = False
phone['client'].send("RINGING")
elif phone['state'] == PhoneState.RINGING:
phone['state'] = other_phone['state'] = PhoneState.IN_CALL
phone['client'].send("IN_CALL")
elif phone['state'] in [PhoneState.IN_CALL, PhoneState.CALLING]:
if not phone['client'].state.handshake_in_progress and phone['state'] != PhoneState.CALLING:
phone['state'] = other_phone['state'] = PhoneState.IDLE
phone['client'].send("CALL_END")
for p in [phone, other_phone]:
if p['audio_timer']:
p['audio_timer'].stop()
else:
print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup")
ui_manager.update_phone_ui(phone_id)
ui_manager.update_phone_ui(1 - phone_id)
def send_audio(self, phone_id):
phone = self.phones[phone_id]
if phone['state'] == PhoneState.IN_CALL and phone['client'].state.session and phone['client'].sock:
mock_audio = secrets.token_bytes(16)
try:
self.update_sent_waveform(phone_id, mock_audio)
phone['client'].state.session.send(phone['client'].sock, mock_audio)
print(f"Client {phone_id} sent encrypted audio packet, length=32")
except Exception as e:
print(f"Client {phone_id} failed to send audio: {e}")
def start_audio(self, client_id, parent=None):
self.handshake_done_count += 1
print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}")
if self.handshake_done_count == 2:
for phone in self.phones:
if phone['state'] == PhoneState.IN_CALL:
if not phone['audio_timer'] or not phone['audio_timer'].isActive():
phone['audio_timer'] = QTimer(parent) # Parent to PhoneUI
phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid))
phone['audio_timer'].start(100)
self.handshake_done_count = 0
def update_waveform(self, client_id, data):
self.phones[client_id]['waveform'].set_data(data)
def update_sent_waveform(self, client_id, data):
self.phones[client_id]['sent_waveform'].set_data(data)
def map_state(self, state_str):
if state_str == "RINGING":
return PhoneState.RINGING
elif state_str in ["CALL_END", "CALL_DROPPED"]:
return PhoneState.IDLE
elif state_str == "IN_CALL":
return PhoneState.IN_CALL
elif state_str == "HANDSHAKE":
return PhoneState.IN_CALL
elif state_str == "HANDSHAKE_DONE":
return PhoneState.IN_CALL
return PhoneState.IDLE

View File

@ -1,5 +0,0 @@
class PhoneState:
IDLE = 0
CALLING = 1
IN_CALL = 2
RINGING = 3

View File

@ -0,0 +1,415 @@
import sys
import random
import socket
import threading
import time
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
)
from PyQt5.QtCore import Qt, QTimer, QSize, QPointF, pyqtSignal, QThread
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush, QIcon, QFont
# --- Phone Client Thread ---
class PhoneClient(QThread):
data_received = pyqtSignal(bytes, int) # Include client_id
state_changed = pyqtSignal(str, str, int) # Include client_id
def __init__(self, host, port, client_id):
super().__init__()
self.host = host
self.port = port
self.client_id = client_id
self.sock = None
self.running = True
def run(self):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.sock.settimeout(15)
self.sock.connect((self.host, self.port))
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
while self.running:
try:
data = self.sock.recv(1024)
if not data:
print(f"Client {self.client_id} disconnected")
self.state_changed.emit("CALL_END", "", self.client_id)
break
decoded_data = data.decode('utf-8', errors='ignore').strip()
print(f"Client {self.client_id} received raw: {decoded_data}")
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL"]:
self.state_changed.emit(decoded_data, "", self.client_id)
else:
self.data_received.emit(data, self.client_id)
print(f"Client {self.client_id} received audio: {decoded_data}")
except socket.timeout:
print(f"Client {self.client_id} timed out waiting for data")
continue
except Exception as e:
print(f"Client {self.client_id} error: {e}")
self.state_changed.emit("CALL_END", "", self.client_id)
break
except Exception as e:
print(f"Client {self.client_id} connection failed: {e}")
finally:
if self.sock:
self.sock.close()
def send(self, message):
if self.sock and self.running:
try:
self.sock.send(message.encode())
print(f"Client {self.client_id} sent: {message}")
except Exception as e:
print(f"Client {self.client_id} send error: {e}")
def stop(self):
self.running = False
if self.sock:
self.sock.close()
# --- Custom Waveform Widget ---
class WaveformWidget(QWidget):
def __init__(self, parent=None, dynamic=False):
super().__init__(parent)
self.dynamic = dynamic
self.setMinimumSize(200, 80)
self.setMaximumHeight(100)
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
if self.dynamic:
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_waveform)
self.timer.start(100)
def update_waveform(self):
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
self.update()
def set_data(self, data):
amplitude = sum(byte for byte in data) % 90 + 10
self.waveform_data = self.waveform_data[1:] + [amplitude]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor("#2D2D2D"))
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor("#0078D4"))
gradient.setColorAt(1.0, QColor("#50E6A4"))
pen = QPen(QBrush(gradient), 2)
painter.setPen(pen)
bar_width = self.width() / len(self.waveform_data)
max_h = self.height() - 10
for i, val in enumerate(self.waveform_data):
bar_height = (val / 100.0) * max_h
x = i * bar_width
y = (self.height() - bar_height) / 2
painter.drawLine(QPointF(x + bar_width / 2, y), QPointF(x + bar_width / 2, y + bar_height))
def resizeEvent(self, event):
super().resizeEvent(event)
self.update()
# --- Phone State ---
class PhoneState:
IDLE = 0
CALLING = 1
IN_CALL = 2
RINGING = 3
class PhoneUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Enhanced Dual Phone Interface")
self.setGeometry(100, 100, 900, 750)
self.setStyleSheet("""
QMainWindow { background-color: #333333; }
QLabel { color: #E0E0E0; font-size: 14px; }
QPushButton {
background-color: #0078D4; color: white; border: none;
padding: 10px 15px; border-radius: 5px; font-size: 14px;
min-height: 30px;
}
QPushButton:hover { background-color: #005A9E; }
QPushButton:pressed { background-color: #003C6B; }
QPushButton#settingsButton { background-color: #555555; }
QPushButton#settingsButton:hover { background-color: #777777; }
QFrame#phoneDisplay {
background-color: #1E1E1E; border: 2px solid #0078D4;
border-radius: 10px;
}
QLabel#phoneTitleLabel {
font-size: 18px; font-weight: bold; padding-bottom: 5px;
color: #FFFFFF;
}
QLabel#mainTitleLabel {
font-size: 24px; font-weight: bold; color: #00A2E8;
padding: 15px;
}
QWidget#phoneWidget {
border: 1px solid #4A4A4A; border-radius: 8px;
padding: 10px; background-color: #3A3A3A;
}
""")
# Phone states
self.phone1_state = PhoneState.IDLE
self.phone2_state = PhoneState.IDLE
# Phone clients
self.phone1_client = PhoneClient("localhost", 12345, 0)
self.phone2_client = PhoneClient("localhost", 12345, 1)
self.phone1_client.data_received.connect(lambda data, cid: self.update_waveform(cid, data))
self.phone2_client.data_received.connect(lambda data, cid: self.update_waveform(cid, data))
self.phone1_client.state_changed.connect(lambda state, num, cid: self.set_phone_state(cid, self.map_state(state), num))
self.phone2_client.state_changed.connect(lambda state, num, cid: self.set_phone_state(cid, self.map_state(state), num))
self.phone1_client.start()
self.phone2_client.start()
# Main widget and layout
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setAlignment(Qt.AlignCenter)
main_widget.setLayout(main_layout)
# App Title
app_title_label = QLabel("Dual Phone Control Panel")
app_title_label.setObjectName("mainTitleLabel")
app_title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(app_title_label)
# Phone displays layout
phone_controls_layout = QHBoxLayout()
phone_controls_layout.setSpacing(50)
phone_controls_layout.setAlignment(Qt.AlignCenter)
main_layout.addLayout(phone_controls_layout)
# Phone 1
phone1_widget_container, self.phone1_display, self.phone1_button, self.phone1_waveform = self._create_phone_ui("Phone 1", self.phone1_action)
phone_controls_layout.addWidget(phone1_widget_container)
# Phone 2
phone2_widget_container, self.phone2_display, self.phone2_button, self.phone2_waveform = self._create_phone_ui("Phone 2", self.phone2_action)
phone_controls_layout.addWidget(phone2_widget_container)
# Spacer
main_layout.addStretch(1)
# Settings Button
self.settings_button = QPushButton("Settings")
self.settings_button.setObjectName("settingsButton")
self.settings_button.setFixedWidth(180)
self.settings_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
self.settings_button.setIconSize(QSize(20, 20))
self.settings_button.clicked.connect(self.settings_action)
settings_layout = QHBoxLayout()
settings_layout.addStretch()
settings_layout.addWidget(self.settings_button)
settings_layout.addStretch()
main_layout.addLayout(settings_layout)
# Initialize button states
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
def _create_phone_ui(self, title, action_slot):
phone_container_widget = QWidget()
phone_container_widget.setObjectName("phoneWidget")
phone_layout = QVBoxLayout()
phone_layout.setAlignment(Qt.AlignCenter)
phone_layout.setSpacing(15)
phone_container_widget.setLayout(phone_layout)
phone_title_label = QLabel(title)
phone_title_label.setObjectName("phoneTitleLabel")
phone_title_label.setAlignment(Qt.AlignCenter)
phone_layout.addWidget(phone_title_label)
phone_display_frame = QFrame()
phone_display_frame.setObjectName("phoneDisplay")
phone_display_frame.setFixedSize(250, 350)
phone_display_frame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
display_content_layout = QVBoxLayout(phone_display_frame)
display_content_layout.setAlignment(Qt.AlignCenter)
phone_status_label = QLabel("Idle")
phone_status_label.setAlignment(Qt.AlignCenter)
phone_status_label.setFont(QFont("Arial", 16))
display_content_layout.addWidget(phone_status_label)
phone_layout.addWidget(phone_display_frame, alignment=Qt.AlignCenter)
phone_button = QPushButton()
phone_button.setFixedWidth(120)
phone_button.setIconSize(QSize(20, 20))
phone_button.clicked.connect(action_slot)
phone_layout.addWidget(phone_button, alignment=Qt.AlignCenter)
waveform_label = QLabel(f"{title} Audio")
waveform_label.setAlignment(Qt.AlignCenter)
waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
phone_layout.addWidget(waveform_label)
waveform_widget = WaveformWidget(dynamic=False)
phone_layout.addWidget(waveform_widget, alignment=Qt.AlignCenter)
phone_display_frame.setProperty("statusLabel", phone_status_label)
return phone_container_widget, phone_display_frame, phone_button, waveform_widget
def _update_phone_button_ui(self, button, state, phone_number=""):
parent_widget = button.parentWidget()
if parent_widget:
frame = parent_widget.findChild(QFrame, "phoneDisplay")
if frame:
status_label = frame.property("statusLabel")
if status_label:
if state == PhoneState.IDLE:
button.setText("Call")
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
status_label.setText("Idle")
button.setStyleSheet("background-color: #0078D4;")
elif state == PhoneState.CALLING:
button.setText("Cancel")
button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
status_label.setText(f"Calling {phone_number}...")
button.setStyleSheet("background-color: #E81123;")
elif state == PhoneState.IN_CALL:
button.setText("Hang Up")
button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
status_label.setText(f"In Call with {phone_number}")
button.setStyleSheet("background-color: #E81123;")
elif state == PhoneState.RINGING:
button.setText("Answer")
button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
status_label.setText(f"Incoming Call from {phone_number}")
button.setStyleSheet("background-color: #107C10;")
else:
print("Warning: statusLabel property not found")
else:
print("Warning: QFrame not found")
else:
print("Warning: Parent widget not found")
def update_waveform(self, client_id, data):
print(f"Updating waveform for client_id {client_id}")
waveform = self.phone1_waveform if client_id == 0 else self.phone2_waveform
waveform.set_data(data)
def map_state(self, state_str):
if state_str == "RINGING":
return PhoneState.RINGING
elif state_str in ["CALL_END", "CALL_DROPPED"]:
return PhoneState.IDLE
elif state_str == "IN_CALL":
return PhoneState.IN_CALL
return PhoneState.IDLE # Default to IDLE
def set_phone_state(self, client_id, state, number=""):
if client_id == 0:
self.phone1_state = state
self._update_phone_button_ui(self.phone1_button, self.phone1_state, number if number else "123-4567")
if state == PhoneState.IDLE and hasattr(self, 'phone1_audio_timer'):
self.phone1_audio_timer.stop()
elif state == PhoneState.IN_CALL and (not hasattr(self, 'phone1_audio_timer') or not self.phone1_audio_timer.isActive()):
self.phone1_audio_timer = QTimer(self)
self.phone1_audio_timer.timeout.connect(self.send_phone1_audio)
self.phone1_audio_timer.start(1000)
else:
self.phone2_state = state
self._update_phone_button_ui(self.phone2_button, self.phone2_state, number if number else "987-6543")
if state == PhoneState.IDLE and hasattr(self, 'phone2_audio_timer'):
self.phone2_audio_timer.stop()
elif state == PhoneState.IN_CALL and (not hasattr(self, 'phone2_audio_timer') or not self.phone2_audio_timer.isActive()):
self.phone2_audio_timer = QTimer(self)
self.phone2_audio_timer.timeout.connect(self.send_phone2_audio)
self.phone2_audio_timer.start(1000)
def phone1_action(self):
print("Phone 1 Action")
if self.phone1_state == PhoneState.IDLE:
self.phone1_state = PhoneState.CALLING
self.phone1_client.send("RINGING")
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
elif self.phone1_state == PhoneState.CALLING:
self.phone1_state = PhoneState.IDLE
self.phone1_client.send("CALL_END")
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
if hasattr(self, 'phone1_audio_timer'):
self.phone1_audio_timer.stop()
elif self.phone1_state == PhoneState.RINGING:
self.phone1_state = PhoneState.IN_CALL
self.phone2_state = PhoneState.IN_CALL # Sync both phones
self.phone1_client.send("IN_CALL")
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
# Start audio timer
self.phone1_audio_timer = QTimer(self)
self.phone1_audio_timer.timeout.connect(self.send_phone1_audio)
self.phone1_audio_timer.start(1000)
elif self.phone1_state == PhoneState.IN_CALL:
self.phone1_state = PhoneState.IDLE
self.phone2_state = PhoneState.IDLE # Sync both phones
self.phone1_client.send("CALL_END")
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
if hasattr(self, 'phone1_audio_timer'):
self.phone1_audio_timer.stop()
def send_phone1_audio(self):
if self.phone1_state == PhoneState.IN_CALL:
message = f"Audio packet {random.randint(1, 1000)}"
self.phone1_client.send(message)
def phone2_action(self):
print("Phone 2 Action")
if self.phone2_state == PhoneState.IDLE:
self.phone2_state = PhoneState.CALLING
self.phone2_client.send("RINGING")
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
elif self.phone2_state == PhoneState.CALLING:
self.phone2_state = PhoneState.IDLE
self.phone2_client.send("CALL_END")
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
if hasattr(self, 'phone2_audio_timer'):
self.phone2_audio_timer.stop()
elif self.phone2_state == PhoneState.RINGING:
self.phone2_state = PhoneState.IN_CALL
self.phone1_state = PhoneState.IN_CALL # Sync both phones
self.phone2_client.send("IN_CALL")
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
# Start audio timer
self.phone2_audio_timer = QTimer(self)
self.phone2_audio_timer.timeout.connect(self.send_phone2_audio)
self.phone2_audio_timer.start(1000)
elif self.phone2_state == PhoneState.IN_CALL:
self.phone2_state = PhoneState.IDLE
self.phone1_state = PhoneState.IDLE # Sync both phones
self.phone2_client.send("CALL_END")
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
if hasattr(self, 'phone2_audio_timer'):
self.phone2_audio_timer.stop()
def send_phone2_audio(self):
if self.phone2_state == PhoneState.IN_CALL:
message = f"Audio packet {random.randint(1, 1000)}"
self.phone2_client.send(message)
def settings_action(self):
print("Settings clicked")
def closeEvent(self, event):
self.phone1_client.stop()
self.phone2_client.stop()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = PhoneUI()
window.show()
sys.exit(app.exec_())

View File

@ -1,196 +0,0 @@
import socket
import logging
from dissononce.processing.impl.handshakestate import HandshakeState
from dissononce.processing.impl.symmetricstate import SymmetricState
from dissononce.processing.impl.cipherstate import CipherState
from dissononce.processing.handshakepatterns.interactive.XK import XKHandshakePattern
from dissononce.cipher.chachapoly import ChaChaPolyCipher
from dissononce.dh.x25519.x25519 import X25519DH
from dissononce.dh.keypair import KeyPair
from dissononce.dh.x25519.public import PublicKey
from dissononce.hash.sha256 import SHA256Hash
# Configure root logger for debug output
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
class NoiseXKSession:
@staticmethod
def generate_keypair() -> KeyPair:
"""
Generate a static X25519 KeyPair.
Returns:
KeyPair object with .private and .public attributes.
"""
return X25519DH().generate_keypair()
def __init__(self, local_kp: KeyPair, peer_pubkey: PublicKey):
"""
Initialize with our KeyPair and the peer's PublicKey.
"""
self.local_kp: KeyPair = local_kp
self.peer_pubkey: PublicKey = peer_pubkey
# Build the Noise handshake state (X25519 DH, ChaChaPoly cipher, SHA256 hash)
cipher = ChaChaPolyCipher()
dh = X25519DH()
hshash = SHA256Hash()
symmetric = SymmetricState(CipherState(cipher), hshash)
self._hs = HandshakeState(symmetric, dh)
self._send_cs = None # type: CipherState
self._recv_cs = None
def handshake(self, sock: socket.socket, initiator: bool) -> None:
"""
Perform the XK handshake over the socket. Branches on initiator/responder
so that each side reads or writes in the correct message order.
On completion, self._send_cs and self._recv_cs hold the two CipherStates.
"""
logging.debug(f"[handshake] start (initiator={initiator})")
# initialize with our KeyPair and their PublicKey
if initiator:
# initiator knows peers static out-of-band
self._hs.initialize(
XKHandshakePattern(),
True,
b'',
s=self.local_kp,
rs=self.peer_pubkey
)
else:
logging.debug("[handshake] responder initializing without rs")
# responder must NOT supply rs here
self._hs.initialize(
XKHandshakePattern(),
False,
b'',
s=self.local_kp
)
cs_pair = None
if initiator:
# 1) -> e
buf1 = bytearray()
cs_pair = self._hs.write_message(b'', buf1)
logging.debug(f"[-> e] {buf1.hex()}")
self._send_all(sock, buf1)
# 2) <- e, es, s, ss
msg2 = self._recv_all(sock)
logging.debug(f"[<- msg2] {msg2.hex()}")
self._hs.read_message(msg2, bytearray())
# 3) -> se (final)
buf3 = bytearray()
cs_pair = self._hs.write_message(b'', buf3)
logging.debug(f"[-> se] {buf3.hex()}")
self._send_all(sock, buf3)
else:
# 1) <- e
msg1 = self._recv_all(sock)
logging.debug(f"[<- e] {msg1.hex()}")
self._hs.read_message(msg1, bytearray())
# 2) -> e, es, s, ss
buf2 = bytearray()
cs_pair = self._hs.write_message(b'', buf2)
logging.debug(f"[-> msg2] {buf2.hex()}")
self._send_all(sock, buf2)
# 3) <- se (final)
msg3 = self._recv_all(sock)
logging.debug(f"[<- se] {msg3.hex()}")
cs_pair = self._hs.read_message(msg3, bytearray())
# on the final step, we must get exactly two CipherStates
if not cs_pair or len(cs_pair) != 2:
raise RuntimeError("Handshake did not complete properly")
cs0, cs1 = cs_pair
# the library returns (cs_encrypt_for_initiator, cs_decrypt_for_initiator)
if initiator:
# initiator: cs0 encrypts, cs1 decrypts
self._send_cs, self._recv_cs = cs0, cs1
else:
# responder must swap
self._send_cs, self._recv_cs = cs1, cs0
# dump the raw symmetric keys & nonces (if available)
self._dump_cipherstate("HANDSHAKE→ SEND", self._send_cs)
self._dump_cipherstate("HANDSHAKE→ RECV", self._recv_cs)
def send(self, sock: socket.socket, plaintext: bytes) -> None:
"""
Encrypt and send a message.
"""
if self._send_cs is None:
raise RuntimeError("Handshake not complete")
ct = self._send_cs.encrypt_with_ad(b'', plaintext)
logging.debug(f"[ENCRYPT] {ct.hex()}")
# self._dump_cipherstate("SEND→ after encrypt", self._send_cs)
self._send_all(sock, ct)
def receive(self, sock: socket.socket) -> bytes:
"""
Receive and decrypt a message.
"""
if self._recv_cs is None:
raise RuntimeError("Handshake not complete")
ct = self._recv_all(sock)
logging.debug(f"[CIPHERTEXT] {ct.hex()}")
# self._dump_cipherstate("RECV→ before decrypt", self._recv_cs)
pt = self._recv_cs.decrypt_with_ad(b'', ct)
logging.debug(f"[DECRYPT] {pt!r}")
return pt
def decrypt(self, ciphertext: bytes) -> bytes:
"""
Decrypt a ciphertext received as bytes.
"""
if self._recv_cs is None:
raise RuntimeError("Handshake not complete")
# Remove 2-byte length prefix if present
if len(ciphertext) >= 2 and int.from_bytes(ciphertext[:2], 'big') == len(ciphertext) - 2:
logging.debug(f"[DECRYPT] Stripping 2-byte length prefix from {len(ciphertext)}-byte input")
ciphertext = ciphertext[2:]
logging.debug(f"[CIPHERTEXT] {ciphertext.hex()}")
# self._dump_cipherstate("DECRYPT→ before decrypt", self._recv_cs)
pt = self._recv_cs.decrypt_with_ad(b'', ciphertext)
logging.debug(f"[DECRYPT] {pt!r}")
return pt
def _send_all(self, sock: socket.socket, data: bytes) -> None:
# Length-prefix (2 bytes big-endian) + data
length = len(data).to_bytes(2, 'big')
logging.debug(f"[SEND] length={length.hex()}, data={data.hex()}")
sock.sendall(length + data)
def _recv_all(self, sock: socket.socket) -> bytes:
# Read 2-byte length prefix, then the payload
hdr = self._read_exact(sock, 2)
length = int.from_bytes(hdr, 'big')
logging.debug(f"[RECV] length={length} ({hdr.hex()})")
data = self._read_exact(sock, length)
logging.debug(f"[RECV] data={data.hex()}")
return data
@staticmethod
def _read_exact(sock: socket.socket, n: int) -> bytes:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("Socket closed during read")
buf.extend(chunk)
return bytes(buf)
def _dump_cipherstate(self, label: str, cs: CipherState) -> None:
"""
Print the symmetric key (cs._k) and nonce counter (cs._n) for inspection.
"""
key = cs._key
nonce = getattr(cs, "_n", None)
if isinstance(key, (bytes, bytearray)):
key_hex = key.hex()
else:
key_hex = repr(key)
logging.debug(f"[{label}] key={key_hex}")

View File

@ -1,46 +0,0 @@
import random
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import QTimer, QSize, QPointF
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush
class WaveformWidget(QWidget):
def __init__(self, parent=None, dynamic=False):
super().__init__(parent)
self.dynamic = dynamic
self.setMinimumSize(200, 80)
self.setMaximumHeight(100)
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
if self.dynamic:
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_waveform)
self.timer.start(100)
def update_waveform(self):
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
self.update()
def set_data(self, data):
amplitude = sum(byte for byte in data) % 90 + 10
self.waveform_data = self.waveform_data[1:] + [amplitude]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor("#2D2D2D"))
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor("#0078D4"))
gradient.setColorAt(1.0, QColor("#50E6A4"))
pen = QPen(QBrush(gradient), 2)
painter.setPen(pen)
bar_width = self.width() / len(self.waveform_data)
max_h = self.height() - 10
for i, val in enumerate(self.waveform_data):
bar_height = (val / 100.0) * max_h
x = i * bar_width
y = (self.height() - bar_height) / 2
painter.drawLine(QPointF(x + bar_width / 2, y), QPointF(x + bar_width / 2, y + bar_height))
def resizeEvent(self, event):
super().resizeEvent(event)
self.update()

View File

@ -1,13 +0,0 @@
simulator/
├── gsm_simulator.py # gsm_simulator
├── launch_gsm_simulator.sh # use to start docker and simulator, run in terminal
2 clients nect to gsm_simulator and simulate a call using noise protocol
UI/
├── main.py # UI setup and event handling
├── phone_manager.py # Phone state, client init, audio logic
├── phone_client.py # Socket communication and threading
├── client_state.py # Client state and command processing
├── session.py # Noise XK crypto session
├── waveform_widget.py # Waveform UI component
├── phone_state.py # State constants

View File

@ -5,7 +5,7 @@ import time
def connect(): def connect():
caller_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) caller_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
caller_socket.connect(('localhost', 12345)) caller_socket.connect(('localhost', 5555))
caller_socket.send("CALLER".encode()) caller_socket.send("CALLER".encode())
print("Connected to GSM simulator as CALLER") print("Connected to GSM simulator as CALLER")
time.sleep(2) # Wait 2 seconds for receiver to connect time.sleep(2) # Wait 2 seconds for receiver to connect

View File

@ -4,7 +4,7 @@ import socket
def connect(): def connect():
receiver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) receiver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
receiver_socket.settimeout(15) # Increase timeout to 15 seconds receiver_socket.settimeout(15) # Increase timeout to 15 seconds
receiver_socket.connect(('localhost', 12345)) receiver_socket.connect(('localhost', 5555))
receiver_socket.send("RECEIVER".encode()) receiver_socket.send("RECEIVER".encode())
print("Connected to GSM simulator as RECEIVER") print("Connected to GSM simulator as RECEIVER")

View File

@ -0,0 +1,59 @@
#gsm_simulator.py
import socket
import threading
import time
HOST = "0.0.0.0"
PORT = 12345
FRAME_SIZE = 1000
FRAME_DELAY = 0.02
clients = []
def handle_client(client_sock, client_id):
print(f"Starting handle_client for Client {client_id}")
while True:
try:
other_client = clients[1 - client_id] if len(clients) == 2 else None
print(f"Client {client_id} waiting for data, other_client exists: {other_client is not None}")
data = client_sock.recv(1024)
if not data:
print(f"Client {client_id} disconnected or no data received")
break
if other_client:
for i in range(0, len(data), FRAME_SIZE):
frame = data[i:i + FRAME_SIZE]
other_client.send(frame)
time.sleep(FRAME_DELAY)
print(f"Forwarded {len(data)} bytes from Client {client_id} to Client {1 - client_id}")
except Exception as e:
print(f"Error with Client {client_id}: {e}")
break
print(f"Closing connection for Client {client_id}")
client_sock.close()
def start_simulator():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(2)
print(f"GSM Simulator listening on {HOST}:{PORT}...")
while len(clients) < 2:
client_sock, addr = server.accept()
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Keep connection alive
clients.append(client_sock)
client_id = len(clients) - 1
print(f"Client {client_id} connected from {addr}")
threading.Thread(target=handle_client, args=(client_sock, client_id), daemon=True).start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down simulator...")
for client in clients:
client.close()
server.close()
if __name__ == "__main__":
start_simulator()

Binary file not shown.

View File

@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
Integrated protocol for DryBox - combines Icing protocol with GSM simulator
Supports encrypted voice communication with 4FSK modulation
"""
import socket
import os
import time
import threading
import subprocess
import sys
import struct
from pathlib import Path
# Add parent directory to path to import protocol modules
parent_dir = str(Path(__file__).parent.parent)
current_dir = str(Path(__file__).parent)
# Remove current directory from path temporarily to avoid importing local protocol.py
if current_dir in sys.path:
sys.path.remove(current_dir)
# Add parent directory at the beginning
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
# Import from parent directory
from protocol import IcingProtocol
from voice_codec import VoiceProtocol, FSKModem, Codec2Wrapper, Codec2Mode
from encryption import encrypt_message, decrypt_message, generate_iv
import transmission
# Add current directory back
if current_dir not in sys.path:
sys.path.append(current_dir)
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class IntegratedDryBoxProtocol:
"""Integrates Icing protocol with DryBox GSM simulator"""
def __init__(self, gsm_host="localhost", gsm_port=12345, mode="sender"):
"""
Initialize integrated protocol
Args:
gsm_host: GSM simulator host
gsm_port: GSM simulator port
mode: "sender" or "receiver"
"""
self.gsm_host = gsm_host
self.gsm_port = gsm_port
self.mode = mode
# Initialize Icing protocol
self.protocol = IcingProtocol()
# GSM connection
self.gsm_socket = None
self.connected = False
# Voice processing
self.voice_protocol = None
self.modem = FSKModem(sample_rate=8000, baud_rate=600)
self.codec = Codec2Wrapper(Codec2Mode.MODE_1200)
# Audio files
self.input_file = "input.wav"
self.output_file = "received.wav"
# Threading
self.receive_thread = None
self.running = False
print(f"{GREEN}[DRYBOX]{RESET} Initialized in {mode} mode")
def connect_gsm(self):
"""Connect to GSM simulator"""
try:
self.gsm_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.gsm_socket.connect((self.gsm_host, self.gsm_port))
self.connected = True
print(f"{GREEN}[GSM]{RESET} Connected to simulator at {self.gsm_host}:{self.gsm_port}")
# Start receive thread
self.running = True
self.receive_thread = threading.Thread(target=self._receive_loop)
self.receive_thread.daemon = True
self.receive_thread.start()
return True
except Exception as e:
print(f"{RED}[ERROR]{RESET} GSM connection failed: {e}")
return False
def setup_protocol_connection(self, peer_port=None, peer_identity=None):
"""
Setup Icing protocol connection
Args:
peer_port: Port to connect to (for initiator)
peer_identity: Peer's identity public key hex (required)
"""
if peer_identity:
self.protocol.set_peer_identity(peer_identity)
if peer_port:
# Connect to peer
self.protocol.connect_to_peer(peer_port)
print(f"{GREEN}[PROTOCOL]{RESET} Connected to peer on port {peer_port}")
else:
print(f"{GREEN}[PROTOCOL]{RESET} Listening on port {self.protocol.local_port}")
return self.protocol.local_port
def initiate_key_exchange(self, cipher_type=1):
"""
Initiate key exchange with ChaCha20-Poly1305 by default
Args:
cipher_type: 0=AES-GCM, 1=ChaCha20-Poly1305
"""
print(f"{BLUE}[KEY-EXCHANGE]{RESET} Starting key exchange...")
# Enable auto mode for automatic handshake
self.protocol.configure_auto_mode(
ping_response_accept=True,
preferred_cipher=cipher_type,
active_mode=True
)
self.protocol.start_auto_mode()
# Send initial ping
self.protocol.send_ping_request(cipher_type)
# Wait for key exchange to complete
timeout = 10
start_time = time.time()
while not self.protocol.state.get("key_exchange_complete") and time.time() - start_time < timeout:
time.sleep(0.1)
if self.protocol.state.get("key_exchange_complete"):
print(f"{GREEN}[KEY-EXCHANGE]{RESET} Key exchange completed!")
print(f" Cipher: {'ChaCha20-Poly1305' if self.protocol.cipher_type == 1 else 'AES-256-GCM'}")
print(f" HKDF Key: {self.protocol.hkdf_key[:16]}...")
# Initialize voice protocol with encryption key
self.voice_protocol = VoiceProtocol(self.protocol)
return True
else:
print(f"{RED}[ERROR]{RESET} Key exchange timeout")
return False
def send_voice(self):
"""Send voice data through GSM channel"""
if not self.connected:
print(f"{RED}[ERROR]{RESET} Not connected to GSM")
return
if not self.protocol.hkdf_key:
print(f"{RED}[ERROR]{RESET} No encryption key available")
return
# Encode audio with GSM codec
if os.path.exists(self.input_file):
print(f"{BLUE}[VOICE]{RESET} Processing {self.input_file}...")
# Convert to 8kHz mono if needed
input_8k = "input_8k_mono.wav"
subprocess.run([
"sox", self.input_file, "-r", "8000", "-c", "1", input_8k
], capture_output=True)
# Read PCM audio
with open(input_8k, 'rb') as f:
# Skip WAV header (44 bytes)
f.seek(44)
pcm_data = f.read()
# Convert to samples
samples = struct.unpack(f'{len(pcm_data)//2}h', pcm_data)
# Process through voice protocol (compress, encrypt, modulate)
modulated = self.voice_protocol.process_voice_input(samples)
if modulated is not None:
# Convert float samples to bytes for transmission
if hasattr(modulated, 'tobytes'):
# numpy array
transmit_data = (modulated * 32767).astype('int16').tobytes()
else:
# array.array
transmit_data = struct.pack(f'{len(modulated)}h',
*[int(s * 32767) for s in modulated])
# Send through GSM
self.gsm_socket.send(transmit_data)
print(f"{GREEN}[VOICE]{RESET} Sent {len(transmit_data)} bytes")
# Clean up
os.remove(input_8k)
else:
print(f"{RED}[ERROR]{RESET} Voice processing failed")
else:
print(f"{RED}[ERROR]{RESET} Input file {self.input_file} not found")
def _receive_loop(self):
"""Background thread to receive data from GSM"""
self.gsm_socket.settimeout(0.5)
received_data = b""
while self.running:
try:
data = self.gsm_socket.recv(4096)
if not data:
print(f"{YELLOW}[GSM]{RESET} Connection closed")
break
received_data += data
# Process when we have enough data (at least 1 second of audio)
if len(received_data) >= 16000: # 8000 Hz * 2 bytes * 1 second
self._process_received_audio(received_data)
received_data = b""
except socket.timeout:
# Process any remaining data
if received_data:
self._process_received_audio(received_data)
received_data = b""
except Exception as e:
print(f"{RED}[ERROR]{RESET} Receive error: {e}")
break
def _process_received_audio(self, data):
"""Process received audio data"""
if not self.voice_protocol:
print(f"{YELLOW}[WARN]{RESET} Voice protocol not initialized, storing raw audio")
# Just save raw audio
with open("received_raw.pcm", "wb") as f:
f.write(data)
return
print(f"{BLUE}[RECEIVE]{RESET} Processing {len(data)} bytes...")
try:
# Convert bytes to float samples
samples = struct.unpack(f'{len(data)//2}h', data)
float_samples = [s / 32768.0 for s in samples]
# Demodulate, decrypt, decompress
pcm_output = self.voice_protocol.process_voice_output(float_samples)
if pcm_output is not None:
# Save as WAV file
self._save_wav(pcm_output, self.output_file)
print(f"{GREEN}[VOICE]{RESET} Saved decoded audio to {self.output_file}")
else:
print(f"{YELLOW}[WARN]{RESET} Could not decode audio")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Audio processing failed: {e}")
import traceback
traceback.print_exc()
def _save_wav(self, samples, filename):
"""Save PCM samples as WAV file"""
import wave
with wave.open(filename, 'wb') as wav:
wav.setnchannels(1) # Mono
wav.setsampwidth(2) # 16-bit
wav.setframerate(8000) # 8kHz
if hasattr(samples, 'tobytes'):
# numpy array
wav.writeframes(samples.tobytes())
else:
# array.array or list
if hasattr(samples, 'tobytes'):
wav.writeframes(samples.tobytes())
else:
# Convert list to bytes
wav.writeframes(struct.pack(f'{len(samples)}h', *samples))
def send_encrypted_message(self, message):
"""Send an encrypted text message"""
if self.protocol.hkdf_key:
self.protocol.send_encrypted_message(message)
print(f"{GREEN}[MESSAGE]{RESET} Sent encrypted: {message}")
else:
print(f"{RED}[ERROR]{RESET} No encryption key available")
def close(self):
"""Clean up connections"""
self.running = False
if self.receive_thread:
self.receive_thread.join(timeout=1)
if self.gsm_socket:
self.gsm_socket.close()
self.protocol.stop()
print(f"{RED}[SHUTDOWN]{RESET} Protocol closed")
def get_identity_key(self):
"""Get our identity public key"""
return self.protocol.identity_pubkey.hex()
def show_status(self):
"""Show protocol status"""
self.protocol.show_state()
def test_integrated_protocol():
"""Test the integrated protocol"""
import sys
mode = sys.argv[1] if len(sys.argv) > 1 else "sender"
# Create protocol instance
drybox = IntegratedDryBoxProtocol(mode=mode)
# Connect to GSM simulator
if not drybox.connect_gsm():
return
print(f"\n{YELLOW}=== DryBox Protocol Test ==={RESET}")
print(f"Mode: {mode}")
print(f"Identity key: {drybox.get_identity_key()[:32]}...")
if mode == "sender":
# Get receiver's identity (in real app, this would be exchanged out-of-band)
receiver_identity = input("\nEnter receiver's identity key (or press Enter to use test key): ").strip()
if not receiver_identity:
# Use a test key
receiver_identity = "b472a6f5707d4e5e9c6f7e8d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
# Setup protocol connection
peer_port = int(input("Enter peer's protocol port: "))
drybox.setup_protocol_connection(peer_port=peer_port, peer_identity=receiver_identity)
# Initiate key exchange
if drybox.initiate_key_exchange(cipher_type=1): # Use ChaCha20
# Send test message
drybox.send_encrypted_message("Hello from DryBox!")
# Send voice
time.sleep(1)
drybox.send_voice()
else: # receiver
# Setup protocol listener
port = drybox.setup_protocol_connection()
print(f"\nTell sender to connect to port: {port}")
print(f"Your identity key: {drybox.get_identity_key()}")
# Wait for connection
print("\nWaiting for connection...")
# Keep running
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n\nShutting down...")
drybox.close()
if __name__ == "__main__":
test_integrated_protocol()

View File

@ -5,8 +5,7 @@
# Variables # Variables
IMAGE_NAME="gsm-simulator" IMAGE_NAME="gsm-simulator"
CONTAINER_NAME="gsm-sim" CONTAINER_NAME="gsm-sim"
PORT="12345" PORT="5555"
LOG_FILE="gsm_simulator.log"
# Check if Docker is installed # Check if Docker is installed
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
@ -14,7 +13,7 @@ if ! command -v docker &> /dev/null; then
exit 1 exit 1
fi fi
# Check if gsm_simulator.py exists # Check if the gsm_simulator.py file exists in the current directory
if [ ! -f "gsm_simulator.py" ]; then if [ ! -f "gsm_simulator.py" ]; then
echo "Error: gsm_simulator.py not found in the current directory." echo "Error: gsm_simulator.py not found in the current directory."
echo "Please ensure gsm_simulator.py is present and try again." echo "Please ensure gsm_simulator.py is present and try again."
@ -27,16 +26,11 @@ if [ ! -f "Dockerfile" ]; then
cat <<EOF > Dockerfile cat <<EOF > Dockerfile
FROM python:3.9-slim FROM python:3.9-slim
WORKDIR /app WORKDIR /app
COPY gsm_simulator.py . COPY gsm_simulator.py /app
EXPOSE 12345
CMD ["python", "gsm_simulator.py"] CMD ["python", "gsm_simulator.py"]
EOF EOF
fi fi
# Ensure log file is writable
touch $LOG_FILE
chmod 666 $LOG_FILE
# Build the Docker image # Build the Docker image
echo "Building Docker image: $IMAGE_NAME..." echo "Building Docker image: $IMAGE_NAME..."
docker build -t $IMAGE_NAME . docker build -t $IMAGE_NAME .
@ -47,7 +41,7 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
# Stop and remove any existing container # Stop and remove any existing container with the same name
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
echo "Stopping existing container: $CONTAINER_NAME..." echo "Stopping existing container: $CONTAINER_NAME..."
docker stop $CONTAINER_NAME docker stop $CONTAINER_NAME
@ -57,12 +51,16 @@ if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then
docker rm $CONTAINER_NAME docker rm $CONTAINER_NAME
fi fi
# Clean up dangling images # Run the Docker container
docker image prune -f
# Run the Docker container interactively
echo "Launching GSM Simulator in Docker container: $CONTAINER_NAME..." echo "Launching GSM Simulator in Docker container: $CONTAINER_NAME..."
docker run -it --rm -p $PORT:$PORT --name $CONTAINER_NAME $IMAGE_NAME | tee $LOG_FILE docker run -d -p $PORT:$PORT --name $CONTAINER_NAME $IMAGE_NAME
# Note: Script will block here until container exits # Check if the container is running
echo "GSM Simulator stopped. Logs saved to $LOG_FILE." if [ $? -eq 0 ]; then
echo "GSM Simulator is running on port $PORT."
echo "Container ID: $(docker ps -q -f name=$CONTAINER_NAME)"
echo "You can now connect your external Python programs to localhost:$PORT."
else
echo "Error: Failed to launch the container."
exit 1
fi

View File

@ -0,0 +1,212 @@
import socket
import os
import time
import subprocess
import sys
from pathlib import Path
# Add parent directory to path
parent_dir = str(Path(__file__).parent.parent)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
# Import the integrated protocol
try:
# Try importing from same directory first
from .integrated_protocol import IntegratedDryBoxProtocol
HAS_INTEGRATED = True
except ImportError:
try:
# Try absolute import
from integrated_protocol import IntegratedDryBoxProtocol
HAS_INTEGRATED = True
except ImportError:
HAS_INTEGRATED = False
print("Warning: Integrated protocol not available, using basic mode")
# Configuration
HOST = "localhost"
PORT = 12345
INPUT_FILE = "input.wav"
OUTPUT_FILE = "received.wav"
# Global protocol instance
protocol_instance = None
def encrypt_data(data):
"""Encrypt data using the integrated protocol if available"""
global protocol_instance
if HAS_INTEGRATED and protocol_instance and protocol_instance.protocol.hkdf_key:
# Use ChaCha20 encryption from protocol
from encryption import encrypt_message, generate_iv
key = bytes.fromhex(protocol_instance.protocol.hkdf_key)
# Generate IV
if protocol_instance.protocol.last_iv is None:
iv = generate_iv(initial=True)
else:
iv = generate_iv(initial=False, previous_iv=protocol_instance.protocol.last_iv)
protocol_instance.protocol.last_iv = iv
# Encrypt with minimal header
encrypted = encrypt_message(
plaintext=data,
key=key,
flag=0xABCD,
retry=0,
connection_status=0,
iv=iv,
cipher_type=protocol_instance.protocol.cipher_type
)
return encrypted
else:
return data # Fallback to no encryption
def decrypt_data(data):
"""Decrypt data using the integrated protocol if available"""
global protocol_instance
if HAS_INTEGRATED and protocol_instance and protocol_instance.protocol.hkdf_key:
# Use decryption from protocol
from encryption import decrypt_message
key = bytes.fromhex(protocol_instance.protocol.hkdf_key)
try:
decrypted = decrypt_message(data, key, protocol_instance.protocol.cipher_type)
return decrypted
except Exception as e:
print(f"Decryption failed: {e}")
return data
else:
return data # Fallback to no decryption
def run_protocol(send_mode=True):
"""Connect to the simulator and send/receive data."""
global protocol_instance
# Initialize integrated protocol if available
if HAS_INTEGRATED:
mode = "sender" if send_mode else "receiver"
protocol_instance = IntegratedDryBoxProtocol(gsm_host=HOST, gsm_port=PORT, mode=mode)
# For testing, use predefined keys
if send_mode:
# Sender needs receiver's identity
receiver_identity = "b472a6f5707d4e5e9c6f7e8d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
protocol_instance.setup_protocol_connection(peer_port=40000, peer_identity=receiver_identity)
# Try to establish key exchange
if protocol_instance.initiate_key_exchange(cipher_type=1):
print("Key exchange successful, using encrypted communication")
else:
print("Key exchange failed, falling back to unencrypted")
else:
# Receiver listens
port = protocol_instance.setup_protocol_connection()
print(f"Protocol listening on port {port}")
# Original GSM connection
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
print(f"Connected to simulator at {HOST}:{PORT}")
if send_mode:
# Check if we should use integrated voice processing
if HAS_INTEGRATED and protocol_instance and protocol_instance.protocol.hkdf_key:
# Use integrated voice processing with encryption and FSK
print("Using integrated voice protocol with encryption and 4FSK modulation")
protocol_instance.gsm_socket = sock
protocol_instance.connected = True
protocol_instance.send_voice()
else:
# Fallback to original GSM-only mode
print("Using basic GSM mode (no encryption)")
# Sender mode: Encode audio with toast
os.system(f"toast -p -l {INPUT_FILE}") # Creates input.wav.gsm
input_gsm_file = f"{INPUT_FILE}.gsm"
if not os.path.exists(input_gsm_file):
print(f"Error: {input_gsm_file} not created")
sock.close()
return
with open(input_gsm_file, "rb") as f:
voice_data = f.read()
encrypted_data = encrypt_data(voice_data)
sock.send(encrypted_data)
print(f"Sent {len(encrypted_data)} bytes")
os.remove(input_gsm_file) # Clean up
else:
# Receiver mode
if HAS_INTEGRATED and protocol_instance:
# Use integrated receiver with decryption
print("Using integrated voice protocol receiver")
protocol_instance.gsm_socket = sock
protocol_instance.connected = True
protocol_instance.running = True
# Start receive thread
import threading
receive_thread = threading.Thread(target=protocol_instance._receive_loop)
receive_thread.daemon = True
receive_thread.start()
# Wait for data
try:
time.sleep(30) # Wait up to 30 seconds
except KeyboardInterrupt:
pass
else:
# Fallback to original receiver
print("Using basic GSM receiver (no decryption)")
print("Waiting for data from sender...")
received_data = b""
sock.settimeout(5.0)
try:
while True:
print("Calling recv()...")
data = sock.recv(1024)
print(f"Received {len(data)} bytes")
if not data:
print("Connection closed by sender or simulator")
break
received_data += data
except socket.timeout:
print("Timed out waiting for data")
if received_data:
with open("received.gsm", "wb") as f:
f.write(decrypt_data(received_data))
print(f"Wrote {len(received_data)} bytes to received.gsm")
# Decode with untoast, then convert to WAV with sox
result = subprocess.run(["untoast", "received.gsm"], capture_output=True, text=True)
print(f"untoast return code: {result.returncode}")
print(f"untoast stderr: {result.stderr}")
if result.returncode == 0:
if os.path.exists("received"):
# Convert raw PCM to WAV (8 kHz, mono, 16-bit)
subprocess.run(["sox", "-t", "raw", "-r", "8000", "-e", "signed", "-b", "16", "-c", "1", "received",
OUTPUT_FILE])
os.remove("received")
print(f"Received and saved {len(received_data)} bytes to {OUTPUT_FILE}")
else:
print("Error: 'received' file not created by untoast")
else:
print(f"untoast failed: {result.stderr}")
else:
print("No data received from simulator")
sock.close()
# Clean up protocol instance
if protocol_instance:
protocol_instance.close()
if __name__ == "__main__":
mode = input("Enter 'send' to send data or 'receive' to receive data: ").strip().lower()
run_protocol(send_mode=(mode == "send"))

View File

@ -0,0 +1,39 @@
#!/bin/bash
# Launcher script for DryBox integrated protocol
echo "DryBox Integrated Protocol Launcher"
echo "==================================="
echo ""
echo "1. Start GSM Simulator"
echo "2. Run Integrated UI"
echo "3. Run Sender (CLI)"
echo "4. Run Receiver (CLI)"
echo "5. Run Test Suite"
echo ""
read -p "Select option (1-5): " choice
case $choice in
1)
echo "Starting GSM simulator..."
python3 gsm_simulator.py
;;
2)
echo "Starting integrated UI..."
python3 UI/integrated_ui.py
;;
3)
echo "Starting sender..."
python3 integrated_protocol.py sender
;;
4)
echo "Starting receiver..."
python3 integrated_protocol.py receiver
;;
5)
echo "Running test suite..."
cd .. && python3 test_drybox_integration.py
;;
*)
echo "Invalid option"
;;
esac

View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Simple auto test for the integrated UI - tests basic functionality"""
import sys
import time
import subprocess
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
from integrated_protocol import IntegratedDryBoxProtocol
def test_basic_connection():
"""Test basic protocol connection and key exchange"""
print("="*50)
print("Simple Auto Test")
print("="*50)
# Create two protocol instances
print("\n1. Creating protocol instances...")
phone1 = IntegratedDryBoxProtocol(mode="sender")
phone2 = IntegratedDryBoxProtocol(mode="receiver")
print(f"✓ Phone 1 (sender) created - Port: {phone1.protocol.local_port}")
print(f"✓ Phone 2 (receiver) created - Port: {phone2.protocol.local_port}")
# Exchange identities
print("\n2. Exchanging identities...")
phone1_id = phone1.get_identity_key()
phone2_id = phone2.get_identity_key()
print(f"✓ Phone 1 identity: {phone1_id[:32]}...")
print(f"✓ Phone 2 identity: {phone2_id[:32]}...")
# Connect to GSM simulator
print("\n3. Connecting to GSM simulator...")
# Check if GSM simulator is running by trying to connect
import socket
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(1)
test_sock.connect(("localhost", 12345))
test_sock.close()
print("✓ GSM simulator is running")
except:
print("❌ GSM simulator not running. Start it with: python3 gsm_simulator.py")
return False
if not phone1.connect_gsm():
print("❌ Phone 1 failed to connect to GSM")
return False
if not phone2.connect_gsm():
print("❌ Phone 2 failed to connect to GSM")
return False
print("✓ Both phones connected to GSM simulator")
# Setup protocol connections
print("\n4. Setting up protocol connections...")
phone1.setup_protocol_connection(
peer_port=phone2.protocol.local_port,
peer_identity=phone2_id
)
phone2.setup_protocol_connection(
peer_port=phone1.protocol.local_port,
peer_identity=phone1_id
)
time.sleep(1) # Give connections time to establish
print("✓ Protocol connections established")
# Test ChaCha20 key exchange
print("\n5. Testing ChaCha20-Poly1305 key exchange...")
if phone1.initiate_key_exchange(cipher_type=1):
print("✓ Key exchange successful")
print(f" Cipher: ChaCha20-Poly1305")
print(f" HKDF Key: {phone1.protocol.hkdf_key[:32]}...")
# Send test message
print("\n6. Testing encrypted message...")
test_msg = "Hello from automated test!"
phone1.send_encrypted_message(test_msg)
time.sleep(1)
print(f"✓ Sent encrypted message: {test_msg}")
# Test voice if available
if Path("input.wav").exists():
print("\n7. Testing voice transmission...")
phone1.send_voice()
print("✓ Voice transmission initiated")
else:
print("\n7. Skipping voice test (input.wav not found)")
else:
print("❌ Key exchange failed")
return False
# Cleanup
print("\n8. Cleaning up...")
phone1.close()
phone2.close()
print("✓ Protocols closed")
print("\n" + "="*50)
print("✓ All tests passed!")
print("="*50)
return True
if __name__ == "__main__":
if test_basic_connection():
sys.exit(0)
else:
sys.exit(1)

View File

@ -1,85 +0,0 @@
import socket
import threading
import time
HOST = "0.0.0.0"
PORT = 12345
FRAME_SIZE = 1000
FRAME_DELAY = 0.02
clients = []
clients_lock = threading.Lock()
def handle_client(client_sock, client_id):
print(f"Starting handle_client for Client {client_id}")
try:
while True:
other_client = None
with clients_lock:
if len(clients) == 2 and client_id < len(clients):
other_client = clients[1 - client_id]
print(f"Client {client_id} waiting for data, other_client exists: {other_client is not None}")
try:
data = client_sock.recv(1024)
if not data:
print(f"Client {client_id} disconnected or no data received")
break
if other_client:
for i in range(0, len(data), FRAME_SIZE):
frame = data[i:i + FRAME_SIZE]
other_client.send(frame)
time.sleep(FRAME_DELAY)
print(f"Forwarded {len(data)} bytes from Client {client_id} to Client {1 - client_id}")
except socket.error as e:
print(f"Socket error with Client {client_id}: {e}")
break
finally:
print(f"Closing connection for Client {client_id}")
with clients_lock:
if client_id < len(clients) and clients[client_id] == client_sock:
clients.pop(client_id)
print(f"Removed Client {client_id} from clients list")
client_sock.close()
def start_simulator():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen(2)
print(f"GSM Simulator listening on {HOST}:{PORT}...")
try:
while True:
client_sock, addr = server.accept()
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Keep connection alive
with clients_lock:
if len(clients) < 2:
clients.append(client_sock)
client_id = len(clients) - 1
else:
# Replace a closed socket or reject if both slots are active
replaced = False
for i in range(len(clients)):
if clients[i].fileno() == -1: # Check if socket is closed
clients[i] = client_sock
client_id = i
replaced = True
break
if not replaced:
print(f"Rejecting new client from {addr}: max clients (2) reached")
client_sock.close()
continue
print(f"Client {client_id} connected from {addr}")
threading.Thread(target=handle_client, args=(client_sock, client_id), daemon=True).start()
except KeyboardInterrupt:
print("Shutting down simulator...")
finally:
with clients_lock:
for client in clients:
client.close()
clients.clear()
server.close()
if __name__ == "__main__":
start_simulator()

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Test script to verify the auto-test button functionality"""
import sys
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
# Check if UI components are available
try:
from PyQt5.QtWidgets import QApplication
print("✓ PyQt5 is available")
except ImportError:
print("✗ PyQt5 is not available. Install with: pip install PyQt5")
sys.exit(1)
# Check if protocol components are available
try:
from integrated_protocol import IntegratedDryBoxProtocol
from UI.integrated_ui import IntegratedPhoneUI
print("✓ Protocol components available")
except ImportError as e:
print(f"✗ Failed to import protocol components: {e}")
sys.exit(1)
# Verify auto-test functionality
print("\nAuto-test button functionality:")
print("- Automatically detects and fills peer ports")
print("- Tests AES-256-GCM encryption")
print("- Tests ChaCha20-Poly1305 encryption")
print("- Tests voice transmission (if input.wav exists)")
print("- Provides comprehensive status logging")
print("\nTo run the UI with auto-test:")
print("1. cd DryBox")
print("2. python3 UI/integrated_ui.py")
print("3. Click 'Start GSM Simulator'")
print("4. Click 'Run Auto Test' (green button)")
print("\n✓ Auto-test functionality is already implemented!")

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Basic test of DryBox integrated protocol without UI"""
import sys
import time
from pathlib import Path
# Setup imports
sys.path.insert(0, str(Path(__file__).parent))
from integrated_protocol import IntegratedDryBoxProtocol
def test_protocol_creation():
"""Test creating protocol instances"""
print("Testing protocol creation...")
# Create sender
sender = IntegratedDryBoxProtocol(mode="sender")
print(f"✓ Sender created")
print(f" Identity: {sender.get_identity_key()[:32]}...")
# Create receiver
receiver = IntegratedDryBoxProtocol(mode="receiver")
print(f"✓ Receiver created")
print(f" Identity: {receiver.get_identity_key()[:32]}...")
# Show protocol info
print(f"\nProtocol Information:")
print(f" Sender port: {sender.protocol.local_port}")
print(f" Receiver port: {receiver.protocol.local_port}")
print(f" Cipher support: AES-256-GCM, ChaCha20-Poly1305")
print(f" Voice codec: Codec2 @ 1200 bps")
print(f" Modulation: 4-FSK @ 600 baud")
return True
if __name__ == "__main__":
print("DryBox Basic Functionality Test")
print("="*50)
if test_protocol_creation():
print("\n✓ All basic tests passed!")
print("\nTo run the full system:")
print("1. Start GSM simulator: python3 gsm_simulator.py")
print("2. Run UI: python3 UI/integrated_ui.py")
print("3. Or run CLI: python3 integrated_protocol.py [sender|receiver]")
else:
print("\n✗ Tests failed!")
sys.exit(1)

View File

@ -1,86 +0,0 @@
import socket
import os
import time
import subprocess
# Configuration
HOST = "localhost"
PORT = 12345
INPUT_FILE = "wav/input.wav"
OUTPUT_FILE = "wav/received.wav"
def encrypt_data(data):
return data # Replace with your encryption protocol
def decrypt_data(data):
return data # Replace with your decryption protocol
def run_protocol(send_mode=True):
"""Connect to the simulator and send/receive data."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
print(f"Connected to simulator at {HOST}:{PORT}")
if send_mode:
# Sender mode: Encode audio with toast
os.system(f"toast -p -l {INPUT_FILE}") # Creates input.wav.gsm
input_gsm_file = f"{INPUT_FILE}.gsm"
if not os.path.exists(input_gsm_file):
print(f"Error: {input_gsm_file} not created")
sock.close()
return
with open(input_gsm_file, "rb") as f:
voice_data = f.read()
encrypted_data = encrypt_data(voice_data)
sock.send(encrypted_data)
print(f"Sent {len(encrypted_data)} bytes")
os.remove(input_gsm_file) # Clean up
else:
# Receiver mode: Wait for and receive data
print("Waiting for data from sender...")
received_data = b""
sock.settimeout(5.0)
try:
while True:
print("Calling recv()...")
data = sock.recv(1024)
print(f"Received {len(data)} bytes")
if not data:
print("Connection closed by sender or simulator")
break
received_data += data
except socket.timeout:
print("Timed out waiting for data")
if received_data:
with open("received.gsm", "wb") as f:
f.write(decrypt_data(received_data))
print(f"Wrote {len(received_data)} bytes to received.gsm")
# Decode with untoast, then convert to WAV with sox
result = subprocess.run(["untoast", "received.gsm"], capture_output=True, text=True)
print(f"untoast return code: {result.returncode}")
print(f"untoast stderr: {result.stderr}")
if result.returncode == 0:
if os.path.exists("received"):
# Convert raw PCM to WAV (8 kHz, mono, 16-bit)
subprocess.run(["sox", "-t", "raw", "-r", "8000", "-e", "signed", "-b", "16", "-c", "1", "received",
OUTPUT_FILE])
os.remove("received")
print(f"Received and saved {len(received_data)} bytes to {OUTPUT_FILE}")
else:
print("Error: 'received' file not created by untoast")
else:
print(f"untoast failed: {result.stderr}")
else:
print("No data received from simulator")
sock.close()
if __name__ == "__main__":
mode = input("Enter 'send' to send data or 'receive' to receive data: ").strip().lower()
run_protocol(send_mode=(mode == "send"))

View File

@ -1,4 +1,4 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.16 Chrome/132.0.6834.196 Electron/34.2.0 Safari/537.36" version="26.0.16" pages="4"> <mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/27.0.9 Chrome/134.0.6998.205 Electron/35.4.0 Safari/537.36" version="27.0.9" pages="3">
<diagram id="C5RBs43oDa-KdzZeNtuy" name="Logique"> <diagram id="C5RBs43oDa-KdzZeNtuy" name="Logique">
<mxGraphModel dx="735" dy="407" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0"> <mxGraphModel dx="735" dy="407" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root> <root>
@ -259,16 +259,8 @@
</root> </root>
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>
<diagram id="c7L-flsM9ZWaCx455Pfy" name="Transport Layer - 0">
<mxGraphModel dx="1434" dy="835" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
</root>
</mxGraphModel>
</diagram>
<diagram id="4Sb7mgJDpsadGym-U4wz" name="Protocol Layer - 0"> <diagram id="4Sb7mgJDpsadGym-U4wz" name="Protocol Layer - 0">
<mxGraphModel dx="1434" dy="835" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> <mxGraphModel dx="1188" dy="637" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root> <root>
<mxCell id="0" /> <mxCell id="0" />
<mxCell id="1" parent="0" /> <mxCell id="1" parent="0" />
@ -284,55 +276,9 @@
<mxCell id="b_xV4iUWIxmdZCAYY4YR-2" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1"> <mxCell id="b_xV4iUWIxmdZCAYY4YR-2" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
<mxGeometry x="385" y="65" width="80" height="40" as="geometry" /> <mxGeometry x="385" y="65" width="80" height="40" as="geometry" />
</mxCell> </mxCell>
<mxCell id="n3lF8vaYaHAhAfaeaFZn-1" value="Nonce" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="180" y="130" width="105" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="n3lF8vaYaHAhAfaeaFZn-6" value="&lt;div&gt;sha256 (&lt;/div&gt;&lt;div&gt;numéro alice +&lt;/div&gt;&lt;div&gt;numéro bob +&lt;/div&gt;&lt;div&gt;timestamp +&lt;/div&gt;&lt;div&gt;random&lt;br&gt;&lt;/div&gt;&lt;div&gt;) / ~2 (left part)&lt;br&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=7;" parent="n3lF8vaYaHAhAfaeaFZn-1" vertex="1">
<mxGeometry y="25" width="100" height="55" as="geometry" />
</mxCell>
<mxCell id="n3lF8vaYaHAhAfaeaFZn-2" value="Version" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="305" y="130" width="58.75" height="80" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-1" value="(0-128)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="n3lF8vaYaHAhAfaeaFZn-2" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="n3lF8vaYaHAhAfaeaFZn-4" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="455" y="130" width="90" height="80" as="geometry" />
</mxCell>
<mxCell id="n3lF8vaYaHAhAfaeaFZn-7" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="n3lF8vaYaHAhAfaeaFZn-4" vertex="1">
<mxGeometry x="15" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-1" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="1" vertex="1"> <mxCell id="XvZTtdEB18xY6m2a5fJO-1" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="1" vertex="1">
<mxGeometry x="280" y="280" width="410" height="190" as="geometry" /> <mxGeometry x="280" y="280" width="410" height="190" as="geometry" />
</mxCell> </mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-2" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="300" y="290" width="90" height="60" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-3" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="XvZTtdEB18xY6m2a5fJO-2" vertex="1">
<mxGeometry x="11.25" y="27.5" width="67.5" height="25" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-4" value="Version" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="405.63" y="290" width="58.75" height="60" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-11" value="0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-4" vertex="1">
<mxGeometry y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-5" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="590" y="380" width="85" height="60" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-6" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-5" vertex="1">
<mxGeometry x="12.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-9" value="Answer" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="482.5" y="290" width="57.5" height="60" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-10" value="&lt;div&gt;YES&lt;/div&gt;&lt;div&gt;NO&lt;br&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="484.38" y="315" width="53.75" height="30" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-13" value="HANDSHAKE" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1"> <mxCell id="XvZTtdEB18xY6m2a5fJO-13" value="HANDSHAKE" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
<mxGeometry x="350" y="510" width="170" height="40" as="geometry" /> <mxGeometry x="350" y="510" width="170" height="40" as="geometry" />
</mxCell> </mxCell>
@ -416,33 +362,6 @@
<mxCell id="pP7SjZfcCiBg3d1TCkzP-59" value="ENCRYPTED COMS" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1"> <mxCell id="pP7SjZfcCiBg3d1TCkzP-59" value="ENCRYPTED COMS" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
<mxGeometry x="305" y="1100" width="240" height="40" as="geometry" /> <mxGeometry x="305" y="1100" width="240" height="40" as="geometry" />
</mxCell> </mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-60" value="129b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="200" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-61" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="303.75" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-62" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="470" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-63" value="= 172b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="530" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-66" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="313" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-67" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="406.75" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-68" value="1b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="479.25" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-69" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="600.5" y="440" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-70" value="= 76b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="426.25" y="420" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-71" value="264b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> <mxCell id="pP7SjZfcCiBg3d1TCkzP-71" value="264b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="193" y="660" width="60" height="30" as="geometry" /> <mxGeometry x="193" y="660" width="60" height="30" as="geometry" />
</mxCell> </mxCell>
@ -514,24 +433,6 @@
<mxCell id="_H5URFloX_BVB2BL7kO6-16" value="= (180b ~ 212b) + yyy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> <mxCell id="_H5URFloX_BVB2BL7kO6-16" value="= (180b ~ 212b) + yyy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="465" y="1285" width="130" height="30" as="geometry" /> <mxGeometry x="465" y="1285" width="130" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-2" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="375" y="130" width="58.75" height="80" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-3" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-2" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-4" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="375" y="210" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-5" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="600" y="290" width="58.75" height="60" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-6" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-5" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-7" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="601.88" y="350" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-8" value="status" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1"> <mxCell id="pWkGvNQAXuiST1IiWYlx-8" value="status" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="425" y="1170" width="65" height="60" as="geometry" /> <mxGeometry x="425" y="1170" width="65" height="60" as="geometry" />
</mxCell> </mxCell>
@ -572,233 +473,257 @@
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>
<diagram name="Protocol Layer - 1" id="_rkrwzJg5buKJxYS8faK"> <diagram name="Protocol Layer - 1" id="_rkrwzJg5buKJxYS8faK">
<mxGraphModel dx="1062" dy="1719" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> <mxGraphModel dx="4004" dy="2923" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root> <root>
<mxCell id="0ZE-dMPOFneTTZtpNr96-0" /> <mxCell id="0ZE-dMPOFneTTZtpNr96-0" />
<mxCell id="0ZE-dMPOFneTTZtpNr96-1" parent="0ZE-dMPOFneTTZtpNr96-0" /> <mxCell id="0ZE-dMPOFneTTZtpNr96-1" parent="0ZE-dMPOFneTTZtpNr96-0" />
<mxCell id="0ZE-dMPOFneTTZtpNr96-2" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-2" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="160" y="120" width="260" height="120" as="geometry" /> <mxGeometry x="160" y="140" width="170" height="80" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-3" value="&lt;font style=&quot;color: rgb(255, 65, 27);&quot;&gt;&lt;b&gt;ALICE&amp;nbsp; (INITIATOR)&lt;/b&gt;&lt;/font&gt;" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};participant=umlEntity;strokeWidth=2;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-3" value="&lt;font style=&quot;color: rgb(255, 65, 27);&quot;&gt;&lt;b&gt;ALICE&amp;nbsp; (INITIATOR)&lt;/b&gt;&lt;/font&gt;" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};participant=umlEntity;strokeWidth=2;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="120" y="40" width="40" height="3110" as="geometry" /> <mxGeometry x="120" y="40" width="40" height="3110" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-4" value="&lt;font&gt;BOB&lt;/font&gt;" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};participant=umlEntity;strokeWidth=2;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-4" value="&lt;font&gt;&lt;b&gt;BOB&lt;/b&gt;&lt;/font&gt;&lt;div&gt;&lt;font&gt;&lt;b&gt;(Responder)&lt;/b&gt;&lt;/font&gt;&lt;/div&gt;" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};participant=umlEntity;strokeWidth=2;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="690" y="40" width="40" height="3110" as="geometry" /> <mxGeometry x="690" y="40" width="40" height="3110" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-5" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-5" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="385" y="65" width="80" height="40" as="geometry" /> <mxGeometry x="385" y="65" width="80" height="40" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-8" value="Version" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-8" value="InitFlag" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="171.25" y="130" width="58.75" height="80" as="geometry" /> <mxGeometry x="214.25" y="145" width="58.75" height="70" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-9" value="(0-128)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-8"> <mxCell id="0ZE-dMPOFneTTZtpNr96-9" value="3" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="0ZE-dMPOFneTTZtpNr96-8" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" /> <mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-12" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-12" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="350" y="280" width="340" height="190" as="geometry" /> <mxGeometry x="460" y="290" width="230" height="90" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-13" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="0ZE-dMPOFneTTZtpNr96-21" value="DATA TRANSMISSION" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="380" y="290" width="90" height="60" as="geometry"> <mxGeometry x="303" y="1090" width="260" height="40" as="geometry" />
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-14" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-13"> <mxCell id="0ZE-dMPOFneTTZtpNr96-22" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="11.25" y="27.5" width="67.5" height="25" as="geometry" /> <mxGeometry x="155.5" y="1160" width="294.5" height="110" as="geometry" />
</mxCell> </mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-15" value="Version" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1"> <mxCell id="1WmqBiAd3lf2sxgMsw2H-0" value="Best Case Scenario&lt;div&gt;Noise_XK_25519_ChaChaPoly_SHA256&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=25;fontStyle=1" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="479.76" y="290" width="58.75" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-16" value="0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-15">
<mxGeometry y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-19" value="Answer" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="548.76" y="290" width="57.5" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-20" value="&lt;div&gt;YES&lt;/div&gt;&lt;div&gt;NO&lt;br&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="533.76" y="315" width="53.75" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-21" value="HANDSHAKE" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="350" y="510" width="170" height="40" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-22" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="160" y="570" width="410" height="220" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-23" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="170" y="580" width="105" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-24" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-23">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-25" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="285" y="580" width="105" height="80" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-29" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="402.81" y="580" width="71.88" height="80" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-31" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="285" y="830" width="410" height="180" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-32" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="305" y="840" width="105" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-33" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-32">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-34" value="" style="swimlane;whiteSpace=wrap;html=1;startSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="420" y="840" width="105" height="80" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-38" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="537.81" y="840" width="71.88" height="80" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-40" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="182.5" y="690" width="80" height="70" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-41" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-40">
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-42" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="606.25" y="930" width="80" height="70" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-43" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-42">
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-44" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="160" y="1160" width="450" height="200" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-45" value="ENCRYPTED COMS" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="305" y="1100" width="240" height="40" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-47" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="170" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-49" value="= 48b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="350" y="210" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-50" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="393" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-51" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="479.13" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-52" value="1b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="545.51" y="350" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-54" value="= 32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="465" y="395" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-55" value="264b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="193" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-56" value="512b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="307.5" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-57" value="256b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="409.38" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-59" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="192.5" y="760" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-60" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="315" y="750" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-61" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="327.5" y="970" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-66" value="nbretry" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="344.38" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-67" value="y" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-66">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-70" value="msg" style="swimlane;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="187.5" y="1270" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-71" value="BBB" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-70">
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-73" value="8b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="349.38" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-77" value="= (180b ~ 212b) + yyy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="465" y="1285" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-78" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="240" y="130" width="58.75" height="80" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-79" value="(0-32)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-78">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-80" value="5b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="240" y="210" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-81" value="Cypher Offset" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="616.88" y="290" width="58.75" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-82" value="(0-8)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-81">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-83" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="618.76" y="350" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-84" value="status" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="425" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-85" value="CRC ?" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-84">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-86" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="428.75" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-89" value="BBB b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="193" y="1330" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-90" value="MAC" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="286.13" y="1270" width="63.25" height="60" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-91" value="AEAD" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-90">
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-92" value="128b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="290.25" y="1330" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-93" value="Green = clear data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="10" y="1170" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-94" value="&lt;font style=&quot;color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;White = additional data&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=none;strokeColor=light-dark(#6C8EBF,#FFFFFF);" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry y="1220" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="0ZE-dMPOFneTTZtpNr96-95" value="Blue = encrypted data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="10" y="1270" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-0" value="Quality" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="308.75" y="130" width="58.75" height="80" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-1" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="el23RTDFL3Ay36EOE6FN-0">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-2" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="308.75" y="210" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-3" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="616.88" y="440" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-4" value="Quality" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="616.88" y="380" width="58.75" height="60" as="geometry" />
</mxCell>
<mxCell id="el23RTDFL3Ay36EOE6FN-5" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="el23RTDFL3Ay36EOE6FN-4">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="1WmqBiAd3lf2sxgMsw2H-0" value="Best Case Scenario&lt;div&gt;Noise_XK_25519_ChaChaPoly_SHA256&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=25;fontStyle=1" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="223" y="-20" width="405" height="30" as="geometry" /> <mxGeometry x="223" y="-20" width="405" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-4" value="RespFlag" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="481" y="300" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-5" value="3" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="7dWv1ZeKX4zgsIkHRz9K-4" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-9" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="160" y="470" width="370" height="100" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-10" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="392" y="620" width="298" height="110" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-15" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="155.5" y="800" width="224.5" height="80" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-16" value="Start" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="170" y="805" width="59.370000000000005" height="70" as="geometry" />
</mxCell>
<mxCell id="4Znnxz_OPFGO144isFnZ-0" value="0-2" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="7dWv1ZeKX4zgsIkHRz9K-16" vertex="1">
<mxGeometry x="-0.31999999999999545" y="30" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-18" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="469" y="950" width="220" height="90" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-22" value="InitFlag / Consent" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="548" y="300" width="122" height="70" as="geometry" />
</mxCell>
<mxCell id="7dWv1ZeKX4zgsIkHRz9K-23" value="3 (+x)&amp;nbsp; || 0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="7dWv1ZeKX4zgsIkHRz9K-22" vertex="1">
<mxGeometry x="3.75" y="30" width="116.25" height="25" as="geometry" />
</mxCell>
<mxCell id="4Znnxz_OPFGO144isFnZ-1" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="239.25" y="805" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="4Znnxz_OPFGO144isFnZ-2" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="4Znnxz_OPFGO144isFnZ-1" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="4Znnxz_OPFGO144isFnZ-3" value="CompIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="246" y="485" width="69" height="70" as="geometry" />
</mxCell>
<mxCell id="4Znnxz_OPFGO144isFnZ-4" value="0-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="4Znnxz_OPFGO144isFnZ-3" vertex="1">
<mxGeometry x="8.879999999999999" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-0" value="RespFlag" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="325.5" y="485" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-1" value="3 (+x)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-0" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-2" value="QualIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="180.5" y="485" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-3" value="1-10" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-2" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-8" value="CompIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="539.07" y="640" width="69" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-9" value="0-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-8" vertex="1">
<mxGeometry x="8.879999999999999" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-10" value="QualIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="473.57" y="640" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-11" value="1-10" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-10" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-12" value="Knldge" style="swimlane;whiteSpace=wrap;html=1;startSize=23;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="393.63" y="485" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-13" value="0-2" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-12" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-14" value="Knldge" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="615.07" y="640" width="58.75" height="70" as="geometry">
<mxRectangle x="600" y="650" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-15" value="0-3" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-14" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-16" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="458.5" y="485" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-17" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-16" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-19" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="408.19" y="640" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-20" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-19" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-22" value="Start" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="487.07" y="960" width="59.370000000000005" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-23" value="0-2" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-22" vertex="1">
<mxGeometry x="-0.6299999999999955" y="35" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-24" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="556.32" y="960" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-25" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-24" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-26" value="QualIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="305.5" y="805" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-27" value="1-10" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-26" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-32" value="QualIndex" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="625.5" y="960" width="58.75" height="70" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-33" value="1-10" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="Jo51-aL9BOL5VHmLmAAK-32" vertex="1">
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-34" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="168" y="1170" width="82" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-35" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="Jo51-aL9BOL5VHmLmAAK-34" vertex="1">
<mxGeometry x="6.5" y="35" width="69" height="30" as="geometry" />
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-40" value="PayloadFrame" style="swimlane;whiteSpace=wrap;html=1;" parent="0ZE-dMPOFneTTZtpNr96-1" vertex="1">
<mxGeometry x="292.5" y="1170" width="105" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Jo51-aL9BOL5VHmLmAAK-41" value="[0 &amp;lt;= X &amp;lt;= 128, X, X, X]" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="Jo51-aL9BOL5VHmLmAAK-40" vertex="1">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-1" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;b style=&quot;font-size: 12px; text-align: center;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;Initiliatisation Flag&lt;/font&gt;&lt;/b&gt;&lt;/h1&gt;&lt;p&gt;An incrementing flag sent by the Initiator as a ping to check its peer&#39;s ability to communicate through the modulation layer.&lt;/p&gt;&lt;p&gt;Starts at 3 and increases at each check iteration.&lt;/p&gt;&lt;p&gt;I.E: Settings are set so the ping is retried X times every T ms ; InitFlag will be equal between 3 and 3 + X.&lt;/p&gt;&lt;p&gt;X is recommended to be set at the maximum of the modulation layer&#39;s frame length to fit in a single frame.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-270" y="30" width="350" height="210" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-2" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;b style=&quot;font-size: 12px; text-align: center;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;Response Flag&lt;/font&gt;&lt;/b&gt;&lt;/h1&gt;&lt;p&gt;An incrementing flag sent by the Responder to confirm its ability to communicate through the modulation layer.&lt;/p&gt;&lt;p&gt;Starts at 3 and increases at each iteration.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="780" y="275" width="280" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-3" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;b style=&quot;font-size: 12px; text-align: center;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;InitFlag / Consent&lt;/font&gt;&lt;/b&gt;&lt;/h1&gt;&lt;p&gt;Should be returned, in addition to the Response Flag, the latest Initialisation Flag captured OR a denial flag.&lt;/p&gt;&lt;p&gt;The InitFlag echo serves to calculate latency and data integrity (later summarized in a Quality Index) thanks to the potential incremental offset it contains.&lt;/p&gt;&lt;p&gt;A denial flag is a simple 0, expliciting the Responder does not want to engage in the protocol.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1080" y="275" width="420" height="175" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-4" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Quality Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 1 to 10 (a range of ten values) corresponding to a Quality Index Range value, defined in the documentation.&lt;/p&gt;&lt;p&gt;Normalization of the Quality Index helps keeping a simple yet useful information of the peer&#39;s latency and data integrity.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-790" y="395" width="280" height="160" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-5" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Compatibility Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 0 to 32 (a range of 33 values) indicating the peer&#39;s sub-protocol compatibility.&lt;br&gt;&lt;/p&gt;&lt;p&gt;Normalization of the Compatibility Index is defined in the documentation permit peer synchronisation on possible sub-protocol configuration.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-500" y="395" width="280" height="160" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-6" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;RespFlag&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;The latest Response Flag captured, which potential offset helps define the peer&#39;s Quality Index&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-210" y="395" width="230" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-7" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Identity Knowledge Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;A value from 0 to 1 (a boolean) indicating the Initiator&#39;s knowledge of any identity information about its peer.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;0 = No knowledge, probably the first connexion&amp;nbsp;&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;leading to a Noise_XX handshake pattern.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;1 = Initiator knows peer&#39;s Identity (or static public key in Noise&#39;s terms), encouraging a Noise_XK handshake pattern.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-790" y="555" width="540" height="155" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-8" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-210" y="555" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-9" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Quality Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;Responder&#39;s Quality Index.&lt;/p&gt;&lt;p&gt;Main and sub-protocols configurations are adapted to the lowest of both QIs.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="750" y="520" width="230" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-10" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Compatibility Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;Responder&#39;s Compatibility Index.&lt;/p&gt;&lt;p&gt;Sub-protocols configurations are adapted to the lowest of both CIs.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1010" y="520" width="200" height="110" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-12" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Identity Knowledge Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;A value from 0 to 1 (a boolean) indicating the Responder&#39;s knowledge of any identity information about its peer.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;0 = No knowledge, probably the first connexion leading to a Noise_XX handshake pattern.&lt;/p&gt;&lt;p&gt;1 = Responder knows peer&#39;s Identity (or static public key in Noise&#39;s terms), encouraging a Noise_XK handshake pattern.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1220" y="520" width="560" height="170" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-13" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="750" y="640" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-15" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Start Instruction&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 0 to 2 (a range of three values), indicating the peer&#39;s willingness to start the handshake pattern.&lt;/p&gt;&lt;p&gt;0 = Not ready (or don&#39;t want, but should not used as so)&lt;/p&gt;&lt;p&gt;1 = Go ; When both peers are in Go mode, Initiator initiates the handshake pattern.&lt;/p&gt;&lt;p&gt;2 = Ready, but wait ; In the case of a voluntary delay. At the end of the delay, both should peers should switch to Go&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-790" y="790" width="410" height="220" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-16" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-350" y="790" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-17" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Quality Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 1 to 10 (a range of ten values) corresponding to a Quality Index Range value, defined in the documentation.&lt;/p&gt;&lt;p&gt;Normalization of the Quality Index helps keeping a simple yet useful information of the peer&#39;s latency and data integrity.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-350" y="910" width="280" height="160" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-18" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Start Instruction&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 0 to 2 (a range of three values), indicating the peer&#39;s willingness to start the handshake pattern.&lt;/p&gt;&lt;p&gt;0 = Not ready (or don&#39;t want, but should not used as so)&lt;/p&gt;&lt;p&gt;1 = Go ; When both peers are in Go mode, Initiator initiates the handshake pattern.&lt;/p&gt;&lt;p&gt;2 = Ready, but wait ; In the case of a voluntary delay. At the end of the delay, both should peers should switch to Go&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="730" y="890" width="410" height="220" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-19" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1170" y="890" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-20" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Quality Index&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A value from 1 to 10 (a range of ten values) corresponding to a Quality Index Range value, defined in the documentation.&lt;/p&gt;&lt;p&gt;Normalization of the Quality Index helps keeping a simple yet useful information of the peer&#39;s latency and data integrity.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1170" y="1010" width="280" height="160" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-21" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-480" y="1150" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-22" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Payload Frame&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A payload frame is a sequence of 4 values from 0 to 128.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="-190" y="1150" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-23" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;direction=west;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="385" y="1375" width="294.5" height="110" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-24" value="Counter" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="410" y="1390" width="82" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-25" value="0-128" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="NK1ntCzQ9HUgUvh9xWXv-24">
<mxGeometry x="6.5" y="35" width="69" height="30" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-26" value="PayloadFrame" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="534.5" y="1390" width="105" height="80" as="geometry">
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-27" value="[0 &amp;lt;= X &amp;lt;= 128, X, X, X]" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="NK1ntCzQ9HUgUvh9xWXv-26">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-28" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Retry Counter&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A 0 to 128 incremental value, changing on every retry.&lt;/p&gt;&lt;p&gt;Sent to keep the Quality Index up to date.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="730" y="1360" width="270" height="120" as="geometry" />
</mxCell>
<mxCell id="NK1ntCzQ9HUgUvh9xWXv-29" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Payload Frame&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;A payload frame is a sequence of 4 values from 0 to 128.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
<mxGeometry x="1010" y="1360" width="270" height="120" as="geometry" />
</mxCell>
</root> </root>
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>

View File

@ -1,91 +0,0 @@
import argparse
import threading
import sys
from noise_xk.session import NoiseXKSession
from noise_xk.transport import P2PTransport
from dissononce.dh.x25519.public import PublicKey
def main():
parser = argparse.ArgumentParser(prog="noise_xk")
parser.add_argument(
"--listen-port", type=int, required=True,
help="Port on which to bind+listen"
)
parser.add_argument(
"--peer-host", type=str, default="127.0.0.1",
help="Peer host to dial (default: 127.0.0.1)"
)
parser.add_argument(
"--peer-port", type=int, required=True,
help="Peer port to dial"
)
args = parser.parse_args()
# 1. Generate static keypair and print our static public key
kp = NoiseXKSession.generate_keypair()
# kp.public is a PublicKey; .data holds raw bytes
local_priv = kp.private # carried implicitly in NoiseXKSession
local_pub = kp.public
print(f"[My static pubkey:] {local_pub.data.hex()}")
# 2. Read peer pubkey from user input
peer_pubkey = None
while True:
line = input(">>> ").strip()
if line.startswith("peer_pubkey "):
hexstr = line.split(None, 1)[1]
try:
raw = bytes.fromhex(hexstr)
peer_pubkey = PublicKey(raw) # wrap raw bytes in PublicKey
break
except ValueError:
print("Invalid hex; please retry.")
else:
print("Use: peer_pubkey <hex>")
# 3. Establish P2P connection (race listen vs. dial)
transport = P2PTransport(
listen_port=args.listen_port,
peer_host=args.peer_host,
peer_port=args.peer_port
)
print(
f"Racing connect/listen on ports "
f"{args.listen_port}{args.peer_host}:{args.peer_port}"
)
sock, initiator = transport.connect()
print(f"Connected (initiator={initiator}); performing handshake…")
# 4. Perform Noise XK handshake
session = NoiseXKSession(kp, peer_pubkey)
session.handshake(sock, initiator)
print("Handshake complete! You can now type messages.")
# 5. Reader thread for incoming messages
def reader():
while True:
try:
pt = session.receive(sock)
print(f"\n< {pt.decode()}")
except Exception as e:
print(f"\n[Receive error ({type(e).__name__}): {e!r}]")
break
thread = threading.Thread(target=reader, daemon=True)
thread.start()
# 6. Main loop: send user input
try:
for line in sys.stdin:
text = line.rstrip("\n")
if not text:
continue
session.send(sock, text.encode())
except KeyboardInterrupt:
pass
finally:
sock.close()
if __name__ == "__main__":
main()

View File

@ -1,179 +0,0 @@
# noise_xk/session.py
import socket
import logging
from dissononce.processing.impl.handshakestate import HandshakeState
from dissononce.processing.impl.symmetricstate import SymmetricState
from dissononce.processing.impl.cipherstate import CipherState
from dissononce.processing.handshakepatterns.interactive.XK import XKHandshakePattern
from dissononce.cipher.chachapoly import ChaChaPolyCipher
from dissononce.dh.x25519.x25519 import X25519DH
from dissononce.dh.keypair import KeyPair
from dissononce.dh.x25519.public import PublicKey
from dissononce.hash.sha256 import SHA256Hash
# Configure root logger for debug output
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
class NoiseXKSession:
@staticmethod
def generate_keypair() -> KeyPair:
"""
Generate a static X25519 KeyPair.
Returns:
KeyPair object with .private and .public attributes.
"""
return X25519DH().generate_keypair()
def __init__(self, local_kp: KeyPair, peer_pubkey: PublicKey):
"""
Initialize with our KeyPair and the peer's PublicKey.
"""
self.local_kp: KeyPair = local_kp
self.peer_pubkey: PublicKey = peer_pubkey
# Build the Noise handshake state (X25519 DH, ChaChaPoly cipher, SHA256 hash)
cipher = ChaChaPolyCipher()
dh = X25519DH()
hshash = SHA256Hash()
symmetric = SymmetricState(CipherState(cipher), hshash)
self._hs = HandshakeState(symmetric, dh)
self._send_cs = None # type: CipherState
self._recv_cs = None
def handshake(self, sock: socket.socket, initiator: bool) -> None:
"""
Perform the XK handshake over the socket. Branches on initiator/responder
so that each side reads or writes in the correct message order.
On completion, self._send_cs and self._recv_cs hold the two CipherStates.
"""
logging.debug(f"[handshake] start (initiator={initiator})")
# initialize with our KeyPair and their PublicKey
if initiator:
# initiator knows peers static out-of-band
self._hs.initialize(
XKHandshakePattern(),
True,
b'',
s=self.local_kp,
rs=self.peer_pubkey
)
else:
logging.debug("[handshake] responder initializing without rs")
# responder must NOT supply rs here
self._hs.initialize(
XKHandshakePattern(),
False,
b'',
s=self.local_kp
)
cs_pair = None
if initiator:
# 1) -> e
buf1 = bytearray()
cs_pair = self._hs.write_message(b'', buf1)
logging.debug(f"[-> e] {buf1.hex()}")
self._send_all(sock, buf1)
# 2) <- e, es, s, ss
msg2 = self._recv_all(sock)
logging.debug(f"[<- msg2] {msg2.hex()}")
self._hs.read_message(msg2, bytearray())
# 3) -> se (final)
buf3 = bytearray()
cs_pair = self._hs.write_message(b'', buf3)
logging.debug(f"[-> se] {buf3.hex()}")
self._send_all(sock, buf3)
else:
# 1) <- e
msg1 = self._recv_all(sock)
logging.debug(f"[<- e] {msg1.hex()}")
self._hs.read_message(msg1, bytearray())
# 2) -> e, es, s, ss
buf2 = bytearray()
cs_pair = self._hs.write_message(b'', buf2)
logging.debug(f"[-> msg2] {buf2.hex()}")
self._send_all(sock, buf2)
# 3) <- se (final)
msg3 = self._recv_all(sock)
logging.debug(f"[<- se] {msg3.hex()}")
cs_pair = self._hs.read_message(msg3, bytearray())
# on the final step, we must get exactly two CipherStates
if not cs_pair or len(cs_pair) != 2:
raise RuntimeError("Handshake did not complete properly")
cs0, cs1 = cs_pair
# the library returns (cs_encrypt_for_initiator, cs_decrypt_for_initiator)
if initiator:
# initiator: cs0 encrypts, cs1 decrypts
self._send_cs, self._recv_cs = cs0, cs1
else:
# responder must swap
self._send_cs, self._recv_cs = cs1, cs0
# dump the raw symmetric keys & nonces (if available)
self._dump_cipherstate("HANDSHAKE→ SEND", self._send_cs)
self._dump_cipherstate("HANDSHAKE→ RECV", self._recv_cs)
def send(self, sock: socket.socket, plaintext: bytes) -> None:
"""
Encrypt and send a message.
"""
if self._send_cs is None:
raise RuntimeError("Handshake not complete")
ct = self._send_cs.encrypt_with_ad(b'', plaintext)
logging.debug(f"[ENCRYPT] {ct.hex()}")
self._dump_cipherstate("SEND→ after encrypt", self._send_cs)
self._send_all(sock, ct)
def receive(self, sock: socket.socket) -> bytes:
"""
Receive and decrypt a message.
"""
if self._recv_cs is None:
raise RuntimeError("Handshake not complete")
ct = self._recv_all(sock)
logging.debug(f"[CIPHERTEXT] {ct.hex()}")
self._dump_cipherstate("RECV→ before decrypt", self._recv_cs)
pt = self._recv_cs.decrypt_with_ad(b'', ct)
logging.debug(f"[DECRYPT] {pt!r}")
return pt
def _send_all(self, sock: socket.socket, data: bytes) -> None:
# Length-prefix (2 bytes big-endian) + data
length = len(data).to_bytes(2, 'big')
sock.sendall(length + data)
def _recv_all(self, sock: socket.socket) -> bytes:
# Read 2-byte length prefix, then the payload
hdr = self._read_exact(sock, 2)
length = int.from_bytes(hdr, 'big')
return self._read_exact(sock, length)
@staticmethod
def _read_exact(sock: socket.socket, n: int) -> bytes:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("Socket closed during read")
buf.extend(chunk)
return bytes(buf)
def _dump_cipherstate(self, label: str, cs: CipherState) -> None:
"""
Print the symmetric key (cs._k) and nonce counter (cs._n) for inspection.
"""
key = cs._key
nonce = getattr(cs, "_n", None)
if isinstance(key, (bytes, bytearray)):
key_hex = key.hex()
else:
key_hex = repr(key)
logging.debug(f"[{label}] key={key_hex}")

View File

@ -1,99 +0,0 @@
import socket
import threading
import time
class P2PTransport:
def __init__(self, listen_port: int, peer_host: str, peer_port: int):
"""
Args:
listen_port: port to bind() and accept()
peer_host: host to dial()
peer_port: port to dial()
"""
self.listen_port = listen_port
self.peer_host = peer_host
self.peer_port = peer_port
def connect(self) -> (socket.socket, bool):
"""
Race bind+listen vs. dial:
- If dial succeeds first, return (sock, True) # we are initiator
- If accept succeeds first, return (sock, False) # we are responder
"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', self.listen_port))
server.listen(1)
result = {}
event = threading.Event()
lock = threading.Lock()
def accept_thread():
try:
conn, _ = server.accept()
with lock:
if not event.is_set():
result['sock'] = conn
result['initiator'] = False
event.set()
except Exception:
pass
def dial_thread():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
while not event.is_set():
try:
sock.connect((self.peer_host, self.peer_port))
with lock:
if not event.is_set():
result['sock'] = sock
result['initiator'] = True
event.set()
return
except (ConnectionRefusedError, socket.timeout):
time.sleep(0.1)
except Exception:
break
t1 = threading.Thread(target=accept_thread, daemon=True)
t2 = threading.Thread(target=dial_thread, daemon=True)
t1.start()
t2.start()
event.wait()
sock, initiator = result['sock'], result['initiator']
# close the listening socket—weve got our P2P link
server.close()
# ensure this socket is in blocking mode (no lingering timeouts)
sock.settimeout(None)
return sock, initiator
def send_packet(self, sock: socket.socket, data: bytes) -> None:
"""
Send a 2-byte big-endian length prefix followed by data.
"""
length = len(data).to_bytes(2, 'big')
sock.sendall(length + data)
def recv_packet(self, sock: socket.socket) -> bytes:
"""
Receive a 2-byte length prefix, then that many payload bytes.
"""
hdr = self._read_exact(sock, 2)
length = int.from_bytes(hdr, 'big')
return self._read_exact(sock, length)
@staticmethod
def _read_exact(sock: socket.socket, n: int) -> bytes:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("Socket closed during read")
buf.extend(chunk)
return bytes(buf)
def close(self, sock: socket.socket) -> None:
sock.close()

View File

@ -0,0 +1,119 @@
# Voice-over-GSM Protocol Implementation
This implementation provides encrypted voice communication over standard GSM voice channels without requiring CSD/HSCSD.
## Architecture
### 1. Voice Codec (`voice_codec.py`)
- **Codec2Wrapper**: Simulates Codec2 compression
- Supports multiple bitrates (700-3200 bps)
- Default: 1200 bps for GSM robustness
- 40ms frames (48 bits/frame at 1200 bps)
- **FSKModem**: 4-FSK modulation for voice channels
- Frequency band: 300-3400 Hz (GSM compatible)
- Symbol rate: 600 baud
- 4 frequencies: 600, 1200, 1800, 2400 Hz
- Preamble: 800 Hz for 100ms
- **VoiceProtocol**: Integration layer
- Manages codec and modem
- Handles encryption with ChaCha20-CTR
- Frame-based processing
### 2. Protocol Messages (`messages.py`)
- **VoiceStart** (20 bytes): Initiates voice call
- Version, codec mode, FEC type
- Session ID (64 bits)
- Initial sequence number
- **VoiceAck** (16 bytes): Accepts/rejects call
- Status (accept/reject)
- Negotiated codec and FEC
- **VoiceEnd** (12 bytes): Terminates call
- Session ID for confirmation
- **VoiceSync** (20 bytes): Synchronization
- Sequence number and timestamp
- For jitter buffer management
### 3. Encryption (`encryption.py`)
- **ChaCha20-CTR**: Stream cipher for voice
- No authentication overhead (HMAC per second)
- 12-byte nonce with frame counter
- Uses HKDF-derived key from main protocol
### 4. Protocol Integration (`protocol.py`)
- Voice session management
- Message handlers for all voice messages
- Methods:
- `start_voice_call()`: Initiate call
- `accept_voice_call()`: Accept incoming
- `end_voice_call()`: Terminate
- `send_voice_audio()`: Process audio
## Usage Example
```python
# After key exchange is complete
alice.start_voice_call(codec_mode=5, fec_type=0)
# Bob automatically accepts if in auto mode
# Or manually: bob.accept_voice_call(session_id, codec_mode, fec_type)
# Send audio
audio_samples = generate_audio() # 8kHz, 16-bit PCM
alice.send_voice_audio(audio_samples)
# End call
alice.end_voice_call()
```
## Key Features
1. **Codec2 @ 1200 bps**
- Optimal for GSM vocoder survival
- Intelligible but "robotic" quality
2. **4-FSK Modulation**
- Survives GSM/AMR/EVS vocoders
- 2400 baud with FEC
3. **ChaCha20-CTR Encryption**
- Low latency stream cipher
- Frame-based IV management
4. **Forward Error Correction**
- Repetition code (3x)
- Future: Convolutional or LDPC
5. **No Special Requirements**
- Works over standard voice calls
- Compatible with any phone
- Software-only solution
## Testing
Run the test scripts:
- `test_voice_simple.py`: Basic voice call setup
- `test_voice_protocol.py`: Full test with audio simulation (requires numpy)
## Implementation Notes
1. Message disambiguation: VoiceStart sets high bit in flags field to distinguish from VoiceSync (both 20 bytes)
2. The actual Codec2 library would need to be integrated for production use
3. FEC implementation is simplified (repetition code) - production would use convolutional codes
4. Audio I/O integration needed for real voice calls
5. Jitter buffer and timing recovery needed for production
## Security Considerations
- Voice frames use ChaCha20-CTR without per-frame authentication
- HMAC computed over 1-second blocks for efficiency
- Session binding through encrypted session ID
- PFS maintained through main protocol key rotation

View File

@ -0,0 +1,430 @@
import time
import threading
import queue
from typing import Optional, Dict, Any, List, Callable, Tuple
# ANSI colors for logging
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class AutoModeConfig:
"""Configuration parameters for the automatic mode behavior."""
def __init__(self):
# Ping behavior
self.ping_response_accept = True # Whether to accept incoming pings
self.ping_auto_initiate = False # Whether to initiate pings when connected
self.ping_retry_count = 3 # Number of ping retries
self.ping_retry_delay = 5.0 # Seconds between ping retries
self.ping_timeout = 10.0 # Seconds to wait for ping response
self.preferred_cipher = 0 # 0=AES-GCM, 1=ChaCha20-Poly1305
# Handshake behavior
self.handshake_retry_count = 3 # Number of handshake retries
self.handshake_retry_delay = 5.0 # Seconds between handshake retries
self.handshake_timeout = 10.0 # Seconds to wait for handshake
# Messaging behavior
self.auto_message_enabled = False # Whether to auto-send messages
self.message_interval = 10.0 # Seconds between auto messages
self.message_content = "Hello, secure world!" # Default message
# General behavior
self.active_mode = False # If true, initiates protocol instead of waiting
class AutoMode:
"""
Manages automated behavior for the Icing protocol.
Handles automatic progression through the protocol stages:
1. Connection setup
2. Ping/discovery
3. Key exchange
4. Encrypted communication
"""
def __init__(self, protocol_interface):
"""
Initialize the AutoMode manager.
Args:
protocol_interface: An object implementing the required protocol methods
"""
self.protocol = protocol_interface
self.config = AutoModeConfig()
self.active = False
self.state = "idle"
# Message queue for automated sending
self.message_queue = queue.Queue()
# Tracking variables
self.ping_attempts = 0
self.handshake_attempts = 0
self.last_action_time = 0
self.timer_tasks = [] # List of active timer tasks (for cleanup)
def start(self):
"""Start the automatic mode."""
if self.active:
return
self.active = True
self.state = "idle"
self.ping_attempts = 0
self.handshake_attempts = 0
self.last_action_time = time.time()
self._log_info("Automatic mode started")
# Start in active mode if configured
if self.config.active_mode and self.protocol.connections:
self._start_ping_sequence()
def stop(self):
"""Stop the automatic mode and clean up any pending tasks."""
if not self.active:
return
# Cancel any pending timers
for timer in self.timer_tasks:
if timer.is_alive():
timer.cancel()
self.timer_tasks = []
self.active = False
self.state = "idle"
self._log_info("Automatic mode stopped")
def handle_connection_established(self):
"""Called when a new connection is established."""
if not self.active:
return
self._log_info("Connection established")
# If in active mode, start pinging
if self.config.active_mode:
self._start_ping_sequence()
def handle_ping_received(self, index: int):
"""
Handle a received ping request.
Args:
index: Index of the ping request in the protocol's inbound message queue
"""
if not self.active or not self._is_valid_message_index(index):
return
self._log_info(f"Ping request received (index={index})")
# Automatically respond to ping if configured to accept
if self.config.ping_response_accept:
self._log_info(f"Auto-responding to ping with accept={self.config.ping_response_accept}")
try:
# Schedule the response with a small delay to simulate real behavior
timer = threading.Timer(0.5, self._respond_to_ping, args=[index])
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
except Exception as e:
self._log_error(f"Failed to auto-respond to ping: {e}")
def handle_ping_response_received(self, accepted: bool):
"""
Handle a received ping response.
Args:
accepted: Whether the ping was accepted
"""
if not self.active:
return
self.ping_attempts = 0 # Reset ping attempts counter
if accepted:
self._log_info("Ping accepted! Proceeding with handshake")
# Send handshake if not already done
if self.state != "handshake_sent":
self._ensure_ephemeral_keys()
self._start_handshake_sequence()
else:
self._log_info("Ping rejected by peer. Stopping auto-protocol sequence.")
self.state = "idle"
def handle_handshake_received(self, index: int):
"""
Handle a received handshake.
Args:
index: Index of the handshake in the protocol's inbound message queue
"""
if not self.active or not self._is_valid_message_index(index):
return
self._log_info(f"Handshake received (index={index})")
try:
# Ensure we have ephemeral keys
self._ensure_ephemeral_keys()
# Process the handshake (compute ECDH)
self.protocol.generate_ecdhe(index)
# Derive HKDF key
self.protocol.derive_hkdf()
# If we haven't sent our handshake yet, send it
if self.state != "handshake_sent":
timer = threading.Timer(0.5, self.protocol.send_handshake)
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
self.state = "handshake_sent"
else:
self.state = "key_exchange_complete"
# Start sending queued messages if auto messaging is enabled
if self.config.auto_message_enabled:
self._start_message_sequence()
except Exception as e:
self._log_error(f"Failed to process handshake: {e}")
def handle_encrypted_received(self, index: int):
"""
Handle a received encrypted message.
Args:
index: Index of the encrypted message in the protocol's inbound message queue
"""
if not self.active or not self._is_valid_message_index(index):
return
# Try to decrypt automatically
try:
plaintext = self.protocol.decrypt_received_message(index)
self._log_info(f"Auto-decrypted message: {plaintext}")
except Exception as e:
self._log_error(f"Failed to auto-decrypt message: {e}")
def queue_message(self, message: str):
"""
Add a message to the auto-send queue.
Args:
message: Message text to send
"""
self.message_queue.put(message)
self._log_info(f"Message queued for sending: {message}")
# If we're in the right state, start sending messages
if self.active and self.state == "key_exchange_complete" and self.config.auto_message_enabled:
self._process_message_queue()
def _start_ping_sequence(self):
"""Start the ping sequence to discover the peer."""
if self.ping_attempts >= self.config.ping_retry_count:
self._log_warning(f"Maximum ping attempts ({self.config.ping_retry_count}) reached")
self.state = "idle"
return
self.state = "pinging"
self.ping_attempts += 1
self._log_info(f"Sending ping request (attempt {self.ping_attempts}/{self.config.ping_retry_count})")
try:
self.protocol.send_ping_request(self.config.preferred_cipher)
self.last_action_time = time.time()
# Schedule next ping attempt if needed
timer = threading.Timer(
self.config.ping_retry_delay,
self._check_ping_response
)
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
except Exception as e:
self._log_error(f"Failed to send ping: {e}")
def _check_ping_response(self):
"""Check if we got a ping response, retry if not."""
if not self.active or self.state != "pinging":
return
# If we've waited long enough for a response, retry
if time.time() - self.last_action_time >= self.config.ping_timeout:
self._log_warning("No ping response received, retrying")
self._start_ping_sequence()
def _respond_to_ping(self, index: int):
"""
Respond to a ping request.
Args:
index: Index of the ping request in the inbound messages
"""
if not self.active or not self._is_valid_message_index(index):
return
try:
answer = 1 if self.config.ping_response_accept else 0
self.protocol.respond_to_ping(index, answer)
if answer == 1:
# If we accepted, we should expect a handshake
self.state = "accepted_ping"
self._ensure_ephemeral_keys()
# Set a timer to send our handshake if we don't receive one
timer = threading.Timer(
self.config.handshake_timeout,
self._check_handshake_received
)
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
self.last_action_time = time.time()
except Exception as e:
self._log_error(f"Failed to respond to ping: {e}")
def _check_handshake_received(self):
"""Check if we've received a handshake after accepting a ping."""
if not self.active or self.state != "accepted_ping":
return
# If we've waited long enough and haven't received a handshake, initiate one
if time.time() - self.last_action_time >= self.config.handshake_timeout:
self._log_warning("No handshake received after accepting ping, initiating handshake")
self._start_handshake_sequence()
def _start_handshake_sequence(self):
"""Start the handshake sequence."""
if self.handshake_attempts >= self.config.handshake_retry_count:
self._log_warning(f"Maximum handshake attempts ({self.config.handshake_retry_count}) reached")
self.state = "idle"
return
self.state = "handshake_sent"
self.handshake_attempts += 1
self._log_info(f"Sending handshake (attempt {self.handshake_attempts}/{self.config.handshake_retry_count})")
try:
self.protocol.send_handshake()
self.last_action_time = time.time()
# Schedule handshake retry check
timer = threading.Timer(
self.config.handshake_retry_delay,
self._check_handshake_response
)
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
except Exception as e:
self._log_error(f"Failed to send handshake: {e}")
def _check_handshake_response(self):
"""Check if we've completed the key exchange, retry handshake if not."""
if not self.active or self.state != "handshake_sent":
return
# If we've waited long enough for a response, retry
if time.time() - self.last_action_time >= self.config.handshake_timeout:
self._log_warning("No handshake response received, retrying")
self._start_handshake_sequence()
def _start_message_sequence(self):
"""Start the automated message sending sequence."""
if not self.config.auto_message_enabled:
return
self._log_info("Starting automated message sequence")
# Add the default message if queue is empty
if self.message_queue.empty():
self.message_queue.put(self.config.message_content)
# Start processing the queue
self._process_message_queue()
def _process_message_queue(self):
"""Process messages in the queue and send them."""
if not self.active or self.state != "key_exchange_complete" or not self.config.auto_message_enabled:
return
if not self.message_queue.empty():
message = self.message_queue.get()
self._log_info(f"Sending queued message: {message}")
try:
self.protocol.send_encrypted_message(message)
# Schedule next message send
timer = threading.Timer(
self.config.message_interval,
self._process_message_queue
)
timer.daemon = True
timer.start()
self.timer_tasks.append(timer)
except Exception as e:
self._log_error(f"Failed to send queued message: {e}")
# Put the message back in the queue
self.message_queue.put(message)
def _ensure_ephemeral_keys(self):
"""Ensure ephemeral keys are generated if needed."""
if not hasattr(self.protocol, 'ephemeral_pubkey') or self.protocol.ephemeral_pubkey is None:
self._log_info("Generating ephemeral keys")
self.protocol.generate_ephemeral_keys()
def _is_valid_message_index(self, index: int) -> bool:
"""
Check if a message index is valid in the protocol's inbound_messages queue.
Args:
index: The index to check
Returns:
bool: True if the index is valid, False otherwise
"""
if not hasattr(self.protocol, 'inbound_messages'):
self._log_error("Protocol has no inbound_messages attribute")
return False
if index < 0 or index >= len(self.protocol.inbound_messages):
self._log_error(f"Invalid message index: {index}")
return False
return True
# Helper methods for logging
def _log_info(self, message: str):
print(f"{BLUE}[AUTO]{RESET} {message}")
if hasattr(self, 'verbose_logging') and self.verbose_logging:
state_info = f"(state={self.state})"
if 'pinging' in self.state and hasattr(self, 'ping_attempts'):
state_info += f", attempts={self.ping_attempts}/{self.config.ping_retry_count}"
elif 'handshake' in self.state and hasattr(self, 'handshake_attempts'):
state_info += f", attempts={self.handshake_attempts}/{self.config.handshake_retry_count}"
print(f"{BLUE}[AUTO-DETAIL]{RESET} {state_info}")
def _log_warning(self, message: str):
print(f"{YELLOW}[AUTO-WARN]{RESET} {message}")
if hasattr(self, 'verbose_logging') and self.verbose_logging:
timer_info = f"Active timers: {len(self.timer_tasks)}"
print(f"{YELLOW}[AUTO-WARN-DETAIL]{RESET} {timer_info}")
def _log_error(self, message: str):
print(f"{RED}[AUTO-ERROR]{RESET} {message}")
if hasattr(self, 'verbose_logging') and self.verbose_logging:
print(f"{RED}[AUTO-ERROR-DETAIL]{RESET} Current state: {self.state}, Active: {self.active}")

328
protocol_prototype/cli.py Normal file
View File

@ -0,0 +1,328 @@
import sys
import argparse
import shlex
from protocol import IcingProtocol
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
RESET = "\033[0m"
def print_help():
"""Display all available commands."""
print(f"\n{YELLOW}=== Available Commands ==={RESET}")
print(f"\n{CYAN}Basic Protocol Commands:{RESET}")
print(" help - Show this help message")
print(" peer_id <hex_pubkey> - Set peer identity public key")
print(" connect <port> - Connect to a peer at the specified port")
print(" show_state - Display current protocol state")
print(" exit - Exit the program")
print(f"\n{CYAN}Manual Protocol Operation:{RESET}")
print(" generate_ephemeral_keys - Generate ephemeral ECDH keys")
print(" send_ping [cipher] - Send PING request (cipher: 0=AES-GCM, 1=ChaCha20-Poly1305, default: 0)")
print(" respond_ping <index> <0|1> - Respond to a PING (0=reject, 1=accept)")
print(" send_handshake - Send handshake with ephemeral keys")
print(" generate_ecdhe <index> - Process handshake at specified index")
print(" derive_hkdf - Derive encryption key using HKDF")
print(" send_encrypted <plaintext> - Encrypt and send a message")
print(" decrypt <index> - Decrypt received message at index")
print(f"\n{CYAN}Automatic Mode Commands:{RESET}")
print(" auto start - Start automatic mode")
print(" auto stop - Stop automatic mode")
print(" auto status - Show current auto mode status and configuration")
print(" auto config <param> <value> - Configure auto mode parameters")
print(" auto config list - Show all configurable parameters")
print(" auto message <text> - Queue message for automatic sending")
print(" auto passive - Configure as passive peer (responds to pings but doesn't initiate)")
print(" auto active - Configure as active peer (initiates protocol)")
print(" auto log - Toggle detailed logging for auto mode")
print(f"\n{CYAN}Debugging Commands:{RESET}")
print(" debug_message <index> - Display detailed information about a message in the queue")
print(f"\n{CYAN}Legacy Commands:{RESET}")
print(" auto_responder <on|off> - Enable/disable legacy auto responder (deprecated)")
def main():
protocol = IcingProtocol()
print(f"{YELLOW}\n======================================")
print(" Icing Protocol - Secure Communication ")
print("======================================\n" + RESET)
print(f"Listening on port: {protocol.local_port}")
print(f"Your identity public key (hex): {protocol.identity_pubkey.hex()}")
print_help()
while True:
try:
line = input(f"{MAGENTA}Cmd>{RESET} ").strip()
except EOFError:
break
if not line:
continue
parts = shlex.split(line) # Handle quoted arguments properly
cmd = parts[0].lower()
try:
# Basic commands
if cmd == "exit":
protocol.stop()
break
elif cmd == "help":
print_help()
elif cmd == "show_state":
protocol.show_state()
elif cmd == "peer_id":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: peer_id <hex_pubkey>")
continue
try:
protocol.set_peer_identity(parts[1])
except ValueError as e:
print(f"{RED}[ERROR]{RESET} Invalid public key: {e}")
elif cmd == "connect":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: connect <port>")
continue
try:
port = int(parts[1])
protocol.connect_to_peer(port)
except ValueError:
print(f"{RED}[ERROR]{RESET} Invalid port number.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Connection failed: {e}")
# Manual protocol operation
elif cmd == "generate_ephemeral_keys":
protocol.generate_ephemeral_keys()
elif cmd == "send_ping":
# Optional cipher parameter (0 = AES-GCM, 1 = ChaCha20-Poly1305)
cipher = 0 # Default to AES-GCM
if len(parts) >= 2:
try:
cipher = int(parts[1])
if cipher not in (0, 1):
print(f"{YELLOW}[WARNING]{RESET} Unsupported cipher code {cipher}. Using AES-GCM (0).")
cipher = 0
except ValueError:
print(f"{YELLOW}[WARNING]{RESET} Invalid cipher code. Using AES-GCM (0).")
protocol.send_ping_request(cipher)
elif cmd == "send_handshake":
protocol.send_handshake()
elif cmd == "respond_ping":
if len(parts) != 3:
print(f"{RED}[ERROR]{RESET} Usage: respond_ping <index> <0|1>")
continue
try:
idx = int(parts[1])
answer = int(parts[2])
if answer not in (0, 1):
print(f"{RED}[ERROR]{RESET} Answer must be 0 (reject) or 1 (accept).")
continue
protocol.respond_to_ping(idx, answer)
except ValueError:
print(f"{RED}[ERROR]{RESET} Index and answer must be integers.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to respond to ping: {e}")
elif cmd == "generate_ecdhe":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: generate_ecdhe <index>")
continue
try:
idx = int(parts[1])
protocol.generate_ecdhe(idx)
except ValueError:
print(f"{RED}[ERROR]{RESET} Index must be an integer.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to process handshake: {e}")
elif cmd == "derive_hkdf":
try:
protocol.derive_hkdf()
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to derive HKDF key: {e}")
elif cmd == "send_encrypted":
if len(parts) < 2:
print(f"{RED}[ERROR]{RESET} Usage: send_encrypted <plaintext>")
continue
plaintext = " ".join(parts[1:])
try:
protocol.send_encrypted_message(plaintext)
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to send encrypted message: {e}")
elif cmd == "decrypt":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: decrypt <index>")
continue
try:
idx = int(parts[1])
protocol.decrypt_received_message(idx)
except ValueError:
print(f"{RED}[ERROR]{RESET} Index must be an integer.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to decrypt message: {e}")
# Debugging commands
elif cmd == "debug_message":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: debug_message <index>")
continue
try:
idx = int(parts[1])
protocol.debug_message(idx)
except ValueError:
print(f"{RED}[ERROR]{RESET} Index must be an integer.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to debug message: {e}")
# Automatic mode commands
elif cmd == "auto":
if len(parts) < 2:
print(f"{RED}[ERROR]{RESET} Usage: auto <command> [options]")
print("Available commands: start, stop, status, config, message, passive, active")
continue
subcmd = parts[1].lower()
if subcmd == "start":
protocol.start_auto_mode()
print(f"{GREEN}[AUTO]{RESET} Automatic mode started")
elif subcmd == "stop":
protocol.stop_auto_mode()
print(f"{GREEN}[AUTO]{RESET} Automatic mode stopped")
elif subcmd == "status":
config = protocol.get_auto_mode_config()
print(f"{YELLOW}=== Auto Mode Status ==={RESET}")
print(f"Active: {protocol.auto_mode.active}")
print(f"State: {protocol.auto_mode.state}")
print(f"\n{YELLOW}--- Configuration ---{RESET}")
for key, value in vars(config).items():
print(f" {key}: {value}")
elif subcmd == "config":
if len(parts) < 3:
print(f"{RED}[ERROR]{RESET} Usage: auto config <param> <value> or auto config list")
continue
if parts[2].lower() == "list":
config = protocol.get_auto_mode_config()
print(f"{YELLOW}=== Auto Mode Configuration Parameters ==={RESET}")
for key, value in vars(config).items():
print(f" {key} ({type(value).__name__}): {value}")
continue
if len(parts) != 4:
print(f"{RED}[ERROR]{RESET} Usage: auto config <param> <value>")
continue
param = parts[2]
value_str = parts[3]
# Convert the string value to the appropriate type
config = protocol.get_auto_mode_config()
if not hasattr(config, param):
print(f"{RED}[ERROR]{RESET} Unknown parameter: {param}")
print("Use 'auto config list' to see all available parameters")
continue
current_value = getattr(config, param)
try:
if isinstance(current_value, bool):
if value_str.lower() in ("true", "yes", "on", "1"):
value = True
elif value_str.lower() in ("false", "no", "off", "0"):
value = False
else:
raise ValueError(f"Boolean value must be true/false/yes/no/on/off/1/0")
elif isinstance(current_value, int):
value = int(value_str)
elif isinstance(current_value, float):
value = float(value_str)
elif isinstance(current_value, str):
value = value_str
else:
value = value_str # Default to string
protocol.configure_auto_mode(**{param: value})
print(f"{GREEN}[AUTO]{RESET} Set {param} = {value}")
except ValueError as e:
print(f"{RED}[ERROR]{RESET} Invalid value for {param}: {e}")
elif subcmd == "message":
if len(parts) < 3:
print(f"{RED}[ERROR]{RESET} Usage: auto message <text>")
continue
message = " ".join(parts[2:])
protocol.queue_auto_message(message)
print(f"{GREEN}[AUTO]{RESET} Message queued for sending: {message}")
elif subcmd == "passive":
# Configure as passive peer (responds but doesn't initiate)
protocol.configure_auto_mode(
ping_response_accept=True,
ping_auto_initiate=False,
active_mode=False
)
print(f"{GREEN}[AUTO]{RESET} Configured as passive peer")
elif subcmd == "active":
# Configure as active peer (initiates protocol)
protocol.configure_auto_mode(
ping_response_accept=True,
ping_auto_initiate=True,
active_mode=True
)
print(f"{GREEN}[AUTO]{RESET} Configured as active peer")
else:
print(f"{RED}[ERROR]{RESET} Unknown auto mode command: {subcmd}")
print("Available commands: start, stop, status, config, message, passive, active")
# Legacy commands
elif cmd == "auto_responder":
if len(parts) != 2:
print(f"{RED}[ERROR]{RESET} Usage: auto_responder <on|off>")
continue
val = parts[1].lower()
if val not in ("on", "off"):
print(f"{RED}[ERROR]{RESET} Value must be 'on' or 'off'.")
continue
protocol.enable_auto_responder(val == "on")
print(f"{YELLOW}[WARNING]{RESET} Using legacy auto responder. Consider using 'auto' commands instead.")
else:
print(f"{RED}[ERROR]{RESET} Unknown command: {cmd}")
print("Type 'help' for a list of available commands.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Command failed: {e}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nExiting...")
except Exception as e:
print(f"{RED}[FATAL ERROR]{RESET} {e}")
sys.exit(1)

View File

@ -0,0 +1,165 @@
import os
from typing import Tuple
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
def generate_identity_keys() -> Tuple[ec.EllipticCurvePrivateKey, bytes]:
"""
Generate an ECDSA (P-256) identity key pair.
Returns:
Tuple containing:
- private_key: EllipticCurvePrivateKey object
- public_key_bytes: Raw x||y format (64 bytes, 512 bits)
"""
private_key = ec.generate_private_key(ec.SECP256R1())
public_numbers = private_key.public_key().public_numbers()
x_bytes = public_numbers.x.to_bytes(32, byteorder='big')
y_bytes = public_numbers.y.to_bytes(32, byteorder='big')
pubkey_bytes = x_bytes + y_bytes # 64 bytes total
return private_key, pubkey_bytes
def load_peer_identity_key(pubkey_bytes: bytes) -> ec.EllipticCurvePublicKey:
"""
Convert a raw public key (64 bytes, x||y format) to a cryptography public key object.
Args:
pubkey_bytes: Raw 64-byte public key (x||y format)
Returns:
EllipticCurvePublicKey object
Raises:
ValueError: If the pubkey_bytes is not exactly 64 bytes
"""
if len(pubkey_bytes) != 64:
raise ValueError("Peer identity pubkey must be exactly 64 bytes (x||y).")
x_int = int.from_bytes(pubkey_bytes[:32], byteorder='big')
y_int = int.from_bytes(pubkey_bytes[32:], byteorder='big')
public_numbers = ec.EllipticCurvePublicNumbers(x_int, y_int, ec.SECP256R1())
return public_numbers.public_key()
def sign_data(private_key: ec.EllipticCurvePrivateKey, data: bytes) -> bytes:
"""
Sign data with ECDSA using a P-256 private key.
Args:
private_key: EllipticCurvePrivateKey for signing
data: Bytes to sign
Returns:
DER-encoded signature (variable length, up to ~70-72 bytes)
"""
signature = private_key.sign(data, ec.ECDSA(hashes.SHA256()))
return signature
def verify_signature(public_key: ec.EllipticCurvePublicKey, signature: bytes, data: bytes) -> bool:
"""
Verify a DER-encoded ECDSA signature.
Args:
public_key: EllipticCurvePublicKey for verification
signature: DER-encoded signature
data: Original signed data
Returns:
True if signature is valid, False otherwise
"""
try:
public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False
def get_ephemeral_keypair() -> Tuple[ec.EllipticCurvePrivateKey, bytes]:
"""
Generate an ephemeral ECDH key pair (P-256).
Returns:
Tuple containing:
- private_key: EllipticCurvePrivateKey object
- pubkey_bytes: Raw x||y format (64 bytes, 512 bits)
"""
private_key = ec.generate_private_key(ec.SECP256R1())
numbers = private_key.public_key().public_numbers()
x_bytes = numbers.x.to_bytes(32, 'big')
y_bytes = numbers.y.to_bytes(32, 'big')
return private_key, x_bytes + y_bytes # 64 bytes total
def compute_ecdh_shared_key(private_key: ec.EllipticCurvePrivateKey, peer_pubkey_bytes: bytes) -> bytes:
"""
Compute a shared secret using ECDH.
Args:
private_key: Local ECDH private key
peer_pubkey_bytes: Peer's ephemeral public key (64 bytes, raw x||y format)
Returns:
Shared secret bytes
Raises:
ValueError: If peer_pubkey_bytes is not 64 bytes
"""
if len(peer_pubkey_bytes) != 64:
raise ValueError("Peer public key must be 64 bytes (x||y format)")
x_int = int.from_bytes(peer_pubkey_bytes[:32], 'big')
y_int = int.from_bytes(peer_pubkey_bytes[32:], 'big')
# Create public key object from raw components
peer_public_numbers = ec.EllipticCurvePublicNumbers(x_int, y_int, ec.SECP256R1())
peer_public_key = peer_public_numbers.public_key()
# Perform key exchange
shared_key = private_key.exchange(ec.ECDH(), peer_public_key)
return shared_key
def der_to_raw(der_sig: bytes) -> bytes:
"""
Convert a DER-encoded ECDSA signature to a raw 64-byte signature (r||s).
Args:
der_sig: DER-encoded signature
Returns:
Raw 64-byte signature (r||s format), with each component padded to 32 bytes
"""
r, s = decode_dss_signature(der_sig)
r_bytes = r.to_bytes(32, byteorder='big')
s_bytes = s.to_bytes(32, byteorder='big')
return r_bytes + s_bytes
def raw_signature_to_der(raw_sig: bytes) -> bytes:
"""
Convert a raw signature (64 bytes, concatenated r||s) to DER-encoded signature.
Args:
raw_sig: Raw 64-byte signature (r||s format)
Returns:
DER-encoded signature
Raises:
ValueError: If raw_sig is not 64 bytes
"""
if len(raw_sig) != 64:
raise ValueError("Raw signature must be 64 bytes (r||s).")
r = int.from_bytes(raw_sig[:32], 'big')
s = int.from_bytes(raw_sig[32:], 'big')
return encode_dss_signature(r, s)

View File

@ -0,0 +1,307 @@
import os
import struct
from typing import Optional, Tuple
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
class MessageHeader:
"""
Header of an encrypted message (18 bytes total):
Clear Text Section (4 bytes):
- flag: 16 bits (0xBEEF by default)
- data_len: 16 bits (length of encrypted payload excluding tag)
Associated Data (14 bytes):
- retry: 8 bits (retry counter)
- connection_status: 4 bits (e.g., CRC required) + 4 bits padding
- iv/messageID: 96 bits (12 bytes)
"""
def __init__(self, flag: int, data_len: int, retry: int, connection_status: int, iv: bytes):
if not (0 <= flag < 65536):
raise ValueError("Flag must fit in 16 bits (0..65535)")
if not (0 <= data_len < 65536):
raise ValueError("Data length must fit in 16 bits (0..65535)")
if not (0 <= retry < 256):
raise ValueError("Retry must fit in 8 bits (0..255)")
if not (0 <= connection_status < 16):
raise ValueError("Connection status must fit in 4 bits (0..15)")
if len(iv) != 12:
raise ValueError("IV must be 12 bytes (96 bits)")
self.flag = flag # 16 bits
self.data_len = data_len # 16 bits
self.retry = retry # 8 bits
self.connection_status = connection_status # 4 bits
self.iv = iv # 96 bits (12 bytes)
def pack(self) -> bytes:
"""Pack header into 18 bytes."""
# Pack flag and data_len (4 bytes)
header = struct.pack('>H H', self.flag, self.data_len)
# Pack retry and connection_status (2 bytes)
# connection_status in high 4 bits of second byte, 4 bits padding as zero
ad_byte = (self.connection_status & 0x0F) << 4
ad_packed = struct.pack('>B B', self.retry, ad_byte)
# Append IV (12 bytes)
return header + ad_packed + self.iv
def get_associated_data(self) -> bytes:
"""Get the associated data for AEAD encryption (retry, conn_status, iv)."""
# Pack retry and connection_status
ad_byte = (self.connection_status & 0x0F) << 4
ad_packed = struct.pack('>B B', self.retry, ad_byte)
# Append IV
return ad_packed + self.iv
@classmethod
def unpack(cls, data: bytes) -> 'MessageHeader':
"""Unpack 18 bytes into a MessageHeader object."""
if len(data) < 18:
raise ValueError(f"Header data too short: {len(data)} bytes, expected 18")
flag, data_len = struct.unpack('>H H', data[:4])
retry, ad_byte = struct.unpack('>B B', data[4:6])
connection_status = (ad_byte >> 4) & 0x0F
iv = data[6:18]
return cls(flag, data_len, retry, connection_status, iv)
class EncryptedMessage:
"""
Encrypted message packet format:
- Header (18 bytes):
* flag: 16 bits
* data_len: 16 bits
* retry: 8 bits
* connection_status: 4 bits (+ 4 bits padding)
* iv/messageID: 96 bits (12 bytes)
- Payload: variable length encrypted data
- Footer:
* Authentication tag: 128 bits (16 bytes)
* CRC32: 32 bits (4 bytes) - optional, based on connection_status
"""
def __init__(self, plaintext: bytes, key: bytes, flag: int = 0xBEEF,
retry: int = 0, connection_status: int = 0, iv: bytes = None,
cipher_type: int = 0):
self.plaintext = plaintext
self.key = key
self.flag = flag
self.retry = retry
self.connection_status = connection_status
self.iv = iv or generate_iv(initial=True)
self.cipher_type = cipher_type # 0 = AES-256-GCM, 1 = ChaCha20-Poly1305
# Will be set after encryption
self.ciphertext = None
self.tag = None
self.header = None
def encrypt(self) -> bytes:
"""Encrypt the plaintext and return the full encrypted message."""
# Create header with correct data_len (which will be set after encryption)
self.header = MessageHeader(
flag=self.flag,
data_len=0, # Will be updated after encryption
retry=self.retry,
connection_status=self.connection_status,
iv=self.iv
)
# Get associated data for AEAD
aad = self.header.get_associated_data()
# Encrypt using the appropriate cipher
if self.cipher_type == 0: # AES-256-GCM
cipher = AESGCM(self.key)
ciphertext_with_tag = cipher.encrypt(self.iv, self.plaintext, aad)
elif self.cipher_type == 1: # ChaCha20-Poly1305
cipher = ChaCha20Poly1305(self.key)
ciphertext_with_tag = cipher.encrypt(self.iv, self.plaintext, aad)
else:
raise ValueError(f"Unsupported cipher type: {self.cipher_type}")
# Extract ciphertext and tag
self.tag = ciphertext_with_tag[-16:]
self.ciphertext = ciphertext_with_tag[:-16]
# Update header with actual data length
self.header.data_len = len(self.ciphertext)
# Pack everything together
packed_header = self.header.pack()
# Check if CRC is required (based on connection_status)
if self.connection_status & 0x01: # Lowest bit indicates CRC required
import zlib
# Compute CRC32 of header + ciphertext + tag
crc = zlib.crc32(packed_header + self.ciphertext + self.tag) & 0xffffffff
crc_bytes = struct.pack('>I', crc)
return packed_header + self.ciphertext + self.tag + crc_bytes
else:
return packed_header + self.ciphertext + self.tag
@classmethod
def decrypt(cls, data: bytes, key: bytes, cipher_type: int = 0) -> Tuple[bytes, MessageHeader]:
"""
Decrypt an encrypted message and return the plaintext and header.
Args:
data: The full encrypted message
key: The encryption key
cipher_type: 0 for AES-256-GCM, 1 for ChaCha20-Poly1305
Returns:
Tuple of (plaintext, header)
"""
if len(data) < 18 + 16: # Header + minimum tag size
raise ValueError("Message too short")
# Extract header
header_bytes = data[:18]
header = MessageHeader.unpack(header_bytes)
# Get ciphertext and tag
data_len = header.data_len
ciphertext_start = 18
ciphertext_end = ciphertext_start + data_len
if ciphertext_end + 16 > len(data):
raise ValueError("Message length does not match header's data_len")
ciphertext = data[ciphertext_start:ciphertext_end]
tag = data[ciphertext_end:ciphertext_end + 16]
# Get associated data for AEAD
aad = header.get_associated_data()
# Combine ciphertext and tag for decryption
ciphertext_with_tag = ciphertext + tag
# Decrypt using the appropriate cipher
try:
if cipher_type == 0: # AES-256-GCM
cipher = AESGCM(key)
plaintext = cipher.decrypt(header.iv, ciphertext_with_tag, aad)
elif cipher_type == 1: # ChaCha20-Poly1305
cipher = ChaCha20Poly1305(key)
plaintext = cipher.decrypt(header.iv, ciphertext_with_tag, aad)
else:
raise ValueError(f"Unsupported cipher type: {cipher_type}")
return plaintext, header
except Exception as e:
raise ValueError(f"Decryption failed: {e}")
def generate_iv(initial: bool = False, previous_iv: bytes = None) -> bytes:
"""
Generate a 96-bit IV (12 bytes).
Args:
initial: If True, return a random IV
previous_iv: The previous IV to increment
Returns:
A new IV
"""
if initial or previous_iv is None:
return os.urandom(12) # 96 bits
else:
# Increment the previous IV by 1 modulo 2^96
iv_int = int.from_bytes(previous_iv, 'big')
iv_int = (iv_int + 1) % (1 << 96)
return iv_int.to_bytes(12, 'big')
# Convenience functions to match original API
def encrypt_message(plaintext: bytes, key: bytes, flag: int = 0xBEEF,
retry: int = 0, connection_status: int = 0,
iv: bytes = None, cipher_type: int = 0) -> bytes:
"""
Encrypt a message using the specified parameters.
Args:
plaintext: The data to encrypt
key: The encryption key (32 bytes for AES-256-GCM, 32 bytes for ChaCha20-Poly1305)
flag: 16-bit flag value (default: 0xBEEF)
retry: 8-bit retry counter
connection_status: 4-bit connection status
iv: Optional 96-bit IV (if None, a random one will be generated)
cipher_type: 0 for AES-256-GCM, 1 for ChaCha20-Poly1305
Returns:
The full encrypted message
"""
message = EncryptedMessage(
plaintext=plaintext,
key=key,
flag=flag,
retry=retry,
connection_status=connection_status,
iv=iv,
cipher_type=cipher_type
)
return message.encrypt()
def decrypt_message(message: bytes, key: bytes, cipher_type: int = 0) -> bytes:
"""
Decrypt a message.
Args:
message: The full encrypted message
key: The encryption key
cipher_type: 0 for AES-256-GCM, 1 for ChaCha20-Poly1305
Returns:
The decrypted plaintext
"""
plaintext, _ = EncryptedMessage.decrypt(message, key, cipher_type)
return plaintext
# ChaCha20-CTR functions for voice streaming (without authentication)
def chacha20_encrypt(plaintext: bytes, key: bytes, nonce: bytes) -> bytes:
"""
Encrypt plaintext using ChaCha20 in CTR mode (no authentication).
Args:
plaintext: Data to encrypt
key: 32-byte key
nonce: 16-byte nonce (for ChaCha20 in cryptography library)
Returns:
Ciphertext
"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
if len(key) != 32:
raise ValueError("ChaCha20 key must be 32 bytes")
if len(nonce) != 16:
raise ValueError("ChaCha20 nonce must be 16 bytes")
cipher = Cipher(
algorithms.ChaCha20(key, nonce),
mode=None,
backend=default_backend()
)
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
def chacha20_decrypt(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes:
"""
Decrypt ciphertext using ChaCha20 in CTR mode (no authentication).
Args:
ciphertext: Data to decrypt
key: 32-byte key
nonce: 12-byte nonce
Returns:
Plaintext
"""
# ChaCha20 is symmetrical - encryption and decryption are the same
return chacha20_encrypt(ciphertext, key, nonce)

View File

@ -0,0 +1,463 @@
import os
import struct
import time
import zlib
import hashlib
from typing import Tuple, Optional
def crc32_of(data: bytes) -> int:
"""
Compute CRC-32 of 'data'.
"""
return zlib.crc32(data) & 0xffffffff
# ---------------------------------------------------------------------------
# PING REQUEST (new format)
# Fields (in order):
# - session_nonce: 129 bits (from the top 129 bits of 17 random bytes)
# - version: 7 bits
# - cipher: 4 bits (0 = AES-256-GCM, 1 = ChaCha20-poly1305; for now only 0 is used)
# - CRC: 32 bits
#
# Total bits: 129 + 7 + 4 + 32 = 172 bits. We pack into 22 bytes (176 bits) with 4 spare bits.
# ---------------------------------------------------------------------------
class PingRequest:
"""
PING REQUEST format (172 bits / 22 bytes):
- session_nonce: 129 bits (from top 129 bits of 17 random bytes)
- version: 7 bits
- cipher: 4 bits (0 = AES-256-GCM, 1 = ChaCha20-poly1305)
- CRC: 32 bits
"""
def __init__(self, version: int, cipher: int, session_nonce: bytes = None):
if not (0 <= version < 128):
raise ValueError("Version must fit in 7 bits (0..127)")
if not (0 <= cipher < 16):
raise ValueError("Cipher must fit in 4 bits (0..15)")
self.version = version
self.cipher = cipher
# Generate session nonce if not provided
if session_nonce is None:
# Generate 17 random bytes
nonce_full = os.urandom(17)
# Use top 129 bits
nonce_int_full = int.from_bytes(nonce_full, 'big')
nonce_129_int = nonce_int_full >> 7 # drop lowest 7 bits
self.session_nonce = nonce_129_int.to_bytes(17, 'big')
else:
if len(session_nonce) != 17:
raise ValueError("Session nonce must be 17 bytes (136 bits)")
self.session_nonce = session_nonce
def serialize(self) -> bytes:
"""Serialize the ping request into a 22-byte packet."""
# Convert session_nonce to integer (129 bits)
nonce_int = int.from_bytes(self.session_nonce, 'big')
# Pack fields: shift nonce left by 11 bits, add version and cipher
partial_int = (nonce_int << 11) | (self.version << 4) | (self.cipher & 0x0F)
# This creates 129+7+4 = 140 bits; pack into 18 bytes
partial_bytes = partial_int.to_bytes(18, 'big')
# Compute CRC over these 18 bytes
cval = crc32_of(partial_bytes)
# Combine partial data with 32-bit CRC
final_int = (int.from_bytes(partial_bytes, 'big') << 32) | cval
return final_int.to_bytes(22, 'big')
@classmethod
def deserialize(cls, data: bytes) -> Optional['PingRequest']:
"""Deserialize a 22-byte packet into a PingRequest object."""
if len(data) != 22:
return None
# Extract 176-bit integer
final_int = int.from_bytes(data, 'big')
# Extract CRC and verify
crc_in = final_int & 0xffffffff
partial_int = final_int >> 32 # 140 bits
partial_bytes = partial_int.to_bytes(18, 'big')
crc_calc = crc32_of(partial_bytes)
if crc_calc != crc_in:
return None
# Extract fields
cipher = partial_int & 0x0F
version = (partial_int >> 4) & 0x7F
nonce_129_int = partial_int >> 11 # 129 bits
session_nonce = nonce_129_int.to_bytes(17, 'big')
return cls(version, cipher, session_nonce)
# ---------------------------------------------------------------------------
# PING RESPONSE (new format)
# Fields:
# - timestamp: 32 bits (we take the lower 32 bits of the time in ms)
# - version: 7 bits
# - cipher: 4 bits
# - answer: 1 bit
# - CRC: 32 bits
#
# Total bits: 32 + 7 + 4 + 1 + 32 = 76 bits; pack into 10 bytes (80 bits) with 4 spare bits.
# ---------------------------------------------------------------------------
class PingResponse:
"""
PING RESPONSE format (76 bits / 10 bytes):
- timestamp: 32 bits (milliseconds since epoch, lower 32 bits)
- version: 7 bits
- cipher: 4 bits
- answer: 1 bit (0 = no, 1 = yes)
- CRC: 32 bits
"""
def __init__(self, version: int, cipher: int, answer: int, timestamp: int = None):
if not (0 <= version < 128):
raise ValueError("Version must fit in 7 bits")
if not (0 <= cipher < 16):
raise ValueError("Cipher must fit in 4 bits")
if answer not in (0, 1):
raise ValueError("Answer must be 0 or 1")
self.version = version
self.cipher = cipher
self.answer = answer
self.timestamp = timestamp or (int(time.time() * 1000) & 0xffffffff)
def serialize(self) -> bytes:
"""Serialize the ping response into a 10-byte packet."""
# Pack timestamp, version, cipher, answer: 32+7+4+1 = 44 bits
# Shift left by 4 to put spare bits at the end
partial_val = (self.timestamp << (7+4+1)) | (self.version << (4+1)) | (self.cipher << 1) | self.answer
partial_val_shifted = partial_val << 4 # Add 4 spare bits at the end
partial_bytes = partial_val_shifted.to_bytes(6, 'big') # 6 bytes = 48 bits
# Compute CRC
cval = crc32_of(partial_bytes)
# Combine with CRC
final_val = (int.from_bytes(partial_bytes, 'big') << 32) | cval
return final_val.to_bytes(10, 'big')
@classmethod
def deserialize(cls, data: bytes) -> Optional['PingResponse']:
"""Deserialize a 10-byte packet into a PingResponse object."""
if len(data) != 10:
return None
# Extract 80-bit integer
final_int = int.from_bytes(data, 'big')
# Extract CRC and verify
crc_in = final_int & 0xffffffff
partial_int = final_int >> 32 # 48 bits
partial_bytes = partial_int.to_bytes(6, 'big')
crc_calc = crc32_of(partial_bytes)
if crc_calc != crc_in:
return None
# Extract fields (discard 4 spare bits)
partial_int >>= 4 # now 44 bits
answer = partial_int & 0x01
cipher = (partial_int >> 1) & 0x0F
version = (partial_int >> (1+4)) & 0x7F
timestamp = partial_int >> (1+4+7)
return cls(version, cipher, answer, timestamp)
# =============================================================================
# 3) Handshake
# - 32-bit timestamp
# - 64-byte ephemeral pubkey (raw x||y = 512 bits)
# - 64-byte ephemeral signature (raw r||s = 512 bits)
# - 32-byte PFS hash (256 bits)
# - 32-bit CRC
# => total 4 + 64 + 64 + 32 + 4 = 168 bytes = 1344 bits
# =============================================================================
class Handshake:
"""
HANDSHAKE format (1344 bits / 168 bytes):
- timestamp: 32 bits
- ephemeral_pubkey: 512 bits (64 bytes, raw x||y format)
- ephemeral_signature: 512 bits (64 bytes, raw r||s format)
- pfs_hash: 256 bits (32 bytes)
- CRC: 32 bits
"""
def __init__(self, ephemeral_pubkey: bytes, ephemeral_signature: bytes, pfs_hash: bytes, timestamp: int = None):
if len(ephemeral_pubkey) != 64:
raise ValueError("ephemeral_pubkey must be 64 bytes (raw x||y)")
if len(ephemeral_signature) != 64:
raise ValueError("ephemeral_signature must be 64 bytes (raw r||s)")
if len(pfs_hash) != 32:
raise ValueError("pfs_hash must be 32 bytes")
self.ephemeral_pubkey = ephemeral_pubkey
self.ephemeral_signature = ephemeral_signature
self.pfs_hash = pfs_hash
self.timestamp = timestamp or (int(time.time() * 1000) & 0xffffffff)
def serialize(self) -> bytes:
"""Serialize the handshake into a 168-byte packet."""
# Pack timestamp and other fields
partial = struct.pack("!I", self.timestamp) + self.ephemeral_pubkey + self.ephemeral_signature + self.pfs_hash
# Compute CRC
cval = crc32_of(partial)
# Append CRC
return partial + struct.pack("!I", cval)
@classmethod
def deserialize(cls, data: bytes) -> Optional['Handshake']:
"""Deserialize a 168-byte packet into a Handshake object."""
if len(data) != 168:
return None
# Extract and verify CRC
partial = data[:-4]
crc_in = struct.unpack("!I", data[-4:])[0]
crc_calc = crc32_of(partial)
if crc_calc != crc_in:
return None
# Extract fields
timestamp = struct.unpack("!I", partial[:4])[0]
ephemeral_pubkey = partial[4:4+64]
ephemeral_signature = partial[68:68+64]
pfs_hash = partial[132:132+32]
return cls(ephemeral_pubkey, ephemeral_signature, pfs_hash, timestamp)
# =============================================================================
# 4) PFS Hash Helper
# If no previous session, return 32 zero bytes
# Otherwise, compute sha256(session_number || last_shared_secret).
# =============================================================================
def compute_pfs_hash(session_number: int, shared_secret_hex: str) -> bytes:
"""
Compute the PFS hash field for handshake messages:
- If no previous session (session_number < 0), return 32 zero bytes
- Otherwise, compute sha256(session_number || shared_secret)
"""
if session_number < 0:
return b"\x00" * 32
# Convert shared_secret_hex to raw bytes
secret_bytes = bytes.fromhex(shared_secret_hex)
# Pack session_number as 4 bytes
sn_bytes = struct.pack("!I", session_number)
# Compute hash
return hashlib.sha256(sn_bytes + secret_bytes).digest()
# Helper function for CRC32 calculations
def compute_crc32(data: bytes) -> int:
"""Compute CRC32 of data (for consistency with crc32_of)."""
return zlib.crc32(data) & 0xffffffff
# =============================================================================
# Voice Protocol Messages
# =============================================================================
class VoiceStart:
"""
Voice call initiation message (20 bytes).
Fields:
- version: 8 bits (protocol version)
- codec_mode: 8 bits (Codec2 mode)
- fec_type: 8 bits (0=repetition, 1=convolutional, 2=LDPC)
- flags: 8 bits (reserved for future use)
- session_id: 64 bits (unique voice session identifier)
- initial_sequence: 32 bits (starting sequence number)
- crc32: 32 bits
"""
def __init__(self, version: int = 0, codec_mode: int = 5, fec_type: int = 0,
flags: int = 0, session_id: int = None, initial_sequence: int = 0):
self.version = version
self.codec_mode = codec_mode
self.fec_type = fec_type
self.flags = flags | 0x80 # Set high bit to distinguish from VoiceSync
self.session_id = session_id or int.from_bytes(os.urandom(8), 'big')
self.initial_sequence = initial_sequence
def serialize(self) -> bytes:
"""Serialize to 20 bytes."""
# Pack all fields except CRC
data = struct.pack('>BBBBQII',
self.version,
self.codec_mode,
self.fec_type,
self.flags,
self.session_id,
self.initial_sequence,
0 # CRC placeholder
)
# Calculate and append CRC
crc = compute_crc32(data[:-4])
return data[:-4] + struct.pack('>I', crc)
@classmethod
def deserialize(cls, data: bytes) -> Optional['VoiceStart']:
"""Deserialize from bytes."""
if len(data) != 20:
return None
try:
version, codec_mode, fec_type, flags, session_id, initial_seq, crc = struct.unpack('>BBBBQII', data)
# Verify CRC
expected_crc = compute_crc32(data[:-4])
if crc != expected_crc:
return None
return cls(version, codec_mode, fec_type, flags, session_id, initial_seq)
except struct.error:
return None
class VoiceAck:
"""
Voice call acknowledgment message (16 bytes).
Fields:
- version: 8 bits
- status: 8 bits (0=reject, 1=accept)
- codec_mode: 8 bits (negotiated codec mode)
- fec_type: 8 bits (negotiated FEC type)
- session_id: 64 bits (echo of received session_id)
- crc32: 32 bits
"""
def __init__(self, version: int = 0, status: int = 1, codec_mode: int = 5,
fec_type: int = 0, session_id: int = 0):
self.version = version
self.status = status
self.codec_mode = codec_mode
self.fec_type = fec_type
self.session_id = session_id
def serialize(self) -> bytes:
"""Serialize to 16 bytes."""
data = struct.pack('>BBBBQI',
self.version,
self.status,
self.codec_mode,
self.fec_type,
self.session_id,
0 # CRC placeholder
)
crc = compute_crc32(data[:-4])
return data[:-4] + struct.pack('>I', crc)
@classmethod
def deserialize(cls, data: bytes) -> Optional['VoiceAck']:
"""Deserialize from bytes."""
if len(data) != 16:
return None
try:
version, status, codec_mode, fec_type, session_id, crc = struct.unpack('>BBBBQI', data)
expected_crc = compute_crc32(data[:-4])
if crc != expected_crc:
return None
return cls(version, status, codec_mode, fec_type, session_id)
except struct.error:
return None
class VoiceEnd:
"""
Voice call termination message (12 bytes).
Fields:
- session_id: 64 bits
- crc32: 32 bits
"""
def __init__(self, session_id: int):
self.session_id = session_id
def serialize(self) -> bytes:
"""Serialize to 12 bytes."""
data = struct.pack('>QI', self.session_id, 0)
crc = compute_crc32(data[:-4])
return data[:-4] + struct.pack('>I', crc)
@classmethod
def deserialize(cls, data: bytes) -> Optional['VoiceEnd']:
"""Deserialize from bytes."""
if len(data) != 12:
return None
try:
session_id, crc = struct.unpack('>QI', data)
expected_crc = compute_crc32(data[:-4])
if crc != expected_crc:
return None
return cls(session_id)
except struct.error:
return None
class VoiceSync:
"""
Voice synchronization frame (20 bytes).
Used for maintaining sync and providing timing information.
Fields:
- session_id: 64 bits
- sequence: 32 bits
- timestamp: 32 bits (milliseconds since voice start)
- crc32: 32 bits
"""
def __init__(self, session_id: int, sequence: int, timestamp: int):
self.session_id = session_id
self.sequence = sequence
self.timestamp = timestamp
def serialize(self) -> bytes:
"""Serialize to 20 bytes."""
data = struct.pack('>QIII', self.session_id, self.sequence, self.timestamp, 0)
crc = compute_crc32(data[:-4])
return data[:-4] + struct.pack('>I', crc)
@classmethod
def deserialize(cls, data: bytes) -> Optional['VoiceSync']:
"""Deserialize from bytes."""
if len(data) != 20:
return None
try:
session_id, sequence, timestamp, crc = struct.unpack('>QIII', data)
expected_crc = compute_crc32(data[:-4])
if crc != expected_crc:
return None
return cls(session_id, sequence, timestamp)
except struct.error:
return None

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
# System install
Docker
Python3
# Venv install
PyQt5
dissononce

View File

@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Test script for DryBox integration with Icing protocol
Tests encrypted voice communication with 4FSK modulation
"""
import time
import subprocess
import sys
import os
import threading
from pathlib import Path
# Add DryBox to path
sys.path.append(str(Path(__file__).parent / "DryBox"))
from integrated_protocol import IntegratedDryBoxProtocol
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def start_gsm_simulator():
"""Start GSM simulator in background"""
print(f"{BLUE}[TEST]{RESET} Starting GSM simulator...")
gsm_path = Path(__file__).parent / "DryBox" / "gsm_simulator.py"
process = subprocess.Popen(
[sys.executable, str(gsm_path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
time.sleep(2) # Give it time to start
print(f"{GREEN}[TEST]{RESET} GSM simulator started")
return process
def test_key_exchange():
"""Test key exchange between two DryBox instances"""
print(f"\n{YELLOW}=== Testing Key Exchange ==={RESET}")
# Create sender and receiver
sender = IntegratedDryBoxProtocol(mode="sender")
receiver = IntegratedDryBoxProtocol(mode="receiver")
# Connect to GSM
if not sender.connect_gsm():
print(f"{RED}[ERROR]{RESET} Sender failed to connect to GSM")
return False
if not receiver.connect_gsm():
print(f"{RED}[ERROR]{RESET} Receiver failed to connect to GSM")
return False
# Exchange identities
sender_identity = sender.get_identity_key()
receiver_identity = receiver.get_identity_key()
print(f"{BLUE}[SENDER]{RESET} Identity: {sender_identity[:32]}...")
print(f"{BLUE}[RECEIVER]{RESET} Identity: {receiver_identity[:32]}...")
# Setup connections
receiver_port = receiver.setup_protocol_connection(peer_identity=sender_identity)
print(f"{BLUE}[RECEIVER]{RESET} Listening on port {receiver_port}")
sender.setup_protocol_connection(peer_port=receiver_port, peer_identity=receiver_identity)
print(f"{BLUE}[SENDER]{RESET} Connected to receiver")
# Initiate key exchange with ChaCha20
success = sender.initiate_key_exchange(cipher_type=1)
if success:
print(f"{GREEN}[SUCCESS]{RESET} Key exchange completed!")
print(f" Cipher: {'ChaCha20-Poly1305' if sender.protocol.cipher_type == 1 else 'AES-256-GCM'}")
print(f" Sender HKDF Key: {sender.protocol.hkdf_key[:16]}...")
# Wait for receiver to complete
time.sleep(2)
if receiver.protocol.hkdf_key:
print(f" Receiver HKDF Key: {receiver.protocol.hkdf_key[:16]}...")
# Keys should match
if sender.protocol.hkdf_key == receiver.protocol.hkdf_key:
print(f"{GREEN}[PASS]{RESET} Keys match!")
else:
print(f"{RED}[FAIL]{RESET} Keys don't match!")
return False
else:
print(f"{RED}[FAIL]{RESET} Key exchange failed")
return False
# Test encrypted message
print(f"\n{YELLOW}=== Testing Encrypted Messages ==={RESET}")
sender.send_encrypted_message("Hello from sender!")
time.sleep(1)
# Check receiver got it
if receiver.protocol.inbound_messages:
last_msg = receiver.protocol.inbound_messages[-1]
if last_msg["type"] == "ENCRYPTED_MESSAGE":
decrypted = receiver.protocol.decrypt_received_message(len(receiver.protocol.inbound_messages) - 1)
if decrypted:
print(f"{GREEN}[PASS]{RESET} Message decrypted: {decrypted}")
else:
print(f"{RED}[FAIL]{RESET} Failed to decrypt message")
# Clean up
sender.close()
receiver.close()
return True
def test_voice_transmission():
"""Test voice transmission with encryption and FSK"""
print(f"\n{YELLOW}=== Testing Voice Transmission ==={RESET}")
# Check if input.wav exists
input_file = Path(__file__).parent / "DryBox" / "input.wav"
if not input_file.exists():
print(f"{YELLOW}[SKIP]{RESET} input.wav not found, creating test file...")
# Create a test tone
subprocess.run([
"sox", "-n", str(input_file),
"synth", "1", "sine", "440",
"rate", "8000"
], capture_output=True)
# Create sender and receiver
sender = IntegratedDryBoxProtocol(mode="sender")
receiver = IntegratedDryBoxProtocol(mode="receiver")
# Connect and exchange keys
if not sender.connect_gsm() or not receiver.connect_gsm():
print(f"{RED}[ERROR]{RESET} Failed to connect to GSM")
return False
# Setup protocol
receiver_port = receiver.setup_protocol_connection()
sender.setup_protocol_connection(
peer_port=receiver_port,
peer_identity=receiver.get_identity_key()
)
# Key exchange
if not sender.initiate_key_exchange(cipher_type=1):
print(f"{RED}[ERROR]{RESET} Key exchange failed")
return False
print(f"{BLUE}[TEST]{RESET} Sending voice with 4FSK modulation...")
# Send voice
sender.send_voice()
# Wait for transmission
time.sleep(5)
# Check if receiver created output file
output_file = Path(__file__).parent / "DryBox" / "received.wav"
if output_file.exists():
print(f"{GREEN}[PASS]{RESET} Voice received and decoded!")
print(f" Output file: {output_file}")
# Clean up output
os.remove(output_file)
else:
print(f"{RED}[FAIL]{RESET} Voice not received")
return False
# Clean up
sender.close()
receiver.close()
return True
def test_full_integration():
"""Run all integration tests"""
print(f"{BLUE}{'='*60}{RESET}")
print(f"{BLUE}DryBox Integration Test Suite{RESET}")
print(f"{BLUE}{'='*60}{RESET}")
# Start GSM simulator
gsm_process = start_gsm_simulator()
try:
# Run tests
tests_passed = 0
tests_total = 0
# Test 1: Key Exchange
tests_total += 1
if test_key_exchange():
tests_passed += 1
# Test 2: Voice Transmission
tests_total += 1
if test_voice_transmission():
tests_passed += 1
# Summary
print(f"\n{BLUE}{'='*60}{RESET}")
print(f"{BLUE}Test Summary:{RESET}")
print(f" Passed: {tests_passed}/{tests_total}")
if tests_passed == tests_total:
print(f"{GREEN} All tests passed!{RESET}")
else:
print(f"{RED} Some tests failed{RESET}")
finally:
# Stop GSM simulator
print(f"\n{BLUE}[CLEANUP]{RESET} Stopping GSM simulator...")
gsm_process.terminate()
gsm_process.wait()
def manual_test():
"""Interactive manual test"""
print(f"{BLUE}{'='*60}{RESET}")
print(f"{BLUE}DryBox Manual Test Mode{RESET}")
print(f"{BLUE}{'='*60}{RESET}")
# Start GSM simulator
gsm_process = start_gsm_simulator()
try:
mode = input("\nEnter mode (sender/receiver): ").strip().lower()
# Create protocol instance
protocol = IntegratedDryBoxProtocol(mode=mode)
if not protocol.connect_gsm():
print(f"{RED}[ERROR]{RESET} Failed to connect to GSM")
return
print(f"\n{YELLOW}Protocol Information:{RESET}")
print(f" Mode: {mode}")
print(f" Identity: {protocol.get_identity_key()}")
print(f" Protocol port: {protocol.protocol.local_port}")
if mode == "sender":
peer_port = input("\nEnter receiver's protocol port: ")
peer_identity = input("Enter receiver's identity key: ")
protocol.setup_protocol_connection(
peer_port=int(peer_port),
peer_identity=peer_identity
)
print("\nInitiating key exchange...")
if protocol.initiate_key_exchange(cipher_type=1):
print(f"{GREEN}Key exchange successful!{RESET}")
while True:
print("\nOptions:")
print(" 1. Send encrypted message")
print(" 2. Send voice")
print(" 3. Show status")
print(" 4. Exit")
choice = input("\nChoice: ")
if choice == "1":
msg = input("Enter message: ")
protocol.send_encrypted_message(msg)
elif choice == "2":
protocol.send_voice()
elif choice == "3":
protocol.show_status()
elif choice == "4":
break
else:
# Receiver mode
port = protocol.setup_protocol_connection()
print(f"\nTell sender to connect to port: {port}")
print("Waiting for connection...")
try:
while True:
time.sleep(1)
if protocol.protocol.state.get("key_exchange_complete"):
print(f"{GREEN}Key exchange completed!{RESET}")
print("Listening for messages...")
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
protocol.close()
gsm_process.terminate()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Test DryBox Integration")
parser.add_argument("--manual", action="store_true", help="Run manual test mode")
args = parser.parse_args()
if args.manual:
manual_test()
else:
test_full_integration()

116
protocol_prototype/test_gsm_ui.py Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Test script for GSM simulator and UI together.
This script starts the GSM simulator in a separate process and launches the UI.
"""
import subprocess
import time
import sys
import os
import signal
def main():
"""Main function to run GSM simulator and UI together."""
gsm_process = None
ui_process = None
try:
print("Starting GSM and UI Test...")
print("-" * 50)
# Change to DryBox directory
drybox_dir = os.path.join(os.path.dirname(__file__), 'DryBox')
os.chdir(drybox_dir)
# Start GSM simulator
print("1. Starting GSM simulator...")
gsm_process = subprocess.Popen(
[sys.executable, 'gsm_simulator.py'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# Give the GSM simulator time to start
time.sleep(2)
# Check if GSM simulator started successfully
if gsm_process.poll() is not None:
stderr = gsm_process.stderr.read()
print(f"ERROR: GSM simulator failed to start: {stderr}")
return 1
print(" GSM simulator started successfully on port 12345")
# Start UI
print("\n2. Starting Phone UI...")
ui_process = subprocess.Popen(
[sys.executable, 'UI/python_ui.py'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# Give the UI time to start
time.sleep(2)
# Check if UI started successfully
if ui_process.poll() is not None:
stderr = ui_process.stderr.read()
print(f"ERROR: UI failed to start: {stderr}")
return 1
print(" UI started successfully")
print("\n" + "=" * 50)
print("GSM Simulator and UI are running!")
print("=" * 50)
print("\nInstructions:")
print("- The UI shows two phones that can call each other")
print("- Click 'Call' on Phone 1 to call Phone 2")
print("- Phone 2 will show 'Incoming Call' - click 'Answer' to accept")
print("- During the call, audio packets will be exchanged")
print("- Click 'Hang Up' to end the call")
print("\nPress Ctrl+C to stop the test...")
# Wait for user interruption
while True:
time.sleep(1)
# Check if processes are still running
if gsm_process.poll() is not None:
print("\nWARNING: GSM simulator has stopped!")
break
if ui_process.poll() is not None:
print("\nINFO: UI has been closed by user")
break
except KeyboardInterrupt:
print("\n\nStopping test...")
except Exception as e:
print(f"\nERROR: {e}")
return 1
finally:
# Clean up processes
if gsm_process and gsm_process.poll() is None:
print("Stopping GSM simulator...")
gsm_process.terminate()
try:
gsm_process.wait(timeout=5)
except subprocess.TimeoutExpired:
gsm_process.kill()
if ui_process and ui_process.poll() is None:
print("Stopping UI...")
ui_process.terminate()
try:
ui_process.wait(timeout=5)
except subprocess.TimeoutExpired:
ui_process.kill()
print("Test completed.")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
Test script for the Icing protocol.
This script demonstrates the full protocol flow between two peers:
1. Connection establishment
2. Ping exchange
3. Key exchange (ECDH + HKDF)
4. Encrypted messaging
"""
import time
import sys
import threading
from protocol import IcingProtocol
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def test_manual_protocol():
"""Test the protocol with manual step-by-step progression."""
print(f"\n{BLUE}=== Manual Protocol Test ==={RESET}")
print("This test demonstrates manual control of the protocol flow.\n")
# Create two protocol instances
alice = IcingProtocol()
bob = IcingProtocol()
print(f"Alice listening on port: {alice.local_port}")
print(f"Bob listening on port: {bob.local_port}")
# Exchange identity keys
print(f"\n{YELLOW}1. Exchanging identity keys...{RESET}")
alice.set_peer_identity(bob.identity_pubkey.hex())
bob.set_peer_identity(alice.identity_pubkey.hex())
print(" Identity keys exchanged.")
# Establish connection
print(f"\n{YELLOW}2. Establishing connection...{RESET}")
alice.connect_to_peer(bob.local_port)
time.sleep(1) # Allow connection to establish
print(" Connection established.")
# Send ping from Alice
print(f"\n{YELLOW}3. Sending PING request...{RESET}")
alice.send_ping_request(cipher_type=0) # AES-256-GCM
time.sleep(1) # Allow ping to be received
# Bob responds to ping
if bob.inbound_messages:
print(f" Bob received PING, responding...")
bob.respond_to_ping(0, answer=1) # Accept
time.sleep(1)
# Generate ephemeral keys
print(f"\n{YELLOW}4. Generating ephemeral keys...{RESET}")
alice.generate_ephemeral_keys()
bob.generate_ephemeral_keys()
print(" Ephemeral keys generated.")
# Alice sends handshake
print(f"\n{YELLOW}5. Sending handshake...{RESET}")
alice.send_handshake()
time.sleep(1)
# Bob processes handshake and responds
if bob.inbound_messages:
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "HANDSHAKE":
print(f" Bob processing handshake...")
bob.generate_ecdhe(i)
bob.send_handshake()
break
time.sleep(1)
# Alice processes Bob's handshake
if alice.inbound_messages:
for i, msg in enumerate(alice.inbound_messages):
if msg["type"] == "HANDSHAKE":
print(f" Alice processing handshake...")
alice.generate_ecdhe(i)
break
# Derive HKDF keys
print(f"\n{YELLOW}6. Deriving encryption keys...{RESET}")
alice.derive_hkdf()
bob.derive_hkdf()
print(" HKDF keys derived.")
# Send encrypted messages
print(f"\n{YELLOW}7. Sending encrypted messages...{RESET}")
alice.send_encrypted_message("Hello Bob! This is a secure message.")
time.sleep(1)
# Bob decrypts the message
if bob.inbound_messages:
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "ENCRYPTED_MESSAGE":
print(f" Bob decrypting message...")
bob.decrypt_received_message(i)
break
# Bob sends a reply
bob.send_encrypted_message("Hi Alice! Message received securely.")
time.sleep(1)
# Alice decrypts the reply
if alice.inbound_messages:
for i, msg in enumerate(alice.inbound_messages):
if msg["type"] == "ENCRYPTED_MESSAGE":
print(f" Alice decrypting message...")
alice.decrypt_received_message(i)
break
# Show final state
print(f"\n{YELLOW}8. Final protocol state:{RESET}")
print("\nAlice:")
alice.show_state()
print("\nBob:")
bob.show_state()
# Cleanup
alice.stop()
bob.stop()
print(f"\n{GREEN}Manual test completed successfully!{RESET}")
def test_auto_mode_protocol():
"""Test the protocol using automatic mode."""
print(f"\n{BLUE}=== Automatic Mode Protocol Test ==={RESET}")
print("This test demonstrates the automatic protocol flow.\n")
# Create two protocol instances
alice = IcingProtocol()
bob = IcingProtocol()
print(f"Alice listening on port: {alice.local_port}")
print(f"Bob listening on port: {bob.local_port}")
# Exchange identity keys
print(f"\n{YELLOW}1. Setting up peers...{RESET}")
alice.set_peer_identity(bob.identity_pubkey.hex())
bob.set_peer_identity(alice.identity_pubkey.hex())
# Configure auto mode for Alice (initiator)
print(f"\n{YELLOW}2. Configuring auto mode...{RESET}")
alice.configure_auto_mode(
active_mode=True,
ping_auto_initiate=True,
preferred_cipher=0, # AES-256-GCM
auto_message_enabled=True,
message_interval=2.0,
message_content="Auto-generated secure message from Alice"
)
# Configure auto mode for Bob (responder)
bob.configure_auto_mode(
ping_response_accept=True,
auto_message_enabled=True,
message_interval=2.0,
message_content="Auto-generated secure reply from Bob"
)
# Start auto mode
print(f" Starting auto mode for both peers...")
alice.start_auto_mode()
bob.start_auto_mode()
# Establish connection (this will trigger the auto protocol)
print(f"\n{YELLOW}3. Establishing connection...{RESET}")
alice.connect_to_peer(bob.local_port)
# Let the protocol run automatically
print(f"\n{YELLOW}4. Running automatic protocol exchange...{RESET}")
print(" Waiting for automatic protocol completion...")
# Monitor progress
for i in range(10):
time.sleep(2)
print(f"\n Progress check {i+1}/10:")
print(f" Alice state: {alice.auto_mode.state}")
print(f" Bob state: {bob.auto_mode.state}")
# Check if key exchange is complete
if alice.state.get("key_exchange_complete") and bob.state.get("key_exchange_complete"):
print(f"\n{GREEN} Key exchange completed!{RESET}")
break
# Queue some additional messages
print(f"\n{YELLOW}5. Queueing additional messages...{RESET}")
alice.queue_auto_message("Custom message 1 from Alice")
alice.queue_auto_message("Custom message 2 from Alice")
bob.queue_auto_message("Custom reply from Bob")
# Let messages be exchanged
time.sleep(5)
# Show final state
print(f"\n{YELLOW}6. Final protocol state:{RESET}")
print("\nAlice:")
alice.show_state()
print("\nBob:")
bob.show_state()
# Stop auto mode
alice.stop_auto_mode()
bob.stop_auto_mode()
# Cleanup
alice.stop()
bob.stop()
print(f"\n{GREEN}Automatic mode test completed successfully!{RESET}")
def main():
"""Main function to run protocol tests."""
print(f"{BLUE}{'='*60}{RESET}")
print(f"{BLUE} Icing Protocol Test Suite{RESET}")
print(f"{BLUE}{'='*60}{RESET}")
print("\nSelect test mode:")
print("1. Manual protocol test (step-by-step)")
print("2. Automatic mode test (auto protocol flow)")
print("3. Run both tests")
print("0. Exit")
try:
choice = input("\nEnter your choice (0-3): ").strip()
if choice == "1":
test_manual_protocol()
elif choice == "2":
test_auto_mode_protocol()
elif choice == "3":
test_manual_protocol()
print(f"\n{YELLOW}{'='*60}{RESET}\n")
test_auto_mode_protocol()
elif choice == "0":
print("Exiting...")
return 0
else:
print(f"{RED}Invalid choice. Please enter 0-3.{RESET}")
return 1
except KeyboardInterrupt:
print(f"\n\n{YELLOW}Test interrupted by user.{RESET}")
return 0
except Exception as e:
print(f"\n{RED}ERROR: {e}{RESET}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Basic test for voice protocol components."""
import sys
from voice_codec import Codec2Wrapper, FSKModem, Codec2Mode
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def test_codec2():
"""Test Codec2 wrapper."""
print(f"\n{BLUE}=== Testing Codec2 ==={RESET}")
codec = Codec2Wrapper(Codec2Mode.MODE_1200)
# Create simple test data
test_samples = [0] * 320 # Silent frame
# Encode
frame = codec.encode(test_samples)
if frame:
print(f"{GREEN}✓ Encoded frame: {len(frame.bits)} bytes{RESET}")
# Decode
decoded = codec.decode(frame)
print(f"{GREEN}✓ Decoded: {len(decoded)} samples{RESET}")
else:
print(f"{RED}✗ Encoding failed{RESET}")
def test_fsk_modem():
"""Test FSK modem."""
print(f"\n{BLUE}=== Testing FSK Modem ==={RESET}")
modem = FSKModem()
# Test data
test_data = b"Hello"
# Modulate
modulated = modem.modulate(test_data)
print(f"{GREEN}✓ Modulated {len(test_data)} bytes to {len(modulated)} samples{RESET}")
# Demodulate
demodulated, confidence = modem.demodulate(modulated)
if demodulated == test_data:
print(f"{GREEN}✓ Demodulation successful (confidence: {confidence:.1%}){RESET}")
else:
print(f"{RED}✗ Demodulation failed{RESET}")
print(f" Expected: {test_data}")
print(f" Got: {demodulated}")
def test_voice_protocol():
"""Test voice protocol integration."""
print(f"\n{BLUE}=== Testing Voice Protocol Integration ==={RESET}")
from protocol import IcingProtocol
import time
# Create protocol instances
alice = IcingProtocol()
bob = IcingProtocol()
# Simple key exchange
alice.set_peer_identity(bob.identity_pubkey.hex())
bob.set_peer_identity(alice.identity_pubkey.hex())
# Wait for servers
time.sleep(0.5)
# Connect
alice.connect_to_peer(bob.local_port)
time.sleep(0.5)
# Quick key exchange
alice.send_ping_request(1)
time.sleep(0.1)
bob.respond_to_ping(0, 1)
time.sleep(0.1)
alice.generate_ephemeral_keys()
bob.generate_ephemeral_keys()
alice.send_handshake()
time.sleep(0.1)
if bob.inbound_messages:
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "HANDSHAKE":
bob.generate_ecdhe(i)
bob.send_handshake()
break
time.sleep(0.1)
if alice.inbound_messages:
for i, msg in enumerate(alice.inbound_messages):
if msg["type"] == "HANDSHAKE":
alice.generate_ecdhe(i)
break
alice.derive_hkdf()
bob.derive_hkdf()
# Test voice call
print(f"\n{YELLOW}Testing voice call setup...{RESET}")
if alice.start_voice_call():
print(f"{GREEN}✓ Voice call initiated{RESET}")
time.sleep(0.1)
# Bob accepts
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "VOICE_START":
vs = msg["parsed"]
bob.accept_voice_call(vs.session_id, vs.codec_mode, vs.fec_type)
print(f"{GREEN}✓ Voice call accepted{RESET}")
break
time.sleep(0.1)
if alice.voice_session_active and bob.voice_session_active:
print(f"{GREEN}✓ Voice session established{RESET}")
else:
print(f"{RED}✗ Voice session failed{RESET}")
else:
print(f"{RED}✗ Failed to start voice call{RESET}")
# Cleanup
alice.stop()
bob.stop()
def main():
"""Run all tests."""
print(f"{BLUE}{'='*50}{RESET}")
print(f"{BLUE}Voice Protocol Component Tests{RESET}")
print(f"{BLUE}{'='*50}{RESET}")
try:
test_codec2()
test_fsk_modem()
test_voice_protocol()
print(f"\n{GREEN}All tests completed!{RESET}")
except Exception as e:
print(f"\n{RED}Test failed: {e}{RESET}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""
Test script for the voice-over-GSM protocol integration.
This demonstrates encrypted voice transmission using Codec2 and FSK modulation.
"""
import time
import sys
import array
from protocol import IcingProtocol
from voice_codec import Codec2Mode
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def generate_test_audio(duration_ms: int, frequency: int = 440) -> array.array:
"""Generate test audio (sine wave)."""
import math
sample_rate = 8000
samples = int(sample_rate * duration_ms / 1000)
audio = array.array('h') # 16-bit signed integers
for i in range(samples):
t = i / sample_rate
value = int(math.sin(2 * math.pi * frequency * t) * 16384)
audio.append(value)
return audio
def test_voice_protocol():
"""Test voice protocol with two peers."""
print(f"\n{BLUE}=== Voice Protocol Test ==={RESET}")
print("This test demonstrates encrypted voice communication.\n")
# Create two protocol instances
alice = IcingProtocol()
bob = IcingProtocol()
print(f"Alice listening on port: {alice.local_port}")
print(f"Bob listening on port: {bob.local_port}")
# Exchange identity keys
print(f"\n{YELLOW}1. Setting up secure channel...{RESET}")
alice.set_peer_identity(bob.identity_pubkey.hex())
bob.set_peer_identity(alice.identity_pubkey.hex())
# Establish connection
alice.connect_to_peer(bob.local_port)
time.sleep(0.5)
# Perform key exchange
print(f"\n{YELLOW}2. Performing key exchange...{RESET}")
# Send ping
alice.send_ping_request(cipher_type=1) # Use ChaCha20
time.sleep(0.5)
# Bob responds
if bob.inbound_messages:
bob.respond_to_ping(0, answer=1)
time.sleep(0.5)
# Generate ephemeral keys
alice.generate_ephemeral_keys()
bob.generate_ephemeral_keys()
# Exchange handshakes
alice.send_handshake()
time.sleep(0.5)
# Bob processes and responds
if bob.inbound_messages:
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "HANDSHAKE":
bob.generate_ecdhe(i)
bob.send_handshake()
break
time.sleep(0.5)
# Alice processes Bob's handshake
if alice.inbound_messages:
for i, msg in enumerate(alice.inbound_messages):
if msg["type"] == "HANDSHAKE":
alice.generate_ecdhe(i)
break
# Derive keys
alice.derive_hkdf()
bob.derive_hkdf()
print(f"{GREEN} Secure channel established!{RESET}")
# Start voice call
print(f"\n{YELLOW}3. Initiating voice call...{RESET}")
alice.start_voice_call(codec_mode=5, fec_type=0) # 1200bps, repetition FEC
time.sleep(0.5)
# Check if Bob received the call and accept it manually
voice_active = False
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "VOICE_START":
voice_start = msg["parsed"]
print(f" Bob accepting voice call...")
bob.accept_voice_call(voice_start.session_id, voice_start.codec_mode, voice_start.fec_type)
time.sleep(0.5)
voice_active = True
break
if voice_active and alice.voice_session_active:
print(f"{GREEN} Voice call established!{RESET}")
print(f" Session ID: {alice.voice_session_id:016x}")
else:
print(f"{RED} Voice call failed to establish{RESET}")
if voice_active:
# Test voice transmission
print(f"\n{YELLOW}4. Testing voice transmission...{RESET}")
# Generate test audio (440Hz tone for 200ms)
test_audio = generate_test_audio(200, 440)
print(f" Generated {len(test_audio)} audio samples")
# Alice sends audio
print(f"\n Alice sending audio...")
# Convert array to numpy array if needed
import array
if isinstance(test_audio, array.array):
# Voice protocol expects raw array or list
audio_data = test_audio
else:
audio_data = test_audio
success = alice.send_voice_audio(audio_data)
if success:
print(f"{GREEN} Audio processed and modulated{RESET}")
else:
print(f"{RED} Failed to process audio{RESET}")
# Test voice codec directly
print(f"\n{YELLOW}5. Testing voice codec components...{RESET}")
if alice.voice_protocol:
# Test Codec2
print(f"\n Testing Codec2 compression...")
# Get one frame worth of samples
if hasattr(test_audio, '__getitem__'):
frame_audio = test_audio[:320] if len(test_audio) >= 320 else test_audio
else:
frame_audio = list(test_audio)[:320]
codec_frame = alice.voice_protocol.codec.encode(frame_audio)
if codec_frame:
print(f" Compressed to {len(codec_frame.bits)} bytes")
# Test decompression
decoded = alice.voice_protocol.codec.decode(codec_frame)
print(f" Decompressed to {len(decoded)} samples")
# Test FSK modulation
print(f"\n Testing FSK modulation...")
test_data = b"Voice test data"
modulated = alice.voice_protocol.modem.modulate(test_data)
print(f" Modulated {len(test_data)} bytes to {len(modulated)} audio samples")
# Test demodulation
demodulated, confidence = alice.voice_protocol.modem.demodulate(modulated)
print(f" Demodulated with {confidence:.1%} confidence")
print(f" Data match: {demodulated == test_data}")
# Send sync frame
print(f"\n{YELLOW}6. Testing synchronization...{RESET}")
from messages import VoiceSync
sync_msg = VoiceSync(
session_id=alice.voice_session_id,
sequence=1,
timestamp=100
)
alice._send_packet(alice.connections[0], sync_msg.serialize(), "VOICE_SYNC")
time.sleep(0.5)
# End voice call
print(f"\n{YELLOW}7. Ending voice call...{RESET}")
alice.end_voice_call()
time.sleep(0.5)
# Show final state
print(f"\n{YELLOW}8. Final state:{RESET}")
print("\nAlice voice status:")
print(f" Active: {alice.voice_session_active}")
print(f" Voice codec initialized: {alice.voice_protocol is not None}")
print("\nBob voice status:")
print(f" Active: {bob.voice_session_active}")
print(f" Voice codec initialized: {bob.voice_protocol is not None}")
# Cleanup
alice.stop()
bob.stop()
print(f"\n{GREEN}Voice protocol test completed!{RESET}")
def test_codec_modes():
"""Test different Codec2 modes."""
print(f"\n{BLUE}=== Codec2 Mode Comparison ==={RESET}")
from voice_codec import Codec2Wrapper, Codec2Mode
modes = [
(Codec2Mode.MODE_3200, "3200 bps"),
(Codec2Mode.MODE_2400, "2400 bps"),
(Codec2Mode.MODE_1600, "1600 bps"),
(Codec2Mode.MODE_1400, "1400 bps"),
(Codec2Mode.MODE_1300, "1300 bps"),
(Codec2Mode.MODE_1200, "1200 bps (recommended)"),
(Codec2Mode.MODE_700C, "700 bps")
]
# Generate test audio
test_audio = generate_test_audio(100, 440)
print("\nMode comparison:")
print("-" * 50)
for mode, description in modes:
try:
codec = Codec2Wrapper(mode)
# Process one frame
if hasattr(test_audio, '__getitem__'):
frame_audio = test_audio[:codec.frame_samples]
else:
frame_audio = list(test_audio)[:codec.frame_samples]
if len(frame_audio) < codec.frame_samples:
# Pad if necessary
frame_audio = frame_audio + [0] * (codec.frame_samples - len(frame_audio))
frame = codec.encode(frame_audio)
if frame:
efficiency = (codec.frame_bits / 8) / (codec.frame_ms / 1000) / 1000 # KB/s
print(f"{description:20} | {codec.frame_bits:3} bits/frame | "
f"{codec.frame_ms:2}ms | {efficiency:.2f} KB/s")
except Exception as e:
print(f"{description:20} | Error: {e}")
print("-" * 50)
print(f"\n{YELLOW}Note: Lower bitrates provide better GSM vocoder survival{RESET}")
print(f"{YELLOW} but reduced voice quality. 1200 bps is recommended.{RESET}")
def main():
"""Main test function."""
print(f"{BLUE}{'='*60}{RESET}")
print(f"{BLUE} Voice-over-GSM Protocol Test Suite{RESET}")
print(f"{BLUE}{'='*60}{RESET}")
print("\nSelect test:")
print("1. Full voice protocol test")
print("2. Codec2 mode comparison")
print("3. Run both tests")
print("0. Exit")
try:
choice = input("\nEnter your choice (0-3): ").strip()
if choice == "1":
test_voice_protocol()
elif choice == "2":
test_codec_modes()
elif choice == "3":
test_voice_protocol()
print(f"\n{YELLOW}{'='*60}{RESET}\n")
test_codec_modes()
elif choice == "0":
print("Exiting...")
return 0
else:
print(f"{RED}Invalid choice.{RESET}")
return 1
except KeyboardInterrupt:
print(f"\n\n{YELLOW}Test interrupted.{RESET}")
return 0
except Exception as e:
print(f"\n{RED}ERROR: {e}{RESET}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
Simple test for voice protocol without numpy dependency.
"""
import time
import sys
from protocol import IcingProtocol
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def test_voice_protocol():
"""Test voice protocol with two peers."""
print(f"\n{BLUE}=== Simple Voice Protocol Test ==={RESET}")
print("Testing voice call setup and messaging.\n")
# Create two protocol instances
alice = IcingProtocol()
bob = IcingProtocol()
print(f"Alice listening on port: {alice.local_port}")
print(f"Bob listening on port: {bob.local_port}")
# Configure auto mode for easier testing
alice.configure_auto_mode(
active_mode=True,
ping_auto_initiate=True,
preferred_cipher=1, # ChaCha20
)
bob.configure_auto_mode(
ping_response_accept=True,
)
# Start auto mode
alice.start_auto_mode()
bob.start_auto_mode()
# Exchange identity keys
print(f"\n{YELLOW}1. Setting up secure channel...{RESET}")
alice.set_peer_identity(bob.identity_pubkey.hex())
bob.set_peer_identity(alice.identity_pubkey.hex())
# Wait for servers to start
time.sleep(0.5)
# Establish connection - auto mode will handle the protocol
alice.connect_to_peer(bob.local_port)
# Wait for key exchange to complete
print(f"\n{YELLOW}2. Waiting for automatic key exchange...{RESET}")
max_wait = 10
for i in range(max_wait):
time.sleep(1)
if alice.state.get("key_exchange_complete") and bob.state.get("key_exchange_complete"):
print(f"{GREEN} Key exchange completed!{RESET}")
break
print(f" Waiting... {i+1}/{max_wait}")
else:
print(f"{RED} Key exchange failed to complete{RESET}")
alice.stop()
bob.stop()
return
# Test voice call
print(f"\n{YELLOW}3. Testing voice call setup...{RESET}")
# Alice initiates voice call
success = alice.start_voice_call(codec_mode=5, fec_type=0)
if success:
print(f"{GREEN} Alice initiated voice call{RESET}")
else:
print(f"{RED} Failed to initiate voice call{RESET}")
alice.stop()
bob.stop()
return
# Wait for Bob to receive and auto-accept
time.sleep(1)
# Check voice status
print(f"\n{YELLOW}4. Voice call status:{RESET}")
print(f" Alice voice active: {alice.voice_session_active}")
print(f" Bob voice active: {bob.voice_session_active}")
if alice.voice_session_active and bob.voice_session_active:
print(f"{GREEN} Voice call established successfully!{RESET}")
print(f" Session ID: {alice.voice_session_id:016x}")
# Test sending encrypted messages during voice call
print(f"\n{YELLOW}5. Testing encrypted messaging during voice call...{RESET}")
alice.send_encrypted_message("Voice call test message from Alice")
time.sleep(0.5)
# Bob decrypts
for i, msg in enumerate(bob.inbound_messages):
if msg["type"] == "ENCRYPTED_MESSAGE":
plaintext = bob.decrypt_received_message(i)
if plaintext:
print(f" Bob received: {plaintext}")
# End voice call
print(f"\n{YELLOW}6. Ending voice call...{RESET}")
alice.end_voice_call()
time.sleep(0.5)
print(f" Voice call ended")
else:
print(f"{RED} Voice call failed to establish{RESET}")
# Show final states
print(f"\n{YELLOW}7. Final states:{RESET}")
print("\nAlice state:")
alice.show_state()
print("\nBob state:")
bob.show_state()
# Cleanup
alice.stop()
bob.stop()
print(f"\n{GREEN}Test completed!{RESET}")
if __name__ == "__main__":
try:
test_voice_protocol()
except KeyboardInterrupt:
print(f"\n{YELLOW}Test interrupted.{RESET}")
except Exception as e:
print(f"\n{RED}ERROR: {e}{RESET}")
import traceback
traceback.print_exc()

View File

@ -0,0 +1,100 @@
import socket
import threading
from typing import Callable
class PeerConnection:
"""
Represents a live, two-way connection to a peer.
We keep a socket open, read data in a background thread,
and can send data from the main thread at any time.
"""
def __init__(self, sock: socket.socket, on_data_received: Callable[['PeerConnection', bytes], None]):
self.sock = sock
self.on_data_received = on_data_received
self.alive = True
self.read_thread = threading.Thread(target=self.read_loop, daemon=True)
self.read_thread.start()
def read_loop(self):
while self.alive:
try:
data = self.sock.recv(4096)
if not data:
break
self.on_data_received(self, data)
except OSError:
break
self.alive = False
self.sock.close()
print("[PeerConnection] Connection closed.")
def send(self, data: bytes):
if not self.alive:
print("[PeerConnection.send] Cannot send, connection not alive.")
return
try:
self.sock.sendall(data)
except OSError:
print("[PeerConnection.send] Send failed, connection might be closed.")
self.alive = False
def close(self):
self.alive = False
try:
self.sock.shutdown(socket.SHUT_RDWR)
except OSError:
pass
self.sock.close()
class ServerListener(threading.Thread):
"""
A thread that listens on a given port. When a new client connects,
it creates a PeerConnection for that client.
"""
def __init__(self, host: str, port: int,
on_new_connection: Callable[[PeerConnection], None],
on_data_received: Callable[[PeerConnection, bytes], None]):
super().__init__(daemon=True)
self.host = host
self.port = port
self.on_new_connection = on_new_connection
self.on_data_received = on_data_received
self.server_socket = None
self.stop_event = threading.Event()
def run(self):
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
self.server_socket.settimeout(1.0)
print(f"[ServerListener] Listening on {self.host}:{self.port}")
while not self.stop_event.is_set():
try:
client_sock, addr = self.server_socket.accept()
print(f"[ServerListener] Accepted connection from {addr}")
conn = PeerConnection(client_sock, self.on_data_received)
self.on_new_connection(conn)
except socket.timeout:
pass
except OSError:
break
if self.server_socket:
self.server_socket.close()
def stop(self):
self.stop_event.set()
if self.server_socket:
self.server_socket.close()
def connect_to_peer(host: str, port: int,
on_data_received: Callable[[PeerConnection, bytes], None]) -> PeerConnection:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
print(f"[connect_to_peer] Connected to {host}:{port}")
conn = PeerConnection(sock, on_data_received)
return conn

View File

@ -0,0 +1,716 @@
"""
Voice codec integration for encrypted voice over GSM.
Implements Codec2 compression with FSK modulation for transmitting
encrypted voice data over standard GSM voice channels.
"""
import array
import math
import struct
from typing import Optional, Tuple, List
from dataclasses import dataclass
from enum import IntEnum
try:
import numpy as np
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class Codec2Mode(IntEnum):
"""Codec2 bitrate modes."""
MODE_3200 = 0 # 3200 bps
MODE_2400 = 1 # 2400 bps
MODE_1600 = 2 # 1600 bps
MODE_1400 = 3 # 1400 bps
MODE_1300 = 4 # 1300 bps
MODE_1200 = 5 # 1200 bps (recommended for robustness)
MODE_700C = 6 # 700 bps
@dataclass
class Codec2Frame:
"""Represents a single Codec2 compressed voice frame."""
mode: Codec2Mode
bits: bytes
timestamp: float
frame_number: int
class Codec2Wrapper:
"""
Wrapper for Codec2 voice codec.
In production, this would use py_codec2 or ctypes bindings to libcodec2.
This is a simulation interface for protocol development.
"""
# Frame sizes in bits for each mode
FRAME_BITS = {
Codec2Mode.MODE_3200: 64,
Codec2Mode.MODE_2400: 48,
Codec2Mode.MODE_1600: 64,
Codec2Mode.MODE_1400: 56,
Codec2Mode.MODE_1300: 52,
Codec2Mode.MODE_1200: 48,
Codec2Mode.MODE_700C: 28
}
# Frame duration in ms
FRAME_MS = {
Codec2Mode.MODE_3200: 20,
Codec2Mode.MODE_2400: 20,
Codec2Mode.MODE_1600: 40,
Codec2Mode.MODE_1400: 40,
Codec2Mode.MODE_1300: 40,
Codec2Mode.MODE_1200: 40,
Codec2Mode.MODE_700C: 40
}
def __init__(self, mode: Codec2Mode = Codec2Mode.MODE_1200):
"""
Initialize Codec2 wrapper.
Args:
mode: Codec2 bitrate mode (default 1200 bps for robustness)
"""
self.mode = mode
self.frame_bits = self.FRAME_BITS[mode]
self.frame_bytes = (self.frame_bits + 7) // 8
self.frame_ms = self.FRAME_MS[mode]
self.frame_samples = int(8000 * self.frame_ms / 1000) # 8kHz sampling
self.frame_counter = 0
print(f"{GREEN}[CODEC2]{RESET} Initialized in mode {mode.name} "
f"({self.frame_bits} bits/frame, {self.frame_ms}ms duration)")
def encode(self, audio_samples) -> Optional[Codec2Frame]:
"""
Encode PCM audio samples to Codec2 frame.
Args:
audio_samples: PCM samples (8kHz, 16-bit signed)
Returns:
Codec2Frame or None if insufficient samples
"""
if len(audio_samples) < self.frame_samples:
return None
# In production: call codec2_encode(state, bits, samples)
# Simulation: create pseudo-compressed data
compressed = self._simulate_compression(audio_samples[:self.frame_samples])
frame = Codec2Frame(
mode=self.mode,
bits=compressed,
timestamp=self.frame_counter * self.frame_ms / 1000.0,
frame_number=self.frame_counter
)
self.frame_counter += 1
return frame
def decode(self, frame: Codec2Frame):
"""
Decode Codec2 frame to PCM audio samples.
Args:
frame: Codec2 compressed frame
Returns:
PCM samples (8kHz, 16-bit signed)
"""
if frame.mode != self.mode:
raise ValueError(f"Frame mode {frame.mode} doesn't match decoder mode {self.mode}")
# In production: call codec2_decode(state, samples, bits)
# Simulation: decompress to audio
return self._simulate_decompression(frame.bits)
def _simulate_compression(self, samples) -> bytes:
"""Simulate Codec2 compression (for testing)."""
# Convert to list if needed
if hasattr(samples, 'tolist'):
sample_list = samples.tolist()
elif hasattr(samples, '__iter__'):
sample_list = list(samples)
else:
sample_list = samples
# Extract basic features for simulation
if HAS_NUMPY and hasattr(samples, '__array__'):
# Convert to numpy array if needed
np_samples = np.asarray(samples, dtype=np.float32)
if len(np_samples) > 0:
mean_square = np.mean(np_samples ** 2)
energy = np.sqrt(mean_square) if not np.isnan(mean_square) else 0.0
zero_crossings = np.sum(np.diff(np.sign(np_samples)) != 0)
else:
energy = 0.0
zero_crossings = 0
else:
# Manual calculation without numpy
if sample_list and len(sample_list) > 0:
energy = math.sqrt(sum(s**2 for s in sample_list) / len(sample_list))
zero_crossings = sum(1 for i in range(1, len(sample_list))
if (sample_list[i-1] >= 0) != (sample_list[i] >= 0))
else:
energy = 0.0
zero_crossings = 0
# Pack into bytes (simplified)
# Ensure values are valid
energy_int = max(0, min(65535, int(energy)))
zc_int = max(0, min(65535, int(zero_crossings)))
data = struct.pack('<HH', energy_int, zc_int)
# Pad to expected frame size
data += b'\x00' * (self.frame_bytes - len(data))
return data[:self.frame_bytes]
def _simulate_decompression(self, compressed: bytes):
"""Simulate Codec2 decompression (for testing)."""
# Unpack features
if len(compressed) >= 4:
energy, zero_crossings = struct.unpack('<HH', compressed[:4])
else:
energy, zero_crossings = 1000, 100
# Generate synthetic speech-like signal
if HAS_NUMPY:
t = np.linspace(0, self.frame_ms/1000, self.frame_samples)
# Base frequency from zero crossings
freq = zero_crossings * 10 # Simplified mapping
# Generate harmonics
signal = np.zeros(self.frame_samples)
for harmonic in range(1, 4):
signal += np.sin(2 * np.pi * freq * harmonic * t) / harmonic
# Apply energy envelope
signal *= energy / 10000.0
# Convert to 16-bit PCM
return (signal * 32767).astype(np.int16)
else:
# Manual generation without numpy
samples = []
freq = zero_crossings * 10
for i in range(self.frame_samples):
t = i / 8000.0 # 8kHz sample rate
value = 0
for harmonic in range(1, 4):
value += math.sin(2 * math.pi * freq * harmonic * t) / harmonic
value *= energy / 10000.0
# Clamp to 16-bit range
sample = int(value * 32767)
sample = max(-32768, min(32767, sample))
samples.append(sample)
return array.array('h', samples)
class FSKModem:
"""
4-FSK modem for transmitting digital data over voice channels.
Designed to survive GSM/AMR/EVS vocoders.
"""
def __init__(self, sample_rate: int = 8000, baud_rate: int = 600):
"""
Initialize FSK modem.
Args:
sample_rate: Audio sample rate (Hz)
baud_rate: Symbol rate (baud)
"""
self.sample_rate = sample_rate
self.baud_rate = baud_rate
self.samples_per_symbol = int(sample_rate / baud_rate)
# 4-FSK frequencies (300-3400 Hz band)
self.frequencies = [
600, # 00
1200, # 01
1800, # 10
2400 # 11
]
# Preamble for synchronization (800 Hz, 100ms)
self.preamble_freq = 800
self.preamble_duration = 0.1 # seconds
print(f"{GREEN}[FSK]{RESET} Initialized 4-FSK modem "
f"({baud_rate} baud, frequencies: {self.frequencies})")
def modulate(self, data: bytes, add_preamble: bool = True):
"""
Modulate binary data to FSK audio signal.
Args:
data: Binary data to modulate
add_preamble: Whether to add synchronization preamble
Returns:
Audio signal (normalized float32 array or list)
"""
# Convert bytes to dibits (2-bit symbols)
symbols = []
for byte in data:
symbols.extend([
(byte >> 6) & 0x03,
(byte >> 4) & 0x03,
(byte >> 2) & 0x03,
byte & 0x03
])
# Generate audio signal
signal = []
# Add preamble
if add_preamble:
preamble_samples = int(self.preamble_duration * self.sample_rate)
if HAS_NUMPY:
t = np.arange(preamble_samples) / self.sample_rate
preamble = np.sin(2 * np.pi * self.preamble_freq * t)
signal.extend(preamble)
else:
for i in range(preamble_samples):
t = i / self.sample_rate
value = math.sin(2 * math.pi * self.preamble_freq * t)
signal.append(value)
# Modulate symbols
for symbol in symbols:
freq = self.frequencies[symbol]
if HAS_NUMPY:
t = np.arange(self.samples_per_symbol) / self.sample_rate
tone = np.sin(2 * np.pi * freq * t)
signal.extend(tone)
else:
for i in range(self.samples_per_symbol):
t = i / self.sample_rate
value = math.sin(2 * math.pi * freq * t)
signal.append(value)
# Apply smoothing to reduce clicks
if HAS_NUMPY:
audio = np.array(signal, dtype=np.float32)
else:
audio = array.array('f', signal)
audio = self._apply_envelope(audio)
return audio
def demodulate(self, audio) -> Tuple[bytes, float]:
"""
Demodulate FSK audio signal to binary data.
Args:
audio: Audio signal
Returns:
Tuple of (demodulated data, confidence score)
"""
# Find preamble
preamble_start = self._find_preamble(audio)
if preamble_start < 0:
return b'', 0.0
# Skip preamble
data_start = preamble_start + int(self.preamble_duration * self.sample_rate)
# Demodulate symbols
symbols = []
confidence_scores = []
pos = data_start
while pos + self.samples_per_symbol <= len(audio):
symbol_audio = audio[pos:pos + self.samples_per_symbol]
symbol, confidence = self._demodulate_symbol(symbol_audio)
symbols.append(symbol)
confidence_scores.append(confidence)
pos += self.samples_per_symbol
# Convert symbols to bytes
data = bytearray()
for i in range(0, len(symbols), 4):
if i + 3 < len(symbols):
byte = (symbols[i] << 6) | (symbols[i+1] << 4) | (symbols[i+2] << 2) | symbols[i+3]
data.append(byte)
if HAS_NUMPY and confidence_scores:
avg_confidence = np.mean(confidence_scores)
else:
avg_confidence = sum(confidence_scores) / len(confidence_scores) if confidence_scores else 0.0
return bytes(data), avg_confidence
def _find_preamble(self, audio) -> int:
"""Find preamble in audio signal."""
# Simple energy-based detection
window_size = int(0.01 * self.sample_rate) # 10ms window
if HAS_NUMPY:
for i in range(0, len(audio) - window_size, window_size // 2):
window = audio[i:i + window_size]
# Check for preamble frequency
fft = np.fft.fft(window)
freqs = np.fft.fftfreq(len(window), 1/self.sample_rate)
# Find peak near preamble frequency
idx = np.argmax(np.abs(fft[:len(fft)//2]))
peak_freq = abs(freqs[idx])
if abs(peak_freq - self.preamble_freq) < 50: # 50 Hz tolerance
return i
else:
# Simple zero-crossing based detection without FFT
for i in range(0, len(audio) - window_size, window_size // 2):
window = list(audio[i:i + window_size])
# Count zero crossings
zero_crossings = 0
for j in range(1, len(window)):
if (window[j-1] >= 0) != (window[j] >= 0):
zero_crossings += 1
# Estimate frequency from zero crossings
estimated_freq = (zero_crossings * self.sample_rate) / (2 * len(window))
if abs(estimated_freq - self.preamble_freq) < 100: # 100 Hz tolerance
return i
return -1
def _demodulate_symbol(self, audio) -> Tuple[int, float]:
"""Demodulate a single FSK symbol."""
if HAS_NUMPY:
# FFT-based demodulation
fft = np.fft.fft(audio)
freqs = np.fft.fftfreq(len(audio), 1/self.sample_rate)
magnitude = np.abs(fft[:len(fft)//2])
# Find energy at each FSK frequency
energies = []
for freq in self.frequencies:
idx = np.argmin(np.abs(freqs[:len(freqs)//2] - freq))
energy = magnitude[idx]
energies.append(energy)
# Select symbol with highest energy
symbol = np.argmax(energies)
else:
# Goertzel algorithm for specific frequency detection
audio_list = list(audio) if hasattr(audio, '__iter__') else audio
energies = []
for freq in self.frequencies:
# Goertzel algorithm
omega = 2 * math.pi * freq / self.sample_rate
coeff = 2 * math.cos(omega)
s_prev = 0
s_prev2 = 0
for sample in audio_list:
s = sample + coeff * s_prev - s_prev2
s_prev2 = s_prev
s_prev = s
# Calculate magnitude
power = s_prev2 * s_prev2 + s_prev * s_prev - coeff * s_prev * s_prev2
energies.append(math.sqrt(abs(power)))
# Select symbol with highest energy
symbol = energies.index(max(energies))
# Confidence is ratio of strongest to second strongest
sorted_energies = sorted(energies, reverse=True)
confidence = sorted_energies[0] / (sorted_energies[1] + 1e-6)
return symbol, min(confidence, 10.0) / 10.0
def _apply_envelope(self, audio):
"""Apply smoothing envelope to reduce clicks."""
# Simple raised cosine envelope
ramp_samples = int(0.002 * self.sample_rate) # 2ms ramps
if len(audio) > 2 * ramp_samples:
if HAS_NUMPY:
# Fade in
t = np.linspace(0, np.pi/2, ramp_samples)
audio[:ramp_samples] *= np.sin(t) ** 2
# Fade out
audio[-ramp_samples:] *= np.sin(t[::-1]) ** 2
else:
# Manual fade in
for i in range(ramp_samples):
t = (i / ramp_samples) * (math.pi / 2)
factor = math.sin(t) ** 2
audio[i] *= factor
# Manual fade out
for i in range(ramp_samples):
t = ((ramp_samples - 1 - i) / ramp_samples) * (math.pi / 2)
factor = math.sin(t) ** 2
audio[-(i+1)] *= factor
return audio
class VoiceProtocol:
"""
Integrates voice codec and modem with the Icing protocol
for encrypted voice transmission over GSM.
"""
def __init__(self, protocol_instance):
"""
Initialize voice protocol handler.
Args:
protocol_instance: IcingProtocol instance
"""
self.protocol = protocol_instance
self.codec = Codec2Wrapper(Codec2Mode.MODE_1200)
self.modem = FSKModem(sample_rate=8000, baud_rate=600)
# Voice crypto state
self.voice_iv_counter = 0
self.voice_sequence = 0
# Buffers
if HAS_NUMPY:
self.audio_buffer = np.array([], dtype=np.int16)
else:
self.audio_buffer = array.array('h') # 16-bit signed integers
self.frame_buffer = []
print(f"{GREEN}[VOICE]{RESET} Voice protocol initialized")
def process_voice_input(self, audio_samples):
"""
Process voice input: compress, encrypt, and modulate.
Args:
audio_samples: PCM audio samples (8kHz, 16-bit)
Returns:
Modulated audio signal ready for transmission (numpy array or array.array)
"""
# Add to buffer
if HAS_NUMPY:
self.audio_buffer = np.concatenate([self.audio_buffer, audio_samples])
else:
self.audio_buffer.extend(audio_samples)
# Process complete frames
modulated_audio = []
while len(self.audio_buffer) >= self.codec.frame_samples:
# Extract frame
if HAS_NUMPY:
frame_audio = self.audio_buffer[:self.codec.frame_samples]
self.audio_buffer = self.audio_buffer[self.codec.frame_samples:]
else:
frame_audio = array.array('h', self.audio_buffer[:self.codec.frame_samples])
del self.audio_buffer[:self.codec.frame_samples]
# Compress with Codec2
compressed_frame = self.codec.encode(frame_audio)
if not compressed_frame:
continue
# Encrypt frame
encrypted = self._encrypt_voice_frame(compressed_frame)
# Add FEC
protected = self._add_fec(encrypted)
# Modulate to audio
audio_signal = self.modem.modulate(protected, add_preamble=True)
modulated_audio.append(audio_signal)
if modulated_audio:
if HAS_NUMPY:
return np.concatenate(modulated_audio)
else:
# Concatenate array.array objects
result = array.array('f')
for audio in modulated_audio:
result.extend(audio)
return result
return None
def process_voice_output(self, modulated_audio):
"""
Process received audio: demodulate, decrypt, and decompress.
Args:
modulated_audio: Received FSK-modulated audio
Returns:
Decoded PCM audio samples (numpy array or array.array)
"""
# Demodulate
data, confidence = self.modem.demodulate(modulated_audio)
if confidence < 0.5:
print(f"{YELLOW}[VOICE]{RESET} Low demodulation confidence: {confidence:.2f}")
return None
# Remove FEC
frame_data = self._remove_fec(data)
if not frame_data:
return None
# Decrypt
compressed_frame = self._decrypt_voice_frame(frame_data)
if not compressed_frame:
return None
# Decompress
audio_samples = self.codec.decode(compressed_frame)
return audio_samples
def _encrypt_voice_frame(self, frame: Codec2Frame) -> bytes:
"""Encrypt a voice frame using ChaCha20-CTR."""
if not self.protocol.hkdf_key:
raise ValueError("No encryption key available")
# Prepare frame data
frame_data = struct.pack('<BIH',
frame.mode,
frame.frame_number,
len(frame.bits)
) + frame.bits
# Generate IV for this frame (ChaCha20 needs 16 bytes)
iv = struct.pack('<Q', self.voice_iv_counter) + b'\x00' * 8 # 8 + 8 = 16 bytes
self.voice_iv_counter += 1
# Encrypt using ChaCha20
from encryption import chacha20_encrypt
key = bytes.fromhex(self.protocol.hkdf_key)
encrypted = chacha20_encrypt(frame_data, key, iv)
# Add sequence number and IV hint
return struct.pack('<HQ', self.voice_sequence, self.voice_iv_counter) + encrypted
def _decrypt_voice_frame(self, data: bytes) -> Optional[Codec2Frame]:
"""Decrypt a voice frame."""
if len(data) < 10:
return None
# Extract sequence and IV hint
sequence, iv_hint = struct.unpack('<HQ', data[:10])
encrypted = data[10:]
# Generate IV (16 bytes for ChaCha20)
iv = struct.pack('<Q', iv_hint) + b'\x00' * 8
# Decrypt
from encryption import chacha20_decrypt
key = bytes.fromhex(self.protocol.hkdf_key)
try:
decrypted = chacha20_decrypt(encrypted, key, iv)
# Parse frame
mode, frame_num, bits_len = struct.unpack('<BIH', decrypted[:7])
bits = decrypted[7:7+bits_len]
return Codec2Frame(
mode=Codec2Mode(mode),
bits=bits,
timestamp=0, # Will be set by caller
frame_number=frame_num
)
except Exception as e:
print(f"{RED}[VOICE]{RESET} Decryption failed: {e}")
return None
def _add_fec(self, data: bytes) -> bytes:
"""Add forward error correction."""
# Simple repetition code (3x) for testing
# In production: use convolutional code or LDPC
fec_data = bytearray()
for byte in data:
# Repeat each byte 3 times
fec_data.extend([byte, byte, byte])
return bytes(fec_data)
def _remove_fec(self, data: bytes) -> Optional[bytes]:
"""Remove FEC and correct errors."""
if len(data) % 3 != 0:
return None
corrected = bytearray()
for i in range(0, len(data), 3):
# Majority voting
votes = [data[i], data[i+1], data[i+2]]
byte_value = max(set(votes), key=votes.count)
corrected.append(byte_value)
return bytes(corrected)
# Example usage
if __name__ == "__main__":
# Test Codec2 wrapper
print(f"\n{BLUE}=== Testing Codec2 Wrapper ==={RESET}")
codec = Codec2Wrapper(Codec2Mode.MODE_1200)
# Generate test audio
if HAS_NUMPY:
t = np.linspace(0, 0.04, 320) # 40ms at 8kHz
test_audio = (np.sin(2 * np.pi * 440 * t) * 16384).astype(np.int16)
else:
test_audio = array.array('h')
for i in range(320):
t = i * 0.04 / 320
value = int(math.sin(2 * math.pi * 440 * t) * 16384)
test_audio.append(value)
# Encode
frame = codec.encode(test_audio)
print(f"Encoded frame: {len(frame.bits)} bytes")
# Decode
decoded = codec.decode(frame)
print(f"Decoded audio: {len(decoded)} samples")
# Test FSK modem
print(f"\n{BLUE}=== Testing FSK Modem ==={RESET}")
modem = FSKModem()
# Test data
test_data = b"Hello, secure voice!"
# Modulate
modulated = modem.modulate(test_data)
print(f"Modulated: {len(modulated)} samples ({len(modulated)/8000:.2f}s)")
# Demodulate
demodulated, confidence = modem.demodulate(modulated)
print(f"Demodulated: {demodulated}")
print(f"Confidence: {confidence:.2%}")
print(f"Match: {demodulated == test_data}")