Compare commits

...

34 Commits

Author SHA1 Message Date
0b52d602ef Fix + enhancement
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 22:59:15 +02:00
c3b0e94666 Clean
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 22:27:28 +02:00
3baf3f142b Trying to fix PING and session Nonce
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 21:50:14 +02:00
394143b4df Add encryption.py NO DECRYPTION yet
All checks were successful
/ mirror (push) Successful in 5s
2025-03-29 20:55:09 +02:00
79b0491a75 Update diagram, add HKDF derivation
All checks were successful
/ mirror (push) Successful in 4s
2025-03-29 11:17:26 +02:00
6155955cca Merge branch 'Protocol_00' of git.gmoker.com:icing/monorepo into Protocol_00
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 18:06:17 +00:00
0e619309ea add of DryBox 2025-03-28 18:06:07 +00:00
8d45d2e745 Signature fix
All checks were successful
/ mirror (push) Successful in 5s
2025-03-28 19:41:55 +02:00
045d9ad417 Merge remote-tracking branch 'origin/Protocol_00' into Protocol_00
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 19:35:54 +02:00
803ec3712b Signature NOT fix, bad attempt 2025-03-28 19:35:43 +02:00
f9e64a73d9 merge
All checks were successful
/ mirror (push) Successful in 4s
2025-03-28 17:31:38 +00:00
f5930eef82 Need fix signatures
All checks were successful
/ mirror (push) Successful in 4s
2025-03-24 13:04:56 +02:00
1b5bda2eb4 Semi-auto, bad sizes and need adjustments
All checks were successful
/ mirror (push) Successful in 6s
/ build-stealth (push) Successful in 8m30s
/ build (push) Successful in 8m37s
2025-03-23 21:32:00 +02:00
b139e36921 Update flow 2025-03-17 10:51:49 +02:00
e8bbe447c8 Drawio Handshake logic 2025-03-17 10:51:49 +02:00
d56dc33181 Init 2025-03-17 10:51:49 +02:00
0d6322a714 fix: not showing call UI on long press
All checks were successful
/ mirror (push) Successful in 4s
/ build-stealth (push) Successful in 8m21s
/ build (push) Successful in 8m30s
2025-03-14 15:59:54 +02:00
da0c5d1991 feat: call page UI
Some checks failed
/ mirror (push) Waiting to run
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-03-14 15:57:47 +02:00
3129e51eb4 Add CallPage for initiating calls with contact details (#37)
Demo call page avec les features de base:
- Haut parleur
- Couper/activer micro
- keypad
- raccrocher
- Display Icing state (toucher pour switch l'état)

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

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

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

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

View File

@ -10,8 +10,22 @@ jobs:
- uses: actions/checkout@v1
with:
subpath: dialer/
- uses: icing/flutter@main
- uses: docker://git.gmoker.com/icing/flutter:main
- uses: actions/upload-artifact@v1
with:
name: icing-dialer-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk
build-stealth:
runs-on: debian
steps:
- uses: actions/checkout@v1
with:
subpath: dialer/
- uses: docker://git.gmoker.com/icing/flutter:main
with:
args: "build apk --dart-define=STEALTH=true"
- uses: actions/upload-artifact@v1
with:
name: icing-dialer-stealth-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk

View File

@ -7,6 +7,7 @@ gradle-wrapper.jar
/gradle.properties
GeneratedPluginRegistrant.java
gradle.properties
.cxx
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore

View File

@ -1,13 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<application
android:label="Icing Dialer"
@ -23,34 +27,65 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Dialer intent filters (required for default dialer eligibility) -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="tel" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" />
</intent-filter>
</activity>
<service
android:name=".services.MyInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
</service>
<!-- Custom ConnextionService, will be needed at some point when we implement our own protocol -->
<!-- <service
android:name=".services.CallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service> -->
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

View File

@ -1,54 +1,91 @@
package com.icing.dialer.activities
import android.Manifest
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.CallLog
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import android.telecom.TelecomManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallService
import com.icing.dialer.services.MyInCallService
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
// Existing channel for keystore operations.
class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore"
// New channel for call log access.
private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service"
private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started")
Log.d(TAG, "Waiting for Flutter to signal permissions")
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(TAG, "Configuring Flutter engine")
// Call service channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) {
CallService.makeGsmCall(this, phoneNumber)
result.success("Calling $phoneNumber")
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
}
"hangUpCall" -> {
CallService.hangUpCall(this)
result.success("Call ended")
}
else -> result.notImplemented()
}
}
// Set up the keystore channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result ->
// Delegate method calls to KeystoreHelper.
KeystoreHelper(call, result).handleMethodCall()
when (call.method) {
"permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
checkAndRequestDefaultDialer()
result.success(true)
}
"makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) {
val success = CallService.makeGsmCall(this, phoneNumber)
if (success) {
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber))
} else {
result.error("CALL_FAILED", "Failed to initiate call", null)
}
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
}
"hangUpCall" -> {
val success = CallService.hangUpCall(this)
if (success) {
result.success(mapOf("status" to "ended"))
} else {
result.error("HANGUP_FAILED", "Failed to end call", null)
}
}
"answerCall" -> {
val success = MyInCallService.currentCall?.let {
it.answer(0) // 0 for default video state (audio-only)
Log.d(TAG, "Answered call")
true
} ?: false
if (success) {
result.success(mapOf("status" to "answered"))
} else {
result.error("ANSWER_FAILED", "No active call to answer", null)
}
}
else -> result.notImplemented()
}
}
// Set up the call log channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
.setMethodCallHandler { call, result -> KeystoreHelper(call, result).handleMethodCall() }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") {
@ -60,35 +97,78 @@ class MainActivity: FlutterActivity() {
}
}
/**
* Queries the Android call log and returns a list of maps.
* Each map contains keys: "number", "type", "date", and "duration".
*/
private fun checkAndRequestDefaultDialer() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
if (currentDefault != packageName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) && !roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
} else {
Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
}
} else {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched TelecomManager intent for default dialer")
} catch (e: Exception) {
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
launchDefaultAppsSettings()
}
}
} else {
Log.d(TAG, "Already the default dialer")
}
}
private fun launchDefaultAppsSettings() {
val settingsIntent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
startActivity(settingsIntent)
Log.d(TAG, "Opened default apps settings as fallback")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$data")
if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "User accepted default dialer change")
} else {
Log.w(TAG, "Default dialer prompt canceled or failed (resultCode=$resultCode)")
launchDefaultAppsSettings()
}
}
}
private fun getCallLogs(): List<Map<String, Any?>> {
val logsList = mutableListOf<Map<String, Any?>>()
val cursor: Cursor? = contentResolver.query(
CallLog.Calls.CONTENT_URI,
null,
null,
null,
CallLog.Calls.DATE + " DESC"
CallLog.Calls.CONTENT_URI, null, null, null, CallLog.Calls.DATE + " DESC"
)
if (cursor != null) {
while (cursor.moveToNext()) {
val number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
val type = cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION))
cursor?.use {
while (it.moveToNext()) {
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
val map = HashMap<String, Any?>()
map["number"] = number
map["type"] = type // Typically: 1 for incoming, 2 for outgoing, 3 for missed.
map["date"] = date
map["duration"] = duration
val map = mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,
"duration" to duration
)
logsList.add(map)
}
cursor.close()
}
return logsList
}
}
}

