Compare commits

..

16 Commits

Author SHA1 Message Date
e9fba8a3ba fix: remove voicemail tab from home page
All checks were successful
/ mirror (push) Successful in 5s
/ build (push) Successful in 9m1s
/ build-stealth (push) Successful in 11m19s
2025-05-09 16:06:09 +02:00
afa0c5b5a4 Merge branch 'dev' of https://git.gmoker.com/icing/monorepo into rework-app
Some checks failed
/ build-stealth (push) Failing after 8m6s
/ build (push) Successful in 9m34s
/ mirror (push) Successful in 4s
2025-05-09 15:46:52 +02:00
AlexisDanlos
00e7d850b0 refactor: convert ContactPage to StatelessWidget and optimize AlphabetScrollPage for better performance
All checks were successful
/ mirror (push) Successful in 4s
/ build (push) Successful in 10m17s
/ build-stealth (push) Successful in 10m13s
2025-04-05 02:24:56 +02:00
AlexisDanlos
1a020bbbd8 refactor: remove unused imports and obfuscate service from contact page
All checks were successful
/ mirror (push) Successful in 6s
/ build-stealth (push) Successful in 10m16s
/ build (push) Successful in 10m23s
2025-04-05 02:13:07 +02:00
AlexisDanlos
918265744e refactor: enhance stealth mode handling and clean up imports across services
Some checks failed
/ mirror (push) Successful in 4s
/ build-stealth (push) Has been cancelled
/ build (push) Has been cancelled
2025-04-05 02:12:31 +02:00
AlexisDanlos
96427d78d9 refactor: update contact fetching to include accounts, photo, and thumbnail
All checks were successful
/ mirror (push) Successful in 13s
/ build (push) Successful in 10m38s
/ build-stealth (push) Successful in 10m38s
2025-04-05 01:57:37 +02:00
AlexisDanlos
612cccc381 Refactor obfuscate service and avatar widget; move to domain layer and remove unused files
Some checks failed
/ mirror (push) Successful in 5s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
- Moved ObfuscateService and ObfuscatedAvatar from services to domain/services.
- Updated references to use globals for stealth mode instead of AppConfig.
- Removed old obfuscated_avatar.dart file and updated imports in call and incoming call pages.
- Cleaned up imports in composition, contact, and history pages to reflect new structure.
- Deleted unused BlockService, CallService, ContactService, AsymmetricCryptoService, and QRScanner classes.
2025-04-05 01:48:34 +02:00
AlexisDanlos
f89c5440fc refactor: introduce LoadingIndicatorWidget and streamline contact page structure
All checks were successful
/ mirror (push) Successful in 5s
/ build (push) Successful in 9m57s
/ build-stealth (push) Successful in 9m58s
2025-04-04 14:55:21 +02:00
AlexisDanlos
8cb206a640 refactor: optimize contact fetching and processing for improved performance
All checks were successful
/ mirror (push) Successful in 4s
/ build-stealth (push) Successful in 9m52s
/ build (push) Successful in 9m56s
2025-04-04 14:38:32 +02:00
AlexisDanlos
9e04185da1 refactor: remove unused app_router and app_theme files to clean up project structure
All checks were successful
/ build-stealth (push) Successful in 9m20s
/ build (push) Successful in 9m23s
/ mirror (push) Successful in 5s
2025-04-02 23:45:51 +02:00
AlexisDanlos
fd9c469fc1 refactor: remove SettingsCallPage to streamline call settings management
Some checks failed
/ mirror (push) Waiting to run
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-04-02 23:44:37 +02:00
AlexisDanlos
64089f2b3e refactor: update search functionality with SearchAnchor and improve navigation to settings and composition pages
Some checks failed
/ mirror (push) Waiting to run
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-04-02 23:41:43 +02:00
AlexisDanlos
91a739a0cd refactor: remove old main file and enhance blocked numbers settings with local storage preferences
All checks were successful
/ mirror (push) Successful in 4s
/ build (push) Successful in 9m13s
/ build-stealth (push) Successful in 9m17s
2025-04-02 15:54:41 +02:00
AlexisDanlos
62d48dc084 refactor: improve contact fetching and initialization logic for better performance and clarity
All checks were successful
/ mirror (push) Successful in 4s
/ build (push) Successful in 9m17s
/ build-stealth (push) Successful in 9m21s
2025-04-02 15:13:32 +02:00
AlexisDanlos
cef7a27e88 Updated key management to match current version
All checks were successful
/ mirror (push) Successful in 4s
/ build-stealth (push) Successful in 9m11s
/ build (push) Successful in 9m13s
2025-03-31 00:50:35 +02:00
AlexisDanlos
664dd4bb38 WIP: app rework
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m55s
/ build (push) Successful in 8m58s
2025-03-26 22:27:02 +01:00
65 changed files with 2117 additions and 5519 deletions

View File

@ -95,6 +95,7 @@ class MainActivity : FlutterActivity() {
pendingIncomingCall = null
}
}
checkAndRequestDefaultDialer()
result.success(true)
}
"makeGsmCall" -> {
@ -190,33 +191,6 @@ class MainActivity : FlutterActivity() {
result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null)
}
}
"isDefaultDialer" -> {
val isDefault = isDefaultDialer()
Log.d(TAG, "isDefaultDialer called, returning: $isDefault")
result.success(isDefault)
}
"requestDefaultDialer" -> {
checkAndRequestDefaultDialer()
result.success(true)
}
"sendDtmfTone" -> {
val digit = call.argument<String>("digit")
if (digit != null) {
val success = MyInCallService.sendDtmfTone(digit)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "Digit is null", null)
}
}
"isDefaultDialer" -> {
val isDefault = isDefaultDialer()
Log.d(TAG, "isDefaultDialer called, returning: $isDefault")
result.success(isDefault)
}
"requestDefaultDialer" -> {
checkAndRequestDefaultDialer()
result.success(true)
}
else -> result.notImplemented()
}
}
@ -242,13 +216,6 @@ class MainActivity : FlutterActivity() {
}
}
private fun isDefaultDialer(): Boolean {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage
Log.d(TAG, "Checking default dialer: current=$currentDefault, myPackage=$packageName")
return currentDefault == packageName
}
private fun checkAndRequestDefaultDialer() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage

View File

