1package com.peachpasswords.sync23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5import kotlinx.serialization.json.Json678910111213@Serializable14sealed class SyncMessage {15 @Serializable16 @SerialName("SYNC_HELLO")17 data class Hello(18 val vaultId: String,19 val deviceId: String,20 val version: Long,21 val deviceTime: Long,22 232425262728 val protocolVersion: Int? = null,29 30313233 val deviceManifestVersion: Int? = null,34 ) : SyncMessage()3536 @Serializable37 @SerialName("SYNC_MANIFEST")38 data class Manifest(39 val entries: Map<String, Long>,40 val tombstones: Map<String, Long>,41 ) : SyncMessage()4243 @Serializable44 @SerialName("SYNC_REQUEST")45 data class Request(val entryIds: List<String>) : SyncMessage()4647 @Serializable48 @SerialName("SYNC_ENTRIES")49 data class Entries(val entries: Map<String, String>) : SyncMessage()5051 @Serializable52 @SerialName("SYNC_TOMBSTONES")53 data class Tombstones(val tombstones: Map<String, Long>) : SyncMessage()5455 @Serializable56 @SerialName("SYNC_DEVICE_MANIFEST")57 data class DeviceManifest(val manifest: SignedDeviceManifest?) : SyncMessage()5859 @Serializable60 @SerialName("SYNC_DONE")61 data class Done(62 val entriesReceived: Int = 0,63 val entriesSent: Int = 0,64 ) : SyncMessage()6566 @Serializable67 @SerialName("SYNC_ERROR")68 data class Error(val error: String) : SyncMessage()69}7071@Serializable72data class ManifestDeviceEntry(73 val deviceId: String,74 val deviceName: String,75 val deviceType: String,76 val longTermPublicKeyHex: String,77 val signingPublicKeyHex: String,78 val wsPort: Int? = null,79 val relayUrl: String? = null,80 val pairedAt: Long,81 82 val vouchDepth: Int = 0,83)8485@Serializable86data class DeviceManifestBody(87 val manifestVersion: Int,88 val issuerDeviceId: String,89 val issuedAt: Long,90 val devices: List<ManifestDeviceEntry>,91 val revoked: List<DeviceRevocation> = emptyList(),92)93949596979899100101102103104105106107108109110@Serializable111data class DeviceRevocation(112 val deviceId: String,113 114 val longTermPublicKeyHex: String,115 val revokedAt: Long,116 val revokedByDeviceId: String,117 118119120121122 val recordVersion: Int = DEVICE_REVOCATION_RECORD_VERSION,123 124 val signatureHex: String = "",125)126127128129130131132133134135136137138const val DEVICE_REVOCATION_RECORD_VERSION = 2139140@Serializable141data class SignedDeviceManifest(142 val body: DeviceManifestBody,143 144 val signatureHex: String,145)146147148149150151152val SyncJson: Json = Json {153 classDiscriminator = "type"154 encodeDefaults = true155 ignoreUnknownKeys = true156}157158fun SyncMessage.encodeToBytes(): ByteArray =159 SyncJson.encodeToString(SyncMessage.serializer(), this).toByteArray(Charsets.UTF_8)160161fun decodeSyncMessage(bytes: ByteArray): SyncMessage =162 SyncJson.decodeFromString(SyncMessage.serializer(), String(bytes, Charsets.UTF_8))163164165166167168169170171172173174fun SyncMessage.requireBounded() {175 fun checkIds(ids: Collection<String>, cap: Int, what: String) {176 if (ids.size > cap) throw SyncStructureTooLargeException(what)177 if (ids.any { it.length > SyncLimits.MAX_ID_LENGTH }) {178 throw SyncStructureTooLargeException("$what identifier length")179 }180 }181 when (this) {182 is SyncMessage.Manifest -> {183 checkIds(entries.keys, SyncLimits.MAX_ENTRY_COUNT, "manifest entries")184 checkIds(tombstones.keys, SyncLimits.MAX_TOMBSTONE_COUNT, "manifest tombstones")185 }186 is SyncMessage.Request -> checkIds(entryIds, SyncLimits.MAX_REQUEST_ENTRY_COUNT, "request ids")187 is SyncMessage.Entries -> {188 checkIds(entries.keys, SyncLimits.MAX_ENTRY_COUNT, "entries")189 if (entries.values.any { it.length > SyncLimits.MAX_ENTRY_ENVELOPE_BYTES }) {190 throw SyncStructureTooLargeException("entry envelope")191 }192 }193 is SyncMessage.Tombstones -> checkIds(tombstones.keys, SyncLimits.MAX_TOMBSTONE_COUNT, "tombstones")194 else -> Unit195 }196}