View File

@ -0,0 +1,82 @@
// package com.icing.dialer.services
// import android.telecom.Connection
// import android.telecom.ConnectionService
// import android.telecom.PhoneAccountHandle
// import android.telecom.TelecomManager
// import android.telecom.DisconnectCause
// import android.util.Log
// import io.flutter.plugin.common.MethodChannel
// class CallConnectionService : ConnectionService() {
// companion object {
// var channel: MethodChannel? = null
// private const val TAG = "CallConnectionService"
// }
// init {
// Log.d(TAG, "CallConnectionService initialized")
// }
// override fun onCreate() {
// super.onCreate()
// Log.d(TAG, "Service created")
// }
// override fun onDestroy() {
// super.onDestroy()
// Log.d(TAG, "Service destroyed")
// }
// override fun onCreateOutgoingConnection(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onStateChanged(state: Int) {
// super.onStateChanged(state)
// Log.d(TAG, "Connection state changed: $state")
// val stateStr = when (state) {
// STATE_DIALING -> "dialing"
// STATE_ACTIVE -> "active"
// STATE_DISCONNECTED -> "disconnected"
// else -> "unknown"
// }
// channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
// }
// override fun onDisconnect() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setInitialized()
// connection.setDialing()
// return connection
// }
// override fun onCreateIncomingConnection(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onAnswer() {
// Log.d(TAG, "Connection answered")
// setActive()
// }
// override fun onDisconnect() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setRinging()
// return connection
// }
// }