@ -52,22 +52,6 @@ class MyInCallService : InCallService() {
}
} ?: false
}
fun sendDtmfTone(digit: String): Boolean {
return instance?.let { service ->
try {
currentCall?.let { call ->
call.playDtmfTone(digit[0])
call.stopDtmfTone()
Log.d(TAG, "Sent DTMF tone: $digit")
true
} ?: false
} catch (e: Exception) {
Log.e(TAG, "Failed to send DTMF tone: $e")
false
}
} ?: false
}
}
private val callCallback = object : Call.Callback() {
@ -127,7 +111,7 @@ class MyInCallService : InCallService() {
}
call.registerCallback(callCallback)
if (callAudioState != null) {
val audioState = callAudioState
val audioState = callAudioState
channel?.invokeMethod("audioStateChanged", mapOf(
"route" to audioState.route,
"muted" to audioState.isMuted,

View File

@ -0,0 +1,14 @@
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

@ -0,0 +1,57 @@
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/dialer/composition_page.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

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
/// Generates a color based on a string input (typically a name)
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);
}
/// Darkens a color by a percentage (0.0 to 1.0)
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 percentage (0.0 to 1.0)
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

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

View File

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

View File

@ -0,0 +1,57 @@
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,564 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.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:flutter/services.dart';
class CallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const CallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
@override
_CallPageState createState() => _CallPageState();
}
class _CallPageState extends State<CallPage> {
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
bool isMuted = false;
bool isSpeaker = false;
bool isKeypadVisible = false;
bool icingProtocolOk = true;
String _typedDigits = "";
Timer? _callTimer;
int _callSeconds = 0;
String _callStatus = "Calling...";
StreamSubscription<String>? _callStateSubscription;
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
bool get isNumberUnknown => widget.displayName == widget.phoneNumber;
@override
void initState() {
super.initState();
_checkInitialCallState();
_listenToCallState();
_listenToAudioState();
_setInitialAudioState();
}
@override
void dispose() {
_callTimer?.cancel();
_callStateSubscription?.cancel();
_audioStateSubscription?.cancel();
super.dispose();
}
void _setInitialAudioState() {
final initialAudioState = _callService.currentAudioState;
if (initialAudioState != null) {
setState(() {
isMuted = initialAudioState['muted'] ?? false;
isSpeaker = initialAudioState['speaker'] ?? false;
});
}
}
void _checkInitialCallState() async {
try {
final state = await _callService.getCallState();
print('CallPage: Initial call state: $state');
if (mounted && state == "active") {
setState(() {
_callStatus = "00:00";
_startCallTimer();
});
}
} catch (e) {
print('CallPage: Error checking initial state: $e');
}
}
void _listenToCallState() {
_callStateSubscription = _callService.callStateStream.listen((state) {
print('CallPage: Call state changed to $state');
if (mounted) {
setState(() {
if (state == "active") {
_callStatus = "00:00";
_startCallTimer();
} else if (state == "disconnected" || state == "disconnecting") {
_callTimer?.cancel();
_callStatus = "Call Ended";
} else {
_callStatus = "Calling...";
}
});
}
});
}
void _listenToAudioState() {
_audioStateSubscription = _callService.audioStateStream.listen((state) {
if (mounted) {
setState(() {
isMuted = state['muted'] ?? isMuted;
isSpeaker = state['speaker'] ?? isSpeaker;
});
}
});
}
void _startCallTimer() {
_callTimer?.cancel();
_callTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_callSeconds++;
final minutes = (_callSeconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_callSeconds % 60).toString().padLeft(2, '0');
_callStatus = '$minutes:$seconds';
});
}
});
}
void _addDigit(String digit) async {
print('CallPage: Tapped digit: $digit');
setState(() {
_typedDigits += digit;
});
// Send DTMF tone
const channel = MethodChannel('call_service');
try {
final success =
await channel.invokeMethod<bool>('sendDtmfTone', {'digit': digit});
if (success != true) {
print('CallPage: Failed to send DTMF tone for $digit');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send DTMF tone')),
);
}
}
} catch (e) {
print('CallPage: Error sending DTMF tone: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error sending DTMF tone: $e')),
);
}
}
}
void _toggleMute() async {
try {
print('CallPage: Toggling mute, current state: $isMuted');
final result = await _callService.muteCall(context, mute: !isMuted);
print('CallPage: Mute call result: $result');
if (mounted && result['status'] != 'success') {
print('CallPage: Failed to toggle mute: ${result['message']}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to toggle mute: ${result['message']}')),
);
}
} catch (e) {
print('CallPage: Error toggling mute: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error toggling mute: $e')),
);
}
}
}
Future<void> _toggleSpeaker() async {
try {
print('CallPage: Toggling speaker, current state: $isSpeaker');
final result =
await _callService.speakerCall(context, speaker: !isSpeaker);
print('CallPage: Speaker call result: $result');
if (result['status'] != 'success') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to toggle speaker: ${result['message']}')),
);
}
} catch (e) {
print('CallPage: Error toggling speaker: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error toggling speaker: $e')),
);
}
}
}
void _toggleKeypad() {
setState(() {
isKeypadVisible = !isKeypadVisible;
});
}
void _toggleIcingProtocol() {
setState(() {
icingProtocolOk = !icingProtocolOk;
});
}
void _hangUp() async {
try {
print('CallPage: Initiating hangUp');
final result = await _callService.hangUpCall(context);
print('CallPage: Hang up result: $result');
} catch (e) {
print('CallPage: Error hanging up: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error hanging up: $e')),
);
}
}
}
void _addContact() async {
if (await FlutterContacts.requestPermission()) {
final newContact = Contact()..phones = [Phone(widget.phoneNumber)];
final updatedContact =
await FlutterContacts.openExternalInsert(newContact);
if (updatedContact != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Contact added successfully!')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Permission denied for contacts')),
);
}
}
@override
Widget build(BuildContext context) {
final double avatarRadius = isKeypadVisible ? 45.0 : 45.0;
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0;
print(
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
return PopScope(
canPop: _callStatus == "Call Ended",
onPopInvoked: (didPop) {
if (!didPop) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cannot leave during an active call')),
);
}
},
child: 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: TextStyle(
fontSize: nameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: TextStyle(
fontSize: statusFontSize,
color: Colors.white70,
),
),
Text(
_callStatus,
style: TextStyle(
fontSize: statusFontSize,
color: Colors.white70,
),
),
],
),
),
Expanded(
child: Column(
children: [
if (isKeypadVisible) ...[
const Spacer(flex: 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
_typedDigits,
maxLines: 1,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
padding: EdgeInsets.zero,
onPressed: _toggleKeypad,
icon: const Icon(
Icons.close,
color: Colors.white,
),
),
],
),
),
Container(
height: MediaQuery.of(context).size.height * 0.4,
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(8),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.5,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: List.generate(12, (index) {
String label;
if (index < 9) {
label = '${index + 1}';
} else if (index == 9) {
label = '*';
} else if (index == 10) {
label = '0';
} else {
label = '#';
}
return GestureDetector(
onTap: () => _addDigit(label),
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
),
child: Center(
child: Text(
label,
style: const TextStyle(
fontSize: 32,
color: Colors.white,
),
),
),
),
);
}),
),
),
const Spacer(flex: 1),
] else ...[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleMute,
icon: Icon(
isMuted ? Icons.mic_off : Icons.mic,
color: isMuted
? Colors.amber
: Colors.white,
size: 32,
),
),
Text(
isMuted ? 'Unmute' : 'Mute',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleKeypad,
icon: const Icon(
Icons.dialpad,
color: Colors.white,
size: 32,
),
),
const Text(
'Keypad',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleSpeaker,
icon: Icon(
isSpeaker
? Icons.volume_up
: Icons.volume_off,
color: isSpeaker
? Colors.amber
: Colors.white,
size: 32,
),
),
const Text(
'Speaker',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
if (isNumberUnknown)
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _addContact,
icon: const Icon(
Icons.person_add,
color: Colors.white,
size: 32,
),
),
const Text(
'Add Contact',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.sim_card,
color: Colors.white,
size: 32,
),
),
const Text(
'Change SIM',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
],
),
],
),
),
const Spacer(flex: 3),
],
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: GestureDetector(
onTap: _hangUp,
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,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -1,181 +0,0 @@
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,26 +0,0 @@
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,217 +0,0 @@
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,32 +0,0 @@
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,102 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DefaultDialerPromptScreen extends StatelessWidget {
const DefaultDialerPromptScreen({super.key});
Future<void> _requestDefaultDialer(BuildContext context) async {
const channel = MethodChannel('call_service');
try {
await channel.invokeMethod('requestDefaultDialer');
// Navigate to home page after requesting default dialer
Navigator.of(context).pushReplacementNamed('/home');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error requesting default dialer: $e')),
);
}
}
void _exploreApp(BuildContext context) {
// Navigate to home page without requesting default dialer
Navigator.of(context).pushReplacementNamed('/home');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Set as Default Dialer',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'To handle calls effectively, Icing needs to be your default dialer app. This allows Icing to manage incoming and outgoing calls seamlessly.\n\nWithout the permission, Icing will not be able to encrypt calls.',
style: TextStyle(
fontSize: 16,
color: Colors.white70,
),
textAlign: TextAlign.center,
),
],
),
),
Row(
children: [
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ElevatedButton(
onPressed: () => _exploreApp(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[800],
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
child: Text('Explore App first'),
),
),
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ElevatedButton(
onPressed: () => _requestDefaultDialer(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
child: Text('Set as Default Dialer'),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -1,342 +0,0 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_page.dart';
import 'package:dialer/features/favorites/favorites_page.dart';
import 'package:dialer/features/history/history_page.dart';
import 'package:dialer/features/composition/composition.dart';
import 'package:flutter_contacts/flutter_contacts.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 {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService();
final TextEditingController _searchController = TextEditingController();
late SearchController _searchBarController;
String _rawSearchInput = '';
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
_tabController.addListener(_handleTabIndex);
_searchBarController = SearchController();
_searchBarController.addListener(() {
if (_searchController.text != _searchBarController.text) {
_rawSearchInput = _searchBarController.text;
_searchController.text = _rawSearchInput;
_onSearchChanged(_searchBarController.text);
}
});
_fetchContacts();
}
void _fetchContacts() async {
_allContacts = await _contactService.fetchContacts();
_contactSuggestions = List.from(_allContacts);
if (mounted) setState(() {});
}
void _clearSearch() {
_searchController.clear();
_searchBarController.clear();
_rawSearchInput = '';
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
} else {
final normalizedQuery = _normalizeString(query.toLowerCase());
_contactSuggestions = _allContacts.where((contact) {
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
return normalizedName.contains(normalizedQuery);
}).toList();
}
});
}
String _normalizeString(String input) {
const accentMap = {
'àáâãäå': 'a',
'èéêë': 'e',
'ìíîï': 'i',
'òóôõö': 'o',
'ùúûü': 'u',
'ç': 'c',
'ñ': 'n',
};
String normalized = input;
accentMap.forEach((accents, base) {
for (var accent in accents.split('')) {
normalized = normalized.replaceAll(accent, base);
}
});
return normalized;
}
@override
void dispose() {
_searchController.dispose();
_searchBarController.dispose();
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
}
void _handleTabIndex() {
setState(() {});
}
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);
_fetchContacts();
}
} 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) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 10.0,
left: 16.0,
right: 16.0,
),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: Colors.grey.shade800, width: 1),
),
child: SearchAnchor(
searchController: _searchBarController,
builder: (BuildContext context, SearchController controller) {
return GestureDetector(
onTap: () {
controller.openView();
},
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: Colors.grey.shade800, width: 1),
),
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.search, color: Colors.grey, size: 24.0),
const SizedBox(width: 8.0),
Expanded(
child: Text(
_rawSearchInput.isEmpty
? 'Search contacts'
: _rawSearchInput,
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
overflow: TextOverflow.ellipsis,
),
),
if (_rawSearchInput.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
Icons.clear,
color: Colors.grey,
size: 24.0,
),
),
],
),
),
);
},
viewOnChanged: (query) {
if (_searchBarController.text != query) {
_rawSearchInput = query;
_searchBarController.text = query;
_searchController.text = query;
}
_onSearchChanged(query);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
onTap: () {
controller.closeView(contact.displayName);
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) {
_fetchContacts();
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,
);
},
);
},
);
}).toList();
},
),
),
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
const PopupMenuItem<String>(
value: 'settings',
child: Text('Settings'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
);
}
},
),
],
),
),
Expanded(
child: Stack(
children: [
TabBarView(
controller: _tabController,
children: const [
FavoritesPage(),
HistoryPage(),
ContactPage(),
VoicemailPage(),
],
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompositionPage(),
),
);
},
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(45),
),
child: const Icon(Icons.dialpad, color: Colors.white),
),
),
],
),
),
],
),
bottomNavigationBar: Container(
color: Colors.black,
child: TabBar(
controller: _tabController,
tabs: [
Tab(
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border)),
Tab(
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined)),
Tab(
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined)),
Tab(
icon: Icon(_tabController.index == 3
? Icons.voicemail
: Icons.voicemail_outlined),
),
],
labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Colors.white,
),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}

View File

