package com.peachpasswords.sync class ChirpDecodeError(kind: Kind, message: String) : Error(message) { enum class Kind { LENGTH, MAGIC, VERSION, DEVICE_TYPE, CHECKSUM, PADDING, } val kind: Kind = kind } data class ChirpPayload( val version: Int, val deviceType: Byte, val ephemeralPublicKey: ByteArray, val sessionNonce: ByteArray, ) fun encodeChirpPayload(payload: ChirpPayload): ByteArray { require(payload.ephemeralPublicKey.size == 32) { "ephemeralPublicKey must be 32 bytes" } require(payload.sessionNonce.size == 16) { "sessionNonce must be 16 bytes" } val buf = ByteArray(CHIRP_PAYLOAD_LENGTH) CHIRP_MAGIC_BYTES.copyInto(buf, 0) buf[4] = (payload.version and 0xff).toByte() buf[5] = payload.deviceType payload.ephemeralPublicKey.copyInto(buf, 6) payload.sessionNonce.copyInto(buf, 38) val crc = crc32(buf.copyOfRange(0, 54)) buf[54] = ((crc ushr 24) and 0xff).toByte() buf[55] = ((crc ushr 16) and 0xff).toByte() buf[56] = ((crc ushr 8) and 0xff).toByte() buf[57] = (crc and 0xff).toByte() return buf } fun decodeChirpPayload(buf: ByteArray): ChirpPayload { if (buf.size != CHIRP_PAYLOAD_LENGTH) { throw ChirpDecodeError( ChirpDecodeError.Kind.LENGTH, "chirp payload must be $CHIRP_PAYLOAD_LENGTH bytes, got ${buf.size}", ) } for (i in 0 until 4) { if (buf[i] != CHIRP_MAGIC_BYTES[i]) { throw ChirpDecodeError(ChirpDecodeError.Kind.MAGIC, "magic bytes are not PEAC") } } val version = buf[4].toInt() and 0xff if (version != CHIRP_PROTOCOL_VERSION) { throw ChirpDecodeError(ChirpDecodeError.Kind.VERSION, "unsupported chirp protocol version $version") } val deviceType = buf[5] if ((deviceType.toInt() and 0xff) !in 0x01..0x04) { throw ChirpDecodeError( ChirpDecodeError.Kind.DEVICE_TYPE, "unknown device type byte 0x${(deviceType.toInt() and 0xff).toString(16)}", ) } val declared = ((buf[54].toInt() and 0xff) shl 24) or ((buf[55].toInt() and 0xff) shl 16) or ((buf[56].toInt() and 0xff) shl 8) or (buf[57].toInt() and 0xff) val computed = crc32(buf.copyOfRange(0, 54)) if (declared != computed) { throw ChirpDecodeError(ChirpDecodeError.Kind.CHECKSUM, "CRC32 mismatch") } for (i in 58 until CHIRP_PAYLOAD_LENGTH) { if (buf[i] != 0.toByte()) { throw ChirpDecodeError(ChirpDecodeError.Kind.PADDING, "reserved padding bytes must be zero") } } return ChirpPayload( version = version, deviceType = deviceType, ephemeralPublicKey = buf.copyOfRange(6, 38), sessionNonce = buf.copyOfRange(38, 54), ) } private var crcTable: IntArray? = null private fun buildCrcTable(): IntArray { val table = IntArray(256) for (i in 0 until 256) { var c = i for (k in 0 until 8) { c = if ((c and 1) != 0) (0xedb88320.toInt() xor (c ushr 1)) else (c ushr 1) } table[i] = c } return table } fun crc32(bytes: ByteArray): Int { if (crcTable == null) crcTable = buildCrcTable() var crc = -1 val table = crcTable!! for (b in bytes) { crc = table[(crc xor (b.toInt() and 0xff)) and 0xff] xor (crc ushr 8) } return crc xor -1 }