View File

@ -1,30 +1,55 @@
package com.icing.dialer.services
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.telecom.TelecomManager
import android.os.Build
import android.os.Bundle
import android.telecom.TelecomManager
import android.util.Log
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
object CallService {
fun makeGsmCall(context: Context, phoneNumber: String) {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:$phoneNumber")
context.startActivity(intent)
private val TAG = "CallService"
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
return try {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val uri = Uri.parse("tel:$phoneNumber")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
telecomManager.placeCall(uri, Bundle())
Log.d(TAG, "Initiated call to $phoneNumber")
true
} else {
Log.e(TAG, "CALL_PHONE permission not granted")
false
}
} catch (e: Exception) {
Log.e("CallService", "Error making GSM call: ${e.message}")
Log.e(TAG, "Error making GSM call: ${e.message}", e)
false
}
}
fun hangUpCall(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
} else {
Log.e("CallService", "Hangup call is only supported on Android P or later.")
fun hangUpCall(context: Context): Boolean {
return try {
if (MyInCallService.currentCall != null) {
MyInCallService.currentCall?.disconnect()
Log.d(TAG, "Disconnected active call via MyInCallService")
true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
Log.d(TAG, "Ended call via TelecomManager (no active call in MyInCallService)")
true
} else {
Log.e(TAG, "No active call and hangup not supported below Android P")
false
}
} catch (e: Exception) {
Log.e(TAG, "Error hanging up call: ${e.message}", e)
false
}
}
}

View File

@ -0,0 +1,69 @@
package com.icing.dialer.services
import android.telecom.Call
import android.telecom.InCallService
import android.util.Log
import io.flutter.plugin.common.MethodChannel
class MyInCallService : InCallService() {
companion object {
var channel: MethodChannel? = null
var currentCall: Call? = null
private const val TAG = "MyInCallService"
}
private val callCallback = object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
super.onStateChanged(call, state)
val stateStr = when (state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
Call.STATE_DISCONNECTED -> "disconnected"
Call.STATE_DISCONNECTING -> "disconnecting"
Call.STATE_RINGING -> "ringing"
else -> "unknown"
}
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
channel?.invokeMethod("callStateChanged", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
Log.d(TAG, "Call ended: ${call.details.handle}")
channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString()))
currentCall = null
}
}
}
override fun onCallAdded(call: Call) {
super.onCallAdded(call)
currentCall = call
val stateStr = when (call.state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
Call.STATE_RINGING -> "ringing"
else -> "dialing" // Default for outgoing
}
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
channel?.invokeMethod("callAdded", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
call.registerCallback(callCallback)
}
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
Log.d(TAG, "Call removed: ${call.details.handle}")
call.unregisterCallback(callCallback)
channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString()))
currentCall = null
}
override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) {
super.onCallAudioStateChanged(state)
Log.d(TAG, "Audio state changed: route=${state.route}")
channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route))
}
}

View File

@ -2,4 +2,9 @@
IMG=git.gmoker.com/icing/flutter:main
docker run --rm -v "$PWD:/app/" "$IMG" build apk
if [ "$1" == '-s' ]; then
OPT+=(--dart-define=STEALTH=true)
fi
set -x
docker run --rm -v "$PWD:/app/" "$IMG" build apk "${OPT[@]}"

View File

