1package com.peachpasswords.sync23import javax.inject.Inject4import javax.inject.Singleton5import kotlinx.coroutines.withTimeout6import org.json.JSONObject7891011121314@Singleton15class RelayAdoptionReceiver @Inject constructor() {16 1718192021 suspend fun adopt(22 encodedInvitation: String,23 deps: AdoptionReceiverDeps,24 confirm: suspend (sas: String) -> Boolean,25 windowMs: Long = PAIRING_WINDOW_MS,26 ): PairedDevice {27 val invitation = parsePairingInvitation(encodedInvitation)28 ?: throw IllegalArgumentException("invalid_pairing_invitation")29 require(invitation.vaultStatus == VaultStatus.POPULATED) {30 "receiver adopt() expects a populated invitation; got ${invitation.vaultStatus}"31 }32 val socket = OkHttpRelaySocket.connect(invitation.roomUrl())33 return adoptFromRelaySocket(34 socket = socket,35 invitation = invitation,36 deps = deps,37 confirm = confirm,38 windowMs = windowMs,39 )40 }4142 43444546474849 suspend fun grantFromInvitation(50 encodedInvitation: String,51 sourceDeps: AdoptionSourceDeps,52 confirm: suspend (sas: String, request: AdoptionFrame.Request) -> Boolean,53 windowMs: Long = PAIRING_WINDOW_MS,54 ): PairedDevice {55 val invitation = parsePairingInvitation(encodedInvitation)56 ?: throw IllegalArgumentException("invalid_pairing_invitation")57 require(invitation.vaultStatus == VaultStatus.EMPTY) {58 "grantFromInvitation() expects an empty invitation; got ${invitation.vaultStatus}"59 }60 val socket = OkHttpRelaySocket.connect(invitation.roomUrl())61 return grantFromRelaySocket(62 socket = socket,63 invitation = invitation,64 sourceDeps = sourceDeps,65 confirm = confirm,66 windowMs = windowMs,67 )68 }69}70717273747576777879internal suspend fun adoptFromRelaySocket(80 socket: RelayBinarySocket,81 invitation: PairingInvitation,82 deps: AdoptionReceiverDeps,83 confirm: suspend (sas: String) -> Boolean,84 windowMs: Long = PAIRING_WINDOW_MS,85): PairedDevice {86 var completed = false87 return try {88 val adopted =89 withTimeout(windowMs) {90 runRelayAdoptionReceiverSession(91 socket = socket,92 invitation = invitation,93 receiverDeps = deps,94 confirm = confirm,95 ) ?: throw IllegalStateException("adoption_rejected")96 }97 completed = true98 adopted99 } finally {100 if (completed) {101 socket.closeAfterQueuedFrames()102 } else {103 socket.close()104 }105 }106}107108109110111112113114internal suspend fun grantFromRelaySocket(115 socket: RelayBinarySocket,116 invitation: PairingInvitation,117 sourceDeps: AdoptionSourceDeps,118 confirm: suspend (sas: String, request: AdoptionFrame.Request) -> Boolean,119 windowMs: Long = PAIRING_WINDOW_MS,120): PairedDevice {121 var completed = false122 return try {123 val granted = withTimeout(windowMs) {124 runRelayAdoptionInitiatorSourceSession(125 socket = socket,126 invitation = invitation,127 sourceDeps = sourceDeps,128 confirm = confirm,129 ) ?: throw IllegalStateException("adoption_rejected")130 }131 completed = true132 granted133 } finally {134 if (completed) {135 socket.closeAfterQueuedFrames()136 } else {137 socket.close()138 }139 }140}141142143144145146internal suspend fun runRelayAdoptionReceiverSession(147 socket: RelayBinarySocket,148 invitation: PairingInvitation,149 receiverDeps: AdoptionReceiverDeps,150 confirm: suspend (sas: String) -> Boolean,151): PairedDevice? {152 socket.send(153 JSONObject()154 .put("type", "PEACH_HELLO")155 .put("token", invitation.token)156 .toString()157 .toByteArray(Charsets.UTF_8),158 )159160 val ack = JSONObject(String(socket.receive(), Charsets.UTF_8))161 require(ack.optString("type") == "PEACH_HELLO_ACK") {162 "expected PEACH_HELLO_ACK"163 }164 val sourceStatic = hexToPubkey(ack.optString("ephemeralPublicKeyHex"))165 val noise = try {166 noiseNKInitiator(167 sourceStatic,168 socket::send,169 socket::receive,170 )171 } finally {172 sourceStatic.fill(0)173 }174 try {175 val channel = object : PairingTransport {176 override suspend fun send(msg: ByteArray) = socket.send(noise.encrypt(msg))177 override suspend fun receive(): ByteArray = noise.decrypt(socket.receive())178 override suspend fun close() = socket.close()179 }180 val pairState = AdoptionPairState(181 rendezvousSecretHex = Rendezvous.bytesToHex(182 Rendezvous.deriveSecret(noise.handshakeHash),183 ),184 relayUrl = invitation.relayUrl,185 )186 val boundDeps = object : AdoptionReceiverDeps {187 override val deviceName = receiverDeps.deviceName188 override val deviceType = receiverDeps.deviceType189 override val pairState = pairState190 override fun generateDeviceId() = receiverDeps.generateDeviceId()191 override suspend fun confirmSas(): Boolean =192 confirm(SASDerivation.derive(noise.handshakeHash))193194 override suspend fun validateSource(source: AdoptionFrame.Source) {195 receiverDeps.validateSource(source)196 }197198 override suspend fun createVaultFromRootKey(rootKeyHex: String): String =199 receiverDeps.createVaultFromRootKey(rootKeyHex)200201 override suspend fun deriveLocalIdentity(): LocalDeviceIdentity =202 receiverDeps.deriveLocalIdentity()203204 override suspend fun clearPairedDeviceRevocation(deviceId: String) {205 receiverDeps.clearPairedDeviceRevocation(deviceId)206 }207208 override suspend fun upsertPairedDevice(device: PairedDevice) {209 receiverDeps.upsertPairedDevice(device)210 }211212 override suspend fun applyLicense(license: String) {213 receiverDeps.applyLicense(license)214 }215216 override suspend fun license(): String? = receiverDeps.license()217218 override fun onError(error: String) {219 receiverDeps.onError(error)220 }221 }222 return requestAdoption(channel, boundDeps)223 } finally {224 noise.close()225 }226}227228229230231232233234235236internal suspend fun runRelayAdoptionInitiatorSourceSession(237 socket: RelayBinarySocket,238 invitation: PairingInvitation,239 sourceDeps: AdoptionSourceDeps,240 confirm: suspend (sas: String, request: AdoptionFrame.Request) -> Boolean,241): PairedDevice? {242 socket.send(243 JSONObject()244 .put("type", "PEACH_HELLO")245 .put("token", invitation.token)246 .toString()247 .toByteArray(Charsets.UTF_8),248 )249250 val ack = JSONObject(String(socket.receive(), Charsets.UTF_8))251 require(ack.optString("type") == "PEACH_HELLO_ACK") {252 "expected PEACH_HELLO_ACK"253 }254 val responderStatic = hexToPubkey(ack.optString("ephemeralPublicKeyHex"))255 val noise = try {256 noiseNKInitiator(257 responderStatic,258 socket::send,259 socket::receive,260 )261 } finally {262 responderStatic.fill(0)263 }264 try {265 val channel = object : PairingTransport {266 override suspend fun send(msg: ByteArray) = socket.send(noise.encrypt(msg))267 override suspend fun receive(): ByteArray = noise.decrypt(socket.receive())268 override suspend fun close() = socket.close()269 }270 val request = receiveFrame(channel)271 if (!isAdoptRequest(request)) return null272 request as AdoptionFrame.Request273 if (!confirm(SASDerivation.derive(noise.handshakeHash), request)) return null274275 val pairState = AdoptionPairState(276 rendezvousSecretHex = Rendezvous.bytesToHex(277 Rendezvous.deriveSecret(noise.handshakeHash),278 ),279 relayUrl = invitation.relayUrl,280 )281 val boundDeps = object : AdoptionSourceDeps {282 override val sourceIdentity = sourceDeps.sourceIdentity283 override val pairState = pairState284 override suspend fun rootKeyHex(): String = sourceDeps.rootKeyHex()285 override suspend fun vaultId(): String = sourceDeps.vaultId()286 override suspend fun license(): String? = sourceDeps.license()287 override suspend fun applyLicense(license: String) {288 sourceDeps.applyLicense(license)289 }290 override suspend fun onConsent(request: AdoptionFrame.Request): Boolean = true291 override suspend fun upsertPairedDevice(device: PairedDevice) {292 sourceDeps.upsertPairedDevice(device)293 }294 }295 return grantAdoption(channel, request, boundDeps)296 } finally {297 noise.close()298 }299}