@ -1 +1,7 @@
bool isStealthMode = false;
// Global variables accessible throughout the app
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,25 +1,34 @@
import 'package:dialer/features/home/home_page.dart';
import 'package:dialer/features/home/default_dialer_prompt.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 'globals.dart' as globals;
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'core/config/app_config.dart';
import 'core/navigation/app_router.dart';
import 'domain/services/call_service.dart';
import 'domain/services/cryptography/asymmetric_crypto_service.dart';
import 'presentation/common/theme/app_theme.dart';
import 'presentation/features/contacts/contact_state.dart';
void main() async {
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();
await cryptoService.initializeDefaultKeyPair();
try {
await cryptoService.initializeDefaultKeyPair();
} catch (e) {
debugPrint('Error initializing cryptography: $e');
// Continue app initialization even if crypto fails
}
// Request permissions before running the app
await _requestPermissions();
// Initialize call service
CallService();
runApp(
@ -29,65 +38,48 @@ void main() async {
create: (_) => cryptoService,
),
],
child: Dialer(),
child: const DialerApp(),
),
);
}
Future<void> _requestPermissions() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.phone,
Permission.contacts,
Permission.microphone,
].request();
if (statuses.values.every((status) => status.isGranted)) {
print("All required permissions granted");
const channel = MethodChannel('call_service');
await channel.invokeMethod('permissionsGranted');
} else {
print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
try {
Map<Permission, PermissionStatus> statuses = await [
Permission.phone,
Permission.contacts,
Permission.microphone,
].request();
if (statuses.values.every((status) => status.isGranted)) {
debugPrint("All required permissions granted");
const channel = MethodChannel('call_service');
await channel.invokeMethod('permissionsGranted');
} else {
debugPrint("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
}
} catch (e) {
debugPrint("Error requesting permissions: $e");
}
}
class Dialer extends StatelessWidget {
const Dialer({super.key});
Future<bool> _isDefaultDialer() async {
const channel = MethodChannel('call_service');
try {
final isDefault = await channel.invokeMethod<bool>('isDefaultDialer');
return isDefault ?? false;
} catch (e) {
print('Error checking default dialer: $e');
return false;
}
}
class DialerApp extends StatelessWidget {
const DialerApp({super.key});
@override
Widget build(BuildContext context) {
return ContactState(
child: MaterialApp(
title: 'Dialer App',
navigatorKey: CallService.navigatorKey,
theme: ThemeData(
brightness: Brightness.dark,
),
theme: AppTheme.darkTheme,
onGenerateRoute: AppRouter.generateRoute,
initialRoute: '/',
routes: {
'/': (context) => FutureBuilder<bool>(
future: _isDefaultDialer(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data == false) {
return DefaultDialerPromptScreen();
}
return SafeArea(child: MyHomePage());
},
),
'/home': (context) => SafeArea(child: MyHomePage()),
// Add a builder to wrap all routes with SafeArea
builder: (context, child) {
return SafeArea(
child: child ?? const SizedBox.shrink(),
);
},
),
);

View File

@ -0,0 +1,42 @@
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 TabBarTheme(
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

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class LoadingIndicatorWidget extends StatelessWidget {
const LoadingIndicatorWidget({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

View File

@ -0,0 +1,251 @@
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 '../../../core/utils/color_utils.dart';
// import '../../../presentation/common/widgets/obfuscated_avatar.dart';
class CallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const CallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
@override
_CallPageState createState() => _CallPageState();
}
class _CallPageState extends State<CallPage> {
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
bool isMuted = false;
bool isSpeakerOn = false;
bool isKeypadVisible = false;
bool icingProtocolOk = true;
String _typedDigits = "";
void _addDigit(String digit) {
setState(() {
_typedDigits += digit;
});
}
void _toggleMute() {
setState(() {
isMuted = !isMuted;
});
}
void _toggleSpeaker() {
setState(() {
isSpeakerOn = !isSpeakerOn;
});
}
void _toggleKeypad() {
setState(() {
isKeypadVisible = !isKeypadVisible;
});
}
void _hangUp() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
debugPrint("Error hanging up: $e");
}
}
@override
Widget build(BuildContext context) {
final double avatarRadius = 45.0;
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ObfuscatedAvatar(
imageBytes: widget.thumbnail,
radius: avatarRadius,
backgroundColor: generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName,
),
const SizedBox(height: 16),
Text(
_obfuscateService.obfuscateData(widget.displayName),
style: const TextStyle(
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: const TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
Text(
'Calling...',
style: const TextStyle(fontSize: 16, color: Colors.white70),
),
],
),
),
Expanded(
child: isKeypadVisible
? _buildKeypad()
: _buildCallControls(),
),
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: GestureDetector(
onTap: _hangUp,
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call_end,
color: Colors.white,
size: 32,
),
),
),
),
],
),
),
);
}
Widget _buildKeypad() {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Text(
_typedDigits,
style: const TextStyle(
fontSize: 24,
color: Colors.white,
),
),
),
const SizedBox(height: 16),
Expanded(
child: GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.5,
children: [
_buildDialButton('1'),
_buildDialButton('2'),
_buildDialButton('3'),
_buildDialButton('4'),
_buildDialButton('5'),
_buildDialButton('6'),
_buildDialButton('7'),
_buildDialButton('8'),
_buildDialButton('9'),
_buildDialButton('*'),
_buildDialButton('0'),
_buildDialButton('#'),
],
),
),
TextButton(
onPressed: _toggleKeypad,
child: const Text('Hide Keypad'),
),
],
);
}
Widget _buildCallControls() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildControlButton(
icon: isMuted ? Icons.mic_off : Icons.mic,
label: isMuted ? 'Unmute' : 'Mute',
onPressed: _toggleMute,
),
_buildControlButton(
icon: Icons.dialpad,
label: 'Keypad',
onPressed: _toggleKeypad,
),
_buildControlButton(
icon: isSpeakerOn ? Icons.volume_up : Icons.volume_off,
label: 'Speaker',
onPressed: _toggleSpeaker,
iconColor: isSpeakerOn ? Colors.amber : Colors.white,
),
],
),
],
);
}
Widget _buildDialButton(String digit) {
return InkWell(
onTap: () => _addDigit(digit),
child: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[800],
),
child: Center(
child: Text(
digit,
style: const TextStyle(
fontSize: 24,
color: Colors.white,
),
),
),
),
);
}
Widget _buildControlButton({
required IconData icon,
required String label,
required VoidCallback onPressed,
Color iconColor = Colors.white,
}) {
return Column(
children: [
IconButton(
iconSize: 32,
icon: Icon(icon, color: iconColor),
onPressed: onPressed,
),
Text(
label,
style: const TextStyle(color: Colors.white),
),
],
);
}
}

View File

@ -0,0 +1,163 @@
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 '../../../core/utils/color_utils.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,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../services/contact_service.dart';
import '../../services/obfuscate_service.dart';
import '../../services/call_service.dart';
import '../../../domain/services/contact_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../domain/services/call_service.dart';
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget {
const CompositionPage({super.key});

View File

@ -0,0 +1,20 @@
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,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../services/contact_service.dart';
import '../../../domain/services/contact_service.dart';
class ContactState extends StatefulWidget {
final Widget child;
@ -23,7 +23,7 @@ class _ContactStateState extends State<ContactState> {
List<Contact> _favoriteContacts = [];
bool _loading = true;
double _scrollOffset = 0.0;
Contact? _selfContact = Contact();
Contact? _selfContact;
// Getters for all contacts and favorites
List<Contact> get contacts => _allContacts;
@ -35,11 +35,23 @@ class _ContactStateState extends State<ContactState> {
@override
void initState() {
super.initState();
fetchContacts(); // Fetch all contacts by default
_initializeContacts(); // Rename to make it clear this is initialization
FlutterContacts.addListener(_onContactChange);
}
void _onContactChange() => fetchContacts();
// Private method to initialize contacts without setState during build
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
void dispose() {
@ -47,47 +59,92 @@ class _ContactStateState extends State<ContactState> {
super.dispose();
}
// Fetch all contacts
// Fetch all contacts - public method that can be called after build
Future<void> fetchContacts() async {
if (!mounted) return;
setState(() => _loading = true);
try {
List<Contact> contacts = await _contactService.fetchContacts();
_processContacts(contacts);
if (mounted) {
_processContacts(contacts);
}
} catch (e) {
debugPrint('Error fetching contacts: $e');
} finally {
setState(() => _loading = false);
if (mounted) {
setState(() => _loading = false);
}
}
}
// Fetch only favorite contacts
Future<void> fetchFavoriteContacts() async {
if (!mounted) return;
setState(() => _loading = true);
try {
List<Contact> contacts = await _contactService.fetchFavoriteContacts();
setState(() => _favoriteContacts = contacts);
if (mounted) {
setState(() => _favoriteContacts = contacts);
}
} catch (e) {
debugPrint('Error fetching favorite contacts: $e');
} finally {
setState(() => _loading = false);
if (mounted) {
setState(() => _loading = false);
}
}
}
void _processContacts(List<Contact> contacts) {
// 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) {
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(
(contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(),
);
if (_selfContact!.phones.isEmpty) {
debugPrint("Self contact has no phone numbers");
_selfContact = null;
}
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
setState(() {
_allContacts = contacts;
_favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
_selfContact = _selfContact;
_allContacts = filteredContacts;
_favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList();
});
}
@ -102,25 +159,6 @@ 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
Widget build(BuildContext context) {
return _InheritedContactState(
@ -130,7 +168,6 @@ class _ContactStateState extends State<ContactState> {
}
}
class _InheritedContactState extends InheritedWidget {
final _ContactStateState data;

View File

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

View File

@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../../domain/services/obfuscate_service.dart';
import '../../../../core/utils/color_utils.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,11 +1,12 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import '../../../services/block_service.dart';
import '../../../services/contact_service.dart';
import '../../../services/call_service.dart';
import '../../../../widgets/username_color_generator.dart';
import '../../../../widgets/color_darkener.dart';
import '../../../../domain/services/obfuscate_service.dart';
import '../../../../domain/services/block_service.dart';
import '../../../../domain/services/contact_service.dart';
import '../../../../domain/services/call_service.dart';
class ContactModal extends StatefulWidget {
final Contact contact;
@ -30,6 +31,7 @@ class _ContactModalState extends State<ContactModal> {
bool isBlocked = false;
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
final ContactService _contactService = ContactService();
@override
void initState() {
@ -43,9 +45,11 @@ class _ContactModalState extends State<ContactModal> {
Future<void> _checkIfBlocked() async {
if (phoneNumber != 'No phone number') {
bool blocked = await BlockService().isNumberBlocked(phoneNumber);
setState(() {
isBlocked = blocked;
});
if (mounted) {
setState(() {
isBlocked = blocked;
});
}
}
}
@ -54,22 +58,32 @@ class _ContactModalState extends State<ContactModal> {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No phone number to block or unblock')),
);
} else if (isBlocked) {
return;
}
if (isBlocked) {
await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been unblocked')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been unblocked')),
);
}
} else {
await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been blocked')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber has been blocked')),
);
}
}
if (phoneNumber != 'No phone number') {
if (phoneNumber != 'No phone number' && mounted) {
_checkIfBlocked();
}
Navigator.of(context).pop();
if (mounted) {
Navigator.of(context).pop();
}
}
void _launchPhoneDialer(String phoneNumber) async {
@ -119,34 +133,38 @@ class _ContactModalState extends State<ContactModal> {
),
);
if (shouldDelete) {
if (shouldDelete && mounted) {
try {
// Delete the contact
await FlutterContacts.deleteContact(widget.contact);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
);
// Close the modal
Navigator.of(context).pop();
// Close the modal
Navigator.of(context).pop();
}
} catch (e) {
// Handle errors and show a failure message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Failed to delete ${widget.contact.displayName}: $e')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Failed to delete ${widget.contact.displayName}: $e')),
);
}
}
}
}
void _shareContactAsQRCode() {
// Use the ContactService to show the QR code for the contact's vCard
ContactService().showContactQRCodeDialog(context, widget.contact);
_contactService.showContactQRCodeDialog(context, widget.contact);
}
@override
@ -155,6 +173,8 @@ class _ContactModalState extends State<ContactModal> {
? _obfuscateService.obfuscateData(widget.contact.emails.first.address)
: 'No email';
final avatarColor = generateColorFromName(widget.contact.displayName);
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
@ -162,6 +182,7 @@ class _ContactModalState extends State<ContactModal> {
child: GestureDetector(
onTap: () {},
child: FractionallySizedBox(
heightFactor: 0.8,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[900],
@ -205,10 +226,6 @@ class _ContactModalState extends State<ContactModal> {
},
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem<String>(
value: 'show_associated_contacts',
child: Text('Show associated contacts'),
),
const PopupMenuItem<String>(
value: 'delete',
child: Text('Delete'),
@ -217,15 +234,6 @@ class _ContactModalState extends State<ContactModal> {
value: 'share',
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'),
),
];
},
),
@ -238,13 +246,28 @@ class _ContactModalState extends State<ContactModal> {
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ObfuscatedAvatar(
imageBytes: widget.contact.thumbnail,
radius: 50,
backgroundColor:
generateColorFromName(widget.contact.displayName),
fallbackInitial: widget.contact.displayName,
),
widget.contact.thumbnail != null && widget.contact.thumbnail!.isNotEmpty
? ClipOval(
child: Image.memory(
widget.contact.thumbnail!,
fit: BoxFit.cover,
width: 100,
height: 100,
),
)
: CircleAvatar(
backgroundColor: avatarColor,
radius: 50,
child: Text(
widget.contact.displayName.isNotEmpty
? widget.contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(
color: darken(avatarColor),
fontSize: 40,
),
),
),
const SizedBox(height: 10),
Text(
_obfuscateService
@ -259,93 +282,99 @@ class _ContactModalState extends State<ContactModal> {
),
const Divider(color: Colors.grey),
// Contact Actions
ListTile(
leading: const Icon(Icons.phone, color: Colors.green),
title: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(
context,
phoneNumber: phoneNumber,
displayName: widget.contact.displayName,
thumbnail: widget.contact.thumbnail,
);
}
},
),
ListTile(
leading: const Icon(Icons.message, color: Colors.blue),
title: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () {
if (widget.contact.phones.isNotEmpty) {
_launchSms(phoneNumber);
}
},
),
ListTile(
leading: const Icon(Icons.email, color: Colors.orange),
title: Text(
email,
style: const TextStyle(color: Colors.white),
),
onTap: () {
if (widget.contact.emails.isNotEmpty) {
_launchEmail(email);
}
},
),
const Divider(color: Colors.grey),
// Favorite, Edit, and Block/Unblock Buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Favorite button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
widget.onToggleFavorite();
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.phone, color: Colors.green),
title: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context,
phoneNumber: phoneNumber);
}
},
icon: Icon(widget.isFavorite
? Icons.star
: Icons.star_border),
label: Text(
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
),
),
const SizedBox(height: 10),
// Edit button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => widget.onEdit(),
icon: const Icon(Icons.edit),
label: const Text('Edit Contact'),
ListTile(
leading: const Icon(Icons.message, color: Colors.blue),
title: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () {
if (widget.contact.phones.isNotEmpty) {
_launchSms(phoneNumber);
}
},
),
),
const SizedBox(height: 10),
// Block/Unblock button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _toggleBlockState,
icon: Icon(
isBlocked ? Icons.block : Icons.block_flipped),
label: Text(isBlocked ? 'Unblock' : 'Block'),
ListTile(
leading: const Icon(Icons.email, color: Colors.orange),
title: Text(
email,
style: const TextStyle(color: Colors.white),
),
onTap: () {
if (widget.contact.emails.isNotEmpty) {
_launchEmail(email);
}
},
),
),
],
const Divider(color: Colors.grey),
// Favorite, Edit, and Block/Unblock Buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Favorite button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
// First close the modal to avoid unmounted widget issues
Navigator.of(context).pop();
// Then toggle the favorite status
widget.onToggleFavorite();
},
icon: Icon(widget.isFavorite
? Icons.star
: Icons.star_border),
label: Text(
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
),
),
const SizedBox(height: 10),
// Edit button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => widget.onEdit(),
icon: const Icon(Icons.edit),
label: const Text('Edit Contact'),
),
),
const SizedBox(height: 10),
// Block/Unblock button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _toggleBlockState,
icon: Icon(
isBlocked ? Icons.block : Icons.block_flipped),
label: Text(isBlocked ? 'Unblock' : 'Block'),
),
),
],
),
),
const SizedBox(height: 16),
],
),
),
),
const SizedBox(height: 16),
],
),
),