@ -0,0 +1,324 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:dialer/services/call_service.dart';
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.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 _toggleIcingProtocol() {
setState(() {
icingProtocolOk = !icingProtocolOk;
});
}
void _hangUp() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
print("Error hanging up: $e");
}
}
@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;
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: [
SizedBox(height: 35),
ObfuscatedAvatar(
imageBytes: widget.thumbnail, // Uses thumbnail if provided
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(
'Calling...',
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.35,
margin: const EdgeInsets.symmetric(horizontal: 20),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.3,
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: 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(
isSpeakerOn ? Icons.volume_up : Icons.volume_off,
color: isSpeakerOn ? 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: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
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

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

View File

@ -76,7 +76,7 @@ class _CompositionPageState extends State<CompositionPage> {
// Function to call a contact's number using the CallService
void _makeCall(String phoneNumber) async {
try {
await _callService.makeGsmCall(phoneNumber);
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
setState(() {
dialedNumber = phoneNumber;
});

View File

@ -5,7 +5,7 @@ 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 CallService
import '../../../services/call_service.dart';
class ContactModal extends StatefulWidget {
final Contact contact;
@ -29,7 +29,7 @@ class _ContactModalState extends State<ContactModal> {
late String phoneNumber;
bool isBlocked = false;
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService(); // Instantiate CallService
final CallService _callService = CallService();
@override
void initState() {
@ -126,7 +126,9 @@ class _ContactModalState extends State<ContactModal> {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
SnackBar(
content: Text(
'${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
);
// Close the modal
@ -134,7 +136,9 @@ class _ContactModalState extends State<ContactModal> {
} catch (e) {
// Handle errors and show a failure message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')),
SnackBar(
content:
Text('Failed to delete ${widget.contact.displayName}: $e')),
);
}
}
@ -162,7 +166,7 @@ class _ContactModalState extends State<ContactModal> {
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius:
const BorderRadius.vertical(top: Radius.circular(20)),
const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
@ -216,7 +220,7 @@ class _ContactModalState extends State<ContactModal> {
const PopupMenuItem<String>(
value: 'create_shortcut',
child:
Text('Create shortcut (to home screen)'),
Text('Create shortcut (to home screen)'),
),
const PopupMenuItem<String>(
value: 'set_ringtone',
@ -238,12 +242,13 @@ class _ContactModalState extends State<ContactModal> {
imageBytes: widget.contact.thumbnail,
radius: 50,
backgroundColor:
generateColorFromName(widget.contact.displayName),
generateColorFromName(widget.contact.displayName),
fallbackInitial: widget.contact.displayName,
),
const SizedBox(height: 10),
Text(
_obfuscateService.obfuscateData(widget.contact.displayName),
_obfuscateService
.obfuscateData(widget.contact.displayName),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -262,7 +267,8 @@ class _ContactModalState extends State<ContactModal> {
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(phoneNumber);
await _callService.makeGsmCall(context,
phoneNumber: phoneNumber);
}
},
),
@ -307,9 +313,8 @@ class _ContactModalState extends State<ContactModal> {
icon: Icon(widget.isFavorite
? Icons.star
: Icons.star_border),
label: Text(widget.isFavorite
? 'Unfavorite'
: 'Favorite'),
label: Text(
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
),
),
const SizedBox(height: 10),

View File

@ -11,6 +11,7 @@ import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import '../../services/block_service.dart';
import '../contacts/widgets/contact_modal.dart';
import '../../services/call_service.dart';
class History {
final Contact contact;
@ -20,12 +21,12 @@ class History {
final int attempts;
History(
this.contact,
this.date,
this.callType,
this.callStatus,
this.attempts,
);
this.contact,
this.date,
this.callType,
this.callStatus,
this.attempts,
);
}
class HistoryPage extends StatefulWidget {
@ -41,6 +42,7 @@ class _HistoryPageState extends State<HistoryPage>
bool loading = true;
int? _expandedIndex;
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
// Create a MethodChannel instance.
static const MethodChannel _channel = MethodChannel('com.example.calllog');
@ -83,8 +85,8 @@ class _HistoryPageState extends State<HistoryPage>
}
} catch (e) {
print("Error updating favorite status: $e");
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to update favorite status')));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status')));
}
}
@ -155,7 +157,7 @@ class _HistoryPageState extends State<HistoryPage>
// Convert timestamp to DateTime.
DateTime callDate =
DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0);
DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0);
int typeInt = entry['type'] ?? 0;
int duration = entry['duration'] ?? 0;
@ -193,7 +195,8 @@ class _HistoryPageState extends State<HistoryPage>
);
}
callHistories.add(History(matchedContact, callDate, callType, callStatus, 1));
callHistories
.add(History(matchedContact, callDate, callType, callStatus, 1));
}
// Sort histories by most recent.
@ -218,7 +221,7 @@ class _HistoryPageState extends State<HistoryPage>
for (var history in historyList) {
final callDate =
DateTime(history.date.year, history.date.month, history.date.day);
DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) {
todayHistories.add(history);
} else if (callDate == yesterday) {
@ -291,7 +294,7 @@ class _HistoryPageState extends State<HistoryPage>
}
List<History> missedCalls =
histories.where((h) => h.callStatus == 'missed').toList();
histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls);
@ -360,7 +363,8 @@ class _HistoryPageState extends State<HistoryPage>
onEdit: () async {
if (await FlutterContacts.requestPermission()) {
final updatedContact =
await FlutterContacts.openExternalEdit(contact.id);
await FlutterContacts.openExternalEdit(
contact.id);
if (updatedContact != null) {
await _refreshContacts();
Navigator.of(context).pop();
@ -415,18 +419,11 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async {
if (contact.phones.isNotEmpty) {
final Uri callUri =
Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) {
await launchUrl(callUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not launch call')),
);
}
_callService.makeGsmCall(context, phoneNumber: contact.phones.first.number);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
const SnackBar(
content: Text('Contact has no phone number')),
);
}
},
@ -444,7 +441,9 @@ class _HistoryPageState extends State<HistoryPage>
color: Colors.grey[850],
child: FutureBuilder<bool>(
future: BlockService().isNumberBlocked(
contact.phones.isNotEmpty ? contact.phones.first.number : ''),
contact.phones.isNotEmpty
? contact.phones.first.number
: ''),
builder: (context, snapshot) {
final isBlocked = snapshot.data ?? false;
return Row(
@ -460,29 +459,37 @@ class _HistoryPageState extends State<HistoryPage>
await launchUrl(smsUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send message')),
const SnackBar(
content:
Text('Could not send message')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
const SnackBar(
content:
Text('Contact has no phone number')),
);
}
},
icon: const Icon(Icons.message, color: Colors.white),
label: const Text('Message', style: TextStyle(color: Colors.white)),
icon:
const Icon(Icons.message, color: Colors.white),
label: const Text('Message',
style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CallDetailsPage(history: history),
builder: (_) =>
CallDetailsPage(history: history),
),
);
},
icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', style: TextStyle(color: Colors.white)),
label: const Text('Details',
style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () async {
@ -491,24 +498,29 @@ class _HistoryPageState extends State<HistoryPage>
: null;
if (phoneNumber == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
const SnackBar(
content:
Text('Contact has no phone number')),
);
return;
}
if (isBlocked) {
await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber unblocked')),
SnackBar(
content: Text('$phoneNumber unblocked')),
);
} else {
await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber blocked')),
SnackBar(
content: Text('$phoneNumber blocked')),
);
}
setState(() {});
},
icon: Icon(isBlocked ? Icons.lock_open : Icons.block,
icon: Icon(
isBlocked ? Icons.lock_open : Icons.block,
color: Colors.white),
label: Text(isBlocked ? 'Unblock' : 'Block',
style: const TextStyle(color: Colors.white)),
@ -554,21 +566,22 @@ class CallDetailsPage extends StatelessWidget {
children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? ObfuscatedAvatar(
imageBytes: contact.thumbnail,
radius: 30,
backgroundColor: contactBg,
fallbackInitial: contact.displayName,
)
imageBytes: contact.thumbnail,
radius: 30,
backgroundColor: contactBg,
fallbackInitial: contact.displayName,
)
: CircleAvatar(
backgroundColor: generateColorFromName(contact.displayName),
radius: 30,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: contactLetter),
),
),
backgroundColor:
generateColorFromName(contact.displayName),
radius: 30,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: contactLetter),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
@ -600,7 +613,8 @@ class CallDetailsPage extends StatelessWidget {
if (contact.phones.isNotEmpty)
DetailRow(
label: 'Number:',
value: _obfuscateService.obfuscateData(contact.phones.first.number),
value: _obfuscateService
.obfuscateData(contact.phones.first.number),
),
],
),

