1package com.peachpasswords.sync23class ChirpDecodeError(kind: Kind, message: String) : Error(message) {4 enum class Kind {5 LENGTH,6 MAGIC,7 VERSION,8 DEVICE_TYPE,9 CHECKSUM,10 PADDING,11 }1213 val kind: Kind = kind14}1516data class ChirpPayload(17 val version: Int,18 val deviceType: Byte,19 val ephemeralPublicKey: ByteArray,20 val sessionNonce: ByteArray,21)2223fun encodeChirpPayload(payload: ChirpPayload): ByteArray {24 require(payload.ephemeralPublicKey.size == 32) { "ephemeralPublicKey must be 32 bytes" }25 require(payload.sessionNonce.size == 16) { "sessionNonce must be 16 bytes" }2627 val buf = ByteArray(CHIRP_PAYLOAD_LENGTH)28 CHIRP_MAGIC_BYTES.copyInto(buf, 0)29 buf[4] = (payload.version and 0xff).toByte()30 buf[5] = payload.deviceType31 payload.ephemeralPublicKey.copyInto(buf, 6)32 payload.sessionNonce.copyInto(buf, 38)3334 val crc = crc32(buf.copyOfRange(0, 54))35 buf[54] = ((crc ushr 24) and 0xff).toByte()36 buf[55] = ((crc ushr 16) and 0xff).toByte()37 buf[56] = ((crc ushr 8) and 0xff).toByte()38 buf[57] = (crc and 0xff).toByte()39 return buf40}4142fun decodeChirpPayload(buf: ByteArray): ChirpPayload {43 if (buf.size != CHIRP_PAYLOAD_LENGTH) {44 throw ChirpDecodeError(45 ChirpDecodeError.Kind.LENGTH,46 "chirp payload must be $CHIRP_PAYLOAD_LENGTH bytes, got ${buf.size}",47 )48 }49 for (i in 0 until 4) {50 if (buf[i] != CHIRP_MAGIC_BYTES[i]) {51 throw ChirpDecodeError(ChirpDecodeError.Kind.MAGIC, "magic bytes are not PEAC")52 }53 }54 val version = buf[4].toInt() and 0xff55 if (version != CHIRP_PROTOCOL_VERSION) {56 throw ChirpDecodeError(ChirpDecodeError.Kind.VERSION, "unsupported chirp protocol version $version")57 }58 val deviceType = buf[5]59 if ((deviceType.toInt() and 0xff) !in 0x01..0x04) {60 throw ChirpDecodeError(61 ChirpDecodeError.Kind.DEVICE_TYPE,62 "unknown device type byte 0x${(deviceType.toInt() and 0xff).toString(16)}",63 )64 }65 val declared =66 ((buf[54].toInt() and 0xff) shl 24) or67 ((buf[55].toInt() and 0xff) shl 16) or68 ((buf[56].toInt() and 0xff) shl 8) or69 (buf[57].toInt() and 0xff)70 val computed = crc32(buf.copyOfRange(0, 54))71 if (declared != computed) {72 throw ChirpDecodeError(ChirpDecodeError.Kind.CHECKSUM, "CRC32 mismatch")73 }74 for (i in 58 until CHIRP_PAYLOAD_LENGTH) {75 if (buf[i] != 0.toByte()) {76 throw ChirpDecodeError(ChirpDecodeError.Kind.PADDING, "reserved padding bytes must be zero")77 }78 }79 return ChirpPayload(80 version = version,81 deviceType = deviceType,82 ephemeralPublicKey = buf.copyOfRange(6, 38),83 sessionNonce = buf.copyOfRange(38, 54),84 )85}8687private var crcTable: IntArray? = null8889private fun buildCrcTable(): IntArray {90 val table = IntArray(256)91 for (i in 0 until 256) {92 var c = i93 for (k in 0 until 8) {94 c = if ((c and 1) != 0) (0xedb88320.toInt() xor (c ushr 1)) else (c ushr 1)95 }96 table[i] = c97 }98 return table99}100101fun crc32(bytes: ByteArray): Int {102 if (crcTable == null) crcTable = buildCrcTable()103 var crc = -1104 val table = crcTable!!105 for (b in bytes) {106 crc = table[(crc xor (b.toInt() and 0xff)) and 0xff] xor (crc ushr 8)107 }108 return crc xor -1109}