1package com.peachpasswords.sync23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5import kotlinx.serialization.json.Json6789101112131415161718192021@Serializable22sealed class AdoptionFrame {23 @Serializable24 @SerialName("ADOPT_REQUEST")25 data class Request(26 @SerialName("device_id") val deviceId: String,27 @SerialName("device_name") val deviceName: String,28 @SerialName("device_type") val deviceType: String,29 ) : AdoptionFrame()3031 @Serializable32 @SerialName("ADOPT_GRANT")33 data class Grant(34 @SerialName("root_key_hex") val rootKeyHex: String,35 @SerialName("vault_id") val vaultId: String,36 val license: String? = null,37 val source: Source,38 ) : AdoptionFrame()3940 @Serializable41 @SerialName("ADOPT_IDENTITY")42 data class Identity(43 @SerialName("device_id") val deviceId: String,44 @SerialName("device_name") val deviceName: String,45 @SerialName("device_type") val deviceType: String,46 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,47 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,48 ) : AdoptionFrame()4950 @Serializable51 data class Source(52 @SerialName("device_id") val deviceId: String,53 @SerialName("device_name") val deviceName: String,54 @SerialName("device_type") val deviceType: String,55 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,56 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,57 @SerialName("ws_port") val wsPort: Int? = null,58 )59}606162interface AdoptionReceiverDeps {63 val deviceName: String64 val deviceType: DeviceType65 fun generateDeviceId(): String6667 68 suspend fun createVaultFromRootKey(rootKeyHex: String): String69 suspend fun deriveLocalIdentity(): LocalChirpIdentity70 suspend fun upsertPairedDevice(device: PairedDevice)71 suspend fun applyLicense(license: String) {}72 fun onError(error: String)73}747576interface AdoptionSourceDeps {77 val sourceIdentity: LocalChirpIdentity78 suspend fun rootKeyHex(): String79 suspend fun vaultId(): String80 suspend fun license(): String? = null8182 83 suspend fun onConsent(request: AdoptionFrame.Request): Boolean84 suspend fun upsertPairedDevice(device: PairedDevice)85}8687private val Hex64 = Regex("^[0-9a-f]{64}\$")8889fun validateRootKeyHex(rootKeyHex: String?): Boolean = rootKeyHex != null && Hex64.matches(rootKeyHex)9091fun isAdoptRequest(frame: AdoptionFrame?): Boolean =92 frame is AdoptionFrame.Request &&93 frame.deviceId.isNotEmpty() &&94 isDeviceType(frame.deviceType)9596fun isAdoptGrant(frame: AdoptionFrame?): Boolean =97 frame is AdoptionFrame.Grant &&98 validateRootKeyHex(frame.rootKeyHex) &&99 Hex64.matches(frame.vaultId) &&100 isDeviceType(frame.source.deviceType) &&101 Hex64.matches(frame.source.longTermPublicKeyHex)102103fun isAdoptIdentity(frame: AdoptionFrame?): Boolean =104 frame is AdoptionFrame.Identity &&105 frame.deviceId.isNotEmpty() &&106 isDeviceType(frame.deviceType) &&107 Hex64.matches(frame.longTermPublicKeyHex)108109110111112113114115116suspend fun requestAdoption(117 channel: PairingTransport,118 deps: AdoptionReceiverDeps,119 nowMs: () -> Long = { System.currentTimeMillis() },120): PairedDevice? {121 sendFrame(channel, AdoptionFrame.Request(122 deviceId = deps.generateDeviceId(),123 deviceName = deps.deviceName,124 deviceType = deps.deviceType.wireValue,125 ))126127 val frame = receiveFrame(channel)128 if (!isAdoptGrant(frame)) {129 deps.onError("ADOPT_UNEXPECTED_FRAME")130 return null131 }132 val grant = frame as AdoptionFrame.Grant133134 val rootKeyBytes = hexToPubkey(grant.rootKeyHex)135 try {136 if (computeVaultId(rootKeyBytes) != grant.vaultId) {137 deps.onError("ADOPT_VAULT_ID_MISMATCH")138 return null139 }140 } finally {141 rootKeyBytes.fill(0)142 }143144 val vaultId = deps.createVaultFromRootKey(grant.rootKeyHex)145 if (vaultId != grant.vaultId) {146 deps.onError("ADOPT_VAULT_ID_MISMATCH")147 return null148 }149150 grant.license?.let { deps.applyLicense(it) }151152 val identity = deps.deriveLocalIdentity()153 sendFrame(channel, AdoptionFrame.Identity(154 deviceId = identity.deviceId,155 deviceName = identity.deviceName,156 deviceType = identity.deviceType.wireValue,157 longTermPublicKeyHex = identity.longTermPublicKeyHex,158 signingPublicKeyHex = identity.signingPublicKeyHex,159 ))160161 val sourceDevice = PairedDevice(162 deviceId = grant.source.deviceId,163 deviceName = grant.source.deviceName,164 deviceType = grant.source.deviceType,165 longTermPublicKeyHex = grant.source.longTermPublicKeyHex,166 signingPublicKeyHex = grant.source.signingPublicKeyHex,167 trustSource = "direct",168 wsPort = grant.source.wsPort,169 pairedAt = nowMs(),170 )171 deps.upsertPairedDevice(sourceDevice)172 return sourceDevice173}174175176177178179180suspend fun grantAdoption(181 channel: PairingTransport,182 request: AdoptionFrame.Request,183 deps: AdoptionSourceDeps,184 nowMs: () -> Long = { System.currentTimeMillis() },185): PairedDevice? {186 if (!deps.onConsent(request)) return null187188 val rootKeyHex = deps.rootKeyHex()189 if (!validateRootKeyHex(rootKeyHex)) throw IllegalStateException("ADOPT_INVALID_ROOT_KEY")190191 sendFrame(channel, AdoptionFrame.Grant(192 rootKeyHex = rootKeyHex,193 vaultId = deps.vaultId(),194 license = deps.license(),195 source = AdoptionFrame.Source(196 deviceId = deps.sourceIdentity.deviceId,197 deviceName = deps.sourceIdentity.deviceName,198 deviceType = deps.sourceIdentity.deviceType.wireValue,199 longTermPublicKeyHex = deps.sourceIdentity.longTermPublicKeyHex,200 signingPublicKeyHex = deps.sourceIdentity.signingPublicKeyHex,201 wsPort = deps.sourceIdentity.wsPort,202 ),203 ))204205 val identity = receiveFrame(channel)206 if (!isAdoptIdentity(identity)) return null207 identity as AdoptionFrame.Identity208209 val device = PairedDevice(210 deviceId = identity.deviceId,211 deviceName = identity.deviceName,212 deviceType = identity.deviceType,213 longTermPublicKeyHex = identity.longTermPublicKeyHex,214 signingPublicKeyHex = identity.signingPublicKeyHex,215 trustSource = "direct",216 pairedAt = nowMs(),217 )218 deps.upsertPairedDevice(device)219 return device220}221222223224225226227val AdoptJson: Json = Json {228 classDiscriminator = "type"229 encodeDefaults = false230 ignoreUnknownKeys = true231}232233suspend fun sendFrame(channel: PairingTransport, frame: AdoptionFrame) {234 channel.send(AdoptJson.encodeToString(AdoptionFrame.serializer(), frame).toByteArray(Charsets.UTF_8))235}236237238suspend fun receiveFrame(channel: PairingTransport): AdoptionFrame? =239 runCatching {240 AdoptJson.decodeFromString(AdoptionFrame.serializer(), String(channel.receive(), Charsets.UTF_8))241 }.getOrNull()242243private fun isDeviceType(value: String): Boolean =244 DeviceType.entries.any { it.wireValue == value }