View File

@ -8,6 +8,7 @@ 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>
@ -17,6 +18,8 @@ class _MyHomePageState extends State<MyHomePage>
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
@ -32,12 +35,15 @@ class _MyHomePageState extends State<MyHomePage>
setState(() {});
}
void _onSearchChanged(String query) {
print("Search query: $query");
void _clearSearch() {
_searchController.clear();
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
_contactSuggestions = List.from(_allContacts); // Reset suggestions
} else {
_contactSuggestions = _allContacts.where((contact) {
return contact.displayName
@ -50,6 +56,7 @@ class _MyHomePageState extends State<MyHomePage>
@override
void dispose() {
_searchController.dispose();
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
@ -59,6 +66,34 @@ class _MyHomePageState extends State<MyHomePage>
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);
setState(() {
// Updating the contact list after toggling the favorite
_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(
@ -80,63 +115,109 @@ class _MyHomePageState extends State<MyHomePage>
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border(
top: BorderSide(color: Colors.grey.shade800, width: 1),
left: BorderSide(color: Colors.grey.shade800, width: 1),
right: BorderSide(color: Colors.grey.shade800, width: 1),
bottom:
BorderSide(color: Colors.grey.shade800, width: 2),
),
border: Border.all(color: Colors.grey.shade800, width: 1),
),
child: SearchAnchor(
builder:
(BuildContext context, SearchController controller) {
return SearchBar(
controller: controller,
padding:
WidgetStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
left: 16.0,
right: 16.0,
),
),
return GestureDetector(
onTap: () {
controller.openView();
_onSearchChanged('');
controller.openView(); // Open the search view
},
backgroundColor: WidgetStateProperty.all(
const Color.fromARGB(255, 30, 30, 30)),
hintText: 'Search contacts',
hintStyle: WidgetStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0),
),
leading: const Icon(
Icons.search,
color: Colors.grey,
size: 24.0,
),
shape:
WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
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(
_searchController.text.isEmpty
? 'Search contacts'
: _searchController.text,
style: const TextStyle(
color: Colors.grey, fontSize: 16.0),
),
const Spacer(),
if (_searchController.text.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
Icons.clear,
color: Colors.grey,
size: 24.0,
),
),
],
),
),
);
},
viewOnChanged: (query) {
_onSearchChanged(query);
_onSearchChanged(query); // Update immediately
},
suggestionsBuilder:
(BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
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();

View File

@ -1,8 +1,11 @@
import 'package:dialer/features/home/home_page.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';
void main() async {
@ -13,19 +16,38 @@ void main() async {
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
await cryptoService.initializeDefaultKeyPair();
// Request permissions before running the app
await _requestPermissions();
CallService();
runApp(
MultiProvider(
providers: [
Provider<AsymmetricCryptoService>(
create: (_) => cryptoService,
),
// Add other providers here
],
child: Dialer(),
),
);
}
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(', ')}");
}
}
class Dialer extends StatelessWidget {
const Dialer({super.key});
@ -33,11 +55,12 @@ class Dialer extends StatelessWidget {
Widget build(BuildContext context) {
return ContactState(
child: MaterialApp(
navigatorKey: CallService.navigatorKey,
theme: ThemeData(
brightness: Brightness.dark
brightness: Brightness.dark,
),
home: SafeArea(child: MyHomePage()),
)
),
);
}
}
}

