Skip to content

Commit aa082e4

Browse files
committed
Add pairing related events
1 parent e72d736 commit aa082e4

34 files changed

Lines changed: 168 additions & 73 deletions

app/src/main/java/com/ismartcoding/plain/chat/PeerGraphQLClient.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ object PeerGraphQLClient {
171171
val responseBody = response.body.string()
172172

173173
if (response.isSuccessful) {
174-
LogCat.d("GraphQL response: $responseBody")
175174
parseGraphQLResponse(responseBody)
176175
} else {
177176
LogCat.e("GraphQL request failed: ${response.code} - $responseBody")

app/src/main/java/com/ismartcoding/plain/data/DNearbyPair.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ data class DPairingRequest(
1515
val signaturePublicKey: String, // Raw Ed25519 signature public key (32 bytes, Base64 encoded)
1616
val timestamp: Long, // Timestamp for replay attack prevention
1717
val ips: List<String> = emptyList(), // All IP addresses of the requesting device
18-
var signature: String = "" // Ed25519 signature of request content (Base64 encoded)
18+
var signature: String = "", // Ed25519 signature of request content (Base64 encoded)
19+
var fromIp: String = ""
1920
) {
2021
fun toSignatureData(): String {
2122
return "$fromId|$fromName|$port|${deviceType.value}|$ecdhPublicKey|$signaturePublicKey|$timestamp|${ips.joinToString(",")}"
2223
}
2324
}
2425

26+
@Serializable
27+
data class DPairingResult(
28+
val deviceId: String,
29+
val error: String = "",
30+
)
31+
2532
@Serializable
2633
data class DPairingResponse(
2734
val fromId: String,
@@ -33,7 +40,7 @@ data class DPairingResponse(
3340
val accepted: Boolean,
3441
val timestamp: Long, // Timestamp for replay attack prevention
3542
val ips: List<String> = emptyList(), // All IP addresses of the responding device
36-
var signature: String = ""// Ed25519 signature of response content (Base64 encoded)
43+
var signature: String = ""// Ed25519 signature of response content (Base64 encoded)
3744
) {
3845
fun toSignatureData(): String {
3946
return "$fromId|$toId|$port|${deviceType.value}|$ecdhPublicKey|$signaturePublicKey|$accepted|$timestamp|${ips.joinToString(",")}"

app/src/main/java/com/ismartcoding/plain/discover/NearbyDiscoverManager.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import com.ismartcoding.plain.data.DPairingRequest
1818
import com.ismartcoding.plain.data.DPairingResponse
1919
import com.ismartcoding.plain.db.AppDatabase
2020
import com.ismartcoding.plain.enums.NearbyMessageType
21+
import com.ismartcoding.plain.events.EventType
2122
import com.ismartcoding.plain.events.NearbyDeviceFoundEvent
2223
import com.ismartcoding.plain.events.PairingRequestReceivedEvent
24+
import com.ismartcoding.plain.events.WebSocketEvent
2325
import com.ismartcoding.plain.helpers.PhoneHelper
2426
import com.ismartcoding.plain.helpers.TimeHelper
2527
import com.ismartcoding.plain.preferences.NearbyDiscoverablePreference
28+
import com.ismartcoding.plain.web.websocket.WebSocketHelper
2629
import kotlinx.coroutines.Job
2730
import kotlinx.coroutines.delay
2831
import kotlinx.coroutines.isActive
@@ -85,7 +88,9 @@ object NearbyDiscoverManager {
8588
NearbyMessageType.DISCOVER_REPLY -> handleDiscoverReply(payload)
8689
NearbyMessageType.PAIR_REQUEST -> {
8790
val request = JsonHelper.jsonDecode<DPairingRequest>(payload)
88-
sendEvent(PairingRequestReceivedEvent(request, senderIP))
91+
request.fromIp = senderIP
92+
sendEvent(PairingRequestReceivedEvent(request))
93+
sendEvent(WebSocketEvent(EventType.PAIRING_REQUEST_RECEIVED, JsonHelper.jsonEncode(request)))
8994
}
9095

9196
NearbyMessageType.PAIR_RESPONSE -> {

app/src/main/java/com/ismartcoding/plain/discover/NearbyPairManager.kt

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.ismartcoding.plain.db.AppDatabase
1616
import com.ismartcoding.plain.db.DPeer
1717
import com.ismartcoding.plain.enums.DeviceType
1818
import com.ismartcoding.plain.enums.NearbyMessageType
19-
import com.ismartcoding.plain.events.PairingCancelledEvent
19+
import com.ismartcoding.plain.events.PairingCanceledEvent
2020
import com.ismartcoding.plain.events.PairingFailedEvent
2121
import com.ismartcoding.plain.events.PairingSuccessEvent
2222
import com.ismartcoding.plain.helpers.PhoneHelper
@@ -25,11 +25,14 @@ import com.ismartcoding.plain.helpers.TimeHelper
2525
import com.ismartcoding.plain.chat.ChatCacheManager
2626
import java.util.concurrent.ConcurrentHashMap
2727
import android.util.Base64
28+
import com.ismartcoding.plain.data.DPairingResult
29+
import com.ismartcoding.plain.events.EventType
30+
import com.ismartcoding.plain.events.WebSocketEvent
2831
import kotlin.math.abs
2932

3033
object NearbyPairManager {
3134
private val activePairingSessions = ConcurrentHashMap<String, DPairingSession>()
32-
35+
3336
// Maximum allowed time difference for timestamp validation (5 minutes)
3437
private const val MAX_TIMESTAMP_DIFF_MS = 5 * 60 * 1000L
3538

@@ -64,7 +67,7 @@ object NearbyPairManager {
6467

6568
// Generate ECDH key pair for this pairing session
6669
val keyPair = CryptoHelper.generateECDHKeyPair()
67-
70+
6871
// Get our raw Ed25519 signature public key (32 bytes)
6972
val signaturePublicKey = SignatureHelper.getRawPublicKeyBase64Async()
7073
// Create pairing session
@@ -80,7 +83,7 @@ object NearbyPairManager {
8083
val currentTimestamp = System.currentTimeMillis()
8184
val ecdhPublicKeyBytes = CryptoHelper.getPublicKeyBytes(keyPair)
8285
val ecdhPublicKey = Base64.encodeToString(ecdhPublicKeyBytes, Base64.NO_WRAP)
83-
86+
8487
val request = DPairingRequest(
8588
fromId = TempData.clientId,
8689
fromName = deviceName,
@@ -96,48 +99,54 @@ object NearbyPairManager {
9699
sendPairingMessage(NearbyMessageType.PAIR_REQUEST, JsonHelper.jsonEncode(request), bestIp)
97100
} catch (e: Exception) {
98101
LogCat.e("Error starting pairing: ${e.message}")
99-
sendEvent(PairingFailedEvent(device.id, "Failed to send pairing request"))
102+
val event = PairingFailedEvent(device.id, "Failed to send pairing request")
103+
sendEvent(event)
104+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
100105
}
101106
}
102107

103-
suspend fun respondToPairing(request: DPairingRequest, fromIp: String, accepted: Boolean) {
108+
suspend fun respondToPairing(request: DPairingRequest, accepted: Boolean) {
104109
try {
105110
// Verify timestamp to prevent replay attacks
106111
val currentTime = System.currentTimeMillis()
107112
if (abs(currentTime - request.timestamp) > MAX_TIMESTAMP_DIFF_MS) {
108113
LogCat.e("Pairing request timestamp is too old or in the future")
109-
sendEvent(PairingFailedEvent(request.fromId, "Invalid timestamp"))
114+
val event = PairingFailedEvent(request.fromId, "Invalid timestamp")
115+
sendEvent(event)
116+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
110117
return
111118
}
112-
119+
113120
// Verify signature
114121
if (!verifyPairingRequestSignature(request)) {
115122
LogCat.e("Pairing request signature verification failed")
116-
sendEvent(PairingFailedEvent(request.fromId, "Signature verification failed"))
123+
val event = PairingFailedEvent(request.fromId, "Signature verification failed")
124+
sendEvent(event)
125+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
117126
return
118127
}
119-
128+
120129
LogCat.d("Pairing request signature verified successfully")
121-
130+
122131
if (accepted) {
123132
// Generate ECDH key pair for this pairing session
124133
val keyPair = CryptoHelper.generateECDHKeyPair()
125-
134+
126135
// Get our raw Ed25519 signature public key (32 bytes)
127136
val signaturePublicKey = SignatureHelper.getRawPublicKeyBase64Async()
128137
// Create pairing session
129138
val session = DPairingSession(
130139
deviceId = request.fromId,
131140
deviceName = request.fromName,
132-
deviceIp = fromIp,
141+
deviceIp = request.fromIp,
133142
keyPair = keyPair,
134143
)
135144
activePairingSessions[request.fromId] = session
136-
145+
137146
val responseTimestamp = System.currentTimeMillis()
138147
val ecdhPublicKeyBytes = CryptoHelper.getPublicKeyBytes(keyPair)
139148
val ecdhPublicKey = Base64.encodeToString(ecdhPublicKeyBytes, Base64.NO_WRAP)
140-
149+
141150
val response = DPairingResponse(
142151
fromId = TempData.clientId,
143152
toId = request.fromId,
@@ -156,26 +165,27 @@ object NearbyPairManager {
156165
val encryptKey = CryptoHelper.computeECDHSharedKey(keyPair.private, requestEcdhPublicKey)
157166
if (encryptKey != null) {
158167
// Store peer in database with signature public key
159-
val peerIps = (listOf(fromIp) + request.ips).distinct()
168+
val peerIps = (listOf(request.fromIp) + request.ips).distinct()
160169
storePeerInDatabase(
161-
request.fromId,
162-
request.fromName,
163-
peerIps,
164-
request.port,
165-
request.deviceType,
170+
request.fromId,
171+
request.fromName,
172+
peerIps,
173+
request.port,
174+
request.deviceType,
166175
encryptKey,
167176
request.signaturePublicKey
168177
)
169-
sendPairingMessage(NearbyMessageType.PAIR_RESPONSE, JsonHelper.jsonEncode(response), fromIp)
170-
sendEvent(PairingSuccessEvent(request.fromId, request.fromName, fromIp, encryptKey))
178+
sendPairingMessage(NearbyMessageType.PAIR_RESPONSE, JsonHelper.jsonEncode(response), request.fromIp)
179+
sendEvent(PairingSuccessEvent(request.fromId, request.fromName, request.fromIp, encryptKey))
180+
sendEvent(WebSocketEvent(EventType.PAIRING_SUCCESS, JsonHelper.jsonEncode(DPairingResult(response.fromId))))
171181
} else {
172182
throw Exception("Failed to compute shared key")
173183
}
174184
} else {
175185
// Send rejection response with signature for security
176186
val signaturePublicKey = SignatureHelper.getRawPublicKeyBase64Async()
177187
val rejectionTimestamp = System.currentTimeMillis()
178-
188+
179189
val response = DPairingResponse(
180190
fromId = TempData.clientId,
181191
toId = request.fromId,
@@ -186,15 +196,17 @@ object NearbyPairManager {
186196
accepted = false,
187197
timestamp = rejectionTimestamp,
188198
)
189-
199+
190200
response.signature = SignatureHelper.signTextAsync(response.toSignatureData())
191201

192-
sendPairingMessage(NearbyMessageType.PAIR_RESPONSE, JsonHelper.jsonEncode(response), fromIp)
202+
sendPairingMessage(NearbyMessageType.PAIR_RESPONSE, JsonHelper.jsonEncode(response), request.fromIp)
193203
LogCat.d("Signed pairing rejection response sent to ${request.fromName}")
194204
}
195205
} catch (e: Exception) {
196206
LogCat.e("Error responding to pairing: ${e.message}")
197-
sendEvent(PairingFailedEvent(request.fromId, "Failed to respond to pairing request"))
207+
val event = PairingFailedEvent(request.fromId, "Failed to respond to pairing request")
208+
sendEvent(event)
209+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
198210
} finally {
199211
// Clean up session if not accepted
200212
if (!accepted) {
@@ -224,7 +236,9 @@ object NearbyPairManager {
224236
}
225237

226238
activePairingSessions.remove(deviceId)
227-
sendEvent(PairingFailedEvent(deviceId, "Pairing cancelled by user"))
239+
val event = PairingFailedEvent(deviceId, "Pairing cancelled by user")
240+
sendEvent(event)
241+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
228242
LogCat.d("Pairing cancelled for device: $deviceId")
229243
}
230244

@@ -241,17 +255,21 @@ object NearbyPairManager {
241255
val currentTime = System.currentTimeMillis()
242256
if (abs(currentTime - response.timestamp) > MAX_TIMESTAMP_DIFF_MS) {
243257
LogCat.e("Pairing response timestamp is too old or in the future")
244-
sendEvent(PairingFailedEvent(response.fromId, "Invalid timestamp"))
258+
val event = PairingFailedEvent(response.fromId, "Invalid timestamp")
259+
sendEvent(event)
260+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
245261
return
246262
}
247-
263+
248264
// Verify signature for all responses (acceptance and rejection)
249265
if (!verifyPairingResponseSignature(response)) {
250266
LogCat.e("Pairing response signature verification failed")
251-
sendEvent(PairingFailedEvent(response.fromId, "Signature verification failed"))
267+
val event = PairingFailedEvent(response.fromId, "Signature verification failed")
268+
sendEvent(event)
269+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
252270
return
253271
}
254-
272+
255273
LogCat.d("Pairing response signature verified successfully")
256274

257275
if (response.accepted) {
@@ -261,26 +279,31 @@ object NearbyPairManager {
261279
// Store peer in database with signature public key
262280
val peerIps = (listOf(senderIP) + response.ips).distinct()
263281
storePeerInDatabase(
264-
response.fromId,
265-
session.deviceName,
266-
peerIps,
267-
response.port,
268-
response.deviceType,
282+
response.fromId,
283+
session.deviceName,
284+
peerIps,
285+
response.port,
286+
response.deviceType,
269287
encryptKey,
270288
response.signaturePublicKey
271289
)
272290
sendEvent(PairingSuccessEvent(response.fromId, session.deviceName, senderIP, encryptKey))
291+
sendEvent(WebSocketEvent(EventType.PAIRING_SUCCESS, JsonHelper.jsonEncode(DPairingResult(response.fromId))))
273292
LogCat.d("Pairing completed successfully with ${session.deviceName}")
274293
} else {
275294
throw Exception("Failed to compute shared key")
276295
}
277296
} else {
278-
sendEvent(PairingFailedEvent(response.fromId, "Pairing request was rejected"))
297+
val event = PairingFailedEvent(response.fromId, "Pairing request was rejected")
298+
sendEvent(event)
299+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
279300
LogCat.d("Verified pairing rejection from ${session.deviceName}")
280301
}
281302
} catch (e: Exception) {
282303
LogCat.e("Error processing pairing response: ${e.message}")
283-
sendEvent(PairingFailedEvent(response.fromId, "Failed to process pairing response"))
304+
val event = PairingFailedEvent(response.fromId, "Failed to process pairing response")
305+
sendEvent(event)
306+
sendEvent(WebSocketEvent(EventType.PAIRING_FAILED, JsonHelper.jsonEncode(DPairingResult(event.deviceId, event.reason))))
284307
}
285308

286309
// Clean up session
@@ -289,8 +312,8 @@ object NearbyPairManager {
289312

290313
fun handlePairingCancel(cancel: DPairingCancel) {
291314
LogCat.d("Pairing cancelled by remote device: ${cancel.fromId}")
292-
sendEvent(PairingCancelledEvent(cancel.fromId))
293-
315+
sendEvent(PairingCanceledEvent(cancel.fromId))
316+
sendEvent(WebSocketEvent(EventType.PAIRING_CANCELED, JsonHelper.jsonEncode(DPairingResult(cancel.fromId))))
294317
// Clean up session if exists
295318
activePairingSessions.remove(cancel.fromId)
296319
}

app/src/main/java/com/ismartcoding/plain/events/AppEvents.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ import java.io.File
5656
data class NearbyDeviceFoundEvent(val device: DNearbyDevice) : ChannelEvent()
5757

5858
// Pairing events
59-
data class PairingRequestReceivedEvent(val request: DPairingRequest, val fromIp: String) : ChannelEvent()
59+
data class PairingRequestReceivedEvent(val request: DPairingRequest) : ChannelEvent()
6060
data class PairingSuccessEvent(val deviceId: String, val deviceName: String, val deviceIp: String, val key: String) : ChannelEvent()
6161
data class PairingFailedEvent(val deviceId: String, val reason: String) : ChannelEvent()
62-
data class PairingCancelledEvent(val fromId: String) : ChannelEvent()
62+
data class PairingCanceledEvent(val fromId: String) : ChannelEvent()
6363

6464
class FolderKanbanSelectEvent(val data: FolderOption) : ChannelEvent()
6565

app/src/main/java/com/ismartcoding/plain/events/WebSocketEvents.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ enum class EventType(val value: Int) {
5454
IMAGE_SEARCH_UPDATED(19),
5555
PEER_STATUS_UPDATED(20),
5656
DEVICE_NAME_UPDATED(21),
57+
PAIRING_REQUEST_RECEIVED(22),
58+
PAIRING_SUCCESS(23),
59+
PAIRING_FAILED(24),
60+
PAIRING_CANCELED(25),
5761
}
5862

5963

app/src/main/java/com/ismartcoding/plain/features/dlna/receiver/DlnaXmlTemplates.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ object DlnaXmlTemplates {
1212
<URLBase>http://$ip:$port</URLBase>
1313
<device>
1414
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
15-
<friendlyName>Plain (${TempData.deviceName})</friendlyName>
15+
<friendlyName>Plain (${TempData.deviceName.value})</friendlyName>
1616
<manufacturer>PlainApp</manufacturer>
1717
<modelName>PlainApp MediaRenderer</modelName>
1818
<modelNumber>1</modelNumber>

app/src/main/java/com/ismartcoding/plain/ui/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,13 @@ class MainActivity : AppCompatActivity() {
220220
onDeny = {
221221
pendingPairingEvent = null
222222
coIO {
223-
NearbyPairManager.respondToPairing(event.request, event.fromIp, false)
223+
NearbyPairManager.respondToPairing(event.request, false)
224224
}
225225
},
226226
onAllow = {
227227
pendingPairingEvent = null
228228
coIO {
229-
NearbyPairManager.respondToPairing(event.request, event.fromIp, true)
229+
NearbyPairManager.respondToPairing(event.request, true)
230230
}
231231
},
232232
)

app/src/main/java/com/ismartcoding/plain/ui/MainActivityEvents.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import com.ismartcoding.plain.events.ConfirmToAcceptLoginEvent
2020
import com.ismartcoding.plain.events.ExportFileEvent
2121
import com.ismartcoding.plain.events.HttpServerStateChangedEvent
2222
import com.ismartcoding.plain.events.IgnoreBatteryOptimizationEvent
23-
import com.ismartcoding.plain.events.PairingCancelledEvent
23+
import com.ismartcoding.plain.events.PairingCanceledEvent
2424
import com.ismartcoding.plain.events.PairingRequestReceivedEvent
2525
import com.ismartcoding.plain.events.PairingSuccessEvent
2626
import com.ismartcoding.plain.events.PermissionsResultEvent
@@ -142,7 +142,9 @@ internal fun MainActivity.initEvents() {
142142
openNew()
143143
}
144144

145-
is PairingCancelledEvent -> pendingPairingEvent = null
145+
is PairingCanceledEvent -> {
146+
pendingPairingEvent = null
147+
}
146148
is PairingSuccessEvent -> {
147149
withIO { peerVM.loadPeers() }
148150
navControllerState.value?.navigate(Routing.Chat("peer:${event.deviceId}")) { popUpTo<Routing.Nearby> { inclusive = true } }

0 commit comments

Comments
 (0)