package com.peachpasswords.sync 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, ) { 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, ) 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() ?: 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() } } finally { session.close() } }