1package com.peachpasswords.sync23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5import kotlinx.serialization.json.Json6import java.nio.charset.StandardCharsets7import java.security.MessageDigest89101112131415161718data class PairIdentity(19 val deviceId: String,20 val deviceName: String,21 val deviceType: DeviceType,22 val longTermPublicKeyHex: String,23 val signingPublicKeyHex: String?,24 val wsPort: Int?,25 val relayUrl: String?,26)272829303132333435363738@Serializable39private data class PairIdentityFields(40 val type: String = "PAIR_IDENTITY",41 @SerialName("device_id") val deviceId: String,42 @SerialName("device_name") val deviceName: String,43 @SerialName("device_type") val deviceType: String,44 @SerialName("long_term_public_key_hex") val longTermPublicKeyHex: String,45 @SerialName("signing_public_key_hex") val signingPublicKeyHex: String? = null,46 @SerialName("ws_port") val wsPort: Int? = null,47 @SerialName("relay_url") val relayUrl: String? = null,48)4950515253private const val MAX_DEVICE_ID = 6454private const val MAX_DEVICE_NAME = MAX_DEVICE_NAME_CHARS55private const val MAX_PUBKEY_HEX = 6456private const val MAX_RELAY_URL = 20485758private val PairIdentityJson = Json {59 encodeDefaults = true60 ignoreUnknownKeys = true61}6263internal fun encodePairIdentityFrame(id: LocalDeviceIdentity): ByteArray {64 validateField(id.deviceId, "device_id", MAX_DEVICE_ID)65 validateField(id.deviceName, "device_name", MAX_DEVICE_NAME)66 validateHex(id.longTermPublicKeyHex, "long_term_public_key_hex", 32)67 id.signingPublicKeyHex?.let { validateHex(it, "signing_public_key_hex", 32) }68 validateField(id.relayUrl, "relay_url", MAX_RELAY_URL)69 val fields = PairIdentityFields(70 deviceId = id.deviceId,71 deviceName = id.deviceName,72 deviceType = id.deviceType.wireValue,73 longTermPublicKeyHex = id.longTermPublicKeyHex,74 signingPublicKeyHex = id.signingPublicKeyHex,75 wsPort = id.wsPort,76 relayUrl = id.relayUrl,77 )78 return PairIdentityJson.encodeToString(PairIdentityFields.serializer(), fields)79 .toByteArray(StandardCharsets.UTF_8)80}8182internal fun parsePairIdentityFrame(bytes: ByteArray): PairIdentity {83 val fields = PairIdentityJson.decodeFromString(84 PairIdentityFields.serializer(),85 String(bytes, StandardCharsets.UTF_8),86 )87 require(fields.type == "PAIR_IDENTITY") { "expected PAIR_IDENTITY frame" }88 validateField(fields.deviceId, "device_id", MAX_DEVICE_ID)89 validateField(fields.deviceName, "device_name", MAX_DEVICE_NAME)90 validateHex(fields.longTermPublicKeyHex, "long_term_public_key_hex", 32)91 fields.signingPublicKeyHex?.takeIf { it.isNotBlank() }92 ?.let { validateHex(it, "signing_public_key_hex", 32) }93 val deviceType = when (fields.deviceType) {94 "extension" -> DeviceType.EXTENSION95 "android" -> DeviceType.ANDROID96 "ios" -> DeviceType.IOS97 else -> error("device_type invalid: ${fields.deviceType}")98 }99 fields.relayUrl?.let { validateField(it, "relay_url", MAX_RELAY_URL) }100 return PairIdentity(101 deviceId = fields.deviceId,102 deviceName = fields.deviceName,103 deviceType = deviceType,104 longTermPublicKeyHex = fields.longTermPublicKeyHex,105 signingPublicKeyHex = fields.signingPublicKeyHex?.takeIf { it.isNotBlank() },106 wsPort = fields.wsPort,107 relayUrl = fields.relayUrl?.let(Rendezvous::normalizeRelayUrl),108 )109}110111private fun validateField(value: String, name: String, maxLen: Int) {112 require(value.isNotEmpty()) { "$name missing" }113 require(value.length <= maxLen) { "$name exceeds $maxLen chars" }114 115 116 117 118 for (ch in value) {119 require(ch.code != 0x00) { "$name contains NUL" }120 }121}122123private fun validateHex(value: String, name: String, byteLen: Int) {124 require(value.length == byteLen * 2) { "$name not $byteLen-byte hex" }125 require(value.all { it in '0'..'9' || it in 'a'..'f' }) { "$name not lowercase hex" }126}127128129130131132133internal fun identityMatchesAuthenticatedPeer(134 known: PairedDevice,135 presented: PairIdentity,136 authenticatedKey: ByteArray,137): Boolean {138 val expected = known.longTermPublicKeyHex ?: return false139 return known.deviceId == presented.deviceId &&140 CryptoCompare.hex(expected, presented.longTermPublicKeyHex) &&141 MessageDigest.isEqual(authenticatedKey, hexToPubkey(presented.longTermPublicKeyHex))142}