1package com.peachpasswords.sync23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5678910111213141516171819const val ADOPTION_VERSION = 22021private const val REASON_PRO_REQUIRED = "ADOPT_PRO_REQUIRED"22private const val REASON_DEVICE_ID_COLLISION = "ADOPT_DEVICE_ID_COLLISION"232425262728293031internal fun allowLegacyV1(postReset: Boolean): Boolean = !postReset3233343536373839404142@Serializable43data class AdoptGrantV2(44 val type: String,45 @SerialName("root_key_hex") val rootKeyHex: String,46 @SerialName("vault_id") val vaultId: String,47 val license: String? = null,48)4950private const val GRANT_TYPE = "ADOPT_GRANT"5152535455565758interface AdoptionReceiverV2Deps : AdoptionReceiverDeps {59 60 fun localIdentity(): LocalDeviceIdentity6162 suspend fun rotateLocalIdentity(): LocalDeviceIdentity6364 suspend fun validateSource(prepare: AdoptionFrame.Prepare)6566 suspend fun hasSourceIdentityCollision(prepare: AdoptionFrame.Prepare): Boolean67}686970interface AdoptionSourceV2Deps : AdoptionSourceDeps {71 suspend fun onConsent(request: AdoptionFrame.RequestV2): Boolean7273 74 75 override suspend fun rotateSourceIdentity(): LocalDeviceIdentity7677 override fun hasDeviceIdentityCollision(device: PairedDevice): Boolean7879 80818283 suspend fun persistPendingIdentity(device: PairedDevice, nowMs: Long): Boolean8485 86 suspend fun completePendingIdentity(device: PairedDevice): Boolean8788 fun snapshotCapable(): Boolean89}90919293949596979899100101102103internal fun bindSourceSessionDeps(104 sourceDeps: AdoptionSourceV2Deps,105 sourceIdentity: LocalDeviceIdentity,106 pairState: AdoptionPairState,107 sas: String,108 snapshotCapable: Boolean,109 confirm: suspend (sas: String, request: AdoptionFrame.Request) -> Boolean,110): AdoptionSourceV2Deps = object : AdoptionSourceV2Deps {111 override val sourceIdentity = sourceIdentity112 override val pairState = pairState113 override suspend fun rootKeyHex() = sourceDeps.rootKeyHex()114 override suspend fun vaultId() = sourceDeps.vaultId()115 override suspend fun license() = sourceDeps.license()116 override suspend fun applyLicense(license: String) = sourceDeps.applyLicense(license)117 override suspend fun onConsent(request: AdoptionFrame.Request) = true118119 120 121 122 123 124 override suspend fun onConsent(request: AdoptionFrame.RequestV2): Boolean =125 confirm(126 sas,127 AdoptionFrame.Request(128 sessionRequestId = request.deviceId,129 deviceName = request.deviceName,130 deviceType = request.deviceType,131 license = request.license,132 snapshotCapable = request.snapshotCapable,133 ),134 )135136 override suspend fun persistPendingHolder(request: AdoptionFrame.Request, nowMs: Long) =137 sourceDeps.persistPendingHolder(request, nowMs)138139 override suspend fun completePendingHolder(request: AdoptionFrame.Request, device: PairedDevice) =140 sourceDeps.completePendingHolder(request, device)141142 override suspend fun persistPendingIdentity(device: PairedDevice, nowMs: Long) =143 sourceDeps.persistPendingIdentity(device, nowMs)144145 override suspend fun completePendingIdentity(device: PairedDevice) =146 sourceDeps.completePendingIdentity(device)147148 override suspend fun upsertPairedDevice(device: PairedDevice) =149 sourceDeps.upsertPairedDevice(device)150151 override fun hasDeviceIdentityCollision(device: PairedDevice) =152 sourceDeps.hasDeviceIdentityCollision(device)153154 override suspend fun rotateSourceIdentity() = sourceDeps.rotateSourceIdentity()155 override fun snapshotCapable() = snapshotCapable156 override suspend fun vaultSnapshot() = sourceDeps.vaultSnapshot()157 override suspend fun outgoingDeviceManifest() = sourceDeps.outgoingDeviceManifest()158 override suspend fun markSynced(deviceId: String) = sourceDeps.markSynced(deviceId)159 override suspend fun duressSlotJson() = sourceDeps.duressSlotJson()160}161162private fun AdoptionFrame.RequestV2.valid(): Boolean =163 adoptionVersion == ADOPTION_VERSION &&164 deviceId.isNotEmpty() &&165 Hex64.matches(longTermPublicKeyHex) &&166 Hex64.matches(signingPublicKeyHex) &&167 isDeviceType(deviceType)168169private fun AdoptionFrame.Prepare.valid(): Boolean =170 deviceId.isNotEmpty() &&171 Hex64.matches(longTermPublicKeyHex) &&172 Hex64.matches(signingPublicKeyHex) &&173 isDeviceType(deviceType) &&174 Hex64.matches(vaultId)175176177178179180181suspend fun requestAdoptionV2(182 channel: PairingTransport,183 deps: AdoptionReceiverV2Deps,184 185 186 onPrepare: ((AdoptionFrame.Prepare) -> Unit)? = null,187 nowMs: Long = System.currentTimeMillis(),188): PairedDevice? {189 val receiverLicense = deps.license()190191 fun requestFrame(identity: LocalDeviceIdentity): AdoptionFrame.RequestV2 =192 AdoptionFrame.RequestV2(193 adoptionVersion = ADOPTION_VERSION,194 deviceId = identity.deviceId,195 deviceName = identity.deviceName,196 deviceType = identity.deviceType.wireValue,197 longTermPublicKeyHex = identity.longTermPublicKeyHex,198 signingPublicKeyHex = identity.signingPublicKeyHex,199 snapshotCapable = true,200 )201202 203 204 fun rootKeyMatchesVault(rootKeyHex: String, vaultId: String): Boolean {205 if (!validateRootKeyHex(rootKeyHex) || !Hex64.matches(vaultId)) return false206 val rootKeyBytes = hexToPubkey(rootKeyHex)207 return try {208 computeVaultId(rootKeyBytes) == vaultId209 } finally {210 rootKeyBytes.fill(0)211 }212 }213214 sendFrame(channel, requestFrame(deps.localIdentity()))215216 if (receiveFrame(channel) !is AdoptionFrame.ConfirmRequired) {217 deps.onError("ADOPT_UNEXPECTED_FRAME")218 return null219 }220221 val confirmed = deps.confirmSas()222 223 224 225 sendFrame(channel, AdoptionFrame.Confirm(confirmed, license = receiverLicense))226 if (!confirmed) {227 deps.onError("ADOPT_SAS_REJECTED")228 return null229 }230231 232 233 var prepared = receiveFrame(channel)234 if (prepared is AdoptionFrame.Reject && prepared.reason == REASON_DEVICE_ID_COLLISION) {235 sendFrame(channel, requestFrame(deps.rotateLocalIdentity()))236 prepared = receiveFrame(channel)237 }238 if (prepared is AdoptionFrame.Reject) {239 deps.onError(prepared.reason)240 return null241 }242 if (prepared !is AdoptionFrame.Prepare || !prepared.valid()) {243 deps.onError("ADOPT_UNEXPECTED_FRAME")244 return null245 }246247 deps.validateSource(prepared)248249 250 251 if (deps.hasSourceIdentityCollision(prepared)) {252 sendFrame(channel, AdoptionFrame.Reject(REASON_DEVICE_ID_COLLISION))253 val rotated = receiveFrame(channel)254 if (rotated !is AdoptionFrame.Prepare || !rotated.valid()) {255 deps.onError("ADOPT_SOURCE_COLLISION_NOT_RESOLVED")256 return null257 }258 deps.validateSource(rotated)259 if (deps.hasSourceIdentityCollision(rotated)) {260 sendFrame(channel, AdoptionFrame.Reject(REASON_DEVICE_ID_COLLISION))261 deps.onError("ADOPT_SOURCE_COLLISION_NOT_RESOLVED")262 return null263 }264 prepared = rotated265 }266267 if (receiverLicense == null && prepared.license == null) {268 deps.onError(REASON_PRO_REQUIRED)269 return null270 }271272 onPrepare?.invoke(prepared)273274 sendFrame(channel, AdoptionFrame.Ready)275276 val raw = String(channel.receive(), Charsets.UTF_8)277 278 val rejection = runCatching {279 AdoptJson.decodeFromString(AdoptionFrame.serializer(), raw)280 }.getOrNull() as? AdoptionFrame.Reject281 if (rejection != null) {282 deps.onError(rejection.reason)283 return null284 }285 val grant = runCatching {286 AdoptJson.decodeFromString(AdoptGrantV2.serializer(), raw)287 }.getOrNull()288 if (grant == null || grant.type != GRANT_TYPE) {289 deps.onError("ADOPT_UNEXPECTED_FRAME")290 return null291 }292 293 294 if (grant.vaultId != prepared.vaultId || !rootKeyMatchesVault(grant.rootKeyHex, grant.vaultId)) {295 deps.onError("ADOPT_VAULT_ID_MISMATCH")296 return null297 }298299 val vaultId = deps.createVaultFromRootKey(grant.rootKeyHex)300 if (vaultId != grant.vaultId) {301 deps.onError("ADOPT_VAULT_ID_MISMATCH")302 return null303 }304 if (receiverLicense == null) {305 grant.license?.let { deps.applyLicense(it) }306 }307308 309 310 val sourceDevice = PairedDevice(311 deviceId = prepared.deviceId,312 deviceName = prepared.deviceName,313 deviceType = prepared.deviceType,314 longTermPublicKeyHex = prepared.longTermPublicKeyHex,315 signingPublicKeyHex = prepared.signingPublicKeyHex,316 trustSource = "direct",317 wsPort = prepared.wsPort,318 rendezvousSecretHex = deps.pairState?.rendezvousSecretHex,319 relayUrl = deps.pairState?.relayUrl ?: prepared.relayUrl,320 pairedAt = nowMs,321 )322 deps.upsertPairedDeviceAndClearRevocation(sourceDevice)323 sendFrame(channel, AdoptionFrame.Applied)324325 if (receiveFrame(channel) !is AdoptionFrame.Complete) {326 deps.onError("ADOPT_COMPLETION_MISSING")327 return null328 }329 return sourceDevice330}331332333334335336337suspend fun grantAdoptionV2(338 channel: PairingTransport,339 request: AdoptionFrame.RequestV2,340 deps: AdoptionSourceV2Deps,341 nowMs: Long = System.currentTimeMillis(),342): PairedDevice? {343 if (!request.valid()) return null344345 346 347 sendFrame(channel, AdoptionFrame.ConfirmRequired)348 if (!deps.onConsent(request)) return null349350 val confirmation = receiveFrame(channel)351 if (confirmation !is AdoptionFrame.Confirm || !confirmation.confirmed) return null352353 fun receiverFrom(identity: AdoptionFrame.RequestV2): PairedDevice =354 PairedDevice(355 deviceId = identity.deviceId,356 deviceName = identity.deviceName,357 deviceType = identity.deviceType,358 longTermPublicKeyHex = identity.longTermPublicKeyHex,359 signingPublicKeyHex = identity.signingPublicKeyHex,360 trustSource = "direct",361 rendezvousSecretHex = deps.pairState?.rendezvousSecretHex,362 relayUrl = deps.pairState?.relayUrl,363 pairedAt = nowMs,364 )365366 367 368 var receiver = receiverFrom(request)369 if (deps.hasDeviceIdentityCollision(receiver)) {370 sendFrame(channel, AdoptionFrame.Reject(REASON_DEVICE_ID_COLLISION))371 val rotated = receiveFrame(channel)372 if (rotated !is AdoptionFrame.RequestV2 || !rotated.valid()) return null373 receiver = receiverFrom(rotated)374 if (deps.hasDeviceIdentityCollision(receiver)) {375 sendFrame(channel, AdoptionFrame.Reject(REASON_DEVICE_ID_COLLISION))376 return null377 }378 }379380 var license = deps.license()381 382 383 384 val receiverLicense = confirmation.license ?: request.license385 if (license == null && receiverLicense != null) {386 deps.applyLicense(receiverLicense)387 license = deps.license()388 }389 if (license == null) {390 sendFrame(channel, AdoptionFrame.Reject(REASON_PRO_REQUIRED))391 return null392 }393394 395 396 if (!deps.persistPendingIdentity(receiver, nowMs)) return null397398 val rootKeyHex = deps.rootKeyHex()399 if (!validateRootKeyHex(rootKeyHex)) return null400401 var sourceIdentity = deps.sourceIdentity402 var sourceRotations = 0403 while (true) {404 sendFrame(405 channel,406 AdoptionFrame.Prepare(407 deviceId = sourceIdentity.deviceId,408 deviceName = sourceIdentity.deviceName,409 deviceType = sourceIdentity.deviceType.wireValue,410 longTermPublicKeyHex = sourceIdentity.longTermPublicKeyHex,411 signingPublicKeyHex = sourceIdentity.signingPublicKeyHex,412 vaultId = deps.vaultId(),413 wsPort = sourceIdentity.wsPort,414 relayUrl = deps.pairState?.relayUrl,415 license = license,416 snapshotCapable = deps.snapshotCapable(),417 ),418 )419420 val next = receiveFrame(channel)421 if (next is AdoptionFrame.Reject &&422 next.reason == REASON_DEVICE_ID_COLLISION &&423 sourceRotations == 0424 ) {425 sourceIdentity = deps.rotateSourceIdentity()426 sourceRotations++427 continue428 }429 if (next !is AdoptionFrame.Ready) return null430 break431 }432433 434 435 channel.send(436 AdoptJson.encodeToString(437 AdoptGrantV2.serializer(),438 AdoptGrantV2(439 type = GRANT_TYPE,440 rootKeyHex = rootKeyHex,441 vaultId = deps.vaultId(),442 license = license,443 ),444 ).toByteArray(Charsets.UTF_8),445 )446447 if (receiveFrame(channel) !is AdoptionFrame.Applied) return null448449 450 if (!deps.completePendingIdentity(receiver)) return null451 deps.upsertPairedDevice(receiver)452 sendFrame(channel, AdoptionFrame.Complete)453 return receiver454}