ChirpPayload.kt

3.4 KB · raw

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}