View File

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

View File

@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../domain/services/contact_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../domain/services/call_service.dart';
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget {
const CompositionPage({super.key});
@override
_CompositionPageState createState() => _CompositionPageState();
}
class _CompositionPageState extends State<CompositionPage> {
String dialedNumber = "";
List<Contact> _allContacts = [];
List<Contact> _filteredContacts = [];
final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
// Cache for normalized phone numbers to avoid repeated processing
final Map<String, String> _normalizedPhoneCache = {};
@override
void initState() {
super.initState();
_fetchContacts();
}
Future<void> _fetchContacts() async {
_allContacts = await _contactService.fetchContacts();
_filteredContacts = _allContacts;
setState(() {});
}
String _getNormalizedPhone(String phone) {
return _normalizedPhoneCache.putIfAbsent(
phone,
() => phone.replaceAll(RegExp(r'\D'), '')
);
}
void _filterContacts() {
if (dialedNumber.isEmpty) {
setState(() {
_filteredContacts = _allContacts;
});
return;
}
final String normalizedDialed = dialedNumber.replaceAll(RegExp(r'\D'), '');
final String lowerDialed = dialedNumber.toLowerCase();
setState(() {
_filteredContacts = _allContacts.where((contact) {
// Check phone numbers
final phoneMatch = contact.phones.any((phone) =>
_getNormalizedPhone(phone.number).contains(normalizedDialed));
// Only check name if phone doesn't match (optimization)
if (phoneMatch) return true;
// Check display name
return contact.displayName.toLowerCase().contains(lowerDialed);
}).toList();
});
}
void _onNumberPress(String number) {
setState(() {
dialedNumber += number;
_filterContacts();
});
}
void _onDeletePress() {
setState(() {
if (dialedNumber.isNotEmpty) {
dialedNumber = dialedNumber.substring(0, dialedNumber.length - 1);
_filterContacts();
}
});
}
void _onClearPress() {
setState(() {
dialedNumber = "";
_filteredContacts = _allContacts;
});
}
void _makeCall(String phoneNumber) async {
try {
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
setState(() {
dialedNumber = phoneNumber;
});
} catch (e) {
debugPrint("Error making call: $e");
}
}
void _launchSms(String phoneNumber) async {
final uri = Uri(scheme: 'sms', path: phoneNumber);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
debugPrint('Could not send SMS to $phoneNumber');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Column(
children: [
// Top half: Display contacts matching dialed number
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.only(
top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListView(
children: _filteredContacts.isNotEmpty
? _filteredContacts.map((contact) {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
return ListTile(
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.phone,
color: Colors.green[300],
size: 20),
onPressed: () {
_makeCall(phoneNumber);
},
),
IconButton(
icon: Icon(Icons.message,
color: Colors.blue[300],
size: 20),
onPressed: () {
_launchSms(phoneNumber);
},
),
],
),
onTap: () {
// Handle contact selection if needed
},
);
}).toList()
: [],
),
),
],
),
),
),
// Bottom half: Dialpad and Dialed number display with erase button
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Display dialed number with erase button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Align(
alignment: Alignment.center,
child: Text(
dialedNumber,
style: const TextStyle(
fontSize: 24, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
),
IconButton(
onPressed: _onClearPress,
icon: const Icon(Icons.backspace,
color: Colors.white),
),
],
),
const SizedBox(height: 10),
// Dialpad
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('1'),
_buildDialButton('2'),
_buildDialButton('3'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('4'),
_buildDialButton('5'),
_buildDialButton('6'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('7'),
_buildDialButton('8'),
_buildDialButton('9'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('*'),
_buildDialButton('0'),
_buildDialButton('#'),
],
),
],
),
),
),
],
),
),
),
],
),
// Add Contact Button
Positioned(
bottom: 20.0,
left: 0,
right: 0,
child: Center(
child: AddContactButton(),
),
),
// Top Row with Back Arrow
Positioned(
top: 40.0,
left: 16.0,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
);
}
Widget _buildDialButton(String number) {
return ElevatedButton(
onPressed: () => _onNumberPress(number),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: Text(
number,
style: const TextStyle(
fontSize: 24,
color: Colors.white,
),
),
);
}
}

View File

@ -0,0 +1,37 @@
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,17 +1,17 @@
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/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import '../../services/block_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../widgets/color_darkener.dart';
import '../../../widgets/username_color_generator.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 '../../services/call_service.dart';
class History {
final Contact contact;
@ -82,14 +82,19 @@ class _HistoryPageState extends State<HistoryPage>
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
}
await _refreshContacts();
// Check if still mounted before accessing context
if (mounted) {
await _refreshContacts();
}
} else {
print("Could not fetch contact details");
debugPrint("Could not fetch contact details");
}
} catch (e) {
print("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status')));
debugPrint("Error updating favorite status: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status')));
}
}
}

View File

@ -0,0 +1,333 @@
import 'package:flutter/material.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../contacts/contact_page.dart';
import '../favorites/favorites_page.dart';
import '../history/history_page.dart';
import '../dialer/composition_page.dart';
import '../settings/settings.dart';
import '../voicemail/voicemail_page.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/contact_modal.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ObfuscateService _obfuscateService = ObfuscateService();
final SearchController _searchController = SearchController();
bool _isInitialized = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabIndex);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInitialized) {
// Use a post-frame callback to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchContacts();
});
_isInitialized = true;
}
}
void _fetchContacts() async {
final contactState = ContactState.of(context);
// Wait for initial load to finish if it hasn't already
if (contactState.loading) {
await Future.delayed(const Duration(milliseconds: 100));
}
// Then explicitly fetch contacts (which will call setState safely)
await contactState.fetchContacts();
if (mounted) {
setState(() {
_allContacts = contactState.contacts;
_contactSuggestions = _allContacts;
});
}
}
void _onSearchChanged(String query) {
if (query.isEmpty) {
setState(() {
_contactSuggestions = List.from(_allContacts); // Reset suggestions
});
return;
}
// Convert query to lowercase once for efficiency
final lowerQuery = query.toLowerCase();
// Use where with efficient filter
final filtered = _allContacts.where((contact) {
final name = contact.displayName.toLowerCase();
return name.contains(lowerQuery);
}).toList();
setState(() {
_contactSuggestions = filtered;
});
}
@override
void dispose() {
_searchController.dispose();
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
}
void _handleTabIndex() {
setState(() {});
}
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 updating state
if (mounted) {
_fetchContacts();
}
}
}
} catch (e) {
debugPrint("Error updating favorite status: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// Persistent Search Bar
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 10.0,
left: 16.0,
right: 16.0,
),
child: Row(
children: [
Expanded(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return GestureDetector(
onTap: () {
controller.openView(); // Open the search view
},
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: Colors.grey.shade800, width: 1),
),
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.search,
color: Colors.grey, size: 24.0),
const SizedBox(width: 8.0),
Text(
controller.text.isEmpty
? 'Search contacts'
: controller.text,
style: const TextStyle(
color: Colors.grey, fontSize: 16.0),
),
const Spacer(),
if (controller.text.isNotEmpty)
GestureDetector(
onTap: () {
controller.clear();
_onSearchChanged('');
},
child: const Icon(
Icons.clear,
color: Colors.grey,
size: 24.0,
),
),
],
),
),
);
},
viewOnChanged: _onSearchChanged, // Update immediately
suggestionsBuilder:
(BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white)),
onTap: () {
// Clear the search text input
controller.text = '';
// Close the search view
controller.closeView(contact.displayName);
// Show the ContactModal when a contact is tapped
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) {
_fetchContacts();
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,
);
},
);
},
);
}).toList();
},
),
),
// 3-dot menu
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
const PopupMenuItem<String>(
value: 'settings',
child: Text('Settings'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
);
}
},
),
],
),
),
// Main content with TabBarView
Expanded(
child: Stack(
children: [
TabBarView(
controller: _tabController,
children: const [
FavoritesPage(),
HistoryPage(),
ContactPage(),
// VoicemailPage(),
],
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompositionPage(),
),
);
},
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(45),
),
child: const Icon(Icons.dialpad, color: Colors.white),
),
),
],
),
),
],
),
bottomNavigationBar: Container(
color: Colors.black,
child: TabBar(
controller: _tabController,
tabs: [
Tab(
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border)),
Tab(
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined)),
Tab(
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined)),
// Tab(
// icon: Icon(_tabController.index == 3
// ? Icons.voicemail
// : Icons.voicemail_outlined),
// ),
],
labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Colors.white,
),
),
);
}
}

View File

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

View File

@ -0,0 +1,32 @@
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

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
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,51 +0,0 @@
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