View File

@ -1,26 +1,154 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../features/call/call_page.dart';
import '../features/call/incoming_call_page.dart'; // Import the new page
// Service to manage call-related operations
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber;
static bool _isCallPageVisible = false;
// Function to make a GSM call
Future<void> makeGsmCall(String phoneNumber) async {
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
CallService() {
_channel.setMethodCallHandler((call) async {
final context = navigatorKey.currentContext;
print('CallService: Received method ${call.method} with args ${call.arguments}');
if (context == null) {
print('CallService: Navigator context is null, cannot navigate');
return;
}
switch (call.method) {
case "callAdded":
final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
break;
case "callStateChanged":
final state = call.arguments["state"] as String;
print('CallService: State changed to $state');
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
break;
case "callEnded":
case "callRemoved":
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
break;
}
});
}
void _navigateToCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
print('CallService: CallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to CallPage');
Navigator.pushReplacement(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _navigateToIncomingCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
print('CallService: IncomingCallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to IncomingCallPage');
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _closeCallPage(BuildContext context) {
if (!_isCallPageVisible) {
print('CallService: CallPage not visible, skipping pop');
return;
}
if (Navigator.canPop(context)) {
print('CallService: Popping CallPage');
Navigator.pop(context);
_isCallPageVisible = false;
}
}
Future<void> makeGsmCall(
BuildContext context, {
required String phoneNumber,
String? displayName,
Uint8List? thumbnail,
}) async {
try {
await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
currentPhoneNumber = phoneNumber;
print('CallService: Making GSM call to $phoneNumber');
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
print('CallService: makeGsmCall result: $result');
if (result["status"] != "calling") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")),
);
}
} catch (e) {
print("Error making call: $e");
print("CallService: Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error making call: $e")),
);
rethrow;
}
}
// Function to hang up the current call
Future<void> hangUpCall() async {
Future<void> hangUpCall(BuildContext context) async {
try {
await _channel.invokeMethod('hangUpCall');
print('CallService: Hanging up call');
final result = await _channel.invokeMethod('hangUpCall');
print('CallService: hangUpCall result: $result');
if (result["status"] != "ended") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")),
);
}
} catch (e) {
print("Error hanging up call: $e");
print("CallService: Error hanging up call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error hanging up call: $e")),
);
rethrow;
}
}
}
}

