1package com.peachpasswords.sync23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5import kotlinx.serialization.json.Json6789101112131415161718192021@Serializable22sealed class AdoptionFrame {23 24252627282930313233 @Serializable34 @SerialName("ADOPT_REQUEST")35 data class Request(36 @SerialName("device_id") val sessionRequestId: String,37 @SerialName("device_name") val deviceName: String,38 @SerialName("device_type") val deviceType: String,39 ) : AdoptionFrame()4041 @Serializable42 @SerialName("ADOPT_GRANT")43 data class Grant(44 @SerialName("root_key_hex") val rootKeyHex: String,45 @SerialName("vault_id") val vaultId: String,46 val license: String? = null,47 val source: Source,48 ) : AdoptionFrame()4950 @Serializable51 @SerialName("ADOPT_IDENTITY")52 data class Identity(53 @SerialName("device_id") val deviceId: String,54 @SerialName("device_name") val deviceName: String,55 @SerialName("device_type") val deviceType: String,56 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,57 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,58 ) : AdoptionFrame()5960 @Serializable61 data class Source(62 @SerialName("device_id") val deviceId: String,63 @SerialName("device_name") val deviceName: String,64 @SerialName("device_type") val deviceType: String,65 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,66 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,67 @SerialName("ws_port") val wsPort: Int? = null,68 )69}707172interface AdoptionReceiverDeps {73 val deviceName: String74 val deviceType: DeviceType75 fun generateDeviceId(): String7677 78 suspend fun createVaultFromRootKey(rootKeyHex: String): String79 suspend fun deriveLocalIdentity(): LocalChirpIdentity80 suspend fun upsertPairedDevice(device: PairedDevice)81 suspend fun applyLicense(license: String) {}82 fun onError(error: String)83}848586interface AdoptionSourceDeps {87 val sourceIdentity: LocalChirpIdentity88 suspend fun rootKeyHex(): String89 suspend fun vaultId(): String90 suspend fun license(): String? = null9192 939495969798 suspend fun onConsent(request: AdoptionFrame.Request): Boolean99 suspend fun upsertPairedDevice(device: PairedDevice)100}101102private val Hex64 = Regex("^[0-9a-f]{64}\$")103104fun validateRootKeyHex(rootKeyHex: String?): Boolean = rootKeyHex != null && Hex64.matches(rootKeyHex)105106fun isAdoptRequest(frame: AdoptionFrame?): Boolean =107 frame is AdoptionFrame.Request &&108 frame.sessionRequestId.isNotEmpty() &&109 isDeviceType(frame.deviceType)110111fun isAdoptGrant(frame: AdoptionFrame?): Boolean =112 frame is AdoptionFrame.Grant &&113 validateRootKeyHex(frame.rootKeyHex) &&114 Hex64.matches(frame.vaultId) &&115 isDeviceType(frame.source.deviceType) &&116 Hex64.matches(frame.source.longTermPublicKeyHex)117118fun isAdoptIdentity(frame: AdoptionFrame?): Boolean =119 frame is AdoptionFrame.Identity &&120 frame.deviceId.isNotEmpty() &&121 isDeviceType(frame.deviceType) &&122 Hex64.matches(frame.longTermPublicKeyHex)123124125126127128129130131suspend fun requestAdoption(132 channel: PairingTransport,133 deps: AdoptionReceiverDeps,134 nowMs: () -> Long = { System.currentTimeMillis() },135): PairedDevice? {136 137 138 sendFrame(channel, AdoptionFrame.Request(139 sessionRequestId = deps.generateDeviceId(),140 deviceName = deps.deviceName,141 deviceType = deps.deviceType.wireValue,142 ))143144 val frame = receiveFrame(channel)145 if (!isAdoptGrant(frame)) {146 deps.onError("ADOPT_UNEXPECTED_FRAME")147 return null148 }149 val grant = frame as AdoptionFrame.Grant150151 val rootKeyBytes = hexToPubkey(grant.rootKeyHex)152 try {153 if (computeVaultId(rootKeyBytes) != grant.vaultId) {154 deps.onError("ADOPT_VAULT_ID_MISMATCH")155 return null156 }157 } finally {158 rootKeyBytes.fill(0)159 }160161 val vaultId = deps.createVaultFromRootKey(grant.rootKeyHex)162 if (vaultId != grant.vaultId) {163 deps.onError("ADOPT_VAULT_ID_MISMATCH")164 return null165 }166167 grant.license?.let { deps.applyLicense(it) }168169 val identity = deps.deriveLocalIdentity()170 sendFrame(channel, AdoptionFrame.Identity(171 deviceId = identity.deviceId,172 deviceName = identity.deviceName,173 deviceType = identity.deviceType.wireValue,174 longTermPublicKeyHex = identity.longTermPublicKeyHex,175 signingPublicKeyHex = identity.signingPublicKeyHex,176 ))177178 val sourceDevice = PairedDevice(179 deviceId = grant.source.deviceId,180 deviceName = grant.source.deviceName,181 deviceType = grant.source.deviceType,182 longTermPublicKeyHex = grant.source.longTermPublicKeyHex,183 signingPublicKeyHex = grant.source.signingPublicKeyHex,184 trustSource = "direct",185 wsPort = grant.source.wsPort,186 pairedAt = nowMs(),187 )188 deps.upsertPairedDevice(sourceDevice)189 return sourceDevice190}191192193194195196197suspend fun grantAdoption(198 channel: PairingTransport,199 request: AdoptionFrame.Request,200 deps: AdoptionSourceDeps,201 nowMs: () -> Long = { System.currentTimeMillis() },202): PairedDevice? {203 if (!deps.onConsent(request)) return null204205 val rootKeyHex = deps.rootKeyHex()206 if (!validateRootKeyHex(rootKeyHex)) throw IllegalStateException("ADOPT_INVALID_ROOT_KEY")207208 sendFrame(channel, AdoptionFrame.Grant(209 rootKeyHex = rootKeyHex,210 vaultId = deps.vaultId(),211 license = deps.license(),212 source = AdoptionFrame.Source(213 deviceId = deps.sourceIdentity.deviceId,214 deviceName = deps.sourceIdentity.deviceName,215 deviceType = deps.sourceIdentity.deviceType.wireValue,216 longTermPublicKeyHex = deps.sourceIdentity.longTermPublicKeyHex,217 signingPublicKeyHex = deps.sourceIdentity.signingPublicKeyHex,218 wsPort = deps.sourceIdentity.wsPort,219 ),220 ))221222 val identity = receiveFrame(channel)223 if (!isAdoptIdentity(identity)) return null224 identity as AdoptionFrame.Identity225226 val device = PairedDevice(227 deviceId = identity.deviceId,228 deviceName = identity.deviceName,229 deviceType = identity.deviceType,230 longTermPublicKeyHex = identity.longTermPublicKeyHex,231 signingPublicKeyHex = identity.signingPublicKeyHex,232 trustSource = "direct",233 pairedAt = nowMs(),234 )235 deps.upsertPairedDevice(device)236 return device237}238239240241242243244val AdoptJson: Json = Json {245 classDiscriminator = "type"246 encodeDefaults = false247 ignoreUnknownKeys = true248}249250suspend fun sendFrame(channel: PairingTransport, frame: AdoptionFrame) {251 channel.send(AdoptJson.encodeToString(AdoptionFrame.serializer(), frame).toByteArray(Charsets.UTF_8))252}253254255suspend fun receiveFrame(channel: PairingTransport): AdoptionFrame? =256 runCatching {257 AdoptJson.decodeFromString(AdoptionFrame.serializer(), String(channel.receive(), Charsets.UTF_8))258 }.getOrNull()259260private fun isDeviceType(value: String): Boolean =261 DeviceType.entries.any { it.wireValue == value }