@ -1,10 +1,21 @@
import 'package:flutter/material.dart';
Color darken(Color color, [double amount = .1]) {
/// 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 hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return darkened.toColor();
}
return hslDark.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,12 +1,17 @@
import 'dart:math';
import 'package:flutter/material.dart';
/// Generates a deterministic color from a string input
Color generateColorFromName(String name) {
final random = Random(name.hashCode);
return Color.fromARGB(
255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
);
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,14 +0,0 @@
# Use official Python image
FROM python:3.9-slim
# Set working directory
WORKDIR /app
# Copy the simulator script
COPY gsm_simulator.py .
# Expose the port
EXPOSE 12345
# Run the simulator
CMD ["python", "gsm_simulator.py"]

View File

@ -1,415 +0,0 @@
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,24 +0,0 @@
#external_caller.py
import socket
import time
def connect():
caller_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
caller_socket.connect(('localhost', 5555))
caller_socket.send("CALLER".encode())
print("Connected to GSM simulator as CALLER")
time.sleep(2) # Wait 2 seconds for receiver to connect
for i in range(5):
message = f"Audio packet {i + 1}"
caller_socket.send(message.encode())
print(f"Sent: {message}")
time.sleep(1)
caller_socket.send("CALL_END".encode())
print("Call ended.")
caller_socket.close()
if __name__ == "__main__":
connect()

View File

@ -1,37 +0,0 @@
#external_receiver.py
import socket
def connect():
receiver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
receiver_socket.settimeout(15) # Increase timeout to 15 seconds
receiver_socket.connect(('localhost', 5555))
receiver_socket.send("RECEIVER".encode())
print("Connected to GSM simulator as RECEIVER")
while True:
try:
data = receiver_socket.recv(1024).decode().strip()
if not data:
print("No data received. Connection closed.")
break
if data == "RINGING":
print("Incoming call... ringing")
elif data == "CALL_END":
print("Call ended by caller.")
break
elif data == "CALL_DROPPED":
print("Call dropped by network.")
break
else:
print(f"Received: {data}")
except socket.timeout:
print("Timed out waiting for data.")
break
except Exception as e:
print(f"Receiver error: {e}")
break
receiver_socket.close()
if __name__ == "__main__":
connect()

View File

@ -1,59 +0,0 @@
#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

@ -1,66 +0,0 @@
#!/bin/bash
# Script to launch the GSM Simulator in Docker
# Variables
IMAGE_NAME="gsm-simulator"
CONTAINER_NAME="gsm-sim"
PORT="5555"
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed. Please install Docker and try again."
exit 1
fi
# Check if the gsm_simulator.py file exists in the current directory
if [ ! -f "gsm_simulator.py" ]; then
echo "Error: gsm_simulator.py not found in the current directory."
echo "Please ensure gsm_simulator.py is present and try again."
exit 1
fi
# Create Dockerfile if it doesn't exist
if [ ! -f "Dockerfile" ]; then
echo "Creating Dockerfile..."
cat <<EOF > Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY gsm_simulator.py /app
CMD ["python", "gsm_simulator.py"]
EOF
fi
# Build the Docker image
echo "Building Docker image: $IMAGE_NAME..."
docker build -t $IMAGE_NAME .
# Check if the build was successful
if [ $? -ne 0 ]; then
echo "Error: Failed to build Docker image."
exit 1
fi
# Stop and remove any existing container with the same name
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
echo "Stopping existing container: $CONTAINER_NAME..."
docker stop $CONTAINER_NAME
fi
if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then
echo "Removing existing container: $CONTAINER_NAME..."
docker rm $CONTAINER_NAME
fi
# Run the Docker container
echo "Launching GSM Simulator in Docker container: $CONTAINER_NAME..."
docker run -d -p $PORT:$PORT --name $CONTAINER_NAME $IMAGE_NAME
# Check if the container is running
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

@ -1,86 +0,0 @@
import socket
import os
import time
import subprocess
# Configuration
HOST = "localhost"
PORT = 12345
INPUT_FILE = "input.wav"
OUTPUT_FILE = "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,566 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" version="26.1.3" pages="2">
<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">
<root>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="WIyWlLk6GJQsqaUBKTNV-2" value="" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-3" target="WIyWlLk6GJQsqaUBKTNV-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-3" value="Alice appelle Bob" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=3;shadow=0;strokeColor=light-dark(#000000,#370FFF);" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="300" y="90" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-4" value="Yes" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-0" target="WIyWlLk6GJQsqaUBKTNV-10" edge="1">
<mxGeometry y="20" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-5" value="No" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-6" target="WIyWlLk6GJQsqaUBKTNV-7" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-6" value="Bob répond ?" style="rhombus;whiteSpace=wrap;html=1;shadow=0;fontFamily=Helvetica;fontSize=12;align=center;strokeWidth=1;spacing=6;spacingTop=-4;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="310" y="180" width="100" height="80" as="geometry" />
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-7" value="Rien ne se passe" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="460" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-8" value="Négativement" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
<mxGeometry y="40" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-9" value="Positivement" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="WIyWlLk6GJQsqaUBKTNV-12" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-10" value="Bob ping..." style="whiteSpace=wrap;html=1;shadow=0;fontFamily=Helvetica;fontSize=12;align=center;strokeWidth=1;spacing=6;spacingTop=-4;shape=hexagon;perimeter=hexagonPerimeter2;fixedSize=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="310" y="390" width="100" height="80" as="geometry" />
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-11" value="Protocole échoué&lt;div&gt;-&lt;/div&gt;&lt;div&gt;Passage en clair&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=3;shadow=0;strokeColor=light-dark(#000000,#FF1414);" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="300" y="520" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-12" target="FXGPDhTRSO2FZSW48CnP-8" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="599.9999999999998" y="500" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-12" value="Alice envoi son&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;&amp;nbsp;handshake a Bob&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="540" y="410" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-1" value="" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-6" target="FXGPDhTRSO2FZSW48CnP-0" edge="1">
<mxGeometry y="20" relative="1" as="geometry">
<mxPoint as="offset" />
<mxPoint x="360" y="260" as="sourcePoint" />
<mxPoint x="360" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-0" value="Le dialer d&#39;Alice envoi un PING a Bob" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="300" y="300" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-2" value="" style="endArrow=block;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="FXGPDhTRSO2FZSW48CnP-4" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="480" y="450" as="sourcePoint" />
<mxPoint x="190" y="430" as="targetPoint" />
<Array as="points">
<mxPoint x="280" y="430" />
<mxPoint x="280" y="460" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-3" value="Ne ping pas" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-2" vertex="1" connectable="0">
<mxGeometry x="-0.2695" relative="1" as="geometry">
<mxPoint x="-16" y="-10" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-4" target="FXGPDhTRSO2FZSW48CnP-0" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="130" y="320" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-6" value="Reping" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-5" vertex="1" connectable="0">
<mxGeometry x="0.0817" relative="1" as="geometry">
<mxPoint x="23" y="-10" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-4" value="Attendre 1s ? 0.5s ?" style="rounded=1;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="70" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-8" target="FXGPDhTRSO2FZSW48CnP-11" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="530.0380952380951" y="585.0038095238094" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-13" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-12" vertex="1" connectable="0">
<mxGeometry x="-0.4964" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-43" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endArrow=block;endFill=0;strokeColor=light-dark(#000000,#FF0000);" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-8" target="FXGPDhTRSO2FZSW48CnP-27" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="600" y="800" as="targetPoint" />
<Array as="points">
<mxPoint x="600" y="660" />
<mxPoint x="570" y="660" />
<mxPoint x="570" y="700" />
<mxPoint x="210" y="700" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-44" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-43" vertex="1" connectable="0">
<mxGeometry x="-0.8049" y="1" relative="1" as="geometry">
<mxPoint x="8" y="-25" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-8" value="Bob reconnait la clé publique d&#39;Alice ?" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="540" y="545" width="120" height="75" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-11" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="360" y="600" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-17" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-16" vertex="1" connectable="0">
<mxGeometry x="-0.1233" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-20" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-11" target="FXGPDhTRSO2FZSW48CnP-19" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-21" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-20" vertex="1" connectable="0">
<mxGeometry x="-0.275" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-11" value="Bob accepte la clé d&#39;Alice?" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="354" y="620" width="120" height="75" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-23" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-19" target="FXGPDhTRSO2FZSW48CnP-22" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-19" value="Bob envoi sa clé publique en handshake" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="160" y="636.25" width="120" height="42.5" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-22" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="70" y="545" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-25" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-24" vertex="1" connectable="0">
<mxGeometry x="-0.7543" y="3" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-28" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-22" target="FXGPDhTRSO2FZSW48CnP-27" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="70" y="750" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-22" value="Alice accepte la clé publique de Bob&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;&amp;nbsp;?&lt;/span&gt;" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;rounded=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="30" y="617.5" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-47" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-26" target="FXGPDhTRSO2FZSW48CnP-46" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-26" value="Alice et Bob sont d&#39;accord sur la clé symmétrique a utiliser" style="rounded=0;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="340" y="820" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-27" target="FXGPDhTRSO2FZSW48CnP-30" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=light-dark(#000000,#FF1616);endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-27" target="FXGPDhTRSO2FZSW48CnP-26" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="210" y="850" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-27" value="Alice et Bob calculent le secret partagé de leur côté" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="150" y="720" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;jumpStyle=sharp;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-29" target="FXGPDhTRSO2FZSW48CnP-34" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="640" y="680" as="targetPoint" />
<Array as="points">
<mxPoint x="700" y="745" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-36" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-33" vertex="1" connectable="0">
<mxGeometry x="-0.1536" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-29" target="FXGPDhTRSO2FZSW48CnP-26" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="525" y="798" />
<mxPoint x="510" y="798" />
<mxPoint x="510" y="850" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-42" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-41" vertex="1" connectable="0">
<mxGeometry x="-0.7774" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-29" value="Ils sont d&#39;accord ?" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="460" y="715" width="130" height="60" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-30" target="FXGPDhTRSO2FZSW48CnP-29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-30" value="Alice et Bob lisent à haute voix la phrase de sécurité" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="300" y="720" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-34" target="WIyWlLk6GJQsqaUBKTNV-12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-38" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-37" vertex="1" connectable="0">
<mxGeometry x="-0.3086" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;jumpStyle=sharp;jumpSize=8;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-34" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="530" y="690" />
<mxPoint x="530" y="545" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-40" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-39" vertex="1" connectable="0">
<mxGeometry x="-0.7617" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-34" value="Ré-essayer ?" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="660" y="650" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-49" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-46" target="FXGPDhTRSO2FZSW48CnP-48" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-46" value="Alice et Bob utilisent la clé symmétrique pour chiffrer leurs transmissions" style="whiteSpace=wrap;html=1;rounded=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="340" y="920" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="FXGPDhTRSO2FZSW48CnP-48" value="" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="360" y="1020" width="80" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="4Sb7mgJDpsadGym-U4wz" name="Echanges">
<mxGraphModel dx="1195" dy="683" 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" />
<mxCell id="b_xV4iUWIxmdZCAYY4YR-1" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
<mxGeometry x="160" y="120" width="440" height="120" as="geometry" />
</mxCell>
<mxCell id="O_eM33N56VtHnDaMz1H4-1" value="ALICE" 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="1" vertex="1">
<mxGeometry x="120" y="40" width="40" height="3110" as="geometry" />
</mxCell>
<mxCell id="O_eM33N56VtHnDaMz1H4-2" value="BOB" 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="1" vertex="1">
<mxGeometry x="690" y="40" width="40" height="3110" as="geometry" />
</mxCell>
<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" />
</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;" vertex="1" parent="n3lF8vaYaHAhAfaeaFZn-2">
<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">
<mxGeometry x="280" y="280" width="410" height="190" as="geometry" />
</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">
<mxGeometry x="350" y="510" width="170" height="40" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-14" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
<mxGeometry x="160" y="570" width="410" height="220" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-15" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="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="pP7SjZfcCiBg3d1TCkzP-13" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="XvZTtdEB18xY6m2a5fJO-15" vertex="1">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-17" value="Signature" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="285" y="580" width="105" height="80" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-14" value="PubkeyFixe. sign(clé éphémère)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-17" vertex="1">
<mxGeometry y="20" width="100" height="60" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-18" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="486.25" y="580" width="65" height="80" as="geometry" />
</mxCell>
<mxCell id="XvZTtdEB18xY6m2a5fJO-19" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-18" vertex="1">
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-15" value="PFS" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="402.81" y="580" width="71.88" height="80" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-16" value="hash( preuve de convo précédente)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="pP7SjZfcCiBg3d1TCkzP-15" vertex="1">
<mxGeometry x="6.57" y="30" width="60" height="40" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-17" 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="285" y="830" width="410" height="180" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-43" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="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="pP7SjZfcCiBg3d1TCkzP-44" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="pP7SjZfcCiBg3d1TCkzP-43" vertex="1">
<mxGeometry y="30" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-45" value="Signature" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="420" y="840" width="105" height="80" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-46" value="PubkeyFixe. sign(clé éphémère)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pP7SjZfcCiBg3d1TCkzP-45" vertex="1">
<mxGeometry y="20" width="100" height="60" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-47" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="621.25" y="840" width="65" height="80" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-48" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pP7SjZfcCiBg3d1TCkzP-47" vertex="1">
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-49" value="PFS" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="537.81" y="840" width="71.88" height="80" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-50" value="hash( preuve de convo précédente )" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="pP7SjZfcCiBg3d1TCkzP-49" vertex="1">
<mxGeometry x="6.57" y="30" width="60" height="40" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-54" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="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="pP7SjZfcCiBg3d1TCkzP-55" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="pP7SjZfcCiBg3d1TCkzP-54" vertex="1">
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-56" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="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="pP7SjZfcCiBg3d1TCkzP-57" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="pP7SjZfcCiBg3d1TCkzP-56" vertex="1">
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-58" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
<mxGeometry x="160" y="1160" width="450" height="200" as="geometry" />
</mxCell>
<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" />
</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">
<mxGeometry x="193" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-72" value="512b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="307.5" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-73" value="256b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="409.38" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-74" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="488.75" y="660" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-75" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="192.5" y="760" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-76" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="315" y="750" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pP7SjZfcCiBg3d1TCkzP-77" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="327.5" y="970" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-1" value="CRC ?" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
<mxGeometry x="375" y="1270" width="63.25" height="60" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-2" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-1" vertex="1">
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-3" value="Flag" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
<mxGeometry x="180" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-4" value="To determine" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-3" vertex="1">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-5" value="nbretry" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="344.38" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-6" value="y" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-5" vertex="1">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-7" value="msg_len" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
<mxGeometry x="262.5" y="1170" width="65" height="60" as="geometry">
<mxRectangle x="262.5" y="1170" width="90" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-8" value="XXX" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-7" vertex="1">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-9" value="msg" style="swimlane;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" parent="1" vertex="1">
<mxGeometry x="187.5" y="1270" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-10" value="BBB" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-9" vertex="1">
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-11" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="180" y="1230" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-12" value="8b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="349.38" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-13" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="267.5" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-14" value="96b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="510" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="_H5URFloX_BVB2BL7kO6-15" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="379.12" y="1330" width="55" height="30" as="geometry" />
</mxCell>
<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" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-2" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="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;" vertex="1" parent="pWkGvNQAXuiST1IiWYlx-2">
<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;" vertex="1" parent="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;" vertex="1" parent="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;" vertex="1" parent="pWkGvNQAXuiST1IiWYlx-5">
<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;" vertex="1" parent="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;" vertex="1" parent="1">
<mxGeometry x="425" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-9" value="CRC ?" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="pWkGvNQAXuiST1IiWYlx-8">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-10" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="428.75" y="1230" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-11" value="iv" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="505" y="1170" width="65" height="60" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-12" value="random&lt;div&gt;(+Z)&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="pWkGvNQAXuiST1IiWYlx-11">
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-13" value="BBB b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="193" y="1330" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-14" value="MAC" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" vertex="1" parent="1">
<mxGeometry x="286.13" y="1270" width="63.25" height="60" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-15" value="AEAD" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="pWkGvNQAXuiST1IiWYlx-14">
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-16" value="128b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="290.25" y="1330" width="55" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-17" 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="1">
<mxGeometry x="10" y="1170" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-18" 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="1">
<mxGeometry y="1220" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="pWkGvNQAXuiST1IiWYlx-19" 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="1">
<mxGeometry x="10" y="1270" width="110" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -1,430 +0,0 @@
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}")

View File

@ -1,328 +0,0 @@
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

@ -1,165 +0,0 @@
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

@ -1,263 +0,0 @@
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

View File

@ -1,262 +0,0 @@
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
partial_val = (self.timestamp << (7+4+1)) | (self.version << (4+1)) | (self.cipher << 1) | self.answer
partial_bytes = partial_val.to_bytes(6, 'big') # 6 bytes = 48 bits, 4 spare 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()

View File

@ -1,815 +0,0 @@
import random
import os
import time
import threading
from typing import List, Dict, Any, Optional, Tuple
from crypto_utils import (
generate_identity_keys,
load_peer_identity_key,
sign_data,
verify_signature,
get_ephemeral_keypair,
compute_ecdh_shared_key,
der_to_raw,
raw_signature_to_der
)
from messages import (
PingRequest, PingResponse, Handshake,
compute_pfs_hash
)
import transmission
from encryption import (
EncryptedMessage, MessageHeader,
generate_iv, encrypt_message, decrypt_message
)
from auto_mode import AutoMode, AutoModeConfig
# ANSI colors
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
class IcingProtocol:
def __init__(self):
# Identity keys (each 512 bits when printed as hex of 64 bytes)
self.identity_privkey, self.identity_pubkey = generate_identity_keys()
# Peer identity for verifying ephemeral signatures
self.peer_identity_pubkey_obj = None
self.peer_identity_pubkey_bytes = None
# Ephemeral keys (our side)
self.ephemeral_privkey = None
self.ephemeral_pubkey = None
# Last computed shared secret (hex string)
self.shared_secret = None
# Derived HKDF key (hex string, 256 bits)
self.hkdf_key = None
# Negotiated cipher (0 = AES-256-GCM, 1 = ChaCha20-Poly1305)
self.cipher_type = 0
# For PFS: track per-peer session info (session number and last shared secret)
self.pfs_history: Dict[bytes, Tuple[int, str]] = {}
# Protocol flags
self.state = {
"ping_sent": False,
"ping_received": False,
"handshake_sent": False,
"handshake_received": False,
"key_exchange_complete": False
}
# Auto mode for automated protocol operation
self.auto_mode = AutoMode(self)
# Legacy auto-responder toggle (kept for backward compatibility)
self.auto_responder = False
# Active connections list
self.connections = []
# Inbound messages (each message is a dict with keys: type, raw, parsed, connection)
self.inbound_messages: List[Dict[str, Any]] = []
# Store the session nonce (17 bytes but only 129 bits are valid) from first sent or received PING
self.session_nonce: bytes = None
# Last used IV for encrypted messages
self.last_iv: bytes = None
self.local_port = random.randint(30000, 40000)
self.server_listener = transmission.ServerListener(
host="127.0.0.1",
port=self.local_port,
on_new_connection=self.on_new_connection,
on_data_received=self.on_data_received
)
self.server_listener.start()
# -------------------------------------------------------------------------
# Transport callbacks
# -------------------------------------------------------------------------
def on_new_connection(self, conn: transmission.PeerConnection):
print(f"{GREEN}[IcingProtocol]{RESET} New incoming connection.")
self.connections.append(conn)
# Notify auto mode
self.auto_mode.handle_connection_established()
def on_data_received(self, conn: transmission.PeerConnection, data: bytes):
bits_count = len(data) * 8
print(
f"{GREEN}[RECV]{RESET} {bits_count} bits from peer: {data.hex()[:60]}{'...' if len(data.hex()) > 60 else ''}")
# PING REQUEST (22 bytes)
if len(data) == 22:
ping_request = PingRequest.deserialize(data)
if ping_request:
self.state["ping_received"] = True
# If received cipher is not supported, force to 0 (AES-256-GCM)
if ping_request.cipher != 0 and ping_request.cipher != 1:
print(f"{YELLOW}[NOTICE]{RESET} Received PING with unsupported cipher ({ping_request.cipher}); forcing cipher to 0 in response.")
ping_request.cipher = 0
# Store cipher type for future encrypted messages
self.cipher_type = ping_request.cipher
# Store session nonce if not already set
if self.session_nonce is None:
self.session_nonce = ping_request.session_nonce
print(f"{YELLOW}[NOTICE]{RESET} Stored session nonce from received PING.")
index = len(self.inbound_messages)
msg = {
"type": "PING_REQUEST",
"raw": data,
"parsed": ping_request,
"connection": conn
}
self.inbound_messages.append(msg)
# Handle in auto mode (if active)
self.auto_mode.handle_ping_received(index)
# Legacy auto-responder (for backward compatibility)
if self.auto_responder and not self.auto_mode.active:
timer = threading.Timer(2.0, self._auto_respond_ping, args=[index])
timer.daemon = True
timer.start()
return
# PING RESPONSE (10 bytes)
elif len(data) == 10:
ping_response = PingResponse.deserialize(data)
if ping_response:
# Store negotiated cipher type
self.cipher_type = ping_response.cipher
index = len(self.inbound_messages)
msg = {
"type": "PING_RESPONSE",
"raw": data,
"parsed": ping_response,
"connection": conn
}
self.inbound_messages.append(msg)
# Notify auto mode (if active)
self.auto_mode.handle_ping_response_received(ping_response.answer == 1)
return
# HANDSHAKE message (168 bytes)
elif len(data) == 168:
handshake = Handshake.deserialize(data)
if handshake:
self.state["handshake_received"] = True
index = len(self.inbound_messages)
msg = {
"type": "HANDSHAKE",
"raw": data,
"parsed": handshake,
"connection": conn
}
self.inbound_messages.append(msg)
# Notify auto mode (if active)
self.auto_mode.handle_handshake_received(index)
# Legacy auto-responder
if self.auto_responder and not self.auto_mode.active:
timer = threading.Timer(2.0, self._auto_respond_handshake, args=[index])
timer.daemon = True
timer.start()
return
# Check if the message might be an encrypted message (e.g. header of 18 bytes at start)
elif len(data) >= 18:
# Try to parse header
try:
header = MessageHeader.unpack(data[:18])
# If header unpacking is successful and data length matches header expectations
expected_len = 18 + header.data_len + 16 # Header + payload + tag
# Check if CRC is included
has_crc = (header.connection_status & 0x01) != 0
if has_crc:
expected_len += 4 # Add CRC32 length
if len(data) >= expected_len:
index = len(self.inbound_messages)
msg = {
"type": "ENCRYPTED_MESSAGE",
"raw": data,
"parsed": header,
"connection": conn
}
self.inbound_messages.append(msg)
print(f"{YELLOW}[NOTICE]{RESET} Stored inbound ENCRYPTED_MESSAGE at index={index}.")
# Notify auto mode
self.auto_mode.handle_encrypted_received(index)
return
except Exception as e:
print(f"{RED}[ERROR]{RESET} Failed to parse message header: {e}")
# Otherwise, unrecognized/malformed message.
index = len(self.inbound_messages)
msg = {
"type": "UNKNOWN",
"raw": data,
"parsed": None,
"connection": conn
}
self.inbound_messages.append(msg)
print(f"{RED}[WARNING]{RESET} Unrecognized or malformed message stored at index={index}.")
# -------------------------------------------------------------------------
# HKDF Derivation
# -------------------------------------------------------------------------
def derive_hkdf(self):
"""
Derives a 256-bit key using HKDF.
Uses as input keying material (IKM) the shared secret from ECDH.
The salt is computed as SHA256(session_nonce || pfs_param), where:
- session_nonce is taken from self.session_nonce (17 bytes, 129 bits) or defaults to zeros.
- pfs_param is taken from the first inbound HANDSHAKE's pfs_hash field (32 bytes) or zeros.
"""
if not self.shared_secret:
print(f"{RED}[ERROR]{RESET} No shared secret available; cannot derive HKDF key.")
return
# IKM: shared secret converted from hex to bytes.
ikm = bytes.fromhex(self.shared_secret)
# Use stored session_nonce if available; otherwise default to zeros.
session_nonce = self.session_nonce if self.session_nonce is not None else (b"\x00" * 17)
# Determine pfs_param from first HANDSHAKE message (if any)
pfs_param = None
for msg in self.inbound_messages:
if msg["type"] == "HANDSHAKE":
try:
handshake = msg["parsed"]
pfs_param = handshake.pfs_hash
except Exception:
pfs_param = None
break
if pfs_param is None:
print(f"{RED}[WARNING]{RESET} No HANDSHAKE found; using 32 zero bytes for pfs_param.")
pfs_param = b"\x00" * 32 # 256-bit zeros
# Ensure both are bytes
if isinstance(session_nonce, str):
session_nonce = session_nonce.encode()
if isinstance(pfs_param, str):
pfs_param = pfs_param.encode()
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
hasher = hashes.Hash(hashes.SHA256())
hasher.update(session_nonce + pfs_param)
salt_value = hasher.finalize()
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32, # 256 bits
salt=salt_value,
info=b"",
)
derived_key = hkdf.derive(ikm)
self.hkdf_key = derived_key.hex()
self.state["key_exchange_complete"] = True
print(f"{GREEN}[HKDF]{RESET} Derived HKDF key: {self.hkdf_key}")
return True
# -------------------------------------------------------------------------
# Legacy Auto-responder helpers (kept for backward compatibility)
# -------------------------------------------------------------------------
def _auto_respond_ping(self, index: int):
"""
Called by a Timer to respond automatically to a PING_REQUEST after 2s.
"""
print(f"{BLUE}[AUTO]{RESET} Delayed responding to PING at index={index}")
self.respond_to_ping(index, answer=1) # Accept by default
self.show_state()
def _auto_respond_handshake(self, index: int):
"""
Called by a Timer to handle inbound HANDSHAKE automatically.
1) Generate ephemeral keys if not already set
2) Compute ECDH with the inbound ephemeral pub (generate_ecdhe)
3) Send our handshake back
4) Show state
"""
print(f"{BLUE}[AUTO]{RESET} Delayed ECDH process for HANDSHAKE at index={index}")
# 1) Generate ephemeral keys if we don't have them
if not self.ephemeral_privkey or not self.ephemeral_pubkey:
self.generate_ephemeral_keys()
# 2) Compute ECDH from inbound ephemeral pub
self.generate_ecdhe(index)
# 3) Send our handshake to the peer
self.send_handshake()
# 4) Show final state
self.show_state()
# -------------------------------------------------------------------------
# Public Methods for Auto Mode Management
# -------------------------------------------------------------------------
def start_auto_mode(self):
"""Start the automatic protocol operation mode."""
self.auto_mode.start()
def stop_auto_mode(self):
"""Stop the automatic protocol operation mode."""
self.auto_mode.stop()
def configure_auto_mode(self, **kwargs):
"""
Configure the automatic mode parameters.
Args:
**kwargs: Configuration parameters to set. Supported parameters:
- ping_response_accept: bool, whether to accept incoming pings
- ping_auto_initiate: bool, whether to initiate pings on connection
- ping_retry_count: int, number of ping retries
- ping_retry_delay: float, seconds between ping retries
- ping_timeout: float, seconds to wait for ping response
- preferred_cipher: int, preferred cipher (0=AES-GCM, 1=ChaCha20-Poly1305)
- handshake_retry_count: int, number of handshake retries
- handshake_retry_delay: float, seconds between handshake retries
- handshake_timeout: float, seconds to wait for handshake
- auto_message_enabled: bool, whether to auto-send messages
- message_interval: float, seconds between auto messages
- message_content: str, default message content
- active_mode: bool, whether to actively initiate protocol
"""
for key, value in kwargs.items():
if hasattr(self.auto_mode.config, key):
setattr(self.auto_mode.config, key, value)
print(f"{BLUE}[CONFIG]{RESET} Set auto mode {key} = {value}")
else:
print(f"{RED}[ERROR]{RESET} Unknown auto mode configuration parameter: {key}")
def get_auto_mode_config(self):
"""Return the current auto mode configuration."""
return self.auto_mode.config
def queue_auto_message(self, message: str):
"""
Add a message to the auto-send queue.
Args:
message: Message text to send
"""
self.auto_mode.queue_message(message)
def toggle_auto_mode_logging(self):
"""
Toggle detailed logging for auto mode.
When enabled, will show more information about state transitions and decision making.
"""
if not hasattr(self.auto_mode, 'verbose_logging'):
self.auto_mode.verbose_logging = True
else:
self.auto_mode.verbose_logging = not self.auto_mode.verbose_logging
status = "enabled" if self.auto_mode.verbose_logging else "disabled"
print(f"{BLUE}[AUTO-LOG]{RESET} Detailed logging {status}")
def debug_message(self, index: int):
"""
Debug a message in the inbound message queue.
Prints detailed information about the message.
Args:
index: The index of the message in the inbound_messages queue
"""
if index < 0 or index >= len(self.inbound_messages):
print(f"{RED}[ERROR]{RESET} Invalid message index {index}")
return
msg = self.inbound_messages[index]
print(f"\n{YELLOW}=== Message Debug [{index}] ==={RESET}")
print(f"Type: {msg['type']}")
print(f"Length: {len(msg['raw'])} bytes = {len(msg['raw'])*8} bits")
print(f"Raw data: {msg['raw'].hex()}")
if msg['parsed'] is not None:
print(f"\n{YELLOW}--- Parsed Data ---{RESET}")
if msg['type'] == 'PING_REQUEST':
ping = msg['parsed']
print(f"Version: {ping.version}")
print(f"Cipher: {ping.cipher} ({'AES-256-GCM' if ping.cipher == 0 else 'ChaCha20-Poly1305' if ping.cipher == 1 else 'Unknown'})")
print(f"Session nonce: {ping.session_nonce.hex()}")
print(f"CRC32: {ping.crc32:08x}")
elif msg['type'] == 'PING_RESPONSE':
resp = msg['parsed']
print(f"Version: {resp.version}")
print(f"Cipher: {resp.cipher} ({'AES-256-GCM' if resp.cipher == 0 else 'ChaCha20-Poly1305' if resp.cipher == 1 else 'Unknown'})")
print(f"Answer: {resp.answer} ({'Accept' if resp.answer == 1 else 'Reject'})")
print(f"CRC32: {resp.crc32:08x}")
elif msg['type'] == 'HANDSHAKE':
hs = msg['parsed']
print(f"Ephemeral pubkey: {hs.ephemeral_pubkey.hex()[:16]}...")
print(f"Ephemeral signature: {hs.ephemeral_signature.hex()[:16]}...")
print(f"PFS hash: {hs.pfs_hash.hex()[:16]}...")
print(f"Timestamp: {hs.timestamp}")
print(f"CRC32: {hs.crc32:08x}")
elif msg['type'] == 'ENCRYPTED_MESSAGE':
header = msg['parsed']
print(f"Flag: 0x{header.flag:04x}")
print(f"Data length: {header.data_len} bytes")
print(f"Retry: {header.retry}")
print(f"Connection status: {header.connection_status} ({'CRC included' if header.connection_status & 0x01 else 'No CRC'})")
print(f"IV: {header.iv.hex()}")
# Calculate expected message size
expected_len = 18 + header.data_len + 16 # Header + payload + tag
if header.connection_status & 0x01:
expected_len += 4 # Add CRC
print(f"Expected total length: {expected_len} bytes")
print(f"Actual length: {len(msg['raw'])} bytes")
# If we have a key, try to decrypt
if self.hkdf_key:
print("\nAttempting decryption...")
try:
key = bytes.fromhex(self.hkdf_key)
plaintext = decrypt_message(msg['raw'], key, self.cipher_type)
print(f"Decrypted: {plaintext.decode('utf-8')}")
except Exception as e:
print(f"Decryption failed: {e}")
print()
# -------------------------------------------------------------------------
# Public Methods
# -------------------------------------------------------------------------
def connect_to_peer(self, port: int):
conn = transmission.connect_to_peer("127.0.0.1", port, self.on_data_received)
self.connections.append(conn)
print(f"{GREEN}[IcingProtocol]{RESET} Outgoing connection to port {port} established.")
# Notify auto mode
self.auto_mode.handle_connection_established()
def set_peer_identity(self, peer_pubkey_hex: str):
pubkey_bytes = bytes.fromhex(peer_pubkey_hex)
self.peer_identity_pubkey_obj = load_peer_identity_key(pubkey_bytes)
self.peer_identity_pubkey_bytes = pubkey_bytes
print(f"{GREEN}[IcingProtocol]{RESET} Stored peer identity pubkey (hex={peer_pubkey_hex[:16]}...).")
def generate_ephemeral_keys(self):
self.ephemeral_privkey, self.ephemeral_pubkey = get_ephemeral_keypair()
print(f"{GREEN}[IcingProtocol]{RESET} Generated ephemeral key pair: pubkey={self.ephemeral_pubkey.hex()[:16]}...")
# Send PING (session discovery and cipher negotiation)
def send_ping_request(self, cipher_type=0):
"""
Send a ping request to the first connected peer.
Args:
cipher_type: Preferred cipher type (0 = AES-256-GCM, 1 = ChaCha20-Poly1305)
"""
if not self.connections:
print(f"{RED}[ERROR]{RESET} No active connections.")
return False
# Validate cipher type
if cipher_type not in (0, 1):
print(f"{YELLOW}[WARNING]{RESET} Invalid cipher type {cipher_type}, defaulting to AES-256-GCM (0)")
cipher_type = 0
# Create ping request with specified cipher
ping_request = PingRequest(version=0, cipher=cipher_type)
# Store session nonce if not already set
if self.session_nonce is None:
self.session_nonce = ping_request.session_nonce
print(f"{YELLOW}[NOTICE]{RESET} Stored session nonce from sent PING.")
# Serialize and send
pkt = ping_request.serialize()
self._send_packet(self.connections[0], pkt, "PING_REQUEST")
self.state["ping_sent"] = True
return True
def send_handshake(self):
"""
Build and send handshake:
- ephemeral_pubkey (64 bytes, raw x||y)
- ephemeral_signature (64 bytes, raw r||s)
- pfs_hash (32 bytes)
- timestamp (32 bits)
- CRC (32 bits)
"""
if not self.connections:
print(f"{RED}[ERROR]{RESET} No active connections.")
return False
if not self.ephemeral_privkey or not self.ephemeral_pubkey:
print(f"{RED}[ERROR]{RESET} Ephemeral keys not generated.")
return False
if self.peer_identity_pubkey_bytes is None:
print(f"{RED}[ERROR]{RESET} Peer identity not set; needed for PFS tracking.")
return False
# 1) Sign ephemeral_pubkey using identity key
sig_der = sign_data(self.identity_privkey, self.ephemeral_pubkey)
# Convert DER signature to raw r||s format (64 bytes)
raw_signature = der_to_raw(sig_der)
# 2) Compute PFS hash
session_number, last_secret_hex = self.pfs_history.get(self.peer_identity_pubkey_bytes, (-1, ""))
pfs = compute_pfs_hash(session_number, last_secret_hex)
# 3) Create handshake object
handshake = Handshake(
ephemeral_pubkey=self.ephemeral_pubkey,
ephemeral_signature=raw_signature,
pfs_hash=pfs
)
# 4) Serialize and send
pkt = handshake.serialize()
self._send_packet(self.connections[0], pkt, "HANDSHAKE")
self.state["handshake_sent"] = True
return True
def enable_auto_responder(self, enable: bool):
"""
Legacy method for enabling/disabling auto responder.
For new code, use start_auto_mode() and stop_auto_mode() instead.
"""
self.auto_responder = enable
print(f"{YELLOW}[LEGACY]{RESET} Auto responder set to {enable}. Consider using auto_mode instead.")
# -------------------------------------------------------------------------
# Manual Responses
# -------------------------------------------------------------------------
def respond_to_ping(self, index: int, answer: int):
"""
Respond to a ping request with the specified answer (0 = no, 1 = yes).
If answer is 1, we accept the connection and use the cipher specified in the request.
"""
if index < 0 or index >= len(self.inbound_messages):
print(f"{RED}[ERROR]{RESET} Invalid index {index}.")
return False
msg = self.inbound_messages[index]
if msg["type"] != "PING_REQUEST":
print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a PING_REQUEST.")
return False
ping_request = msg["parsed"]
version = ping_request.version
cipher = ping_request.cipher
# Force cipher to 0 or 1 (only AES-256-GCM and ChaCha20-Poly1305 are supported)
if cipher != 0 and cipher != 1:
print(f"{YELLOW}[NOTICE]{RESET} Received PING with unsupported cipher ({cipher}); forcing cipher to 0 in response.")
cipher = 0
# Store the negotiated cipher type if we're accepting
if answer == 1:
self.cipher_type = cipher
conn = msg["connection"]
# Create ping response
ping_response = PingResponse(version, cipher, answer)
resp = ping_response.serialize()
self._send_packet(conn, resp, "PING_RESPONSE")
print(f"{BLUE}[MANUAL]{RESET} Responded to ping with answer={answer}.")
return True
def generate_ecdhe(self, index: int):
"""
Process a handshake message:
1. Verify the ephemeral signature
2. Compute the ECDH shared secret
3. Update PFS history
"""
if index < 0 or index >= len(self.inbound_messages):
print(f"{RED}[ERROR]{RESET} Invalid index {index}.")
return False
msg = self.inbound_messages[index]
if msg["type"] != "HANDSHAKE":
print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a HANDSHAKE.")
return False
handshake = msg["parsed"]
# Convert raw signature to DER for verification
raw_sig = handshake.ephemeral_signature
sig_der = raw_signature_to_der(raw_sig)
# Verify signature
ok = verify_signature(self.peer_identity_pubkey_obj, sig_der, handshake.ephemeral_pubkey)
if not ok:
print(f"{RED}[ERROR]{RESET} Ephemeral signature invalid.")
return False
print(f"{GREEN}[OK]{RESET} Ephemeral signature verified.")
# Check if we have ephemeral keys
if not self.ephemeral_privkey:
print(f"{YELLOW}[WARN]{RESET} No ephemeral_privkey available, cannot compute shared secret.")
return False
# Compute ECDH shared secret
shared = compute_ecdh_shared_key(self.ephemeral_privkey, handshake.ephemeral_pubkey)
self.shared_secret = shared.hex()
print(f"{GREEN}[OK]{RESET} Computed ECDH shared key = {self.shared_secret}")
# Update PFS history
old_session, _ = self.pfs_history.get(self.peer_identity_pubkey_bytes, (-1, ""))
new_session = 1 if old_session < 0 else old_session + 1
self.pfs_history[self.peer_identity_pubkey_bytes] = (new_session, self.shared_secret)
return True
# -------------------------------------------------------------------------
# Utility
# -------------------------------------------------------------------------
def _send_packet(self, conn: transmission.PeerConnection, data: bytes, label: str):
bits_count = len(data) * 8
print(f"{BLUE}[SEND]{RESET} {label} -> {bits_count} bits: {data.hex()[:60]}{'...' if len(data.hex())>60 else ''}")
conn.send(data)
def show_state(self):
print(f"\n{YELLOW}=== Global State ==={RESET}")
print(f"Listening Port: {self.local_port}")
print(f"Identity PubKey: 512 bits => {self.identity_pubkey.hex()[:16]}...")
if self.peer_identity_pubkey_bytes:
print(f"Peer Identity PubKey: 512 bits => {self.peer_identity_pubkey_bytes.hex()[:16]}...")
else:
print("Peer Identity PubKey: [None]")
print("\nEphemeral Keys:")
if self.ephemeral_pubkey:
print(f" ephemeral_pubkey: 512 bits => {self.ephemeral_pubkey.hex()[:16]}...")
else:
print(" ephemeral_pubkey: [None]")
print(f"\nShared Secret: {self.shared_secret if self.shared_secret else '[None]'}")
if self.hkdf_key:
print(f"HKDF Derived Key: {self.hkdf_key} (size: {len(self.hkdf_key)*8} bits)")
else:
print("HKDF Derived Key: [None]")
print(f"Negotiated Cipher: {'AES-256-GCM' if self.cipher_type == 0 else 'ChaCha20-Poly1305'} (code: {self.cipher_type})")
if self.session_nonce:
print(f"Session Nonce: {self.session_nonce.hex()} (129 bits)")
else:
print("Session Nonce: [None]")
if self.last_iv:
print(f"Last IV: {self.last_iv.hex()} (96 bits)")
else:
print("Last IV: [None]")
print("\nProtocol Flags:")
for k, v in self.state.items():
print(f" {k}: {v}")
print("\nAuto Mode Active:", self.auto_mode.active)
print("Auto Mode State:", self.auto_mode.state)
print("Legacy Auto Responder:", self.auto_responder)
print("\nActive Connections:")
for i, c in enumerate(self.connections):
print(f" [{i}] Alive={c.alive}")
print("\nInbound Message Queue:")
for i, m in enumerate(self.inbound_messages):
print(f" [{i}] type={m['type']} len={len(m['raw'])} bytes => {len(m['raw']) * 8} bits")
print()
def stop(self):
"""Stop the protocol and clean up resources."""
# Stop auto mode first
self.auto_mode.stop()
# Stop server listener
self.server_listener.stop()
# Close all connections
for c in self.connections:
c.close()
self.connections.clear()
self.inbound_messages.clear()
print(f"{RED}[STOP]{RESET} Protocol stopped.")
# -------------------------------------------------------------------------
# Encrypted Messaging
# -------------------------------------------------------------------------
def send_encrypted_message(self, plaintext: str):
"""
Encrypts and sends a message using the derived HKDF key and negotiated cipher.
The message format is:
- Header (18 bytes): flag, data_len, retry, connection_status, IV
- Payload: variable length encrypted data
- Footer: Authentication tag (16 bytes) + optional CRC32 (4 bytes)
"""
if not self.connections:
print(f"{RED}[ERROR]{RESET} No active connections.")
return False
if not self.hkdf_key:
print(f"{RED}[ERROR]{RESET} No HKDF key derived. Cannot encrypt message.")
return False
# Get the encryption key
key = bytes.fromhex(self.hkdf_key)
# Convert plaintext to bytes
plaintext_bytes = plaintext.encode('utf-8')
# Generate or increment the IV
if self.last_iv is None:
# First message, generate random IV
iv = generate_iv(initial=True)
else:
# Subsequent message, increment previous IV
iv = generate_iv(initial=False, previous_iv=self.last_iv)
# Store the new IV
self.last_iv = iv
# Create encrypted message (connection_status 0 = no CRC)
encrypted = encrypt_message(
plaintext=plaintext_bytes,
key=key,
flag=0xBEEF, # Default flag
retry=0,
connection_status=0, # No CRC
iv=iv,
cipher_type=self.cipher_type
)
# Send the encrypted message
self._send_packet(self.connections[0], encrypted, "ENCRYPTED_MESSAGE")
print(f"{GREEN}[SEND_ENCRYPTED]{RESET} Encrypted message sent.")
return True
def decrypt_received_message(self, index: int):
"""
Decrypt a received encrypted message using the HKDF key and negotiated cipher.
"""
if index < 0 or index >= len(self.inbound_messages):
print(f"{RED}[ERROR]{RESET} Invalid message index.")
return None
msg = self.inbound_messages[index]
if msg["type"] != "ENCRYPTED_MESSAGE":
print(f"{RED}[ERROR]{RESET} Message at index {index} is not an ENCRYPTED_MESSAGE.")
return None
# Get the encrypted message
encrypted = msg["raw"]
if not self.hkdf_key:
print(f"{RED}[ERROR]{RESET} No HKDF key derived. Cannot decrypt message.")
return None
# Get the encryption key
key = bytes.fromhex(self.hkdf_key)
try:
# Decrypt the message
plaintext = decrypt_message(encrypted, key, self.cipher_type)
# Convert to string
plaintext_str = plaintext.decode('utf-8')
# Update last IV from the header
header = MessageHeader.unpack(encrypted[:18])
self.last_iv = header.iv
print(f"{GREEN}[DECRYPTED]{RESET} Decrypted message: {plaintext_str}")
return plaintext_str
except Exception as e:
print(f"{RED}[ERROR]{RESET} Decryption failed: {e}")
return None

View File

@ -1,100 +0,0 @@
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