package com.peachpasswords.sync import kotlinx.coroutines.CancellationException import org.json.JSONObject /** * One relayed reconnect session where WE speak first — the mirror of * [runCloudRelayResponderSession], and the half Android never had. * * Until this existed the phone could only ever wait, which was fine against a * browser extension (it can only dial) and a deadlock against another phone: * two waiters in the same room, nobody opening. See [weDialPeer] for how the * role is assigned. * * The wire is the extension's, byte for byte: an unencrypted `PEACH_RECONNECT` * selector naming the room, then Noise KK, then a padded PAIR_IDENTITY each * way, then the sync exchange. */ internal suspend fun runCloudRelayInitiatorSession( socket: RelayBinarySocket, known: PairedDevice, rendezvousId: String, ourStaticPrivateKey: ByteArray, ourStaticPublicKey: ByteArray, localIdentity: LocalDeviceIdentity, vaultFactory: () -> SyncVaultAdapter?, onSyncComplete: () -> Unit, report: (SyncOutcomeSummary) -> Unit = {}, ) { val responderKeyHex = known.longTermPublicKeyHex ?: throw IllegalArgumentException("known peer missing static key") val responderKey = hexToPubkey(responderKeyHex) // The capability names the room only; it grants no trust. KK proves both // private keys immediately after, and a wrong room is refused there. socket.send( JSONObject() .put("type", "PEACH_RECONNECT") .put("rendezvous_id", rendezvousId) .toString() .toByteArray(Charsets.UTF_8), ) val session = noiseKKInitiator( ourStaticPrivateKey, ourStaticPublicKey, responderKey, socket::send, socket::receive, ) // KK completed, so a peer is really there and answered. Everything past this // point is a reportable outcome; everything before it is a dial into an // empty room, which is the resting state of a phone, not a failure. try { socket.send(session.encrypt(SyncPadding.pad(encodePairIdentityFrame(localIdentity)))) val theirIdentity = parsePairIdentityFrame( SyncPadding.unpad(session.decrypt(socket.receive())), ) require(identityMatchesAuthenticatedPeer(known, theirIdentity, responderKey)) { "peer identity does not match authenticated KK key" } val vault = vaultFactory() ?: run { report(SyncOutcomeSummary(reason = SyncOutcomeReason.VAULT_LOCKED)) return } vault.bindAuthenticatedPeer(known) val outcome = runSyncInitiator( SyncSession( remoteStaticPublicKey = responderKey.copyOf(), send = { plaintext -> socket.send(session.encrypt(SyncPadding.pad(plaintext))) }, receive = { SyncPadding.unpad(session.decrypt(socket.receive())) }, close = { socket.close() }, ), vault, ) if (outcome.status == SyncOutcomeStatus.COMPLETED) { onSyncComplete() } report(outcome.toSummary()) } catch (e: CancellationException) { throw e } catch (e: Exception) { // Rethrown unchanged: the caller's retry loop owns what happens next. // This only makes the failure sayable. report(SyncOutcomeSummary(reason = classifySyncError(e.message ?: e.toString()), detail = e.message)) throw e } finally { session.close() } }