View File

@ -2,4 +2,9 @@
IMG=git.gmoker.com/icing/flutter:main
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run
if [ "$1" == '-s' ]; then
OPT+=(--dart-define=STEALTH=true)
fi
set -x
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run "${OPTS[@]}"

View File

@ -1,3 +1,4 @@
#!/usr/bin/env bash
echo "Running Icing Dialer in STEALTH mode..."
flutter run --dart-define=STEALTH=true
flutter run --dart-define=STEALTH=true

View File

@ -0,0 +1,4 @@
FROM python:3.9-slim
WORKDIR /app
COPY gsm_simulator.py /app
CMD ["python", "gsm_simulator.py"]

View File

@ -0,0 +1,25 @@
import socket
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

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

@ -0,0 +1,94 @@
import socket
import threading
import random
import time
class GSMSimulator:
def __init__(self, host='0.0.0.0', port=5555):
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.bind((host, port))
self.server_socket.listen(2)
self.clients = {}
print("GSM Simulator started. Waiting for connections...")
def simulate_signal_strength(self):
return random.uniform(0, 1)
def handle_client(self, client_socket, address):
try:
client_socket.settimeout(5)
role = client_socket.recv(1024).decode().strip()
if role not in ["CALLER", "RECEIVER"]:
print(f"Invalid role received from {address}: {role}")
client_socket.close()
return
self.clients[role] = client_socket
print(f"{role} connected from {address}")
if role == "CALLER":
self.handle_caller(client_socket)
elif role == "RECEIVER":
self.handle_receiver(client_socket)
except Exception as e:
print(f"Error handling client {address}: {e}")
finally:
if role in self.clients:
del self.clients[role]
client_socket.close()
def handle_caller(self, caller_socket):
print("Caller connected, waiting for receiver...")
while "RECEIVER" not in self.clients:
time.sleep(1)
receiver_socket = self.clients["RECEIVER"]
print("Receiver found, sending RINGING...")
receiver_socket.send("RINGING".encode())
while True:
try:
data = caller_socket.recv(1024).decode().strip()
if not data:
print("Caller disconnected unexpectedly.")
receiver_socket.send("CALL_END".encode())
break
if data == "CALL_END":
print("Call terminated by caller.")
receiver_socket.send("CALL_END".encode())
break
signal = self.simulate_signal_strength()
if signal < 0.2:
print("Call dropped due to low signal strength.")
caller_socket.send("CALL_DROPPED".encode())
receiver_socket.send("CALL_DROPPED".encode())
break
print(f"Relaying to receiver: {data} (Signal: {signal:.2f})")
receiver_socket.send(data.encode())
time.sleep(0.1) # Small delay to ensure data is sent properly
except Exception as e:
print(f"Error in caller loop: {e}")
receiver_socket.send("CALL_END".encode())
break
def handle_receiver(self, receiver_socket):
print("Receiver connected, waiting for data...")
try:
while True:
data = receiver_socket.recv(1024)
if not data:
print("Receiver disconnected.")
break
except Exception as e:
print(f"Error in receiver loop: {e}")
def run(self):
while True:
client_socket, address = self.server_socket.accept()
print(f"New connection from {address}")
thread = threading.Thread(target=self.handle_client, args=(client_socket, address))
thread.start()
if __name__ == "__main__":
gsm = GSMSimulator()
gsm.run()

