Skip to main content

rs_matter/transport/
session.rs

1/*
2 *
3 *    Copyright (c) 2022-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18use core::fmt;
19use core::num::NonZeroU8;
20use embassy_time::Instant;
21
22use cfg_if::cfg_if;
23
24use rand_core::RngCore;
25
26use crate::crypto::{
27    canon, CanonAeadKey, CanonAeadKeyRef, CanonPkcSharedSecret, CanonPkcSharedSecretRef, Crypto,
28    CryptoSensitive, Kdf,
29};
30use crate::dm::clusters::basic_info::BasicInfoConfig;
31use crate::error::{Error, ErrorCode};
32#[cfg(feature = "groups")]
33use crate::fabric::Fabrics;
34#[cfg(feature = "groups")]
35use crate::group_keys::KeySet;
36#[cfg(feature = "groups")]
37use crate::persist::{KvBlobStore, GROUP_DATA_COUNTER_KEY};
38use crate::sc::SessionParameters;
39use crate::transport::exchange::ExchangeId;
40use crate::transport::mrp::{self, ReliableMessage};
41use crate::transport::TransportRunner;
42use crate::utils::init::{init, Init, IntoFallibleInit};
43use crate::utils::storage::{ParseBuf, Vec, WriteBuf};
44use crate::{Matter, MatterState};
45
46#[cfg(feature = "groups")]
47use super::dedup::GroupCtrStore;
48use super::dedup::RxCtrState;
49use super::exchange::{ExchangeState, MessageMeta, Role};
50use super::mrp::{mrp_log, RetransEntry};
51use super::network::Address;
52use super::packet::PacketHdr;
53use super::plain_hdr::PlainHdr;
54use super::proto_hdr::ProtoHdr;
55#[cfg(feature = "groups")]
56use super::Packet;
57
58pub const MAX_CAT_IDS_PER_NOC: usize = 3;
59pub type NocCatIds = [u32; MAX_CAT_IDS_PER_NOC];
60
61pub const ATT_CHALLENGE_LEN: usize = 16;
62
63canon!(
64    ATT_CHALLENGE_LEN,
65    ATT_CHALLENGE_ZEROED,
66    AttChallenge,
67    AttChallengeRef
68);
69
70#[derive(Debug, PartialEq, Eq, Clone, Default)]
71#[cfg_attr(feature = "defmt", derive(defmt::Format))]
72pub enum SessionMode {
73    // The Case session will capture the local fabric index
74    // and the local fabric index
75    Case {
76        fab_idx: NonZeroU8,
77        cat_ids: NocCatIds,
78    },
79    // The Pase session always starts with a fabric index of 0
80    // (i.e. no fabric) but will be upgraded to the actual fabric index
81    // once AddNOC or UpdateNOC is received
82    Pase {
83        fab_idx: u8,
84    },
85    // A group session used for group (multicast) messaging.
86    Group {
87        fab_idx: NonZeroU8,
88        group_id: u16,
89    },
90    #[default]
91    PlainText,
92}
93
94impl SessionMode {
95    pub fn fab_idx(&self) -> u8 {
96        match self {
97            SessionMode::Case { fab_idx, .. } => fab_idx.get(),
98            SessionMode::Pase { fab_idx, .. } => *fab_idx,
99            SessionMode::Group { fab_idx, .. } => fab_idx.get(),
100            SessionMode::PlainText => 0,
101        }
102    }
103}
104
105pub struct Session {
106    // Internal ID which is guaranteeed to be unique accross all sessions and not change when sessions are added/removed
107    pub(crate) id: u32,
108    peer_addr: Address,
109    local_nodeid: u64,
110    peer_nodeid: Option<u64>,
111    // I find the session initiator/responder role getting confused with exchange initiator/responder
112    // So, we might keep this as enc_key and dec_key for now
113    dec_key: CanonAeadKey,
114    enc_key: CanonAeadKey,
115    /// The ECDH shared secret computed during the original full CASE
116    /// handshake, retained on the session for the sole purpose of
117    /// populating [`crate::sc::case::ResumableSession`] records for
118    /// the resumption cache. Zeroed for non-CASE sessions.
119    ///
120    /// Kept as a field unconditionally so the `new`/`init` constructors
121    /// need no `case-resumption`-specific variant; it is simply written
122    /// but never read when resumption is off.
123    #[cfg_attr(not(feature = "case-resumption"), allow(dead_code))]
124    shared_secret: CanonPkcSharedSecret,
125    att_challenge: AttChallenge,
126    local_sess_id: u16,
127    peer_sess_id: u16,
128    msg_ctr: u32,
129    rx_ctr_state: RxCtrState,
130    mode: SessionMode,
131    pub(crate) exchanges: Vec<Option<ExchangeState>, MAX_EXCHANGES>,
132    last_use: Instant,
133    /// Peer's effective `MRP_SESSION_ACTIVE_INTERVAL` (ms) — drives our
134    /// MRP retransmission base interval when transmitting to this peer.
135    peer_active_interval_ms: u32,
136    /// Peer's effective `MRP_SESSION_IDLE_INTERVAL` (ms) — see
137    /// `peer_active_interval_ms`. Currently informational on the responder
138    /// side until idle-vs-active classification lands in the MRP code.
139    peer_idle_interval_ms: u32,
140    /// Peer's effective `MRP_SESSION_ACTIVE_THRESHOLD` (ms).
141    peer_active_threshold_ms: u16,
142    /// If `true` then the session is considered "expired". Session expiration happens
143    /// for the session on behalf of which a fabric is removed.
144    ///
145    /// Expired sessions can still process their ongoing exchanges, but do not accept any new ones.
146    /// Furthermore, expired sessions are the prime candidates for eviction.
147    expired: bool,
148    reserved: bool,
149}
150
151impl Session {
152    #[allow(clippy::too_many_arguments)]
153    pub fn new(
154        id: u32,
155        msg_ctr: u32,
156        reserved: bool,
157        peer_addr: Address,
158        peer_nodeid: Option<u64>,
159        peer_active_interval_ms: u32,
160        peer_idle_interval_ms: u32,
161        peer_active_threshold_ms: u16,
162    ) -> Self {
163        Self {
164            id,
165            reserved,
166            peer_addr,
167            local_nodeid: 0,
168            peer_nodeid,
169            dec_key: CanonAeadKey::new(),
170            enc_key: CanonAeadKey::new(),
171            shared_secret: CanonPkcSharedSecret::new(),
172            att_challenge: AttChallenge::new(),
173            peer_sess_id: 0,
174            local_sess_id: 0,
175            msg_ctr: msg_ctr & MATTER_MSG_CTR_RANGE,
176            rx_ctr_state: RxCtrState::new(0),
177            mode: SessionMode::PlainText,
178            exchanges: Vec::new(),
179            last_use: Instant::now(),
180            peer_active_interval_ms,
181            peer_idle_interval_ms,
182            peer_active_threshold_ms,
183            expired: false,
184        }
185    }
186
187    #[allow(clippy::too_many_arguments)]
188    pub fn init(
189        id: u32,
190        msg_ctr: u32,
191        reserved: bool,
192        peer_addr: Address,
193        peer_nodeid: Option<u64>,
194        peer_active_interval_ms: u32,
195        peer_idle_interval_ms: u32,
196        peer_active_threshold_ms: u16,
197    ) -> impl Init<Self> {
198        init!(Self {
199            id,
200            reserved,
201            peer_addr,
202            local_nodeid: 0,
203            peer_nodeid,
204            dec_key <- CanonAeadKey::init(),
205            enc_key <- CanonAeadKey::init(),
206            shared_secret <- CanonPkcSharedSecret::init(),
207            att_challenge <- AttChallenge::init(),
208            peer_sess_id: 0,
209            local_sess_id: 0,
210            msg_ctr: msg_ctr & MATTER_MSG_CTR_RANGE,
211            rx_ctr_state: RxCtrState::new(0),
212            mode: SessionMode::PlainText,
213            exchanges <- Vec::init(),
214            last_use: Instant::now(),
215            peer_active_interval_ms,
216            peer_idle_interval_ms,
217            peer_active_threshold_ms,
218            expired: false,
219        })
220    }
221
222    /// Get the internal ID of the session
223    /// This ID is guaranteed to be unique across all sessions
224    pub const fn id(&self) -> u32 {
225        self.id
226    }
227
228    pub fn get_local_sess_id(&self) -> u16 {
229        self.local_sess_id
230    }
231
232    #[cfg(test)]
233    pub fn set_local_sess_id(&mut self, sess_id: u16) {
234        self.local_sess_id = sess_id;
235    }
236
237    pub(crate) fn set_local_nodeid(&mut self, nodeid: u64) {
238        self.local_nodeid = nodeid;
239    }
240
241    pub fn get_peer_sess_id(&self) -> u16 {
242        self.peer_sess_id
243    }
244
245    pub fn get_peer_addr(&self) -> Address {
246        self.peer_addr
247    }
248
249    pub fn is_encrypted(&self) -> bool {
250        match self.mode {
251            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => true,
252            SessionMode::PlainText => false,
253        }
254    }
255
256    pub fn get_peer_node_id(&self) -> Option<u64> {
257        self.peer_nodeid
258    }
259
260    pub fn get_local_fabric_idx(&self) -> u8 {
261        self.mode.fab_idx()
262    }
263
264    pub fn get_session_mode(&self) -> &SessionMode {
265        &self.mode
266    }
267
268    #[cfg(feature = "groups")]
269    pub(crate) fn set_session_mode(&mut self, mode: SessionMode) {
270        self.mode = mode;
271    }
272
273    pub fn get_peer_active_interval_ms(&self) -> u32 {
274        self.peer_active_interval_ms
275    }
276
277    pub fn get_peer_idle_interval_ms(&self) -> u32 {
278        self.peer_idle_interval_ms
279    }
280
281    pub fn get_peer_active_threshold_ms(&self) -> u16 {
282        self.peer_active_threshold_ms
283    }
284
285    /// Record the peer's `session_parameters` (any combination of `sai` /
286    /// `sii` / `sat`) for use by MRP retransmission timing on later sends
287    /// to this peer. Only the fields the peer actually advertised get
288    /// overwritten; absent fields leave the seeded default in place so
289    /// repeated handshakes don't clobber a stronger earlier hint with a
290    /// later-but-emptier one.
291    ///
292    /// `Some(0)` is dropped (with a warning) — this method runs on
293    /// Sigma1 / PBKDFParamRequest TLV from an unauthenticated peer, and
294    /// an "interval" of zero would either collapse the MRP backoff to
295    /// a tight retransmit loop or, in the SAT case, mark the session
296    /// active for zero milliseconds. Neither is a legitimate value, so
297    /// rejecting them protects against a trivial pre-auth DoS.
298    pub(crate) fn set_peer_session_params(&mut self, params: &SessionParameters) {
299        if let Some(sai) = params.sai {
300            if sai > 0 {
301                self.peer_active_interval_ms = sai;
302            } else {
303                warn!("Peer advertised session_parameters.sai=0; ignoring");
304            }
305        }
306
307        if let Some(sii) = params.sii {
308            if sii > 0 {
309                self.peer_idle_interval_ms = sii;
310            } else {
311                warn!("Peer advertised session_parameters.sii=0; ignoring");
312            }
313        }
314
315        if let Some(sat) = params.sat {
316            if sat > 0 {
317                self.peer_active_threshold_ms = sat;
318            } else {
319                warn!("Peer advertised session_parameters.sat=0; ignoring");
320            }
321        }
322    }
323
324    fn get_msg_ctr(&mut self) -> u32 {
325        let ctr = self.msg_ctr;
326        self.msg_ctr += 1;
327        ctr
328    }
329
330    pub fn get_dec_key(&self) -> Option<CanonAeadKeyRef<'_>> {
331        match self.mode {
332            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => {
333                Some(self.dec_key.reference())
334            }
335            SessionMode::PlainText => None,
336        }
337    }
338
339    pub fn get_enc_key(&self) -> Option<CanonAeadKeyRef<'_>> {
340        match self.mode {
341            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => {
342                Some(self.enc_key.reference())
343            }
344            SessionMode::PlainText => None,
345        }
346    }
347
348    /// The ECDH shared secret from the original full CASE handshake,
349    /// or `None` for non-CASE sessions. Consumed by the background
350    /// snapshot task to build [`crate::sc::case::ResumableSession`]
351    /// records — the "SharedSecret" that the Matter spec lists as
352    /// part of the Session Resumption State.
353    #[cfg(feature = "case-resumption")]
354    #[allow(dead_code)]
355    pub fn get_shared_secret(&self) -> Option<CanonPkcSharedSecretRef<'_>> {
356        match self.mode {
357            SessionMode::Case { .. } => Some(self.shared_secret.reference()),
358            SessionMode::Pase { .. } | SessionMode::Group { .. } | SessionMode::PlainText => None,
359        }
360    }
361
362    pub fn get_att_challenge(&self) -> Option<AttChallengeRef<'_>> {
363        match self.mode {
364            SessionMode::Case { .. } | SessionMode::Pase { .. } => {
365                Some(self.att_challenge.reference())
366            }
367            SessionMode::PlainText | SessionMode::Group { .. } => None,
368        }
369    }
370
371    /// Whether this is a CASE session to the given peer node ID and fabric index.
372    pub(crate) fn is_for_node(&self, fabric_idx: NonZeroU8, peer_node_id: u64) -> bool {
373        self.get_local_fabric_idx() == fabric_idx.get()
374            && self.peer_nodeid == Some(peer_node_id)
375            && self.is_encrypted()
376            && !self.reserved
377    }
378
379    /// Whether this is a PASE session to the given peer address.
380    ///
381    /// PASE sessions are all keyed at fabric 0 / node 0 (no operational
382    /// identity yet), so the peer *address* is what distinguishes one PASE
383    /// session from another - which matters on a commissioner that may have
384    /// several PASE sessions (to different devices) in flight at once.
385    pub(crate) fn is_pase_for_addr(&self, peer_addr: &Address) -> bool {
386        matches!(self.mode, SessionMode::Pase { .. })
387            && self.peer_addr.canonical() == peer_addr.canonical()
388            && !self.reserved
389    }
390
391    pub(crate) fn is_for_rx(&self, rx_peer: &Address, rx_plain: &PlainHdr) -> bool {
392        let nodeid_matches = self.peer_nodeid.is_none()
393            || rx_plain.get_src_nodeid().is_none()
394            || self.peer_nodeid == rx_plain.get_src_nodeid();
395
396        // For unsecured sessions, also match by destination node ID (the echoed
397        // ephemeral initiator node ID) to disambiguate multiple unsecured sessions
398        // for the same peer (spec).
399        let dest_nodeid_matches = self.is_encrypted()
400            || self.local_nodeid == 0
401            || rx_plain.get_dst_unicast_nodeid().is_none()
402            || rx_plain.get_dst_unicast_nodeid() == Some(self.local_nodeid);
403
404        nodeid_matches
405            && dest_nodeid_matches
406            && self.local_sess_id == rx_plain.sess_id
407            // Compare canonically: a dual-stack socket may report a peer as
408            // `::ffff:a.b.c.d` on receive while the session stored the plain
409            // `V4` address it was created with (or vice versa). Canonicalizing
410            // only the *comparison* (not the stored address) lets the two match
411            // without disturbing the address used for reply routing. See
412            // `Address::canonical`.
413            && self.peer_addr.canonical() == rx_peer.canonical()
414            && self.is_encrypted() == rx_plain.is_encrypted()
415            && !self.reserved
416    }
417
418    pub(crate) fn is_for_tx(&self, session_id: u32) -> bool {
419        self.id == session_id
420    }
421
422    /// Return `true` if the session is expired.
423    pub(crate) fn is_expired(&self) -> bool {
424        self.expired
425    }
426
427    pub fn upgrade_fabric_idx(&mut self, fabric_idx: NonZeroU8) -> Result<(), Error> {
428        if let SessionMode::Pase { fab_idx } = &mut self.mode {
429            if *fab_idx == 0 {
430                *fab_idx = fabric_idx.get();
431            } else {
432                // Upgrading a PASE session can happen only once
433                Err(ErrorCode::Invalid)?;
434            }
435        } else {
436            // CASE sessions are not upgradeable, as per spec
437            // And for plain text sessions - we shoudn't even get here in the first place
438            Err(ErrorCode::Invalid)?;
439        }
440
441        Ok(())
442    }
443
444    /// Update the session state with the data in the received packet headers.
445    ///
446    /// Return `true` if a new exchange was created, and `false` otherwise.
447    pub(crate) fn post_recv(&mut self, rx_header: &PacketHdr) -> Result<bool, Error> {
448        if !self
449            .rx_ctr_state
450            .post_recv(rx_header.plain.ctr, self.is_encrypted(), false)
451        {
452            Err(ErrorCode::Duplicate)?;
453        }
454
455        let exch_index = self.get_exch_for_rx(&rx_header.proto);
456        if let Some(exch_index) = exch_index {
457            let exch = unwrap!(self.exchanges[exch_index].as_mut());
458
459            exch.post_recv(&rx_header.plain, &rx_header.proto)?;
460
461            Ok(false)
462        } else {
463            if !rx_header.proto.is_initiator()
464                || !MessageMeta::from(&rx_header.proto).is_new_exchange()
465            {
466                // Do not create a new exchange if the peer is not an initiator, or if
467                // the packet is NOT a candidate for a new exchange
468                // (i.e. it is a standalone ACK or a SC status response)
469                Err(ErrorCode::NoExchange)?;
470            }
471
472            if self.expired {
473                // Per Matter Core spec, an expired session must not
474                // accept new inbound messages. Skipping expired sessions here lets the
475                // caller surface a `SessionNotFound` to the peer rather than running
476                // the request through ACL checks against a removed fabric.
477                Err(ErrorCode::NoSession)?;
478            }
479
480            if let Some(exch_index) =
481                self.add_exch(rx_header.proto.exch_id, Role::Responder(Default::default()))
482            {
483                // unwrap is safe as we just created the exchange
484                let exch = unwrap!(self.exchanges[exch_index].as_mut());
485
486                exch.post_recv(&rx_header.plain, &rx_header.proto)?;
487
488                Ok(true)
489            } else {
490                Err(ErrorCode::NoSpaceExchanges)?
491            }
492        }
493    }
494
495    /// Whether the session's peer address is a multicast address — true only
496    /// for TX group sessions (see [`Sessions::get_or_create_for_group_tx`]).
497    pub(crate) fn is_peer_multicast(&self) -> bool {
498        match &self.peer_addr {
499            Address::Udp(crate::transport::network::SocketAddr::V6(addr)) => {
500                addr.ip().is_multicast()
501            }
502            Address::Udp(crate::transport::network::SocketAddr::V4(addr)) => {
503                addr.ip().is_multicast()
504            }
505            _ => false,
506        }
507    }
508
509    pub(crate) fn pre_send(
510        &mut self,
511        exch_index: Option<usize>,
512        tx_header: &mut PacketHdr,
513        session_active_interval_ms: Option<u32>,
514        session_idle_interval_ms: Option<u32>,
515    ) -> Result<(Address, bool), Error> {
516        let ctr = if let Some(exchange_index) = exch_index {
517            let exchange = unwrap!(self.exchanges[exchange_index].as_mut());
518            exchange.mrp.retrans.as_ref().map(RetransEntry::get_msg_ctr)
519        } else {
520            None
521        };
522
523        // The group data message counter value reserved (and made durable)
524        // for this exchange by `Exchange::initiate_group`.
525        #[cfg(feature = "groups")]
526        let group_data_ctr = exch_index.and_then(|exchange_index| {
527            unwrap!(self.exchanges[exchange_index].as_mut())
528                .group_data_ctr
529                .take()
530        });
531
532        let retransmission = ctr.is_some();
533
534        let is_group = matches!(self.mode, SessionMode::Group { .. });
535
536        // Whether this outbound message is a control-plane message
537        // (only MCSP opcodes today). Used to set the `C` bit, to pick
538        // DSIZ = 1 vs. the groupcast destination, and to pick the
539        // per-session vs. the global group data message counter.
540        let is_control = is_group && MessageMeta::from(&tx_header.proto).is_control_msg();
541
542        // For a group session, `local_sess_id == peer_sess_id` — both
543        // hold the same Group Session Id derived from the operational
544        // group key (see `get_or_create_for_group_rx`) — so this line
545        // works uniformly for unicast replies and group control messages.
546        tx_header.plain.sess_id = self.get_peer_sess_id();
547        tx_header.plain.ctr = if let Some(ctr) = ctr {
548            ctr
549        } else if is_group && !is_control {
550            // Group data messages carry the Global Group Encrypted Data
551            // Message Counter — one counter across all groups, so the
552            // per-source counter tracking on the receiving side stays
553            // monotonic. The value was reserved by
554            // `Exchange::initiate_group`, which also made the boundary
555            // covering it durable before this send could happen.
556            #[cfg(feature = "groups")]
557            {
558                group_data_ctr.ok_or(ErrorCode::InvalidState)?
559            }
560            // Group sessions are never created without the `groups` feature.
561            #[cfg(not(feature = "groups"))]
562            {
563                Err(ErrorCode::InvalidState)?
564            }
565        } else {
566            self.get_msg_ctr()
567        };
568
569        // Include the Source Node ID for:
570        // - Unsecured initiator sessions (spec).
571        // - Every outbound message on a group session, so receivers can
572        //   look up the sender's Group Peer State.
573        // CASE/PASE responder-side messages omit it.
574        tx_header.plain.set_src_nodeid(
575            ((!self.is_encrypted() || is_group) && self.local_nodeid != 0)
576                .then_some(self.local_nodeid),
577        );
578
579        // Destination Node ID (DSIZ):
580        // - Plaintext: echo `peer_nodeid` back to the initiator.
581        // - Group control (MCSP reply): DSIZ = 1, destination =
582        //   originator's Node ID.
583        // - Group data: DSIZ = 2, destination = the target Group ID
584        //   (carried by the session mode, see `get_or_create_for_group_tx`).
585        // - CASE/PASE: no DSIZ; clear.
586        #[allow(irrefutable_let_patterns)]
587        if self.mode == SessionMode::PlainText || is_control {
588            tx_header.plain.set_dst_unicast_nodeid(self.peer_nodeid);
589        } else if let SessionMode::Group { group_id, .. } = self.mode {
590            tx_header.plain.set_dst_groupcast_nodeid(Some(group_id));
591        } else {
592            tx_header.plain.set_dst_unicast_nodeid(None);
593        }
594
595        if is_group {
596            use super::plain_hdr::SecFlags;
597            tx_header.plain.sec_flags |= SecFlags::GROUP_SESSION;
598            tx_header.plain.set_control_msg(is_control);
599        }
600
601        tx_header.proto.adjust_reliability(false, &self.peer_addr);
602
603        if is_group && !is_control {
604            // Group DATA messages never use MRP: they are multicast
605            // fire-and-forget, so there is nobody to acknowledge them.
606            // Group CONTROL messages (MCSP) are unicast-addressed and stay
607            // reliable - which is also what keeps their (ephemeral) session
608            // alive until the reply has been encoded.
609            tx_header.proto.unset_reliable();
610            tx_header.proto.set_ack(None);
611        }
612
613        if let Some(exchange_index) = exch_index {
614            let exchange = unwrap!(self.exchanges[exchange_index].as_mut());
615
616            exchange.pre_send(
617                &tx_header.plain,
618                &mut tx_header.proto,
619                session_active_interval_ms,
620                session_idle_interval_ms,
621            )?;
622        }
623
624        Ok((self.peer_addr, retransmission))
625    }
626
627    /// Decode the remaining part of the packet after the plain header and then consume the `ParseBuf`
628    /// instance as it no longer would be necessary.
629    ///
630    /// Returns the range of the decoded packet payload
631    pub(crate) fn decode_remaining<C: Crypto>(
632        &self,
633        crypto: C,
634        rx_header: &mut PacketHdr,
635        mut pb: ParseBuf,
636    ) -> Result<(usize, usize), Error> {
637        rx_header.decode_remaining(
638            crypto,
639            self.get_dec_key(),
640            self.peer_nodeid.unwrap_or_default(),
641            &mut pb,
642        )?;
643
644        rx_header.proto.adjust_reliability(true, &self.peer_addr);
645
646        Ok(pb.slice_range())
647    }
648
649    pub(crate) fn encode<C: Crypto>(
650        &self,
651        crypto: C,
652        tx: &PacketHdr,
653        wb: &mut WriteBuf,
654    ) -> Result<(), Error> {
655        tx.encode(crypto, self.get_enc_key(), self.local_nodeid, wb)
656    }
657
658    fn update_last_used(&mut self) {
659        self.last_use = Instant::now();
660    }
661
662    pub(crate) fn get_exch_for_rx(&self, rx_proto: &ProtoHdr) -> Option<usize> {
663        self.exchanges
664            .iter()
665            .enumerate()
666            .filter(|(_, exch)| {
667                exch.as_ref()
668                    .map(|exch| exch.is_for_rx(rx_proto))
669                    .unwrap_or(false)
670            })
671            .map(|(index, _)| index)
672            .next()
673    }
674
675    pub(crate) fn add_exch(&mut self, exch_id: u16, role: Role) -> Option<usize> {
676        let exch_state = Some(ExchangeState {
677            exch_id,
678            role,
679            mrp: ReliableMessage::new(),
680            #[cfg(feature = "groups")]
681            group_data_ctr: None,
682        });
683
684        let exch_index = if self.exchanges.len() < MAX_EXCHANGES {
685            let _ = self.exchanges.push(exch_state);
686
687            self.exchanges.len() - 1
688        } else {
689            let index = self.exchanges.iter().position(Option::is_none);
690
691            if let Some(index) = index {
692                self.exchanges[index] = exch_state;
693
694                index
695            } else {
696                error!(
697                    "Too many exchanges for session {} [SID:{:x},RSID:{:x}]; exchange creation failed",
698                    self.id,
699                    self.get_local_sess_id(),
700                    self.get_peer_sess_id()
701                );
702
703                return None;
704            }
705        };
706
707        let exch_id = ExchangeId::new(self.id, exch_index);
708
709        debug!("New exchange: {} :: {:?}", exch_id.display(self), role);
710
711        Some(exch_index)
712    }
713
714    pub(crate) fn remove_exch(&mut self, index: usize) -> bool {
715        let exchange = unwrap!(self.exchanges[index].as_mut());
716        let exchange_id = ExchangeId::new(self.id, index);
717
718        if exchange.mrp.is_retrans_pending() {
719            exchange.role.set_dropped_state();
720            error!("Exchange {}: A packet is still (re)transmitted! Marking as dropped, but session will be closed", exchange_id.display(self));
721
722            false
723        } else if exchange.mrp.is_ack_pending() {
724            exchange.role.set_dropped_state();
725            mrp_log!(
726                "Exchange {}: Pending ACK. Marking as dropped",
727                exchange_id.display(self)
728            );
729
730            false
731        } else {
732            trace!("Exchange {}: Dropped cleanly", exchange_id.display(self));
733            self.exchanges[index] = None;
734
735            true
736        }
737    }
738}
739
740impl fmt::Display for Session {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        write!(
743            f,
744            "peer: {:?}, peer_nodeid: {:?}, local: {}, remote: {}, msg_ctr: {}, mode: {:?}, ts: {:?}, expired: {}",
745            self.peer_addr,
746            self.peer_nodeid,
747            self.local_sess_id,
748            self.peer_sess_id,
749            self.msg_ctr,
750            self.mode,
751            self.last_use,
752            self.expired,
753        )
754    }
755}
756
757/// A helper struct for reserving a session slot in the session table when we don't have all the necessary information to create a full session yet.
758///
759/// Public for testing purposes, but should not be used outside of the transport module.
760pub struct ReservedSession<'a> {
761    id: u32,
762    matter: &'a Matter<'a>,
763    complete: bool,
764}
765
766impl<'a> ReservedSession<'a> {
767    pub fn reserve_now<C: Crypto>(matter: &'a Matter<'a>, crypto: C) -> Result<Self, Error> {
768        let dev_det = matter.dev_det();
769        matter.with_state(|state| {
770            let mut rand = crypto.weak_rand()?;
771
772            let id = state
773                .sessions
774                .add(rand.next_u32(), true, Address::new(), None, dev_det)?
775                .id;
776
777            Ok(Self {
778                id,
779                matter,
780                complete: false,
781            })
782        })
783    }
784
785    pub async fn reserve<C: Crypto>(
786        matter: &'a Matter<'a>,
787        crypto: C,
788    ) -> Result<ReservedSession<'a>, Error> {
789        let session = Self::reserve_now(matter, &crypto);
790
791        if let Ok(session) = session {
792            Ok(session)
793        } else {
794            TransportRunner::new(matter, &crypto)
795                .evict_some_session()
796                .await?;
797
798            Self::reserve_now(matter, &crypto)
799        }
800    }
801
802    #[allow(clippy::too_many_arguments)]
803    pub fn update(
804        &mut self,
805        local_nodeid: u64,
806        peer_nodeid: u64,
807        peer_sessid: u16,
808        local_sessid: u16,
809        peer_addr: Address,
810        mode: SessionMode,
811        dec_key: Option<CanonAeadKeyRef<'_>>,
812        enc_key: Option<CanonAeadKeyRef<'_>>,
813        att_challenge: Option<AttChallengeRef<'_>>,
814        shared_secret: Option<CanonPkcSharedSecretRef<'_>>,
815    ) -> Result<(), Error> {
816        self.matter.with_state(|state| {
817            self.update_with_state(
818                state,
819                local_nodeid,
820                peer_nodeid,
821                peer_sessid,
822                local_sessid,
823                peer_addr,
824                mode,
825                dec_key,
826                enc_key,
827                att_challenge,
828                shared_secret,
829            )
830        })
831    }
832
833    #[allow(clippy::too_many_arguments)]
834    pub fn update_with_state(
835        &mut self,
836        state: &mut MatterState,
837        local_nodeid: u64,
838        peer_nodeid: u64,
839        peer_sessid: u16,
840        local_sessid: u16,
841        peer_addr: Address,
842        mode: SessionMode,
843        dec_key: Option<CanonAeadKeyRef<'_>>,
844        enc_key: Option<CanonAeadKeyRef<'_>>,
845        att_challenge: Option<AttChallengeRef<'_>>,
846        shared_secret: Option<CanonPkcSharedSecretRef<'_>>,
847    ) -> Result<(), Error> {
848        let session = state.sessions.get(self.id).ok_or(ErrorCode::NoSession)?;
849
850        session.local_nodeid = local_nodeid;
851        session.peer_nodeid = Some(peer_nodeid);
852        session.peer_sess_id = peer_sessid;
853        session.local_sess_id = local_sessid;
854        session.peer_addr = peer_addr;
855        session.mode = mode;
856
857        if let Some(dec_key) = dec_key {
858            session.dec_key.load(dec_key);
859        }
860
861        if let Some(enc_key) = enc_key {
862            session.enc_key.load(enc_key);
863        }
864
865        if let Some(att_challenge) = att_challenge {
866            session.att_challenge.load(att_challenge);
867        }
868
869        if let Some(shared_secret) = shared_secret {
870            session.shared_secret.load(shared_secret);
871        }
872
873        Ok(())
874    }
875
876    /// Record the peer's MRP `session_parameters` (Sigma1 / Sigma2 /
877    /// PBKDFParamRequest / PBKDFParamResponse) on the reserved session so
878    /// they are in place by the time it transitions to a full Session.
879    /// Per Matter Core spec, MRP retransmission backoff to this
880    /// peer should derive from the peer-advertised `sai` (and idle
881    /// detection later from `sii`/`sat`).
882    pub(crate) fn set_peer_session_params(
883        &mut self,
884        params: &SessionParameters,
885    ) -> Result<(), Error> {
886        self.matter.with_state(|state| {
887            let session = state.sessions.get(self.id).ok_or(ErrorCode::NoSession)?;
888            session.set_peer_session_params(params);
889            Ok(())
890        })
891    }
892
893    pub fn complete(&mut self) {
894        self.complete = true;
895    }
896}
897
898impl Drop for ReservedSession<'_> {
899    fn drop(&mut self) {
900        self.matter.with_state(|state| {
901            if self.complete {
902                let session = unwrap!(state.sessions.get(self.id));
903                session.reserved = false;
904            } else {
905                state.sessions.remove(self.id);
906            }
907        })
908    }
909}
910
911cfg_if! {
912    if #[cfg(feature = "max-sessions-64")] {
913        /// Max number of supported sessions
914        pub const MAX_SESSIONS: usize = 64;
915    } else if #[cfg(feature = "max-sessions-32")] {
916        /// Max number of supported sessions
917        pub const MAX_SESSIONS: usize = 32;
918    } else if #[cfg(feature = "max-sessions-16")] {
919        /// Max number of supported sessions
920        pub const MAX_SESSIONS: usize = 16;
921    } else if #[cfg(feature = "max-sessions-8")] {
922        /// Max number of supported sessions
923        pub const MAX_SESSIONS: usize = 8;
924    } else if #[cfg(feature = "max-sessions-7")] {
925        /// Max number of supported sessions
926        pub const MAX_SESSIONS: usize = 7;
927    } else if #[cfg(feature = "max-sessions-6")] {
928        /// Max number of supported sessions
929        pub const MAX_SESSIONS: usize = 6;
930    } else if #[cfg(feature = "max-sessions-5")] {
931        /// Max number of supported sessions
932        pub const MAX_SESSIONS: usize = 5;
933    } else if #[cfg(feature = "max-sessions-4")] {
934        /// Max number of supported sessions
935        pub const MAX_SESSIONS: usize = 4;
936    } else if #[cfg(feature = "max-sessions-3")] {
937        /// Max number of supported sessions
938        pub const MAX_SESSIONS: usize = 3;
939    } else {
940        /// Max number of supported sessions
941        pub const MAX_SESSIONS: usize = 16;
942    }
943}
944
945cfg_if! {
946    if #[cfg(feature = "max-exchanges-per-session-16")] {
947        /// Max number of supported exchanges per session
948        pub const MAX_EXCHANGES: usize = 16;
949    } else if #[cfg(feature = "max-exchanges-per-session-8")] {
950        /// Max number of supported exchanges per session
951        pub const MAX_EXCHANGES: usize = 8;
952    } else if #[cfg(feature = "max-exchanges-per-session-7")] {
953        /// Max number of supported exchanges per session
954        pub const MAX_EXCHANGES: usize = 7;
955    } else if #[cfg(feature = "max-exchanges-per-session-6")] {
956        /// Max number of supported exchanges per session
957        pub const MAX_EXCHANGES: usize = 6;
958    } else if #[cfg(feature = "max-exchanges-per-session-5")] {
959        /// Max number of supported exchanges per session
960        pub const MAX_EXCHANGES: usize = 5;
961    } else if #[cfg(feature = "max-exchanges-per-session-4")] {
962        /// Max number of supported exchanges per session
963        pub const MAX_EXCHANGES: usize = 4;
964    } else if #[cfg(feature = "max-exchanges-per-session-3")] {
965        /// Max number of supported exchanges per session
966        pub const MAX_EXCHANGES: usize = 3;
967    } else {
968        /// Max number of supported exchanges per session
969        pub const MAX_EXCHANGES: usize = 5;
970    }
971}
972
973const MATTER_MSG_CTR_RANGE: u32 = 0x0fffffff;
974
975/// All sessions
976pub struct Sessions {
977    next_sess_unique_id: u32,
978    next_sess_id: u16,
979    next_exch_id: u16,
980    sessions: Vec<Session, MAX_SESSIONS>,
981    #[cfg(feature = "groups")]
982    group_ctr_store: GroupCtrStore,
983    /// The current Global Group Encrypted Data Message Counter reported
984    /// to peers as the "Synchronized Counter" in `MsgCounterSyncRsp` and
985    /// (once group-data sending is implemented) used to encode outgoing
986    /// group data messages.
987    ///
988    /// `0` means "not yet initialized"; use
989    /// [`Sessions::get_or_init_global_group_data_ctr`] to obtain a valid
990    /// non-zero value.
991    ///
992    /// Persisted with an epoch/stride scheme (see
993    /// [`Sessions::group_data_ctr_boundary`]) so it never goes backwards
994    /// across reboots: a receiver tracking our source in its group counter
995    /// store would otherwise reject our post-restart group data messages as
996    /// replays until the fresh value exceeded the old one.
997    #[cfg(feature = "groups")]
998    global_group_data_ctr: u32,
999    /// The boundary held in durable storage: it covers exactly the values in
1000    /// `[global_group_data_ctr, group_data_ctr_boundary)`, since a restart
1001    /// resumes *at* the stored boundary and is therefore past all of them.
1002    ///
1003    /// Equal to [`Sessions::global_group_data_ctr`] when nothing is covered -
1004    /// the state right after a resume or a first-use seed - which is what
1005    /// makes [`Sessions::reserve_global_group_data_ctr`] extend the boundary
1006    /// and demand a write before handing out a value.
1007    ///
1008    /// `0` when the counter is not yet initialized.
1009    #[cfg(feature = "groups")]
1010    group_data_ctr_boundary: u32,
1011}
1012
1013/// How far ahead of the live Global Group Encrypted Data Message Counter the
1014/// persisted boundary is kept.
1015///
1016/// Every crossing costs one KV write, and every restart burns up to this many
1017/// counter values - so it trades flash wear against counter-space consumption.
1018/// At 1000, a device sending a group message every second writes once every
1019/// ~17 minutes.
1020#[cfg(feature = "groups")]
1021pub const GROUP_DATA_CTR_EPOCH: u32 = 1000;
1022
1023impl Sessions {
1024    /// Create a new Sessions instance.
1025    #[inline(always)]
1026    pub const fn new() -> Self {
1027        Self {
1028            sessions: Vec::new(),
1029            #[cfg(feature = "groups")]
1030            group_ctr_store: GroupCtrStore::new(),
1031            next_sess_unique_id: 0,
1032            next_sess_id: 1,
1033            next_exch_id: 0,
1034            #[cfg(feature = "groups")]
1035            global_group_data_ctr: 0,
1036            #[cfg(feature = "groups")]
1037            group_data_ctr_boundary: 0,
1038        }
1039    }
1040
1041    /// Create an in-place initializer for a new Sessions instance.
1042    pub fn init() -> impl Init<Self> {
1043        // The `init!` macro does not accept `#[cfg]` on fields, so the two
1044        // group-only fields force two variants that differ only by them.
1045        #[cfg(feature = "groups")]
1046        let r = init!(Self {
1047            sessions <- Vec::init(),
1048            group_ctr_store: GroupCtrStore::new(),
1049            next_sess_unique_id: 0,
1050            next_sess_id: 1,
1051            next_exch_id: 0,
1052            global_group_data_ctr: 0,
1053            group_data_ctr_boundary: 0,
1054        });
1055        #[cfg(not(feature = "groups"))]
1056        let r = init!(Self {
1057            sessions <- Vec::init(),
1058            next_sess_unique_id: 0,
1059            next_sess_id: 1,
1060            next_exch_id: 0,
1061        });
1062        r
1063    }
1064
1065    pub fn reset(&mut self) {
1066        self.sessions.clear();
1067        #[cfg(feature = "groups")]
1068        {
1069            self.group_ctr_store = GroupCtrStore::new();
1070        }
1071        self.next_sess_id = 1;
1072        self.next_exch_id = 0;
1073        // Deliberately keep `global_group_data_ctr`: a `reset` isn't a
1074        // factory reset, and rolling it back would break peers that
1075        // just learned it via MCSP.
1076    }
1077}
1078
1079/// Group (multicast) session handling: on-the-fly key derivation for received
1080/// group-encrypted messages, and the global group data-message counter. Behind
1081/// the `groups` feature — a unicast-only device never creates group sessions.
1082#[cfg(feature = "groups")]
1083impl Sessions {
1084    /// Re-hydrate the Global Group Encrypted Data Message Counter from the
1085    /// provided KV store.
1086    ///
1087    /// Resuming at the stored boundary puts the counter past every value the
1088    /// previous run may have used - otherwise peers tracking us in their group
1089    /// counter store would drop our post-restart group messages as replays.
1090    ///
1091    /// Nothing is written here: the resumed counter covers no values yet, so
1092    /// the first group send extends the boundary and stores it before its
1093    /// value reaches the wire. A node that never sends a group message
1094    /// therefore never writes this key.
1095    ///
1096    /// An absent key means "first boot": the counter is instead seeded
1097    /// randomly on first use, which likewise stores its boundary first.
1098    pub fn load_persist<S: KvBlobStore>(
1099        &mut self,
1100        mut store: S,
1101        buf: &mut [u8],
1102    ) -> Result<(), Error> {
1103        if let Some(data) = store.load(GROUP_DATA_COUNTER_KEY, buf)? {
1104            let boundary = u32::from_le_bytes(data.try_into().map_err(|_| ErrorCode::InvalidData)?);
1105
1106            self.resume_global_group_data_ctr(boundary);
1107        }
1108
1109        Ok(())
1110    }
1111
1112    /// Factory-reset the Global Group Encrypted Data Message Counter - both
1113    /// in-memory and in the provided KV store.
1114    ///
1115    /// Unlike [`Sessions::reset`], which deliberately keeps the counter
1116    /// running, a factory reset drops every fabric and group key, so no peer
1117    /// can still be tracking this node's counter. The next group send seeds it
1118    /// afresh at random and stores the new boundary before going on the wire.
1119    pub fn reset_persist<S: KvBlobStore>(
1120        &mut self,
1121        mut store: S,
1122        buf: &mut [u8],
1123    ) -> Result<(), Error> {
1124        self.global_group_data_ctr = 0;
1125        self.group_data_ctr_boundary = 0;
1126
1127        store.remove(GROUP_DATA_COUNTER_KEY, buf)?;
1128
1129        Ok(())
1130    }
1131
1132    /// Return the current Global Group Encrypted Data Message Counter,
1133    /// lazily initialized to a random value in `[1, 2^28 - 1]` on first
1134    /// access. The upper 4 bits are kept zero to leave headroom for
1135    /// wrap-safe monotonic growth.
1136    pub(crate) fn get_or_init_global_group_data_ctr<C: Crypto>(
1137        &mut self,
1138        crypto: C,
1139    ) -> Result<u32, Error> {
1140        if self.global_group_data_ctr == 0 {
1141            // Draw once and fall back to 1 in the (astronomically
1142            // unlikely) all-zero case; peers ignore a `MsgCounterSyncRsp`
1143            // whose Synchronized Counter is 0.
1144            let candidate = crypto.rand()?.next_u32() & MATTER_MSG_CTR_RANGE;
1145            self.set_global_group_data_ctr(if candidate == 0 { 1 } else { candidate });
1146        }
1147        Ok(self.global_group_data_ctr)
1148    }
1149
1150    /// Resume the Global Group Encrypted Data Message Counter at `start` - the
1151    /// boundary read back from durable storage, which is past every value the
1152    /// previous run may have used.
1153    pub(crate) fn resume_global_group_data_ctr(&mut self, start: u32) {
1154        self.set_global_group_data_ctr(if start == 0 { 1 } else { start });
1155    }
1156
1157    /// Set the live counter, with the stored boundary covering nothing beyond
1158    /// it: the next reservation is what extends the boundary by an epoch and
1159    /// demands it be written.
1160    fn set_global_group_data_ctr(&mut self, value: u32) {
1161        self.global_group_data_ctr = value;
1162        self.group_data_ctr_boundary = value;
1163    }
1164
1165    /// Advance a counter value by `delta`, staying inside the Matter message
1166    /// counter range and skipping 0 (the "uninitialized" marker here, and a
1167    /// value peers ignore in `MsgCounterSyncRsp`).
1168    fn advance_group_data_ctr(value: u32, delta: u32) -> u32 {
1169        let next = value.wrapping_add(delta) & MATTER_MSG_CTR_RANGE;
1170        if next == 0 {
1171            1
1172        } else {
1173            next
1174        }
1175    }
1176
1177    /// Reserve the counter value for one outgoing group data message,
1178    /// lazily initializing the counter if this is the first use.
1179    ///
1180    /// Returns `(value, boundary_to_persist)`. The caller MUST durably store
1181    /// `boundary_to_persist` (when `Some`) *before* putting `value` on the
1182    /// wire: only then is a restart guaranteed to resume past it. Group data
1183    /// messages are one-per-exchange, so exactly one value is reserved per
1184    /// [`crate::transport::exchange::Exchange::initiate_group`].
1185    #[must_use = "a returned boundary must be persisted before the value is sent"]
1186    pub(crate) fn reserve_global_group_data_ctr<C: Crypto>(
1187        &mut self,
1188        crypto: C,
1189    ) -> Result<(u32, Option<u32>), Error> {
1190        // First use: seed the counter at random.
1191        self.get_or_init_global_group_data_ctr(crypto)?;
1192
1193        // The stored boundary covers `[ctr, boundary)`. Once the counter has
1194        // caught up with it, the value about to be handed out is not covered
1195        // by anything durable: move the boundary an epoch further and have the
1196        // caller store it before sending. The counter advances one at a time,
1197        // so this happens exactly once per epoch.
1198        let to_persist = if self.global_group_data_ctr == self.group_data_ctr_boundary {
1199            self.group_data_ctr_boundary =
1200                Self::advance_group_data_ctr(self.global_group_data_ctr, GROUP_DATA_CTR_EPOCH);
1201
1202            Some(self.group_data_ctr_boundary)
1203        } else {
1204            None
1205        };
1206
1207        let value = self.global_group_data_ctr;
1208
1209        self.global_group_data_ctr = Self::advance_group_data_ctr(value, 1);
1210
1211        Ok((value, to_persist))
1212    }
1213
1214    /// Get or create a TX group session for sending group data messages to
1215    /// `(fab_idx, group_id)`.
1216    ///
1217    /// The mirror image of [`Sessions::get_or_create_for_group_rx`]: keyed
1218    /// with the group's *active* operational key (the epoch key with the
1219    /// highest start time — RX tries all of them instead), carrying the
1220    /// derived Group Session ID in both session-id slots, and addressed at
1221    /// the group's multicast address per its multicast-address policy.
1222    ///
1223    /// Also seeds the Global Group Encrypted Data Message Counter, which
1224    /// [`Session::pre_send`] stamps into every outgoing group data message
1225    /// (via [`Sessions::next_global_group_data_ctr`]).
1226    pub(crate) fn get_or_create_for_group_tx<C: Crypto>(
1227        &mut self,
1228        crypto: C,
1229        fabrics: &Fabrics,
1230        fab_idx: NonZeroU8,
1231        group_id: u16,
1232        dev_det: &BasicInfoConfig<'_>,
1233    ) -> Result<&mut Session, Error> {
1234        use crate::dm::clusters::decl::groupcast::MulticastAddrPolicyEnum;
1235        use crate::transport::network::{SocketAddr, SocketAddrV6};
1236
1237        let fabric = fabrics.fabric(fab_idx)?;
1238
1239        // The group's security material: the first key set mapped to the
1240        // group, with its active epoch key.
1241        let map_entry = fabric
1242            .groups()
1243            .key_map_iter()
1244            .find(|entry| entry.group_id == group_id)
1245            .ok_or(ErrorCode::NotFound)?;
1246        let key_set = fabric
1247            .groups()
1248            .key_set_get(map_entry.group_key_set_id)
1249            .ok_or(ErrorCode::NotFound)?;
1250        let epoch_key_entry = key_set
1251            .epoch_keys
1252            .iter()
1253            .max_by_key(|entry| entry.epoch_start_time)
1254            .ok_or(ErrorCode::NotFound)?;
1255
1256        let mut derived = KeySet::new();
1257        derived.update(
1258            &crypto,
1259            epoch_key_entry.epoch_key.reference(),
1260            &fabric.compressed_fabric_id(),
1261        )?;
1262        let op_key = derived.op_key();
1263        let session_id = derive_group_session_id(&crypto, op_key)?;
1264
1265        // Destination: the group's multicast address, per its policy. A
1266        // group the sender is no member of (a sender-only role) has no
1267        // group-table entry and uses the `PerGroup` default.
1268        let ip = match fabric
1269            .groups()
1270            .get(group_id)
1271            .map(|entry| entry.effective_mcast_policy())
1272            .unwrap_or(MulticastAddrPolicyEnum::PerGroup)
1273        {
1274            MulticastAddrPolicyEnum::IanaAddr => crate::utils::ipv6::IANA_GROUPCAST_MULTICAST_ADDR,
1275            MulticastAddrPolicyEnum::PerGroup => {
1276                crate::utils::ipv6::compute_group_multicast_addr(fabric.fabric_id(), group_id)
1277            }
1278        };
1279        let peer = Address::Udp(SocketAddr::V6(SocketAddrV6::new(
1280            ip,
1281            crate::MATTER_PORT,
1282            0,
1283            0,
1284        )));
1285        let fabric_node_id = fabric.node_id();
1286
1287        // Seed the global data counter while we hold a crypto instance.
1288        self.get_or_init_global_group_data_ctr(&crypto)?;
1289
1290        // Reuse a previously-created TX session for this group: same mode
1291        // and the multicast peer address (RX-created group sessions carry
1292        // the sender's unicast address instead), still keyed with the
1293        // active key — checked via the derived Group Session ID, so a key
1294        // rotation transparently rolls onto a fresh session.
1295        let existing = self.sessions.iter().position(|sess| {
1296            matches!(
1297                sess.mode,
1298                SessionMode::Group {
1299                    fab_idx: f,
1300                    group_id: g
1301                } if f == fab_idx && g == group_id
1302            ) && sess.peer_addr == peer
1303                && sess.local_sess_id == session_id
1304        });
1305
1306        if let Some(index) = existing {
1307            let session = unwrap!(self.sessions.get_mut(index));
1308            session.update_last_used();
1309            return Ok(session);
1310        }
1311
1312        let mut rand = crypto.weak_rand()?;
1313
1314        let session = match self.add(rand.next_u32(), false, peer, None, dev_det) {
1315            Ok(session) => session,
1316            Err(_) => {
1317                // Session table is full; evict the least-recently-used session
1318                if let Some(lru_id) = self.get_session_for_eviction().map(|sess| sess.id) {
1319                    debug!("Group TX: Evicting session {} to make room", lru_id);
1320                    self.remove(lru_id);
1321                    self.add(rand.next_u32(), false, peer, None, dev_det)?
1322                } else {
1323                    return Err(ErrorCode::NoSpaceSessions.into());
1324                }
1325            }
1326        };
1327
1328        session.set_session_mode(SessionMode::Group { fab_idx, group_id });
1329        session.local_sess_id = session_id;
1330        session.peer_sess_id = session_id;
1331        // Group sessions always emit their Source Node ID.
1332        session.set_local_nodeid(fabric_node_id);
1333        session.enc_key.load(op_key);
1334        session.dec_key.load(op_key);
1335
1336        debug!(
1337            "Group TX: Created group session for fab_idx={}, group_id=0x{:04x}, dst={}",
1338            fab_idx, group_id, peer
1339        );
1340
1341        let session = unwrap!(self.sessions.last_mut());
1342        session.update_last_used();
1343
1344        Ok(session)
1345    }
1346
1347    /// Attempt to decrypt and accept a group-encrypted message.
1348    ///
1349    /// Handles two flavors of incoming group-encrypted packet:
1350    ///
1351    /// * Multicast group data message (groupcast destination Node ID):
1352    ///   match `(session_id, group_id)` against the fabric's group key
1353    ///   map, try each candidate operational key, and validate the
1354    ///   group data message counter (trust-first).
1355    /// * Unicast group-encrypted control message (unicast destination
1356    ///   Node ID equal to one of our fabric identities), e.g. an
1357    ///   MCSP `MsgCounterSyncReq`. There is no `group_id` filter here;
1358    ///   every group key mapped for the matching fabric is tried.
1359    ///
1360    /// On success the derived operational key is copied onto the newly
1361    /// created session (both `enc_key` and `dec_key`) so downstream
1362    /// handlers — in particular the MCSP responder — can encrypt their
1363    /// reply with the same key.
1364    ///
1365    /// Returns the created session and payload range, mirroring how
1366    /// unicast uses `get_for_rx()` + `decode_remaining()`.
1367    pub(crate) fn get_or_create_for_group_rx<const N: usize, C: Crypto>(
1368        &mut self,
1369        crypto: C,
1370        fabrics: &Fabrics,
1371        packet: &mut Packet<N>,
1372        dev_det: &BasicInfoConfig<'_>,
1373    ) -> Result<(&mut Session, (usize, usize)), Error> {
1374        let src_nodeid = packet
1375            .header
1376            .plain
1377            .get_src_nodeid()
1378            .ok_or(ErrorCode::InvalidData)?;
1379        // Either DSIZ = 2 (groupcast) or DSIZ = 1 (unicast to us, MCSP-style).
1380        // Anything else is malformed.
1381        let dst_group_id = packet.header.plain.get_dst_groupcast_nodeid();
1382        let dst_unicast_nodeid = packet.header.plain.get_dst_unicast_nodeid();
1383
1384        if dst_group_id.is_none() && dst_unicast_nodeid.is_none() {
1385            return Err(ErrorCode::InvalidData.into());
1386        }
1387
1388        let expected_sess_id = packet.header.plain.sess_id;
1389        let msg_ctr = packet.header.plain.ctr;
1390        let is_control = packet.header.plain.is_control_msg();
1391
1392        debug!(
1393            "Group: Attempting decrypt for PEER={:?} SID=0x{:04x}, GRP={:?}, DSTU={:?}, SRC=0x{:016x}, CTR={}, C={}",
1394            packet.peer,
1395            expected_sess_id,
1396            dst_group_id,
1397            dst_unicast_nodeid,
1398            src_nodeid,
1399            msg_ctr,
1400            is_control
1401        );
1402
1403        // Parse the plain header to determine encrypted portion offset
1404        let mut pb = ParseBuf::new(&mut packet.buf[packet.payload_start..]);
1405        packet.header.plain.decode(&mut pb)?;
1406
1407        // Save the encrypted payload so we can restore it between decryption attempts
1408        let encrypted_offset = pb.read_off();
1409        let encrypted_len = pb.as_slice().len();
1410        let mut saved_encrypted = [0u8; 1280];
1411
1412        if encrypted_len > saved_encrypted.len() {
1413            return Err(ErrorCode::BufferTooSmall.into());
1414        }
1415
1416        saved_encrypted[..encrypted_len].copy_from_slice(pb.as_slice());
1417
1418        // Derive keys on-the-fly and try each one. When a key decrypts,
1419        // we remember its operational key material so we can copy it
1420        // onto the created session (needed by the MCSP responder to
1421        // encrypt its reply with the same key).
1422        //
1423        // `effective_group_id` is:
1424        //   * the incoming groupcast id for the multicast flow, or
1425        //   * for the unicast/MCSP flow, the first group_id that maps to
1426        //     the matching key set (or 0 when the key set has no
1427        //     mapping) — MCSP itself is bound to a key, not a group, so
1428        //     any value is acceptable here.
1429        struct GroupKeyFound {
1430            fab_idx: NonZeroU8,
1431            group_id: u16,
1432            fabric_node_id: u64,
1433            op_key: CanonAeadKey,
1434            payload_range: (usize, usize),
1435        }
1436
1437        let mut group_key_found: Option<GroupKeyFound> = None;
1438        // Whether at least one candidate key matched the message's group
1439        // session ID (and was therefore actually tried for decryption) -
1440        // distinguishes "authentication failed" from "no key available"
1441        // for Groupcast testing-mode diagnostics.
1442        let mut key_attempted = false;
1443
1444        'outer: for fabric in fabrics.iter() {
1445            // For unicast (MCSP) messages the destination Node ID must
1446            // match one of our fabric identities; skip fabrics whose
1447            // node id doesn't match, to avoid pointlessly trying keys
1448            // that could not have secured a reply to us.
1449            if let Some(dst_node) = dst_unicast_nodeid {
1450                if fabric.node_id() != dst_node {
1451                    continue;
1452                }
1453            }
1454
1455            let fab_idx = fabric.fab_idx();
1456            let compressed_fabric_id = fabric.compressed_fabric_id();
1457            let fabric_node_id = fabric.node_id();
1458
1459            for map_entry in fabric.groups().key_map_iter() {
1460                // Multicast: restrict to the target group. Unicast MCSP:
1461                // any mapping is a candidate — the request is bound to
1462                // a key, not a group.
1463                if let Some(gid) = dst_group_id {
1464                    if map_entry.group_id != gid {
1465                        continue;
1466                    }
1467                }
1468
1469                let Some(key_set_entry) = fabric.groups().key_set_get(map_entry.group_key_set_id)
1470                else {
1471                    continue;
1472                };
1473
1474                for epoch_key_entry in key_set_entry.epoch_keys.iter() {
1475                    let mut temp_key_set = KeySet::new();
1476
1477                    if temp_key_set
1478                        .update(
1479                            &crypto,
1480                            epoch_key_entry.epoch_key.reference(),
1481                            &compressed_fabric_id,
1482                        )
1483                        .is_err()
1484                    {
1485                        continue;
1486                    }
1487
1488                    let op_key_ref = temp_key_set.op_key();
1489
1490                    let Ok(session_id) = derive_group_session_id(&crypto, op_key_ref) else {
1491                        continue;
1492                    };
1493
1494                    if session_id != expected_sess_id {
1495                        continue;
1496                    }
1497
1498                    key_attempted = true;
1499
1500                    if let Some(payload_range) = Self::try_group_decrypt(
1501                        &crypto,
1502                        packet,
1503                        &saved_encrypted[..encrypted_len],
1504                        encrypted_offset,
1505                        op_key_ref,
1506                        src_nodeid,
1507                    ) {
1508                        // Copy the op key so it survives past `temp_key_set`.
1509                        let mut op_key_owned = crate::crypto::AEAD_KEY_ZEROED;
1510                        op_key_owned.load(op_key_ref);
1511                        let effective_group_id = dst_group_id.unwrap_or(map_entry.group_id);
1512                        group_key_found = Some(GroupKeyFound {
1513                            fab_idx,
1514                            group_id: effective_group_id,
1515                            fabric_node_id,
1516                            op_key: op_key_owned,
1517                            payload_range,
1518                        });
1519
1520                        break 'outer;
1521                    }
1522                }
1523            }
1524        }
1525
1526        if group_key_found.is_none() {
1527            debug!(
1528                "Group: No key could decrypt the message (SID=0x{:04x}, GRP={:?}, DSTU={:?})",
1529                expected_sess_id, dst_group_id, dst_unicast_nodeid
1530            );
1531        }
1532
1533        // `NoSession` = no candidate key was available for this message;
1534        // `InvalidSignature` = candidate key(s) matched the group session ID
1535        // but none authenticated the message. The distinction feeds the
1536        // `GroupcastTesting` event's result field (`NoAvailableKey` vs
1537        // `FailedAuth`).
1538        let GroupKeyFound {
1539            fab_idx,
1540            group_id,
1541            fabric_node_id,
1542            op_key,
1543            payload_range,
1544        } = group_key_found.ok_or(if key_attempted {
1545            ErrorCode::InvalidSignature
1546        } else {
1547            ErrorCode::NoSession
1548        })?;
1549
1550        // Only data messages participate in this dedup: control
1551        // messages (`C = 1`) live on a separate control-counter space
1552        // that we don't track yet, and are trust-first regardless.
1553        if !is_control
1554            && !self
1555                .group_ctr_store
1556                .post_recv(fab_idx.get(), src_nodeid, msg_ctr)
1557        {
1558            debug!(
1559                "Group: Duplicate message counter {} from node 0x{:016x} fab_idx={}",
1560                msg_ctr, src_nodeid, fab_idx
1561            );
1562
1563            return Err(ErrorCode::Duplicate.into());
1564        }
1565
1566        // Create ephemeral group session
1567        let peer = packet.peer;
1568        let mut rand = crypto.weak_rand()?;
1569
1570        let session = match self.add(rand.next_u32(), false, peer, Some(src_nodeid), dev_det) {
1571            Ok(session) => session,
1572            Err(_) => {
1573                // Session table is full; evict the least-recently-used session
1574                if let Some(lru_id) = self.get_session_for_eviction().map(|sess| sess.id) {
1575                    debug!("Group: Evicting session {} to make room", lru_id);
1576                    self.remove(lru_id);
1577                    self.add(rand.next_u32(), false, peer, Some(src_nodeid), dev_det)?
1578                } else {
1579                    return Err(ErrorCode::NoSpaceSessions.into());
1580                }
1581            }
1582        };
1583
1584        session.set_session_mode(SessionMode::Group { fab_idx, group_id });
1585        session.local_sess_id = expected_sess_id;
1586        // Mirror the group session id onto the peer side so `pre_send`
1587        // emits the correct value when we reply.
1588        session.peer_sess_id = expected_sess_id;
1589        // Group sessions always emit their Source Node ID, so `pre_send`
1590        // needs this set to our identity for the matching fabric.
1591        session.set_local_nodeid(fabric_node_id);
1592        // Keep the operational key on the session so downstream
1593        // handlers (e.g. MCSP responder) can encrypt the reply with the
1594        // same key that decrypted the request.
1595        session.dec_key.load(op_key.reference());
1596        session.enc_key.load(op_key.reference());
1597
1598        debug!(
1599            "Group: Created group session for fab_idx={}, group_id=0x{:04x}, src_nodeid=0x{:016x}",
1600            fab_idx, group_id, src_nodeid
1601        );
1602
1603        // Re-borrow the current created session for returning
1604        let session = unwrap!(self.sessions.last_mut());
1605        session.update_last_used();
1606
1607        Ok((session, payload_range))
1608    }
1609
1610    /// Try to decrypt a group message with a candidate key.
1611    /// Restores the ciphertext before attempting.
1612    /// On success, returns the payload range; the packet buffer contains decrypted data.
1613    fn try_group_decrypt<const N: usize, C: Crypto>(
1614        crypto: C,
1615        packet: &mut Packet<N>,
1616        saved_encrypted: &[u8],
1617        encrypted_offset: usize,
1618        op_key: CanonAeadKeyRef<'_>,
1619        src_nodeid: u64,
1620    ) -> Option<(usize, usize)> {
1621        // Restore ciphertext
1622        let start = packet.payload_start + encrypted_offset;
1623        let encrypted_len = saved_encrypted.len();
1624        packet.buf[start..start + encrypted_len].copy_from_slice(saved_encrypted);
1625
1626        // Re-create ParseBuf and re-parse plain header
1627        let mut pb = ParseBuf::new(&mut packet.buf[packet.payload_start..]);
1628        if packet.header.plain.decode(&mut pb).is_err() {
1629            error!("Plain header parse error");
1630            return None;
1631        }
1632
1633        if packet
1634            .header
1635            .decode_remaining(crypto, Some(op_key), src_nodeid, &mut pb)
1636            .is_ok()
1637        {
1638            packet.header.proto.adjust_reliability(true, &packet.peer);
1639            Some(pb.slice_range())
1640        } else {
1641            None
1642        }
1643    }
1644}
1645
1646impl Sessions {
1647    pub fn get_next_sess_id(&mut self) -> u16 {
1648        let mut next_sess_id: u16;
1649        loop {
1650            next_sess_id = self.next_sess_id;
1651
1652            // Increment next sess id
1653            self.next_sess_id = self.next_sess_id.overflowing_add(1).0;
1654            if self.next_sess_id == 0 {
1655                self.next_sess_id = 1;
1656            }
1657
1658            // Ensure the currently selected id doesn't match any existing session
1659            if self
1660                .sessions
1661                .iter()
1662                .all(|sess| sess.get_local_sess_id() != next_sess_id)
1663            {
1664                break;
1665            }
1666        }
1667        next_sess_id
1668    }
1669
1670    pub fn get_next_exch_id<C: Crypto>(&mut self, crypto: C) -> Result<u16, Error> {
1671        if self.next_exch_id == 0 {
1672            // Per the Matter Core spec, the first exchange ID of an initiator
1673            // node must be a random integer, with all subsequent ones
1674            // incrementing by one. Seeding is lazy because the constructors
1675            // have no access to an RNG (`Sessions::new` is `const`).
1676            //
1677            // `0` is unambiguous as the "not seeded yet" sentinel, because the
1678            // counter below never assumes that value.
1679            let candidate = crypto.rand()?.next_u32() as u16;
1680            self.next_exch_id = if candidate == 0 { 1 } else { candidate };
1681        }
1682
1683        let mut next_exch_id: u16;
1684        loop {
1685            next_exch_id = self.next_exch_id;
1686
1687            // Increment next exch id
1688            self.next_exch_id = self.next_exch_id.overflowing_add(1).0;
1689            if self.next_exch_id == 0 {
1690                self.next_exch_id = 1;
1691            }
1692
1693            // Ensure the currently selected id doesn't match any existing exchange
1694            if self
1695                .sessions
1696                .iter()
1697                .flat_map(|sess| sess.exchanges.iter())
1698                .filter_map(|exch| exch.as_ref())
1699                .all(|exch| {
1700                    !matches!(exch.role, Role::Responder(_)) || exch.exch_id != next_exch_id
1701                })
1702            {
1703                break;
1704            }
1705        }
1706
1707        Ok(next_exch_id)
1708    }
1709
1710    pub fn get_session_for_eviction(&mut self) -> Option<&mut Session> {
1711        let mut lru_index = None;
1712        let mut lru_ts = Instant::now();
1713        for (i, s) in self.sessions.iter().enumerate() {
1714            if (s.expired || s.last_use < lru_ts)
1715                && !s.reserved
1716                && s.exchanges.iter().all(Option::is_none)
1717            {
1718                lru_ts = s.last_use;
1719                lru_index = Some(i);
1720
1721                if s.expired {
1722                    // Expired sessons are the prime candidates for eviction,
1723                    // so we can break early
1724                    break;
1725                }
1726            }
1727        }
1728
1729        lru_index.map(|index| &mut self.sessions[index])
1730    }
1731
1732    pub fn add(
1733        &mut self,
1734        msg_ctr: u32,
1735        reserved: bool,
1736        peer_addr: Address,
1737        peer_nodeid: Option<u64>,
1738        dev_det: &BasicInfoConfig<'_>,
1739    ) -> Result<&mut Session, Error> {
1740        let session_id = self.next_sess_unique_id;
1741
1742        self.next_sess_unique_id += 1;
1743        if self.next_sess_unique_id > 0x0fff_ffff {
1744            // Reserve the upper 4 bits for the exchange index
1745            self.next_sess_unique_id = 0;
1746        }
1747
1748        // Seed the peer's MRP intervals from our own configured defaults;
1749        // they'll be overwritten by Sigma1 / PBKDFParamRequest (or the
1750        // initiator's Sigma2 / PBKDFParamResponse) once the peer
1751        // advertises its own `session_parameters`.
1752        let (peer_active_interval_ms, peer_idle_interval_ms, peer_active_threshold_ms) =
1753            mrp::default_peer_mrp_params(dev_det);
1754
1755        let session = Session::init(
1756            session_id,
1757            msg_ctr,
1758            reserved,
1759            peer_addr,
1760            peer_nodeid,
1761            peer_active_interval_ms,
1762            peer_idle_interval_ms,
1763            peer_active_threshold_ms,
1764        );
1765
1766        self.sessions
1767            .push_init(session.into_fallible::<Error>(), || {
1768                ErrorCode::NoSpaceSessions.into()
1769            })?;
1770
1771        Ok(unwrap!(self.sessions.last_mut()))
1772    }
1773
1774    /// This assumes that the higher layer has taken care of doing anything required
1775    /// as per the spec before the session is removed
1776    pub fn remove(&mut self, id: u32) -> Option<Session> {
1777        if let Some(index) = self.sessions.iter().position(|sess| sess.id == id) {
1778            Some(self.sessions.swap_remove(index))
1779        } else {
1780            None
1781        }
1782    }
1783
1784    /// This assumes that the higher layer has taken care of doing anything required
1785    /// as per the spec before the sessions are removed or expired
1786    pub fn remove_for_fabric(&mut self, fabric_idx: NonZeroU8, expire_sess_id: Option<u32>) {
1787        while let Some(index) = self.sessions.iter().position(|sess| {
1788            sess.get_local_fabric_idx() == fabric_idx.get() && Some(sess.id) != expire_sess_id
1789        }) {
1790            info!(
1791                "Dropping session with ID {} for fabric index {} immediately",
1792                self.sessions[index].id, fabric_idx
1793            );
1794            self.sessions.swap_remove(index);
1795        }
1796
1797        if let Some(expire_sess_id) = expire_sess_id {
1798            let expire_sess = self
1799                .sessions
1800                .iter_mut()
1801                .find(|sess| sess.id == expire_sess_id);
1802            if let Some(expire_sess) = expire_sess {
1803                expire_sess.expired = true;
1804                info!(
1805                    "Marking session with ID {} as expired for fabric index {}",
1806                    expire_sess_id,
1807                    fabric_idx.get()
1808                );
1809            } else {
1810                warn!(
1811                    "No session with ID {} found for fabric index {} to mark as expired",
1812                    expire_sess_id,
1813                    fabric_idx.get()
1814                );
1815            }
1816        }
1817    }
1818
1819    pub fn get(&mut self, id: u32) -> Option<&mut Session> {
1820        let mut session = self.sessions.iter_mut().find(|sess| sess.id == id);
1821
1822        if let Some(session) = session.as_mut() {
1823            session.update_last_used();
1824        }
1825
1826        session
1827    }
1828
1829    /// Find the operational (CASE) session for a `(fabric, node)` pair.
1830    ///
1831    /// Operational sessions are by definition encrypted and on a real fabric,
1832    /// so the lookup always matches an encrypted session - there is no
1833    /// "unsecured by node" lookup (unsecured/PASE sessions carry no operational
1834    /// identity; see [`Sessions::get_pase_for_addr`]).
1835    pub(crate) fn get_for_node(
1836        &mut self,
1837        fabric_idx: NonZeroU8,
1838        peer_node_id: u64,
1839    ) -> Option<&mut Session> {
1840        // Prefer a TCP-backed session (larger payloads, no MRP fragmentation
1841        // limits) over UDP when both are available for the same peer. This
1842        // is required e.g. for the WebRTC Transport Provider's outbound
1843        // `Answer(sdp)` invoke whose payload can easily exceed a UDP MTU.
1844        //
1845        // Among sessions of the same transport, prefer the most recently used
1846        // one: when several CASE sessions to the same peer exist, the freshest
1847        // is the one the peer is actually communicating on, so reports and other
1848        // outbound traffic reach the session it is listening on.
1849        let idx = self
1850            .sessions
1851            .iter()
1852            .enumerate()
1853            .filter(|(_, s)| !s.expired && s.is_for_node(fabric_idx, peer_node_id))
1854            .max_by_key(|(_, s)| (s.peer_addr.is_tcp(), s.last_use))
1855            .map(|(i, _)| i)?;
1856
1857        let session = &mut self.sessions[idx];
1858
1859        session.update_last_used();
1860
1861        Some(session)
1862    }
1863
1864    /// Find an in-flight PASE session to the given peer address.
1865    ///
1866    /// Used by [`Exchange::initiate_pase`](crate::transport::exchange::Exchange::initiate_pase)
1867    /// to reuse a PASE session per peer (rather than assuming a single global
1868    /// one), so a commissioner can drive several concurrent commissionings.
1869    pub(crate) fn get_pase_for_addr(&mut self, peer_addr: &Address) -> Option<&mut Session> {
1870        let mut session = self
1871            .sessions
1872            .iter_mut()
1873            .find(|s| !s.expired && s.is_pase_for_addr(peer_addr));
1874
1875        if let Some(session) = session.as_mut() {
1876            session.update_last_used();
1877        }
1878
1879        session
1880    }
1881
1882    pub(crate) fn get_for_rx(
1883        &mut self,
1884        rx_peer: &Address,
1885        rx_plain: &PlainHdr,
1886    ) -> Option<&mut Session> {
1887        let mut session = self
1888            .sessions
1889            .iter_mut()
1890            .find(|sess| sess.is_for_rx(rx_peer, rx_plain));
1891
1892        if let Some(session) = session.as_mut() {
1893            session.update_last_used();
1894        }
1895
1896        session
1897    }
1898
1899    pub(crate) fn get_for_tx(&mut self, session_id: u32) -> Option<&mut Session> {
1900        let mut session = self
1901            .sessions
1902            .iter_mut()
1903            .find(|sess| sess.is_for_tx(session_id));
1904
1905        if let Some(session) = session.as_mut() {
1906            session.update_last_used();
1907        }
1908
1909        session
1910    }
1911
1912    pub(crate) fn get_exch<F>(&mut self, f: F) -> Option<(&mut Session, usize)>
1913    where
1914        F: Fn(&Session, &ExchangeState) -> bool,
1915    {
1916        let exch = self
1917            .sessions
1918            .iter()
1919            .flat_map(|sess| {
1920                sess.exchanges
1921                    .iter()
1922                    .enumerate()
1923                    .filter_map(move |(exch_index, exch)| {
1924                        exch.as_ref().map(|exch| (sess, exch, exch_index))
1925                    })
1926            })
1927            .filter(|(sess, exch, _)| f(sess, exch))
1928            .map(|(sess, _, exch_index)| (sess.id, exch_index))
1929            .next();
1930
1931        if let Some((id, exch_index)) = exch {
1932            let session = unwrap!(self.get(id));
1933            session.update_last_used();
1934
1935            Some((session, exch_index))
1936        } else {
1937            None
1938        }
1939    }
1940
1941    /// Iterate over the sessions
1942    pub fn iter(&self) -> impl Iterator<Item = &Session> {
1943        self.sessions.iter()
1944    }
1945
1946    /// Drop every PASE session, whether unpromoted (still
1947    /// `SessionMode::Pase { fab_idx: 0 }`) or already promoted to a
1948    /// fabric. Used by:
1949    ///
1950    /// * `RevokeCommissioning` and a fail-safe expiry over a PASE
1951    ///   session (Matter Core spec): when the
1952    ///   commissioning window is torn down, any in-flight PASE sessions
1953    ///   associated with it must be terminated. A PASE that was
1954    ///   promoted via `AddNOC` is rolled back by the same fail-safe
1955    ///   expiry, so its session must go too.
1956    /// * `CommissioningComplete` (Matter Core spec): once the device
1957    ///   transitions to operational state,
1958    ///   all PASE sessions SHALL be terminated. Without this each
1959    ///   commissioning round leaks the promoted PASE it ran on, and
1960    ///   the session table eventually exhausts — visible as `BUSY` on
1961    ///   the next round's `PBKDFParamRequest`.
1962    ///
1963    /// `expire_sess_id` is the optional ID of a session that should NOT
1964    /// be removed immediately — typically the session that issued the
1965    /// triggering command, so its response can still be sent. That
1966    /// session is marked as `expired` instead, so it stops accepting
1967    /// new exchanges but the in-flight one can complete; the transport
1968    /// reclaims the slot via the usual LRU eviction path.
1969    pub fn remove_pase(&mut self, expire_sess_id: Option<u32>) {
1970        while let Some(index) = self.sessions.iter().position(|sess| {
1971            matches!(sess.get_session_mode(), SessionMode::Pase { .. })
1972                && Some(sess.id) != expire_sess_id
1973        }) {
1974            info!("Dropping PASE session with ID {}", self.sessions[index].id);
1975            self.sessions.swap_remove(index);
1976        }
1977
1978        if let Some(expire_sess_id) = expire_sess_id {
1979            if let Some(sess) = self.sessions.iter_mut().find(|sess| {
1980                sess.id == expire_sess_id
1981                    && matches!(sess.get_session_mode(), SessionMode::Pase { .. })
1982            }) {
1983                sess.expired = true;
1984                info!("Marking PASE session with ID {} as expired", expire_sess_id);
1985            }
1986        }
1987    }
1988}
1989
1990impl Default for Sessions {
1991    fn default() -> Self {
1992        Self::new()
1993    }
1994}
1995
1996impl fmt::Display for Sessions {
1997    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1998        writeln!(f, "{{[")?;
1999        for s in &self.sessions {
2000            writeln!(f, "{{ {}, }},", s)?;
2001        }
2002        write!(f, "], next_sess_id: {}", self.next_sess_id)?;
2003        write!(f, "}}")
2004    }
2005}
2006
2007/// Derive the Group Session ID from an operational group key.
2008///
2009/// Per Matter Spec:
2010/// ```text
2011/// GroupKeyHash = Crypto_KDF(
2012///     InputKey = OperationalGroupKey,
2013///     Salt     = [],
2014///     Info     = "GroupKeyHash",
2015///     Length   = 16 bits
2016/// )
2017/// GroupSessionId = (GroupKeyHash[0] << 8) | GroupKeyHash[1]
2018/// ```
2019pub fn derive_group_session_id<C: Crypto>(
2020    crypto: C,
2021    op_key: CanonAeadKeyRef<'_>,
2022) -> Result<u16, Error> {
2023    const GRP_KEY_HASH_INFO: &[u8] = b"GroupKeyHash";
2024
2025    let mut hash = CryptoSensitive::<2>::new();
2026
2027    crypto
2028        .kdf()?
2029        .expand(&[], op_key, GRP_KEY_HASH_INFO, &mut hash)
2030        .map_err(|_| ErrorCode::InvalidData)?;
2031
2032    let bytes = hash.access();
2033    Ok(((bytes[0] as u16) << 8) | (bytes[1] as u16))
2034}
2035
2036#[cfg(test)]
2037mod tests {
2038    use crate::crypto::{test_only_crypto, AEAD_KEY_ZEROED};
2039    use crate::dm::clusters::basic_info::BasicInfoConfig;
2040    use crate::transport::network::Address;
2041
2042    use super::*;
2043
2044    /// Stand-in `BasicInfoConfig` for tests that don't care about the
2045    /// peer-MRP defaults — `Sessions::add` only reads `sai`/`sii` from it.
2046    const TEST_DEV_DET: BasicInfoConfig<'static> = BasicInfoConfig::new();
2047
2048    #[test]
2049    fn test_next_sess_id_doesnt_reuse() {
2050        let mut sm = Sessions::new();
2051        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
2052        sess.set_local_sess_id(1);
2053        assert_eq!(sm.get_next_sess_id(), 2);
2054        assert_eq!(sm.get_next_sess_id(), 3);
2055        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
2056        sess.set_local_sess_id(4);
2057        assert_eq!(sm.get_next_sess_id(), 5);
2058    }
2059
2060    #[test]
2061    fn test_next_sess_id_overflows() {
2062        let mut sm = Sessions::new();
2063        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
2064        sess.set_local_sess_id(1);
2065        assert_eq!(sm.get_next_sess_id(), 2);
2066        sm.next_sess_id = 65534;
2067        assert_eq!(sm.get_next_sess_id(), 65534);
2068        assert_eq!(sm.get_next_sess_id(), 65535);
2069        assert_eq!(sm.get_next_sess_id(), 2);
2070    }
2071
2072    #[test]
2073    fn test_derive_group_session_id() {
2074        // Spec test vector:
2075        // Operational Group Key: a6:f5:30:6b:af:6d:05:0a:f2:3b:a4:bd:6b:9d:d9:60
2076        // Expected GroupSessionId: 0xB9F7 (47607)
2077        let op_key_bytes: [u8; 16] = [
2078            0xa6, 0xf5, 0x30, 0x6b, 0xaf, 0x6d, 0x05, 0x0a, 0xf2, 0x3b, 0xa4, 0xbd, 0x6b, 0x9d,
2079            0xd9, 0x60,
2080        ];
2081
2082        let mut op_key = AEAD_KEY_ZEROED;
2083        op_key.try_load_from_slice(&op_key_bytes).unwrap();
2084
2085        let crypto = test_only_crypto();
2086        let session_id = derive_group_session_id(&crypto, op_key.reference()).unwrap();
2087
2088        assert_eq!(
2089            session_id, 0xB9F7,
2090            "Group Session ID mismatch: got 0x{:04X}, expected 0xB9F7",
2091            session_id
2092        );
2093    }
2094
2095    /// The persisted-boundary bookkeeping of the global group data message
2096    /// counter. The invariant under test: every reserved value is covered by
2097    /// a boundary the caller was told to persist BEFORE that value can be
2098    /// sent - so a restart always resumes past everything handed out.
2099    #[cfg(feature = "groups")]
2100    #[test]
2101    fn test_group_data_ctr_reserve_boundary() {
2102        let crypto = test_only_crypto();
2103
2104        let mut sessions = Sessions::new();
2105
2106        // Resuming at a stored boundary covers nothing yet.
2107        sessions.resume_global_group_data_ctr(1000);
2108
2109        // The first reservation hands out the stored boundary itself - past
2110        // every value the previous run could have used - and returns the new
2111        // boundary, to be made durable before that value is sent.
2112        let (value, boundary) = sessions.reserve_global_group_data_ctr(&crypto).unwrap();
2113        assert_eq!(value, 1000);
2114        assert_eq!(boundary, Some(1000 + GROUP_DATA_CTR_EPOCH));
2115
2116        // The rest of the epoch is already covered, so no further writes are
2117        // demanded - and every handed-out value stays below what is durable.
2118        let durable = boundary.unwrap();
2119        for expected in 1001..1000 + GROUP_DATA_CTR_EPOCH {
2120            let (value, boundary) = sessions.reserve_global_group_data_ctr(&crypto).unwrap();
2121            assert_eq!(value, expected);
2122            assert_eq!(boundary, None);
2123            assert!(value < durable);
2124        }
2125
2126        // The first value the stored boundary does *not* cover is the boundary
2127        // itself - reserving it demands the next one be written first.
2128        let (value, boundary) = sessions.reserve_global_group_data_ctr(&crypto).unwrap();
2129        assert_eq!(value, durable);
2130        assert_eq!(boundary, Some(1000 + 2 * GROUP_DATA_CTR_EPOCH));
2131    }
2132
2133    /// A first-ever reservation seeds the counter and demands its boundary be
2134    /// persisted before the value is used - no send can precede a write.
2135    #[cfg(feature = "groups")]
2136    #[test]
2137    fn test_group_data_ctr_first_reservation_persists() {
2138        let crypto = test_only_crypto();
2139
2140        let mut sessions = Sessions::new();
2141
2142        let (value, boundary) = sessions.reserve_global_group_data_ctr(&crypto).unwrap();
2143
2144        assert_ne!(value, 0);
2145        let boundary = boundary.expect("the first reservation must demand a persist");
2146        assert!(value < boundary);
2147    }
2148
2149    /// The counter and its boundary stay inside the Matter message counter
2150    /// range and never land on 0 (the "uninitialized" marker, and a value
2151    /// peers ignore in `MsgCounterSyncRsp`).
2152    #[cfg(feature = "groups")]
2153    #[test]
2154    fn test_group_data_ctr_wraps_within_range() {
2155        let crypto = test_only_crypto();
2156        let top = MATTER_MSG_CTR_RANGE;
2157
2158        let mut sessions = Sessions::new();
2159        sessions.resume_global_group_data_ctr(top);
2160
2161        assert_eq!(
2162            sessions.reserve_global_group_data_ctr(&crypto).unwrap().0,
2163            top
2164        );
2165        // `top + 1` masks to 0 -> skipped to 1.
2166        assert_eq!(
2167            sessions.reserve_global_group_data_ctr(&crypto).unwrap().0,
2168            1
2169        );
2170
2171        // A boundary that would land on 0 is likewise skipped.
2172        let mut sessions = Sessions::new();
2173        sessions.resume_global_group_data_ctr(top + 1 - GROUP_DATA_CTR_EPOCH);
2174        assert_eq!(
2175            sessions.reserve_global_group_data_ctr(&crypto).unwrap().1,
2176            Some(1)
2177        );
2178    }
2179
2180    /// The safety invariant of the epoch scheme, checked across the 28-bit
2181    /// wrap: at every point, the boundary last handed out for persisting is a
2182    /// value that has *not* been used yet. A restart resumes exactly there, so
2183    /// this is what guarantees no counter value is ever re-issued - which for
2184    /// group messages would repeat an AEAD nonce under the same group key.
2185    #[cfg(all(feature = "groups", feature = "std"))]
2186    #[test]
2187    fn test_group_data_ctr_persist_covers_every_value_across_wrap() {
2188        let crypto = test_only_crypto();
2189
2190        let mut sessions = Sessions::new();
2191
2192        // Start two epochs below the top of the range, so the walk below runs
2193        // through the wrap.
2194        let start = MATTER_MSG_CTR_RANGE - 2 * GROUP_DATA_CTR_EPOCH;
2195        sessions.resume_global_group_data_ctr(start);
2196
2197        let mut used = std::collections::HashSet::new();
2198        let mut last_stored = start;
2199
2200        for _ in 0..5 * GROUP_DATA_CTR_EPOCH {
2201            let (value, boundary) = sessions.reserve_global_group_data_ctr(&crypto).unwrap();
2202
2203            // `initiate_group` writes the boundary before the value is sent.
2204            if let Some(boundary) = boundary {
2205                last_stored = boundary;
2206            }
2207
2208            used.insert(value);
2209
2210            assert!(
2211                !used.contains(&last_stored),
2212                "a restart would resume at an already used counter value"
2213            );
2214        }
2215    }
2216
2217    /// A resume value of 0 (a corrupt/blank stored boundary) must not leave
2218    /// the counter in the "uninitialized" state.
2219    #[cfg(feature = "groups")]
2220    #[test]
2221    fn test_group_data_ctr_resume_zero() {
2222        let crypto = test_only_crypto();
2223
2224        let mut sessions = Sessions::new();
2225        sessions.resume_global_group_data_ctr(0);
2226
2227        assert_eq!(
2228            sessions.reserve_global_group_data_ctr(&crypto).unwrap().0,
2229            1
2230        );
2231    }
2232
2233    /// An in-memory [`KvBlobStore`](crate::persist::KvBlobStore) for the
2234    /// counter persistence tests below.
2235    #[cfg(all(feature = "groups", feature = "std"))]
2236    struct MemKv(std::collections::HashMap<u16, std::vec::Vec<u8>>);
2237
2238    #[cfg(all(feature = "groups", feature = "std"))]
2239    impl crate::persist::KvBlobStore for &mut MemKv {
2240        fn load<'a>(&mut self, key: u16, buf: &'a mut [u8]) -> Result<Option<&'a [u8]>, Error> {
2241            Ok(self.0.get(&key).map(|v| {
2242                buf[..v.len()].copy_from_slice(v);
2243                &buf[..v.len()]
2244            }))
2245        }
2246
2247        fn store(&mut self, key: u16, data: &[u8], _buf: &mut [u8]) -> Result<(), Error> {
2248            self.0.insert(key, data.to_vec());
2249            Ok(())
2250        }
2251
2252        fn remove(&mut self, key: u16, _buf: &mut [u8]) -> Result<(), Error> {
2253            self.0.remove(&key);
2254            Ok(())
2255        }
2256    }
2257
2258    /// The reboot-survival contract of [`crate::Matter::startup`]: it reads
2259    /// the boundary the previous run stored (same key, same encoding) and
2260    /// resumes the counter *at* it - past everything that run could have sent.
2261    ///
2262    /// Startup itself writes nothing; the first reservation is what extends
2263    /// the boundary and demands it be stored before its value is sent.
2264    #[cfg(all(feature = "groups", feature = "std"))]
2265    #[test]
2266    fn test_group_data_ctr_resumes_from_storage() {
2267        use crate::dm::devices::test::{TEST_DEV_ATT, TEST_DEV_COMM, TEST_DEV_DET};
2268        use crate::persist::GROUP_DATA_COUNTER_KEY;
2269        use crate::Matter;
2270
2271        /// The previous run's stored boundary.
2272        const STORED: u32 = 5000;
2273
2274        let mut kv = MemKv(std::collections::HashMap::new());
2275        kv.0.insert(GROUP_DATA_COUNTER_KEY, STORED.to_le_bytes().to_vec());
2276
2277        let matter = Matter::new(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, 0);
2278        matter.startup(matter.kv(&mut kv)).unwrap();
2279
2280        // Startup itself does not write - a node that never sends a group
2281        // message leaves the key exactly as the previous run left it.
2282        let stored = kv.0.get(&GROUP_DATA_COUNTER_KEY).unwrap();
2283        assert_eq!(
2284            u32::from_le_bytes(stored.as_slice().try_into().unwrap()),
2285            STORED
2286        );
2287
2288        // The first value handed out is the stored boundary itself - past
2289        // everything the previous run could have used - and it comes with the
2290        // next boundary, which `initiate_group` stores before sending.
2291        let (first, boundary) = matter
2292            .with_state(|state| {
2293                state
2294                    .sessions
2295                    .reserve_global_group_data_ctr(test_only_crypto())
2296            })
2297            .unwrap();
2298
2299        assert_eq!(first, STORED);
2300        assert_eq!(boundary, Some(STORED + GROUP_DATA_CTR_EPOCH));
2301    }
2302
2303    /// A factory reset drops the stored boundary along with the fabrics and
2304    /// group keys it covered, and the next use seeds a fresh counter that is
2305    /// again persisted before anything can be sent.
2306    #[cfg(all(feature = "groups", feature = "std"))]
2307    #[test]
2308    fn test_group_data_ctr_factory_reset() {
2309        use crate::dm::devices::test::{TEST_DEV_ATT, TEST_DEV_COMM, TEST_DEV_DET};
2310        use crate::persist::GROUP_DATA_COUNTER_KEY;
2311        use crate::Matter;
2312
2313        let mut kv = MemKv(std::collections::HashMap::new());
2314        kv.0.insert(GROUP_DATA_COUNTER_KEY, 5000u32.to_le_bytes().to_vec());
2315
2316        let matter = Matter::new(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, 0);
2317        matter.startup(matter.kv(&mut kv)).unwrap();
2318
2319        matter.factory_reset(matter.kv(&mut kv)).unwrap();
2320
2321        assert!(!kv.0.contains_key(&GROUP_DATA_COUNTER_KEY));
2322
2323        // The counter is back to "first boot": the next reservation seeds it
2324        // at random and hands back a boundary that must be stored first.
2325        let (value, boundary) = matter
2326            .with_state(|state| {
2327                state
2328                    .sessions
2329                    .reserve_global_group_data_ctr(test_only_crypto())
2330            })
2331            .unwrap();
2332
2333        assert_ne!(value, 0);
2334        assert_eq!(boundary, Some(value + GROUP_DATA_CTR_EPOCH));
2335    }
2336}