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 fun onError(error: String) {217 receiverDeps.onError(error)218 }219 }220 return requestAdoption(channel, boundDeps)221 } finally {222 noise.close()223 }224}225226227228229230231232233234internal suspend fun runRelayAdoptionInitiatorSourceSession(235 socket: RelayBinarySocket,236 invitation: PairingInvitation,237 sourceDeps: AdoptionSourceDeps,238 confirm: suspend (sas: String, request: AdoptionFrame.Request) -> Boolean,239): PairedDevice? {240 socket.send(241 JSONObject()242 .put("type", "PEACH_HELLO")243 .put("token", invitation.token)244 .toString()245 .toByteArray(Charsets.UTF_8),246 )247248 val ack = JSONObject(String(socket.receive(), Charsets.UTF_8))249 require(ack.optString("type") == "PEACH_HELLO_ACK") {250 "expected PEACH_HELLO_ACK"251 }252 val responderStatic = hexToPubkey(ack.optString("ephemeralPublicKeyHex"))253 val noise = try {254 noiseNKInitiator(255 responderStatic,256 socket::send,257 socket::receive,258 )259 } finally {260 responderStatic.fill(0)261 }262 try {263 val channel = object : PairingTransport {264 override suspend fun send(msg: ByteArray) = socket.send(noise.encrypt(msg))265 override suspend fun receive(): ByteArray = noise.decrypt(socket.receive())266 override suspend fun close() = socket.close()267 }268 val request = receiveFrame(channel)269 if (!isAdoptRequest(request)) return null270 request as AdoptionFrame.Request271 if (!confirm(SASDerivation.derive(noise.handshakeHash), request)) return null272273 val pairState = AdoptionPairState(274 rendezvousSecretHex = Rendezvous.bytesToHex(275 Rendezvous.deriveSecret(noise.handshakeHash),276 ),277 relayUrl = invitation.relayUrl,278 )279 val boundDeps = object : AdoptionSourceDeps {280 override val sourceIdentity = sourceDeps.sourceIdentity281 override val pairState = pairState282 override suspend fun rootKeyHex(): String = sourceDeps.rootKeyHex()283 override suspend fun vaultId(): String = sourceDeps.vaultId()284 override suspend fun license(): String? = sourceDeps.license()285 override suspend fun onConsent(request: AdoptionFrame.Request): Boolean = true286 override suspend fun upsertPairedDevice(device: PairedDevice) {287 sourceDeps.upsertPairedDevice(device)288 }289 }290 return grantAdoption(channel, request, boundDeps)291 } finally {292 noise.close()293 }294}