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 adoptFromRelaySocket(26 socket = socket,27 invitation = invitation,28 deps = deps,29 confirm = confirm,30 windowMs = windowMs,31 )32 }33}34353637383940414243internal suspend fun adoptFromRelaySocket(44 socket: RelayBinarySocket,45 invitation: PairingInvitation,46 deps: AdoptionReceiverDeps,47 confirm: suspend (sas: String) -> Boolean,48 windowMs: Long = PAIRING_WINDOW_MS,49): PairedDevice {50 var completed = false51 return try {52 val adopted =53 withTimeout(windowMs) {54 runRelayAdoptionReceiverSession(55 socket = socket,56 invitation = invitation,57 receiverDeps = deps,58 confirm = confirm,59 ) ?: throw IllegalStateException("adoption_rejected")60 }61 completed = true62 adopted63 } finally {64 if (completed) {65 socket.closeAfterQueuedFrames()66 } else {67 socket.close()68 }69 }70}717273747576internal suspend fun runRelayAdoptionReceiverSession(77 socket: RelayBinarySocket,78 invitation: PairingInvitation,79 receiverDeps: AdoptionReceiverDeps,80 confirm: suspend (sas: String) -> Boolean,81): PairedDevice? {82 socket.send(83 JSONObject()84 .put("type", "PEACH_HELLO")85 .put("token", invitation.token)86 .toString()87 .toByteArray(Charsets.UTF_8),88 )8990 val ack = JSONObject(String(socket.receive(), Charsets.UTF_8))91 require(ack.optString("type") == "PEACH_HELLO_ACK") {92 "expected PEACH_HELLO_ACK"93 }94 val sourceStatic = hexToPubkey(ack.optString("ephemeralPublicKeyHex"))95 val noise = try {96 noiseNKInitiator(97 sourceStatic,98 socket::send,99 socket::receive,100 )101 } finally {102 sourceStatic.fill(0)103 }104 try {105 val channel = object : PairingTransport {106 override suspend fun send(msg: ByteArray) = socket.send(noise.encrypt(msg))107 override suspend fun receive(): ByteArray = noise.decrypt(socket.receive())108 override suspend fun close() = socket.close()109 }110 val pairState = AdoptionPairState(111 rendezvousSecretHex = Rendezvous.bytesToHex(112 Rendezvous.deriveSecret(noise.handshakeHash),113 ),114 relayUrl = invitation.relayUrl,115 )116 val boundDeps = object : AdoptionReceiverDeps {117 override val deviceName = receiverDeps.deviceName118 override val deviceType = receiverDeps.deviceType119 override val pairState = pairState120 override fun generateDeviceId() = receiverDeps.generateDeviceId()121 override suspend fun confirmSas(): Boolean =122 confirm(SASDerivation.derive(noise.handshakeHash))123124 override suspend fun validateSource(source: AdoptionFrame.Source) {125 receiverDeps.validateSource(source)126 }127128 override suspend fun createVaultFromRootKey(rootKeyHex: String): String =129 receiverDeps.createVaultFromRootKey(rootKeyHex)130131 override suspend fun deriveLocalIdentity(): LocalDeviceIdentity =132 receiverDeps.deriveLocalIdentity()133134 override suspend fun clearPairedDeviceRevocation(deviceId: String) {135 receiverDeps.clearPairedDeviceRevocation(deviceId)136 }137138 override suspend fun upsertPairedDevice(device: PairedDevice) {139 receiverDeps.upsertPairedDevice(device)140 }141142 override suspend fun applyLicense(license: String) {143 receiverDeps.applyLicense(license)144 }145146 override fun onError(error: String) {147 receiverDeps.onError(error)148 }149 }150 return requestAdoption(channel, boundDeps)151 } finally {152 noise.close()153 }154}