View File

@ -0,0 +1,66 @@
#!/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

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

191
protocol_prototype/cli.py Normal file
View File

@ -0,0 +1,191 @@
import sys
from protocol import IcingProtocol
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
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("\nAvailable commands:")
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(" 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(" auto_responder <on|off> - Enable/disable automatic responses")
print(" show_state - Display current protocol state")
print(" exit - Exit the program\n")
while True:
try:
line = input("Cmd> ").strip()
except EOFError:
break
if not line:
continue
parts = line.split()
cmd = parts[0].lower()
try:
if cmd == "exit":
protocol.stop()
break
elif cmd == "help":
print("\nAvailable commands:")
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(" 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(" auto_responder <on|off> - Enable/disable automatic responses")
print(" show_state - Display current protocol state")
print(" exit - Exit the program")
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}")
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()
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}")
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")
else:
print(f"{RED}[ERROR]{RESET} Unknown command: {cmd}")
print("Type 'help' for a list of available commands.")
except Exception as e:
print(f"{RED}[ERROR]{RESET} Command failed: {e}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nExiting...")
except Exception as e:
print(f"{RED}[FATAL ERROR]{RESET} {e}")
sys.exit(1)

View File

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

View File

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

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

@ -0,0 +1,625 @@
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
)
# 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,
}
# Auto-responder toggle
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)
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)
# Auto-respond if enabled
if self.auto_responder:
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)
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)
# Auto-respond if enabled
if self.auto_responder:
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}.")
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()
print(f"{GREEN}[HKDF]{RESET} Derived HKDF key: {self.hkdf_key}")
# -------------------------------------------------------------------------
# Auto-responder helpers
# -------------------------------------------------------------------------
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
# -------------------------------------------------------------------------
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.")
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):
if not self.connections:
print(f"{RED}[ERROR]{RESET} No active connections.")
return
# Create ping request with our default cipher preference (AES-256-GCM = 0)
ping_request = PingRequest(version=0, cipher=0)
# 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
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
if not self.ephemeral_privkey or not self.ephemeral_pubkey:
print(f"{RED}[ERROR]{RESET} Ephemeral keys not generated.")
return
if self.peer_identity_pubkey_bytes is None:
print(f"{RED}[ERROR]{RESET} Peer identity not set; needed for PFS tracking.")
return
# 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
def enable_auto_responder(self, enable: bool):
self.auto_responder = enable
print(f"{BLUE}[AUTO]{RESET} Auto responder set to {enable}.")
# -------------------------------------------------------------------------
# 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
msg = self.inbound_messages[index]
if msg["type"] != "PING_REQUEST":
print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a PING_REQUEST.")
return
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}.")
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
msg = self.inbound_messages[index]
if msg["type"] != "HANDSHAKE":
print(f"{RED}[ERROR]{RESET} inbound_messages[{index}] is not a HANDSHAKE.")
return
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
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
# 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)
# -------------------------------------------------------------------------
# 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 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):
self.server_listener.stop()
for c in self.connections:
c.close()
self.connections.clear()
self.inbound_messages.clear()
print(f"{RED}[STOP]{RESET} Protocol stopped.")
# New method: Send an encrypted message over the first active connection.
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
if not self.hkdf_key:
print(f"{RED}[ERROR]{RESET} No HKDF key derived. Cannot encrypt message.")
return
# 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.")
# New method: Decrypt an encrypted message from the inbound queue.
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
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
# 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
# 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

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