rs-matter 0.3.0

Native Rust implementation of the Matter (Smart-Home) ecosystem
Documentation
/*
 *
 *    Copyright (c) 2026 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

//! Message Counter Synchronization Protocol (MCSP).
//!
//! MCSP synchronizes the message counter used by senders of messages
//! encrypted with a symmetric operational group key. It has two message
//! types, both carried over
//! [`PROTO_ID_SECURE_CHANNEL`](crate::sc::PROTO_ID_SECURE_CHANNEL) and
//! both sent via unicast UDP, secured with the operational group key
//! being synchronized (Session Type "Group Session"), with the
//! Security-Flags `C` bit set:
//!
//! * `MsgCounterSyncReq` (opcode `0x00`) — 8-byte random challenge.
//! * `MsgCounterSyncRsp` (opcode `0x01`) — 4-byte little-endian
//!   synchronized counter followed by the 8-byte challenge echoed back.
//!
//! This module implements the wire codecs and the responder side. The
//! initiator side is not implemented — this crate does not yet send
//! group data messages nor track per-peer sync state.

use crate::crypto::Crypto;
use crate::error::{Error, ErrorCode};
use crate::fmt::Bytes;
use crate::sc::{check_opcode, OpCode};
use crate::transport::exchange::Exchange;
use crate::transport::session::SessionMode;
use crate::utils::storage::WriteBuf;

/// Length of the challenge / response field in the MCSP messages, in bytes.
pub const MCSP_CHALLENGE_LEN: usize = 8;

/// On-wire length of a `MsgCounterSyncReq` payload (just the 8-byte challenge).
pub const MCSP_SYNC_REQ_LEN: usize = MCSP_CHALLENGE_LEN;

/// On-wire length of a `MsgCounterSyncRsp` payload (4-byte counter +
/// 8-byte echoed challenge).
pub const MCSP_SYNC_RSP_LEN: usize = 4 + MCSP_CHALLENGE_LEN;

/// A `MsgCounterSyncReq` payload: a single 64-bit random challenge
/// generated by the initiator to identify the exchange cryptographically.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MsgCounterSyncReq {
    pub challenge: [u8; MCSP_CHALLENGE_LEN],
}

impl MsgCounterSyncReq {
    /// Parse a `MsgCounterSyncReq` payload. Rejects any length other
    /// than [`MCSP_SYNC_REQ_LEN`].
    pub fn read(payload: &[u8]) -> Result<Self, Error> {
        if payload.len() != MCSP_SYNC_REQ_LEN {
            return Err(ErrorCode::InvalidData.into());
        }
        let mut challenge = [0u8; MCSP_CHALLENGE_LEN];
        challenge.copy_from_slice(&payload[..MCSP_CHALLENGE_LEN]);
        Ok(Self { challenge })
    }

    /// Encode the request into the provided [`WriteBuf`].
    pub fn write(&self, wb: &mut WriteBuf<'_>) -> Result<(), Error> {
        wb.copy_from_slice(&self.challenge)?;
        Ok(())
    }
}

/// A `MsgCounterSyncRsp` payload: the sender's current group data
/// message counter and the 8-byte challenge echoed back from the
/// corresponding `MsgCounterSyncReq`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MsgCounterSyncRsp {
    pub synchronized_counter: u32,
    pub response: [u8; MCSP_CHALLENGE_LEN],
}

impl MsgCounterSyncRsp {
    /// Parse a `MsgCounterSyncRsp` payload. Rejects any length other
    /// than [`MCSP_SYNC_RSP_LEN`].
    ///
    /// A `synchronized_counter` of `0` is semantically invalid (the
    /// initiator must silently ignore such a reply) but is not checked
    /// here — this decoder is purely syntactic.
    pub fn read(payload: &[u8]) -> Result<Self, Error> {
        if payload.len() != MCSP_SYNC_RSP_LEN {
            return Err(ErrorCode::InvalidData.into());
        }
        let synchronized_counter =
            u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
        let mut response = [0u8; MCSP_CHALLENGE_LEN];
        response.copy_from_slice(&payload[4..4 + MCSP_CHALLENGE_LEN]);
        Ok(Self {
            synchronized_counter,
            response,
        })
    }

    /// Encode the response into the provided [`WriteBuf`].
    pub fn write(&self, wb: &mut WriteBuf<'_>) -> Result<(), Error> {
        wb.le_u32(self.synchronized_counter)?;
        wb.copy_from_slice(&self.response)?;
        Ok(())
    }
}

/// Handle an incoming `MsgCounterSyncReq` on the given exchange and
/// reply with a `MsgCounterSyncRsp` echoing the challenge and carrying
/// our current group data message counter.
///
/// The destination-Node-ID check has already been performed at the
/// receive path (a group session was created only because the packet
/// was addressed to one of our fabric identities). Here we additionally
/// require the request to be carried over a group session; unicast
/// (CASE/PASE) deliveries of MCSP are protocol errors.
pub async fn respond<C: Crypto>(crypto: C, mut exchange: Exchange<'_>) -> Result<(), Error> {
    check_opcode(&exchange, OpCode::MsgCounterSyncReq)?;

    let session_mode = exchange.with_state(|state| {
        Ok(exchange
            .id()
            .session(&mut state.sessions)
            .get_session_mode()
            .clone())
    })?;

    if !matches!(session_mode, SessionMode::Group { .. }) {
        error!("MCSP: MsgCounterSyncReq received on non-group session; dropping");
        return Err(ErrorCode::Invalid.into());
    }

    let req = {
        let rx = exchange.recv().await?;
        MsgCounterSyncReq::read(rx.payload())?
    };

    debug!(
        "MCSP: Received MsgCounterSyncReq (challenge={})",
        Bytes(&req.challenge)
    );

    let synchronized_counter =
        exchange.with_state(|state| state.sessions.get_or_init_global_group_data_ctr(crypto))?;

    let rsp = MsgCounterSyncRsp {
        synchronized_counter,
        response: req.challenge,
    };

    debug!(
        "MCSP: Sending MsgCounterSyncRsp (sync_ctr={}, response={})",
        synchronized_counter,
        Bytes(&rsp.response)
    );

    exchange
        .send_with(|_, wb| {
            rsp.write(wb)?;
            Ok(Some(OpCode::MsgCounterSyncResp.meta()))
        })
        .await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn req_roundtrip() {
        let req = MsgCounterSyncReq {
            challenge: [1, 2, 3, 4, 5, 6, 7, 8],
        };
        let mut buf = [0u8; 32];
        let mut wb = WriteBuf::new(&mut buf);
        req.write(&mut wb).unwrap();
        let slice = wb.as_slice();
        assert_eq!(slice.len(), MCSP_SYNC_REQ_LEN);
        assert_eq!(slice, &[1, 2, 3, 4, 5, 6, 7, 8]);

        let decoded = MsgCounterSyncReq::read(slice).unwrap();
        assert_eq!(decoded, req);
    }

    #[test]
    fn req_wrong_len() {
        assert!(MsgCounterSyncReq::read(&[0u8; 7]).is_err());
        assert!(MsgCounterSyncReq::read(&[0u8; 9]).is_err());
        assert!(MsgCounterSyncReq::read(&[]).is_err());
    }

    #[test]
    fn rsp_roundtrip() {
        let rsp = MsgCounterSyncRsp {
            synchronized_counter: 0x1234_5678,
            response: [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22],
        };
        let mut buf = [0u8; 32];
        let mut wb = WriteBuf::new(&mut buf);
        rsp.write(&mut wb).unwrap();
        let slice = wb.as_slice();
        assert_eq!(slice.len(), MCSP_SYNC_RSP_LEN);
        // little-endian 4-byte counter
        assert_eq!(&slice[0..4], &[0x78, 0x56, 0x34, 0x12]);
        // 8-byte response
        assert_eq!(
            &slice[4..12],
            &[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22]
        );

        let decoded = MsgCounterSyncRsp::read(slice).unwrap();
        assert_eq!(decoded, rsp);
    }

    #[test]
    fn rsp_wrong_len() {
        assert!(MsgCounterSyncRsp::read(&[0u8; 11]).is_err());
        assert!(MsgCounterSyncRsp::read(&[0u8; 13]).is_err());
        assert!(MsgCounterSyncRsp::read(&[]).is_err());
    }
}