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 23242526 val deviceManifestVersion: Int? = null,27 ) : SyncMessage()2829 @Serializable30 @SerialName("SYNC_MANIFEST")31 data class Manifest(32 val entries: Map<String, Long>,33 val tombstones: Map<String, Long>,34 ) : SyncMessage()3536 @Serializable37 @SerialName("SYNC_REQUEST")38 data class Request(val entryIds: List<String>) : SyncMessage()3940 @Serializable41 @SerialName("SYNC_ENTRIES")42 data class Entries(val entries: Map<String, String>) : SyncMessage()4344 @Serializable45 @SerialName("SYNC_TOMBSTONES")46 data class Tombstones(val tombstones: Map<String, Long>) : SyncMessage()4748 @Serializable49 @SerialName("SYNC_DEVICE_MANIFEST")50 data class DeviceManifest(val manifest: SignedDeviceManifest?) : SyncMessage()5152 @Serializable53 @SerialName("SYNC_DONE")54 data class Done(55 val entriesReceived: Int = 0,56 val entriesSent: Int = 0,57 ) : SyncMessage()5859 @Serializable60 @SerialName("SYNC_ERROR")61 data class Error(val error: String) : SyncMessage()62}6364@Serializable65data class ManifestDeviceEntry(66 val deviceId: String,67 val deviceName: String,68 val deviceType: String,69 val longTermPublicKeyHex: String,70 val signingPublicKeyHex: String,71 val wsPort: Int? = null,72 val relayUrl: String? = null,73 val pairedAt: Long,74)7576@Serializable77data class DeviceManifestBody(78 val manifestVersion: Int,79 val issuerDeviceId: String,80 val issuedAt: Long,81 val devices: List<ManifestDeviceEntry>,82 val revoked: List<DeviceRevocation> = emptyList(),83)84858687888990919293949596979899100101@Serializable102data class DeviceRevocation(103 val deviceId: String,104 105106107 val longTermPublicKeyHex: String,108 val revokedAt: Long,109 val revokedByDeviceId: String,110)111112@Serializable113data class SignedDeviceManifest(114 val body: DeviceManifestBody,115 116 val signatureHex: String,117)118119120121122123124val SyncJson: Json = Json {125 classDiscriminator = "type"126 encodeDefaults = true127 ignoreUnknownKeys = true128}129130fun SyncMessage.encodeToBytes(): ByteArray =131 SyncJson.encodeToString(SyncMessage.serializer(), this).toByteArray(Charsets.UTF_8)132133fun decodeSyncMessage(bytes: ByteArray): SyncMessage =134 SyncJson.decodeFromString(SyncMessage.serializer(), String(bytes, Charsets.UTF_8))135136137138139140141142143144145146fun SyncMessage.requireBounded() {147 fun checkIds(ids: Collection<String>, cap: Int, what: String) {148 if (ids.size > cap) throw SyncStructureTooLargeException(what)149 if (ids.any { it.length > SyncLimits.MAX_ID_LENGTH }) {150 throw SyncStructureTooLargeException("$what identifier length")151 }152 }153 when (this) {154 is SyncMessage.Manifest -> {155 checkIds(entries.keys, SyncLimits.MAX_ENTRY_COUNT, "manifest entries")156 checkIds(tombstones.keys, SyncLimits.MAX_TOMBSTONE_COUNT, "manifest tombstones")157 }158 is SyncMessage.Request -> checkIds(entryIds, SyncLimits.MAX_REQUEST_ENTRY_COUNT, "request ids")159 is SyncMessage.Entries -> {160 checkIds(entries.keys, SyncLimits.MAX_ENTRY_COUNT, "entries")161 if (entries.values.any { it.length > SyncLimits.MAX_ENTRY_ENVELOPE_BYTES }) {162 throw SyncStructureTooLargeException("entry envelope")163 }164 }165 is SyncMessage.Tombstones -> checkIds(tombstones.keys, SyncLimits.MAX_TOMBSTONE_COUNT, "tombstones")166 else -> Unit167 }168}