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_CONFIRM")43 data class Confirm(44 val confirmed: Boolean,45 ) : AdoptionFrame()4647 @Serializable48 @SerialName("ADOPT_GRANT")49 data class Grant(50 @SerialName("root_key_hex") val rootKeyHex: String,51 @SerialName("vault_id") val vaultId: String,52 val license: String? = null,53 val source: Source,54 ) : AdoptionFrame()5556 @Serializable57 @SerialName("ADOPT_IDENTITY")58 data class Identity(59 @SerialName("device_id") val deviceId: String,60 @SerialName("device_name") val deviceName: String,61 @SerialName("device_type") val deviceType: String,62 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,63 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,64 ) : AdoptionFrame()6566 @Serializable67 data class Source(68 @SerialName("device_id") val deviceId: String,69 @SerialName("device_name") val deviceName: String,70 @SerialName("device_type") val deviceType: String,71 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,72 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,73 @SerialName("ws_port") val wsPort: Int? = null,74 )75}767778interface AdoptionReceiverDeps {79 val deviceName: String80 val deviceType: DeviceType81 fun generateDeviceId(): String82 83 suspend fun confirmSas(): Boolean8485 86 suspend fun createVaultFromRootKey(rootKeyHex: String): String87 suspend fun deriveLocalIdentity(): LocalChirpIdentity88 suspend fun upsertPairedDevice(device: PairedDevice)89 suspend fun applyLicense(license: String) {}90 fun onError(error: String)91}929394interface AdoptionSourceDeps {95 val sourceIdentity: LocalChirpIdentity96 suspend fun rootKeyHex(): String97 suspend fun vaultId(): String98 suspend fun license(): String? = null99100 101102103104105106 suspend fun onConsent(request: AdoptionFrame.Request): Boolean107 suspend fun upsertPairedDevice(device: PairedDevice)108}109110private val Hex64 = Regex("^[0-9a-f]{64}\$")111112fun validateRootKeyHex(rootKeyHex: String?): Boolean = rootKeyHex != null && Hex64.matches(rootKeyHex)113114fun isAdoptRequest(frame: AdoptionFrame?): Boolean =115 frame is AdoptionFrame.Request &&116 frame.sessionRequestId.isNotEmpty() &&117 isDeviceType(frame.deviceType)118119fun isAdoptGrant(frame: AdoptionFrame?): Boolean =120 frame is AdoptionFrame.Grant &&121 validateRootKeyHex(frame.rootKeyHex) &&122 Hex64.matches(frame.vaultId) &&123 isDeviceType(frame.source.deviceType) &&124 Hex64.matches(frame.source.longTermPublicKeyHex)125126fun isAdoptConfirm(frame: AdoptionFrame?): Boolean =127 frame is AdoptionFrame.Confirm128129fun isAdoptIdentity(frame: AdoptionFrame?): Boolean =130 frame is AdoptionFrame.Identity &&131 frame.deviceId.isNotEmpty() &&132 isDeviceType(frame.deviceType) &&133 Hex64.matches(frame.longTermPublicKeyHex)134135136137138139140141142suspend fun requestAdoption(143 channel: PairingTransport,144 deps: AdoptionReceiverDeps,145 nowMs: () -> Long = { System.currentTimeMillis() },146): PairedDevice? {147 148 149 sendFrame(channel, AdoptionFrame.Request(150 sessionRequestId = deps.generateDeviceId(),151 deviceName = deps.deviceName,152 deviceType = deps.deviceType.wireValue,153 ))154155 val confirmed = deps.confirmSas()156 sendFrame(channel, AdoptionFrame.Confirm(confirmed))157 if (!confirmed) {158 deps.onError("ADOPT_SAS_REJECTED")159 return null160 }161162 val frame = receiveFrame(channel)163 if (!isAdoptGrant(frame)) {164 deps.onError("ADOPT_UNEXPECTED_FRAME")165 return null166 }167 val grant = frame as AdoptionFrame.Grant168169 val rootKeyBytes = hexToPubkey(grant.rootKeyHex)170 try {171 if (computeVaultId(rootKeyBytes) != grant.vaultId) {172 deps.onError("ADOPT_VAULT_ID_MISMATCH")173 return null174 }175 } finally {176 rootKeyBytes.fill(0)177 }178179 val vaultId = deps.createVaultFromRootKey(grant.rootKeyHex)180 if (vaultId != grant.vaultId) {181 deps.onError("ADOPT_VAULT_ID_MISMATCH")182 return null183 }184185 grant.license?.let { deps.applyLicense(it) }186187 val identity = deps.deriveLocalIdentity()188 sendFrame(channel, AdoptionFrame.Identity(189 deviceId = identity.deviceId,190 deviceName = identity.deviceName,191 deviceType = identity.deviceType.wireValue,192 longTermPublicKeyHex = identity.longTermPublicKeyHex,193 signingPublicKeyHex = identity.signingPublicKeyHex,194 ))195196 val sourceDevice = PairedDevice(197 deviceId = grant.source.deviceId,198 deviceName = grant.source.deviceName,199 deviceType = grant.source.deviceType,200 longTermPublicKeyHex = grant.source.longTermPublicKeyHex,201 signingPublicKeyHex = grant.source.signingPublicKeyHex,202 trustSource = "direct",203 wsPort = grant.source.wsPort,204 pairedAt = nowMs(),205 )206 deps.upsertPairedDevice(sourceDevice)207 return sourceDevice208}209210211212213214215suspend fun grantAdoption(216 channel: PairingTransport,217 request: AdoptionFrame.Request,218 deps: AdoptionSourceDeps,219 nowMs: () -> Long = { System.currentTimeMillis() },220): PairedDevice? {221 if (!deps.onConsent(request)) return null222223 val confirmation = receiveFrame(channel)224 if (!isAdoptConfirm(confirmation) || !(confirmation as AdoptionFrame.Confirm).confirmed) {225 return null226 }227228 val rootKeyHex = deps.rootKeyHex()229 if (!validateRootKeyHex(rootKeyHex)) throw IllegalStateException("ADOPT_INVALID_ROOT_KEY")230231 sendFrame(channel, AdoptionFrame.Grant(232 rootKeyHex = rootKeyHex,233 vaultId = deps.vaultId(),234 license = deps.license(),235 source = AdoptionFrame.Source(236 deviceId = deps.sourceIdentity.deviceId,237 deviceName = deps.sourceIdentity.deviceName,238 deviceType = deps.sourceIdentity.deviceType.wireValue,239 longTermPublicKeyHex = deps.sourceIdentity.longTermPublicKeyHex,240 signingPublicKeyHex = deps.sourceIdentity.signingPublicKeyHex,241 wsPort = deps.sourceIdentity.wsPort,242 ),243 ))244245 val identity = receiveFrame(channel)246 if (!isAdoptIdentity(identity)) return null247 identity as AdoptionFrame.Identity248249 val device = PairedDevice(250 deviceId = identity.deviceId,251 deviceName = identity.deviceName,252 deviceType = identity.deviceType,253 longTermPublicKeyHex = identity.longTermPublicKeyHex,254 signingPublicKeyHex = identity.signingPublicKeyHex,255 trustSource = "direct",256 pairedAt = nowMs(),257 )258 deps.upsertPairedDevice(device)259 return device260}261262263264265266267val AdoptJson: Json = Json {268 classDiscriminator = "type"269 encodeDefaults = false270 ignoreUnknownKeys = true271}272273suspend fun sendFrame(channel: PairingTransport, frame: AdoptionFrame) {274 channel.send(AdoptJson.encodeToString(AdoptionFrame.serializer(), frame).toByteArray(Charsets.UTF_8))275}276277278suspend fun receiveFrame(channel: PairingTransport): AdoptionFrame? =279 runCatching {280 AdoptJson.decodeFromString(AdoptionFrame.serializer(), String(channel.receive(), Charsets.UTF_8))281 }.getOrNull()282283private fun isDeviceType(value: String): Boolean =284 DeviceType.entries.any { it.wireValue == value }