1package com.peachpasswords.sync23import javax.inject.Inject4import javax.inject.Singleton5import kotlinx.coroutines.withTimeout6import org.json.JSONObject7891011121314@Singleton15class RelayAdoptionReceiver @Inject constructor() {16 suspend fun adopt(17 encodedInvitation: String,18 deps: AdoptionReceiverDeps,19 confirm: suspend (sas: String) -> Boolean,20 windowMs: Long = PAIRING_WINDOW_MS,21 ): PairedDevice {22 val invitation = parsePairingInvitation(encodedInvitation)23 ?: throw IllegalArgumentException("invalid_pairing_invitation")24 val socket = OkHttpRelaySocket.connect(invitation.roomUrl())25 return try {26 withTimeout(windowMs) {27 runRelayAdoptionReceiverSession(28 socket = socket,29 invitation = invitation,30 receiverDeps = deps,31 confirm = confirm,32 ) ?: throw IllegalStateException("adoption_rejected")33 }34 } finally {35 socket.close()36 }37 }38}394041424344internal suspend fun runRelayAdoptionReceiverSession(45 socket: RelayBinarySocket,46 invitation: PairingInvitation,47 receiverDeps: AdoptionReceiverDeps,48 confirm: suspend (sas: String) -> Boolean,49): PairedDevice? {50 socket.send(51 JSONObject()52 .put("type", "PEACH_HELLO")53 .put("token", invitation.token)54 .toString()55 .toByteArray(Charsets.UTF_8),56 )5758 val ack = JSONObject(String(socket.receive(), Charsets.UTF_8))59 require(ack.optString("type") == "PEACH_HELLO_ACK") {60 "expected PEACH_HELLO_ACK"61 }62 val sourceStatic = hexToPubkey(ack.optString("ephemeralPublicKeyHex"))63 val noise = try {64 noiseNKInitiator(65 sourceStatic,66 socket::send,67 socket::receive,68 )69 } finally {70 sourceStatic.fill(0)71 }72 try {73 val channel = object : PairingTransport {74 override suspend fun send(msg: ByteArray) = socket.send(noise.encrypt(msg))75 override suspend fun receive(): ByteArray = noise.decrypt(socket.receive())76 override suspend fun close() = socket.close()77 }78 val pairState = AdoptionPairState(79 rendezvousSecretHex = Rendezvous.bytesToHex(80 Rendezvous.deriveSecret(noise.handshakeHash),81 ),82 relayUrl = invitation.relayUrl,83 )84 val boundDeps = object : AdoptionReceiverDeps {85 override val deviceName = receiverDeps.deviceName86 override val deviceType = receiverDeps.deviceType87 override val pairState = pairState88 override fun generateDeviceId() = receiverDeps.generateDeviceId()89 override suspend fun confirmSas(): Boolean =90 confirm(SASDerivation.derive(noise.handshakeHash))9192 override suspend fun createVaultFromRootKey(rootKeyHex: String): String =93 receiverDeps.createVaultFromRootKey(rootKeyHex)9495 override suspend fun deriveLocalIdentity(): LocalDeviceIdentity =96 receiverDeps.deriveLocalIdentity()9798 override suspend fun clearPairedDeviceRevocation(deviceId: String) {99 receiverDeps.clearPairedDeviceRevocation(deviceId)100 }101102 override suspend fun upsertPairedDevice(device: PairedDevice) {103 receiverDeps.upsertPairedDevice(device)104 }105106 override suspend fun applyLicense(license: String) {107 receiverDeps.applyLicense(license)108 }109110 override fun onError(error: String) {111 receiverDeps.onError(error)112 }113 }114 return requestAdoption(channel, boundDeps)115 } finally {116 noise.close()117 }118}