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::{canon, CanonAeadKey, CanonAeadKeyRef, Crypto, CryptoSensitive, Kdf};
27use crate::dm::clusters::basic_info::BasicInfoConfig;
28use crate::error::{Error, ErrorCode};
29use crate::fabric::Fabrics;
30use crate::group_keys::KeySet;
31use crate::sc::SessionParameters;
32use crate::transport::exchange::ExchangeId;
33use crate::transport::mrp::{self, ReliableMessage};
34use crate::transport::TransportRunner;
35use crate::utils::init::{init, Init, IntoFallibleInit};
36use crate::utils::storage::{ParseBuf, Vec, WriteBuf};
37use crate::{Matter, MatterState};
38
39use super::dedup::{GroupCtrStore, RxCtrState};
40use super::exchange::{ExchangeState, MessageMeta, Role};
41use super::mrp::RetransEntry;
42use super::network::Address;
43use super::packet::PacketHdr;
44use super::plain_hdr::PlainHdr;
45use super::proto_hdr::ProtoHdr;
46use super::Packet;
47
48pub const MAX_CAT_IDS_PER_NOC: usize = 3;
49pub type NocCatIds = [u32; MAX_CAT_IDS_PER_NOC];
50
51pub const ATT_CHALLENGE_LEN: usize = 16;
52
53canon!(
54    ATT_CHALLENGE_LEN,
55    ATT_CHALLENGE_ZEROED,
56    AttChallenge,
57    AttChallengeRef
58);
59
60#[derive(Debug, PartialEq, Eq, Clone, Default)]
61#[cfg_attr(feature = "defmt", derive(defmt::Format))]
62pub enum SessionMode {
63    // The Case session will capture the local fabric index
64    // and the local fabric index
65    Case {
66        fab_idx: NonZeroU8,
67        cat_ids: NocCatIds,
68    },
69    // The Pase session always starts with a fabric index of 0
70    // (i.e. no fabric) but will be upgraded to the actual fabric index
71    // once AddNOC or UpdateNOC is received
72    Pase {
73        fab_idx: u8,
74    },
75    // A group session used for group (multicast) messaging.
76    Group {
77        fab_idx: NonZeroU8,
78        group_id: u16,
79    },
80    #[default]
81    PlainText,
82}
83
84impl SessionMode {
85    pub fn fab_idx(&self) -> u8 {
86        match self {
87            SessionMode::Case { fab_idx, .. } => fab_idx.get(),
88            SessionMode::Pase { fab_idx, .. } => *fab_idx,
89            SessionMode::Group { fab_idx, .. } => fab_idx.get(),
90            SessionMode::PlainText => 0,
91        }
92    }
93}
94
95pub struct Session {
96    // Internal ID which is guaranteeed to be unique accross all sessions and not change when sessions are added/removed
97    pub(crate) id: u32,
98    peer_addr: Address,
99    local_nodeid: u64,
100    peer_nodeid: Option<u64>,
101    // I find the session initiator/responder role getting confused with exchange initiator/responder
102    // So, we might keep this as enc_key and dec_key for now
103    dec_key: CanonAeadKey,
104    enc_key: CanonAeadKey,
105    att_challenge: AttChallenge,
106    local_sess_id: u16,
107    peer_sess_id: u16,
108    msg_ctr: u32,
109    rx_ctr_state: RxCtrState,
110    mode: SessionMode,
111    pub(crate) exchanges: Vec<Option<ExchangeState>, MAX_EXCHANGES>,
112    last_use: Instant,
113    /// Peer's effective `MRP_SESSION_ACTIVE_INTERVAL` (ms) — drives our
114    /// MRP retransmission base interval when transmitting to this peer.
115    peer_active_interval_ms: u32,
116    /// Peer's effective `MRP_SESSION_IDLE_INTERVAL` (ms) — see
117    /// `peer_active_interval_ms`. Currently informational on the responder
118    /// side until idle-vs-active classification lands in the MRP code.
119    peer_idle_interval_ms: u32,
120    /// Peer's effective `MRP_SESSION_ACTIVE_THRESHOLD` (ms).
121    peer_active_threshold_ms: u16,
122    /// If `true` then the session is considered "expired". Session expiration happens
123    /// for the session on behalf of which a fabric is removed.
124    ///
125    /// Expired sessions can still process their ongoing exchanges, but do not accept any new ones.
126    /// Furthermore, expired sessions are the prime candidates for eviction.
127    expired: bool,
128    reserved: bool,
129}
130
131impl Session {
132    #[allow(clippy::too_many_arguments)]
133    pub fn new(
134        id: u32,
135        msg_ctr: u32,
136        reserved: bool,
137        peer_addr: Address,
138        peer_nodeid: Option<u64>,
139        peer_active_interval_ms: u32,
140        peer_idle_interval_ms: u32,
141        peer_active_threshold_ms: u16,
142    ) -> Self {
143        Self {
144            id,
145            reserved,
146            peer_addr,
147            local_nodeid: 0,
148            peer_nodeid,
149            dec_key: CanonAeadKey::new(),
150            enc_key: CanonAeadKey::new(),
151            att_challenge: AttChallenge::new(),
152            peer_sess_id: 0,
153            local_sess_id: 0,
154            msg_ctr: msg_ctr & MATTER_MSG_CTR_RANGE,
155            rx_ctr_state: RxCtrState::new(0),
156            mode: SessionMode::PlainText,
157            exchanges: Vec::new(),
158            last_use: Instant::now(),
159            peer_active_interval_ms,
160            peer_idle_interval_ms,
161            peer_active_threshold_ms,
162            expired: false,
163        }
164    }
165
166    #[allow(clippy::too_many_arguments)]
167    pub fn init(
168        id: u32,
169        msg_ctr: u32,
170        reserved: bool,
171        peer_addr: Address,
172        peer_nodeid: Option<u64>,
173        peer_active_interval_ms: u32,
174        peer_idle_interval_ms: u32,
175        peer_active_threshold_ms: u16,
176    ) -> impl Init<Self> {
177        init!(Self {
178            id,
179            reserved,
180            peer_addr,
181            local_nodeid: 0,
182            peer_nodeid,
183            dec_key <- CanonAeadKey::init(),
184            enc_key <- CanonAeadKey::init(),
185            att_challenge <- AttChallenge::init(),
186            peer_sess_id: 0,
187            local_sess_id: 0,
188            msg_ctr: msg_ctr & MATTER_MSG_CTR_RANGE,
189            rx_ctr_state: RxCtrState::new(0),
190            mode: SessionMode::PlainText,
191            exchanges <- Vec::init(),
192            last_use: Instant::now(),
193            peer_active_interval_ms,
194            peer_idle_interval_ms,
195            peer_active_threshold_ms,
196            expired: false,
197        })
198    }
199
200    /// Get the internal ID of the session
201    /// This ID is guaranteed to be unique across all sessions
202    pub const fn id(&self) -> u32 {
203        self.id
204    }
205
206    pub fn get_local_sess_id(&self) -> u16 {
207        self.local_sess_id
208    }
209
210    #[cfg(test)]
211    pub fn set_local_sess_id(&mut self, sess_id: u16) {
212        self.local_sess_id = sess_id;
213    }
214
215    pub(crate) fn set_local_nodeid(&mut self, nodeid: u64) {
216        self.local_nodeid = nodeid;
217    }
218
219    pub fn get_peer_sess_id(&self) -> u16 {
220        self.peer_sess_id
221    }
222
223    pub fn get_peer_addr(&self) -> Address {
224        self.peer_addr
225    }
226
227    pub fn is_encrypted(&self) -> bool {
228        match self.mode {
229            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => true,
230            SessionMode::PlainText => false,
231        }
232    }
233
234    pub fn get_peer_node_id(&self) -> Option<u64> {
235        self.peer_nodeid
236    }
237
238    pub fn get_local_fabric_idx(&self) -> u8 {
239        self.mode.fab_idx()
240    }
241
242    pub fn get_session_mode(&self) -> &SessionMode {
243        &self.mode
244    }
245
246    pub(crate) fn set_session_mode(&mut self, mode: SessionMode) {
247        self.mode = mode;
248    }
249
250    pub fn get_peer_active_interval_ms(&self) -> u32 {
251        self.peer_active_interval_ms
252    }
253
254    pub fn get_peer_idle_interval_ms(&self) -> u32 {
255        self.peer_idle_interval_ms
256    }
257
258    pub fn get_peer_active_threshold_ms(&self) -> u16 {
259        self.peer_active_threshold_ms
260    }
261
262    /// Record the peer's `session_parameters` (any combination of `sai` /
263    /// `sii` / `sat`) for use by MRP retransmission timing on later sends
264    /// to this peer. Only the fields the peer actually advertised get
265    /// overwritten; absent fields leave the seeded default in place so
266    /// repeated handshakes don't clobber a stronger earlier hint with a
267    /// later-but-emptier one.
268    ///
269    /// `Some(0)` is dropped (with a warning) — this method runs on
270    /// Sigma1 / PBKDFParamRequest TLV from an unauthenticated peer, and
271    /// an "interval" of zero would either collapse the MRP backoff to
272    /// a tight retransmit loop or, in the SAT case, mark the session
273    /// active for zero milliseconds. Neither is a legitimate value, so
274    /// rejecting them protects against a trivial pre-auth DoS.
275    pub(crate) fn set_peer_session_params(&mut self, params: &SessionParameters) {
276        if let Some(sai) = params.sai {
277            if sai > 0 {
278                self.peer_active_interval_ms = sai;
279            } else {
280                warn!("Peer advertised session_parameters.sai=0; ignoring");
281            }
282        }
283
284        if let Some(sii) = params.sii {
285            if sii > 0 {
286                self.peer_idle_interval_ms = sii;
287            } else {
288                warn!("Peer advertised session_parameters.sii=0; ignoring");
289            }
290        }
291
292        if let Some(sat) = params.sat {
293            if sat > 0 {
294                self.peer_active_threshold_ms = sat;
295            } else {
296                warn!("Peer advertised session_parameters.sat=0; ignoring");
297            }
298        }
299    }
300
301    fn get_msg_ctr(&mut self) -> u32 {
302        let ctr = self.msg_ctr;
303        self.msg_ctr += 1;
304        ctr
305    }
306
307    pub fn get_dec_key(&self) -> Option<CanonAeadKeyRef<'_>> {
308        match self.mode {
309            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => {
310                Some(self.dec_key.reference())
311            }
312            SessionMode::PlainText => None,
313        }
314    }
315
316    pub fn get_enc_key(&self) -> Option<CanonAeadKeyRef<'_>> {
317        match self.mode {
318            SessionMode::Case { .. } | SessionMode::Pase { .. } | SessionMode::Group { .. } => {
319                Some(self.enc_key.reference())
320            }
321            SessionMode::PlainText => None,
322        }
323    }
324
325    pub fn get_att_challenge(&self) -> Option<AttChallengeRef<'_>> {
326        match self.mode {
327            SessionMode::Case { .. } | SessionMode::Pase { .. } => {
328                Some(self.att_challenge.reference())
329            }
330            SessionMode::PlainText | SessionMode::Group { .. } => None,
331        }
332    }
333
334    /// Whether this is a CASE session to the given peer node ID and fabric index.
335    pub(crate) fn is_for_node(&self, fabric_idx: NonZeroU8, peer_node_id: u64) -> bool {
336        self.get_local_fabric_idx() == fabric_idx.get()
337            && self.peer_nodeid == Some(peer_node_id)
338            && self.is_encrypted()
339            && !self.reserved
340    }
341
342    /// Whether this is a PASE session to the given peer address.
343    ///
344    /// PASE sessions are all keyed at fabric 0 / node 0 (no operational
345    /// identity yet), so the peer *address* is what distinguishes one PASE
346    /// session from another - which matters on a commissioner that may have
347    /// several PASE sessions (to different devices) in flight at once.
348    pub(crate) fn is_pase_for_addr(&self, peer_addr: &Address) -> bool {
349        matches!(self.mode, SessionMode::Pase { .. })
350            && self.peer_addr.canonical() == peer_addr.canonical()
351            && !self.reserved
352    }
353
354    pub(crate) fn is_for_rx(&self, rx_peer: &Address, rx_plain: &PlainHdr) -> bool {
355        let nodeid_matches = self.peer_nodeid.is_none()
356            || rx_plain.get_src_nodeid().is_none()
357            || self.peer_nodeid == rx_plain.get_src_nodeid();
358
359        // For unsecured sessions, also match by destination node ID (the echoed
360        // ephemeral initiator node ID) to disambiguate multiple unsecured sessions
361        // for the same peer (spec).
362        let dest_nodeid_matches = self.is_encrypted()
363            || self.local_nodeid == 0
364            || rx_plain.get_dst_unicast_nodeid().is_none()
365            || rx_plain.get_dst_unicast_nodeid() == Some(self.local_nodeid);
366
367        nodeid_matches
368            && dest_nodeid_matches
369            && self.local_sess_id == rx_plain.sess_id
370            // Compare canonically: a dual-stack socket may report a peer as
371            // `::ffff:a.b.c.d` on receive while the session stored the plain
372            // `V4` address it was created with (or vice versa). Canonicalizing
373            // only the *comparison* (not the stored address) lets the two match
374            // without disturbing the address used for reply routing. See
375            // `Address::canonical`.
376            && self.peer_addr.canonical() == rx_peer.canonical()
377            && self.is_encrypted() == rx_plain.is_encrypted()
378            && !self.reserved
379    }
380
381    pub(crate) fn is_for_tx(&self, session_id: u32) -> bool {
382        self.id == session_id
383    }
384
385    /// Return `true` if the session is expired.
386    pub(crate) fn is_expired(&self) -> bool {
387        self.expired
388    }
389
390    pub fn upgrade_fabric_idx(&mut self, fabric_idx: NonZeroU8) -> Result<(), Error> {
391        if let SessionMode::Pase { fab_idx } = &mut self.mode {
392            if *fab_idx == 0 {
393                *fab_idx = fabric_idx.get();
394            } else {
395                // Upgrading a PASE session can happen only once
396                Err(ErrorCode::Invalid)?;
397            }
398        } else {
399            // CASE sessions are not upgradeable, as per spec
400            // And for plain text sessions - we shoudn't even get here in the first place
401            Err(ErrorCode::Invalid)?;
402        }
403
404        Ok(())
405    }
406
407    /// Update the session state with the data in the received packet headers.
408    ///
409    /// Return `true` if a new exchange was created, and `false` otherwise.
410    pub(crate) fn post_recv(&mut self, rx_header: &PacketHdr) -> Result<bool, Error> {
411        if !self
412            .rx_ctr_state
413            .post_recv(rx_header.plain.ctr, self.is_encrypted(), false)
414        {
415            Err(ErrorCode::Duplicate)?;
416        }
417
418        let exch_index = self.get_exch_for_rx(&rx_header.proto);
419        if let Some(exch_index) = exch_index {
420            let exch = unwrap!(self.exchanges[exch_index].as_mut());
421
422            exch.post_recv(&rx_header.plain, &rx_header.proto)?;
423
424            Ok(false)
425        } else {
426            if !rx_header.proto.is_initiator()
427                || !MessageMeta::from(&rx_header.proto).is_new_exchange()
428            {
429                // Do not create a new exchange if the peer is not an initiator, or if
430                // the packet is NOT a candidate for a new exchange
431                // (i.e. it is a standalone ACK or a SC status response)
432                Err(ErrorCode::NoExchange)?;
433            }
434
435            if self.expired {
436                // Per Matter Core spec, an expired session must not
437                // accept new inbound messages. Skipping expired sessions here lets the
438                // caller surface a `SessionNotFound` to the peer rather than running
439                // the request through ACL checks against a removed fabric.
440                Err(ErrorCode::NoSession)?;
441            }
442
443            if let Some(exch_index) =
444                self.add_exch(rx_header.proto.exch_id, Role::Responder(Default::default()))
445            {
446                // unwrap is safe as we just created the exchange
447                let exch = unwrap!(self.exchanges[exch_index].as_mut());
448
449                exch.post_recv(&rx_header.plain, &rx_header.proto)?;
450
451                Ok(true)
452            } else {
453                Err(ErrorCode::NoSpaceExchanges)?
454            }
455        }
456    }
457
458    pub(crate) fn pre_send(
459        &mut self,
460        exch_index: Option<usize>,
461        tx_header: &mut PacketHdr,
462        session_active_interval_ms: Option<u32>,
463        session_idle_interval_ms: Option<u32>,
464    ) -> Result<(Address, bool), Error> {
465        let ctr = if let Some(exchange_index) = exch_index {
466            let exchange = unwrap!(self.exchanges[exchange_index].as_mut());
467            exchange.mrp.retrans.as_ref().map(RetransEntry::get_msg_ctr)
468        } else {
469            None
470        };
471
472        let retransmission = ctr.is_some();
473
474        tx_header.plain.sess_id = self.get_peer_sess_id();
475        tx_header.plain.ctr = ctr.unwrap_or_else(|| self.get_msg_ctr());
476        // For unsecured initiator sessions, set Source Node ID to our ephemeral
477        // initiator node ID (spec: "enclosed by initiator as Source Node ID").
478        // Encrypted sessions and responder unsecured sessions (local_nodeid=0) send no Source.
479        tx_header.plain.set_src_nodeid(
480            (!self.is_encrypted() && self.local_nodeid != 0).then_some(self.local_nodeid),
481        );
482        tx_header.plain.set_dst_unicast_nodeid(
483            (self.mode == SessionMode::PlainText)
484                .then_some(self.peer_nodeid)
485                .flatten(),
486        );
487
488        tx_header.proto.adjust_reliability(false, &self.peer_addr);
489
490        if let Some(exchange_index) = exch_index {
491            let exchange = unwrap!(self.exchanges[exchange_index].as_mut());
492
493            exchange.pre_send(
494                &tx_header.plain,
495                &mut tx_header.proto,
496                session_active_interval_ms,
497                session_idle_interval_ms,
498            )?;
499        }
500
501        Ok((self.peer_addr, retransmission))
502    }
503
504    /// Decode the remaining part of the packet after the plain header and then consume the `ParseBuf`
505    /// instance as it no longer would be necessary.
506    ///
507    /// Returns the range of the decoded packet payload
508    pub(crate) fn decode_remaining<C: Crypto>(
509        &self,
510        crypto: C,
511        rx_header: &mut PacketHdr,
512        mut pb: ParseBuf,
513    ) -> Result<(usize, usize), Error> {
514        rx_header.decode_remaining(
515            crypto,
516            self.get_dec_key(),
517            self.peer_nodeid.unwrap_or_default(),
518            &mut pb,
519        )?;
520
521        rx_header.proto.adjust_reliability(true, &self.peer_addr);
522
523        Ok(pb.slice_range())
524    }
525
526    pub(crate) fn encode<C: Crypto>(
527        &self,
528        crypto: C,
529        tx: &PacketHdr,
530        wb: &mut WriteBuf,
531    ) -> Result<(), Error> {
532        tx.encode(crypto, self.get_enc_key(), self.local_nodeid, wb)
533    }
534
535    fn update_last_used(&mut self) {
536        self.last_use = Instant::now();
537    }
538
539    pub(crate) fn get_exch_for_rx(&self, rx_proto: &ProtoHdr) -> Option<usize> {
540        self.exchanges
541            .iter()
542            .enumerate()
543            .filter(|(_, exch)| {
544                exch.as_ref()
545                    .map(|exch| exch.is_for_rx(rx_proto))
546                    .unwrap_or(false)
547            })
548            .map(|(index, _)| index)
549            .next()
550    }
551
552    pub(crate) fn add_exch(&mut self, exch_id: u16, role: Role) -> Option<usize> {
553        let exch_state = Some(ExchangeState {
554            exch_id,
555            role,
556            mrp: ReliableMessage::new(),
557        });
558
559        let exch_index = if self.exchanges.len() < MAX_EXCHANGES {
560            let _ = self.exchanges.push(exch_state);
561
562            self.exchanges.len() - 1
563        } else {
564            let index = self.exchanges.iter().position(Option::is_none);
565
566            if let Some(index) = index {
567                self.exchanges[index] = exch_state;
568
569                index
570            } else {
571                error!(
572                    "Too many exchanges for session {} [SID:{:x},RSID:{:x}]; exchange creation failed",
573                    self.id,
574                    self.get_local_sess_id(),
575                    self.get_peer_sess_id()
576                );
577
578                return None;
579            }
580        };
581
582        let exch_id = ExchangeId::new(self.id, exch_index);
583
584        debug!("New exchange: {} :: {:?}", exch_id.display(self), role);
585
586        Some(exch_index)
587    }
588
589    pub(crate) fn remove_exch(&mut self, index: usize) -> bool {
590        let exchange = unwrap!(self.exchanges[index].as_mut());
591        let exchange_id = ExchangeId::new(self.id, index);
592
593        if exchange.mrp.is_retrans_pending() {
594            exchange.role.set_dropped_state();
595            error!("Exchange {}: A packet is still (re)transmitted! Marking as dropped, but session will be closed", exchange_id.display(self));
596
597            false
598        } else if exchange.mrp.is_ack_pending() {
599            exchange.role.set_dropped_state();
600            warn!(
601                "Exchange {}: Pending ACK. Marking as dropped",
602                exchange_id.display(self)
603            );
604
605            false
606        } else {
607            trace!("Exchange {}: Dropped cleanly", exchange_id.display(self));
608            self.exchanges[index] = None;
609
610            true
611        }
612    }
613}
614
615impl fmt::Display for Session {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        write!(
618            f,
619            "peer: {:?}, peer_nodeid: {:?}, local: {}, remote: {}, msg_ctr: {}, mode: {:?}, ts: {:?}, expired: {}",
620            self.peer_addr,
621            self.peer_nodeid,
622            self.local_sess_id,
623            self.peer_sess_id,
624            self.msg_ctr,
625            self.mode,
626            self.last_use,
627            self.expired,
628        )
629    }
630}
631
632/// 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.
633///
634/// Public for testing purposes, but should not be used outside of the transport module.
635pub struct ReservedSession<'a> {
636    id: u32,
637    matter: &'a Matter<'a>,
638    complete: bool,
639}
640
641impl<'a> ReservedSession<'a> {
642    pub fn reserve_now<C: Crypto>(matter: &'a Matter<'a>, crypto: C) -> Result<Self, Error> {
643        let dev_det = matter.dev_det();
644        matter.with_state(|state| {
645            let mut rand = crypto.weak_rand()?;
646
647            let id = state
648                .sessions
649                .add(rand.next_u32(), true, Address::new(), None, dev_det)?
650                .id;
651
652            Ok(Self {
653                id,
654                matter,
655                complete: false,
656            })
657        })
658    }
659
660    pub async fn reserve<C: Crypto>(
661        matter: &'a Matter<'a>,
662        crypto: C,
663    ) -> Result<ReservedSession<'a>, Error> {
664        let session = Self::reserve_now(matter, &crypto);
665
666        if let Ok(session) = session {
667            Ok(session)
668        } else {
669            TransportRunner::new(matter, &crypto)
670                .evict_some_session()
671                .await?;
672
673            Self::reserve_now(matter, &crypto)
674        }
675    }
676
677    #[allow(clippy::too_many_arguments)]
678    pub fn update(
679        &mut self,
680        local_nodeid: u64,
681        peer_nodeid: u64,
682        peer_sessid: u16,
683        local_sessid: u16,
684        peer_addr: Address,
685        mode: SessionMode,
686        dec_key: Option<CanonAeadKeyRef<'_>>,
687        enc_key: Option<CanonAeadKeyRef<'_>>,
688        att_challenge: Option<AttChallengeRef<'_>>,
689    ) -> Result<(), Error> {
690        self.matter.with_state(|state| {
691            self.update_with_state(
692                state,
693                local_nodeid,
694                peer_nodeid,
695                peer_sessid,
696                local_sessid,
697                peer_addr,
698                mode,
699                dec_key,
700                enc_key,
701                att_challenge,
702            )
703        })
704    }
705
706    #[allow(clippy::too_many_arguments)]
707    pub fn update_with_state(
708        &mut self,
709        state: &mut MatterState,
710        local_nodeid: u64,
711        peer_nodeid: u64,
712        peer_sessid: u16,
713        local_sessid: u16,
714        peer_addr: Address,
715        mode: SessionMode,
716        dec_key: Option<CanonAeadKeyRef<'_>>,
717        enc_key: Option<CanonAeadKeyRef<'_>>,
718        att_challenge: Option<AttChallengeRef<'_>>,
719    ) -> Result<(), Error> {
720        let session = state.sessions.get(self.id).ok_or(ErrorCode::NoSession)?;
721
722        session.local_nodeid = local_nodeid;
723        session.peer_nodeid = Some(peer_nodeid);
724        session.peer_sess_id = peer_sessid;
725        session.local_sess_id = local_sessid;
726        session.peer_addr = peer_addr;
727        session.mode = mode;
728
729        if let Some(dec_key) = dec_key {
730            session.dec_key.load(dec_key);
731        }
732
733        if let Some(enc_key) = enc_key {
734            session.enc_key.load(enc_key);
735        }
736
737        if let Some(att_challenge) = att_challenge {
738            session.att_challenge.load(att_challenge);
739        }
740
741        Ok(())
742    }
743
744    /// Record the peer's MRP `session_parameters` (Sigma1 / Sigma2 /
745    /// PBKDFParamRequest / PBKDFParamResponse) on the reserved session so
746    /// they are in place by the time it transitions to a full Session.
747    /// Per Matter Core spec, MRP retransmission backoff to this
748    /// peer should derive from the peer-advertised `sai` (and idle
749    /// detection later from `sii`/`sat`).
750    pub(crate) fn set_peer_session_params(
751        &mut self,
752        params: &SessionParameters,
753    ) -> Result<(), Error> {
754        self.matter.with_state(|state| {
755            let session = state.sessions.get(self.id).ok_or(ErrorCode::NoSession)?;
756            session.set_peer_session_params(params);
757            Ok(())
758        })
759    }
760
761    pub fn complete(mut self) {
762        self.complete = true;
763    }
764}
765
766impl Drop for ReservedSession<'_> {
767    fn drop(&mut self) {
768        self.matter.with_state(|state| {
769            if self.complete {
770                let session = unwrap!(state.sessions.get(self.id));
771                session.reserved = false;
772            } else {
773                state.sessions.remove(self.id);
774            }
775        })
776    }
777}
778
779cfg_if! {
780    if #[cfg(feature = "max-sessions-64")] {
781        /// Max number of supported sessions
782        pub const MAX_SESSIONS: usize = 64;
783    } else if #[cfg(feature = "max-sessions-32")] {
784        /// Max number of supported sessions
785        pub const MAX_SESSIONS: usize = 32;
786    } else if #[cfg(feature = "max-sessions-16")] {
787        /// Max number of supported sessions
788        pub const MAX_SESSIONS: usize = 16;
789    } else if #[cfg(feature = "max-sessions-8")] {
790        /// Max number of supported sessions
791        pub const MAX_SESSIONS: usize = 8;
792    } else if #[cfg(feature = "max-sessions-7")] {
793        /// Max number of supported sessions
794        pub const MAX_SESSIONS: usize = 7;
795    } else if #[cfg(feature = "max-sessions-6")] {
796        /// Max number of supported sessions
797        pub const MAX_SESSIONS: usize = 6;
798    } else if #[cfg(feature = "max-sessions-5")] {
799        /// Max number of supported sessions
800        pub const MAX_SESSIONS: usize = 5;
801    } else if #[cfg(feature = "max-sessions-4")] {
802        /// Max number of supported sessions
803        pub const MAX_SESSIONS: usize = 4;
804    } else if #[cfg(feature = "max-sessions-3")] {
805        /// Max number of supported sessions
806        pub const MAX_SESSIONS: usize = 3;
807    } else {
808        /// Max number of supported sessions
809        pub const MAX_SESSIONS: usize = 16;
810    }
811}
812
813cfg_if! {
814    if #[cfg(feature = "max-exchanges-per-session-16")] {
815        /// Max number of supported exchanges per session
816        pub const MAX_EXCHANGES: usize = 16;
817    } else if #[cfg(feature = "max-exchanges-per-session-8")] {
818        /// Max number of supported exchanges per session
819        pub const MAX_EXCHANGES: usize = 8;
820    } else if #[cfg(feature = "max-exchanges-per-session-7")] {
821        /// Max number of supported exchanges per session
822        pub const MAX_EXCHANGES: usize = 7;
823    } else if #[cfg(feature = "max-exchanges-per-session-6")] {
824        /// Max number of supported exchanges per session
825        pub const MAX_EXCHANGES: usize = 6;
826    } else if #[cfg(feature = "max-exchanges-per-session-5")] {
827        /// Max number of supported exchanges per session
828        pub const MAX_EXCHANGES: usize = 5;
829    } else if #[cfg(feature = "max-exchanges-per-session-4")] {
830        /// Max number of supported exchanges per session
831        pub const MAX_EXCHANGES: usize = 4;
832    } else if #[cfg(feature = "max-exchanges-per-session-3")] {
833        /// Max number of supported exchanges per session
834        pub const MAX_EXCHANGES: usize = 3;
835    } else {
836        /// Max number of supported exchanges per session
837        pub const MAX_EXCHANGES: usize = 5;
838    }
839}
840
841const MATTER_MSG_CTR_RANGE: u32 = 0x0fffffff;
842
843/// All sessions
844pub struct Sessions {
845    next_sess_unique_id: u32,
846    next_sess_id: u16,
847    next_exch_id: u16,
848    sessions: Vec<Session, MAX_SESSIONS>,
849    group_ctr_store: GroupCtrStore,
850}
851
852impl Sessions {
853    /// Create a new Sessions instance.
854    #[inline(always)]
855    pub const fn new() -> Self {
856        Self {
857            sessions: Vec::new(),
858            group_ctr_store: GroupCtrStore::new(),
859            next_sess_unique_id: 0,
860            next_sess_id: 1,
861            next_exch_id: 1,
862        }
863    }
864
865    /// Create an in-place initializer for a new Sessions instance.
866    pub fn init() -> impl Init<Self> {
867        init!(Self {
868            sessions <- Vec::init(),
869            group_ctr_store: GroupCtrStore::new(),
870            next_sess_unique_id: 0,
871            next_sess_id: 1,
872            next_exch_id: 1,
873        })
874    }
875
876    pub fn reset(&mut self) {
877        self.sessions.clear();
878        self.group_ctr_store = GroupCtrStore::new();
879        self.next_sess_id = 1;
880        self.next_exch_id = 1;
881    }
882
883    /// Attempt to decrypt and accept a group (multicast) message.
884    ///
885    /// Derives group operational keys on-the-fly from `FabricMgr`, matching
886    /// the packet's `(session_id, group_id)`, tries to decrypt with each,
887    /// validates the group message counter, and creates an ephemeral group
888    /// session on success.
889    ///
890    /// Returns the created session and payload range, mirroring how unicast
891    /// uses `get_for_rx()` + `decode_remaining()`.
892    pub(crate) fn get_or_create_for_group_rx<const N: usize, C: Crypto>(
893        &mut self,
894        crypto: C,
895        fabrics: &Fabrics,
896        packet: &mut Packet<N>,
897        dev_det: &BasicInfoConfig<'_>,
898    ) -> Result<(&mut Session, (usize, usize)), Error> {
899        let src_nodeid = packet
900            .header
901            .plain
902            .get_src_nodeid()
903            .ok_or(ErrorCode::InvalidData)?;
904        let group_id = packet
905            .header
906            .plain
907            .get_dst_groupcast_nodeid()
908            .ok_or(ErrorCode::InvalidData)?;
909        let expected_sess_id = packet.header.plain.sess_id;
910        let msg_ctr = packet.header.plain.ctr;
911
912        debug!(
913            "Group: Attempting decrypt for PEER={:?} SID=0x{:04x}, GRP=0x{:04x}, SRC=0x{:016x}, CTR={}",
914            packet.peer, expected_sess_id, group_id, src_nodeid, msg_ctr
915        );
916
917        // Parse the plain header to determine encrypted portion offset
918        let mut pb = ParseBuf::new(&mut packet.buf[packet.payload_start..]);
919        packet.header.plain.decode(&mut pb)?;
920
921        // Save the encrypted payload so we can restore it between decryption attempts
922        let encrypted_offset = pb.read_off();
923        let encrypted_len = pb.as_slice().len();
924        let mut saved_encrypted = [0u8; 1280];
925        if encrypted_len > saved_encrypted.len() {
926            return Err(ErrorCode::BufferTooSmall.into());
927        }
928        saved_encrypted[..encrypted_len].copy_from_slice(pb.as_slice());
929
930        // Derive keys on-the-fly and try each one
931        let mut group_key_found: Option<(NonZeroU8, (usize, usize))> = None;
932
933        'outer: for fabric in fabrics.iter() {
934            let fab_idx = fabric.fab_idx();
935            let compressed_fabric_id = fabric.compressed_fabric_id();
936
937            for map_entry in fabric.groups().key_map_iter() {
938                if map_entry.group_id != group_id {
939                    continue;
940                }
941
942                let Some(key_set_entry) = fabric.groups().key_set_get(map_entry.group_key_set_id)
943                else {
944                    continue;
945                };
946
947                for epoch_key_entry in key_set_entry.epoch_keys.iter() {
948                    let mut temp_key_set = KeySet::new();
949                    if temp_key_set
950                        .update(
951                            &crypto,
952                            epoch_key_entry.epoch_key.reference(),
953                            &compressed_fabric_id,
954                        )
955                        .is_err()
956                    {
957                        continue;
958                    }
959
960                    let op_key_ref = temp_key_set.op_key();
961                    let Ok(session_id) = derive_group_session_id(&crypto, op_key_ref) else {
962                        continue;
963                    };
964
965                    if session_id != expected_sess_id {
966                        continue;
967                    }
968
969                    if let Some(payload_range) = Self::try_group_decrypt(
970                        &crypto,
971                        packet,
972                        &saved_encrypted[..encrypted_len],
973                        encrypted_offset,
974                        op_key_ref,
975                        src_nodeid,
976                    ) {
977                        group_key_found = Some((fab_idx, payload_range));
978                        break 'outer;
979                    }
980                }
981            }
982        }
983
984        if group_key_found.is_none() {
985            debug!(
986                "Group: No key could decrypt the message (SID=0x{:04x}, GRP=0x{:04x})",
987                expected_sess_id, group_id
988            );
989        }
990
991        let (fab_idx, payload_range) = group_key_found.ok_or(ErrorCode::NoSession)?;
992
993        // Validate group message counter before creating the session
994        if !self
995            .group_ctr_store
996            .post_recv(fab_idx.get(), src_nodeid, msg_ctr)
997        {
998            debug!(
999                "Group: Duplicate message counter {} from node 0x{:016x} fab_idx={}",
1000                msg_ctr, src_nodeid, fab_idx
1001            );
1002            return Err(ErrorCode::Duplicate.into());
1003        }
1004
1005        // Create ephemeral group session
1006        let peer = packet.peer;
1007        let mut rand = crypto.weak_rand()?;
1008        let session = match self.add(rand.next_u32(), false, peer, Some(src_nodeid), dev_det) {
1009            Ok(session) => session,
1010            Err(_) => {
1011                // Session table is full; evict the least-recently-used session
1012                if let Some(lru_id) = self.get_session_for_eviction().map(|sess| sess.id) {
1013                    debug!("Group: Evicting session {} to make room", lru_id);
1014                    self.remove(lru_id);
1015                    self.add(rand.next_u32(), false, peer, Some(src_nodeid), dev_det)?
1016                } else {
1017                    return Err(ErrorCode::NoSpaceSessions.into());
1018                }
1019            }
1020        };
1021        session.set_session_mode(SessionMode::Group { fab_idx, group_id });
1022        session.local_sess_id = expected_sess_id;
1023
1024        debug!(
1025            "Group: Created group session for fab_idx={}, group_id=0x{:04x}, src_nodeid=0x{:016x}",
1026            fab_idx, group_id, src_nodeid
1027        );
1028
1029        // Re-borrow the current created session for returning
1030        let session = unwrap!(self.sessions.last_mut());
1031        session.update_last_used();
1032
1033        Ok((session, payload_range))
1034    }
1035
1036    /// Try to decrypt a group message with a candidate key.
1037    /// Restores the ciphertext before attempting.
1038    /// On success, returns the payload range; the packet buffer contains decrypted data.
1039    fn try_group_decrypt<const N: usize, C: Crypto>(
1040        crypto: C,
1041        packet: &mut Packet<N>,
1042        saved_encrypted: &[u8],
1043        encrypted_offset: usize,
1044        op_key: CanonAeadKeyRef<'_>,
1045        src_nodeid: u64,
1046    ) -> Option<(usize, usize)> {
1047        // Restore ciphertext
1048        let start = packet.payload_start + encrypted_offset;
1049        let encrypted_len = saved_encrypted.len();
1050        packet.buf[start..start + encrypted_len].copy_from_slice(saved_encrypted);
1051
1052        // Re-create ParseBuf and re-parse plain header
1053        let mut pb = ParseBuf::new(&mut packet.buf[packet.payload_start..]);
1054        if packet.header.plain.decode(&mut pb).is_err() {
1055            error!("Plain header parse error");
1056            return None;
1057        }
1058
1059        if packet
1060            .header
1061            .decode_remaining(crypto, Some(op_key), src_nodeid, &mut pb)
1062            .is_ok()
1063        {
1064            packet.header.proto.adjust_reliability(true, &packet.peer);
1065            Some(pb.slice_range())
1066        } else {
1067            None
1068        }
1069    }
1070
1071    pub fn get_next_sess_id(&mut self) -> u16 {
1072        let mut next_sess_id: u16;
1073        loop {
1074            next_sess_id = self.next_sess_id;
1075
1076            // Increment next sess id
1077            self.next_sess_id = self.next_sess_id.overflowing_add(1).0;
1078            if self.next_sess_id == 0 {
1079                self.next_sess_id = 1;
1080            }
1081
1082            // Ensure the currently selected id doesn't match any existing session
1083            if self
1084                .sessions
1085                .iter()
1086                .all(|sess| sess.get_local_sess_id() != next_sess_id)
1087            {
1088                break;
1089            }
1090        }
1091        next_sess_id
1092    }
1093
1094    pub fn get_next_exch_id(&mut self) -> u16 {
1095        let mut next_exch_id: u16;
1096        loop {
1097            next_exch_id = self.next_exch_id;
1098
1099            // Increment next exch id
1100            self.next_exch_id = self.next_exch_id.overflowing_add(1).0;
1101            if self.next_exch_id == 0 {
1102                self.next_exch_id = 1;
1103            }
1104
1105            // Ensure the currently selected id doesn't match any existing exchange
1106            if self
1107                .sessions
1108                .iter()
1109                .flat_map(|sess| sess.exchanges.iter())
1110                .filter_map(|exch| exch.as_ref())
1111                .all(|exch| {
1112                    !matches!(exch.role, Role::Responder(_)) || exch.exch_id != next_exch_id
1113                })
1114            {
1115                break;
1116            }
1117        }
1118        next_exch_id
1119    }
1120
1121    pub fn get_session_for_eviction(&mut self) -> Option<&mut Session> {
1122        let mut lru_index = None;
1123        let mut lru_ts = Instant::now();
1124        for (i, s) in self.sessions.iter().enumerate() {
1125            if (s.expired || s.last_use < lru_ts)
1126                && !s.reserved
1127                && s.exchanges.iter().all(Option::is_none)
1128            {
1129                lru_ts = s.last_use;
1130                lru_index = Some(i);
1131
1132                if s.expired {
1133                    // Expired sessons are the prime candidates for eviction,
1134                    // so we can break early
1135                    break;
1136                }
1137            }
1138        }
1139
1140        lru_index.map(|index| &mut self.sessions[index])
1141    }
1142
1143    pub fn add(
1144        &mut self,
1145        msg_ctr: u32,
1146        reserved: bool,
1147        peer_addr: Address,
1148        peer_nodeid: Option<u64>,
1149        dev_det: &BasicInfoConfig<'_>,
1150    ) -> Result<&mut Session, Error> {
1151        let session_id = self.next_sess_unique_id;
1152
1153        self.next_sess_unique_id += 1;
1154        if self.next_sess_unique_id > 0x0fff_ffff {
1155            // Reserve the upper 4 bits for the exchange index
1156            self.next_sess_unique_id = 0;
1157        }
1158
1159        // Seed the peer's MRP intervals from our own configured defaults;
1160        // they'll be overwritten by Sigma1 / PBKDFParamRequest (or the
1161        // initiator's Sigma2 / PBKDFParamResponse) once the peer
1162        // advertises its own `session_parameters`.
1163        let (peer_active_interval_ms, peer_idle_interval_ms, peer_active_threshold_ms) =
1164            mrp::default_peer_mrp_params(dev_det);
1165
1166        let session = Session::init(
1167            session_id,
1168            msg_ctr,
1169            reserved,
1170            peer_addr,
1171            peer_nodeid,
1172            peer_active_interval_ms,
1173            peer_idle_interval_ms,
1174            peer_active_threshold_ms,
1175        );
1176
1177        self.sessions
1178            .push_init(session.into_fallible::<Error>(), || {
1179                ErrorCode::NoSpaceSessions.into()
1180            })?;
1181
1182        Ok(unwrap!(self.sessions.last_mut()))
1183    }
1184
1185    /// This assumes that the higher layer has taken care of doing anything required
1186    /// as per the spec before the session is removed
1187    pub fn remove(&mut self, id: u32) -> Option<Session> {
1188        if let Some(index) = self.sessions.iter().position(|sess| sess.id == id) {
1189            Some(self.sessions.swap_remove(index))
1190        } else {
1191            None
1192        }
1193    }
1194
1195    /// This assumes that the higher layer has taken care of doing anything required
1196    /// as per the spec before the sessions are removed or expired
1197    pub fn remove_for_fabric(&mut self, fabric_idx: NonZeroU8, expire_sess_id: Option<u32>) {
1198        while let Some(index) = self.sessions.iter().position(|sess| {
1199            sess.get_local_fabric_idx() == fabric_idx.get() && Some(sess.id) != expire_sess_id
1200        }) {
1201            info!(
1202                "Dropping session with ID {} for fabric index {} immediately",
1203                self.sessions[index].id, fabric_idx
1204            );
1205            self.sessions.swap_remove(index);
1206        }
1207
1208        if let Some(expire_sess_id) = expire_sess_id {
1209            let expire_sess = self
1210                .sessions
1211                .iter_mut()
1212                .find(|sess| sess.id == expire_sess_id);
1213            if let Some(expire_sess) = expire_sess {
1214                expire_sess.expired = true;
1215                info!(
1216                    "Marking session with ID {} as expired for fabric index {}",
1217                    expire_sess_id,
1218                    fabric_idx.get()
1219                );
1220            } else {
1221                warn!(
1222                    "No session with ID {} found for fabric index {} to mark as expired",
1223                    expire_sess_id,
1224                    fabric_idx.get()
1225                );
1226            }
1227        }
1228    }
1229
1230    pub fn get(&mut self, id: u32) -> Option<&mut Session> {
1231        let mut session = self.sessions.iter_mut().find(|sess| sess.id == id);
1232
1233        if let Some(session) = session.as_mut() {
1234            session.update_last_used();
1235        }
1236
1237        session
1238    }
1239
1240    /// Find the operational (CASE) session for a `(fabric, node)` pair.
1241    ///
1242    /// Operational sessions are by definition encrypted and on a real fabric,
1243    /// so the lookup always matches an encrypted session - there is no
1244    /// "unsecured by node" lookup (unsecured/PASE sessions carry no operational
1245    /// identity; see [`Sessions::get_pase_for_addr`]).
1246    pub(crate) fn get_for_node(
1247        &mut self,
1248        fabric_idx: NonZeroU8,
1249        peer_node_id: u64,
1250    ) -> Option<&mut Session> {
1251        // Prefer a TCP-backed session (larger payloads, no MRP fragmentation
1252        // limits) over UDP when both are available for the same peer. This
1253        // is required e.g. for the WebRTC Transport Provider's outbound
1254        // `Answer(sdp)` invoke whose payload can easily exceed a UDP MTU.
1255        let idx = self
1256            .sessions
1257            .iter()
1258            .enumerate()
1259            .filter(|(_, s)| !s.expired && s.is_for_node(fabric_idx, peer_node_id))
1260            .max_by_key(|(_, s)| i32::from(s.peer_addr.is_tcp()))
1261            .map(|(i, _)| i)?;
1262
1263        let session = &mut self.sessions[idx];
1264
1265        session.update_last_used();
1266
1267        Some(session)
1268    }
1269
1270    /// Find an in-flight PASE session to the given peer address.
1271    ///
1272    /// Used by [`Exchange::initiate_pase`](crate::transport::exchange::Exchange::initiate_pase)
1273    /// to reuse a PASE session per peer (rather than assuming a single global
1274    /// one), so a commissioner can drive several concurrent commissionings.
1275    pub(crate) fn get_pase_for_addr(&mut self, peer_addr: &Address) -> Option<&mut Session> {
1276        let mut session = self
1277            .sessions
1278            .iter_mut()
1279            .find(|s| !s.expired && s.is_pase_for_addr(peer_addr));
1280
1281        if let Some(session) = session.as_mut() {
1282            session.update_last_used();
1283        }
1284
1285        session
1286    }
1287
1288    pub(crate) fn get_for_rx(
1289        &mut self,
1290        rx_peer: &Address,
1291        rx_plain: &PlainHdr,
1292    ) -> Option<&mut Session> {
1293        let mut session = self
1294            .sessions
1295            .iter_mut()
1296            .find(|sess| sess.is_for_rx(rx_peer, rx_plain));
1297
1298        if let Some(session) = session.as_mut() {
1299            session.update_last_used();
1300        }
1301
1302        session
1303    }
1304
1305    pub(crate) fn get_for_tx(&mut self, session_id: u32) -> Option<&mut Session> {
1306        let mut session = self
1307            .sessions
1308            .iter_mut()
1309            .find(|sess| sess.is_for_tx(session_id));
1310
1311        if let Some(session) = session.as_mut() {
1312            session.update_last_used();
1313        }
1314
1315        session
1316    }
1317
1318    pub(crate) fn get_exch<F>(&mut self, f: F) -> Option<(&mut Session, usize)>
1319    where
1320        F: Fn(&Session, &ExchangeState) -> bool,
1321    {
1322        let exch = self
1323            .sessions
1324            .iter()
1325            .flat_map(|sess| {
1326                sess.exchanges
1327                    .iter()
1328                    .enumerate()
1329                    .filter_map(move |(exch_index, exch)| {
1330                        exch.as_ref().map(|exch| (sess, exch, exch_index))
1331                    })
1332            })
1333            .filter(|(sess, exch, _)| f(sess, exch))
1334            .map(|(sess, _, exch_index)| (sess.id, exch_index))
1335            .next();
1336
1337        if let Some((id, exch_index)) = exch {
1338            let session = unwrap!(self.get(id));
1339            session.update_last_used();
1340
1341            Some((session, exch_index))
1342        } else {
1343            None
1344        }
1345    }
1346
1347    /// Iterate over the sessions
1348    pub fn iter(&self) -> impl Iterator<Item = &Session> {
1349        self.sessions.iter()
1350    }
1351
1352    /// Drop every PASE session, whether unpromoted (still
1353    /// `SessionMode::Pase { fab_idx: 0 }`) or already promoted to a
1354    /// fabric. Used by:
1355    ///
1356    /// * `RevokeCommissioning` and a fail-safe expiry over a PASE
1357    ///   session (Matter Core spec): when the
1358    ///   commissioning window is torn down, any in-flight PASE sessions
1359    ///   associated with it must be terminated. A PASE that was
1360    ///   promoted via `AddNOC` is rolled back by the same fail-safe
1361    ///   expiry, so its session must go too.
1362    /// * `CommissioningComplete` (Matter Core spec): once the device
1363    ///   transitions to operational state,
1364    ///   all PASE sessions SHALL be terminated. Without this each
1365    ///   commissioning round leaks the promoted PASE it ran on, and
1366    ///   the session table eventually exhausts — visible as `BUSY` on
1367    ///   the next round's `PBKDFParamRequest`.
1368    ///
1369    /// `expire_sess_id` is the optional ID of a session that should NOT
1370    /// be removed immediately — typically the session that issued the
1371    /// triggering command, so its response can still be sent. That
1372    /// session is marked as `expired` instead, so it stops accepting
1373    /// new exchanges but the in-flight one can complete; the transport
1374    /// reclaims the slot via the usual LRU eviction path.
1375    pub fn remove_pase(&mut self, expire_sess_id: Option<u32>) {
1376        while let Some(index) = self.sessions.iter().position(|sess| {
1377            matches!(sess.get_session_mode(), SessionMode::Pase { .. })
1378                && Some(sess.id) != expire_sess_id
1379        }) {
1380            info!("Dropping PASE session with ID {}", self.sessions[index].id);
1381            self.sessions.swap_remove(index);
1382        }
1383
1384        if let Some(expire_sess_id) = expire_sess_id {
1385            if let Some(sess) = self.sessions.iter_mut().find(|sess| {
1386                sess.id == expire_sess_id
1387                    && matches!(sess.get_session_mode(), SessionMode::Pase { .. })
1388            }) {
1389                sess.expired = true;
1390                info!("Marking PASE session with ID {} as expired", expire_sess_id);
1391            }
1392        }
1393    }
1394}
1395
1396impl Default for Sessions {
1397    fn default() -> Self {
1398        Self::new()
1399    }
1400}
1401
1402impl fmt::Display for Sessions {
1403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1404        writeln!(f, "{{[")?;
1405        for s in &self.sessions {
1406            writeln!(f, "{{ {}, }},", s)?;
1407        }
1408        write!(f, "], next_sess_id: {}", self.next_sess_id)?;
1409        write!(f, "}}")
1410    }
1411}
1412
1413/// Derive the Group Session ID from an operational group key.
1414///
1415/// Per Matter Spec:
1416/// ```text
1417/// GroupKeyHash = Crypto_KDF(
1418///     InputKey = OperationalGroupKey,
1419///     Salt     = [],
1420///     Info     = "GroupKeyHash",
1421///     Length   = 16 bits
1422/// )
1423/// GroupSessionId = (GroupKeyHash[0] << 8) | GroupKeyHash[1]
1424/// ```
1425pub fn derive_group_session_id<C: Crypto>(
1426    crypto: C,
1427    op_key: CanonAeadKeyRef<'_>,
1428) -> Result<u16, Error> {
1429    const GRP_KEY_HASH_INFO: &[u8] = b"GroupKeyHash";
1430
1431    let mut hash = CryptoSensitive::<2>::new();
1432
1433    crypto
1434        .kdf()?
1435        .expand(&[], op_key, GRP_KEY_HASH_INFO, &mut hash)
1436        .map_err(|_| ErrorCode::InvalidData)?;
1437
1438    let bytes = hash.access();
1439    Ok(((bytes[0] as u16) << 8) | (bytes[1] as u16))
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use crate::crypto::{test_only_crypto, AEAD_KEY_ZEROED};
1445    use crate::dm::clusters::basic_info::BasicInfoConfig;
1446    use crate::transport::network::Address;
1447
1448    use super::*;
1449
1450    /// Stand-in `BasicInfoConfig` for tests that don't care about the
1451    /// peer-MRP defaults — `Sessions::add` only reads `sai`/`sii` from it.
1452    const TEST_DEV_DET: BasicInfoConfig<'static> = BasicInfoConfig::new();
1453
1454    #[test]
1455    fn test_next_sess_id_doesnt_reuse() {
1456        let mut sm = Sessions::new();
1457        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
1458        sess.set_local_sess_id(1);
1459        assert_eq!(sm.get_next_sess_id(), 2);
1460        assert_eq!(sm.get_next_sess_id(), 3);
1461        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
1462        sess.set_local_sess_id(4);
1463        assert_eq!(sm.get_next_sess_id(), 5);
1464    }
1465
1466    #[test]
1467    fn test_next_sess_id_overflows() {
1468        let mut sm = Sessions::new();
1469        let sess = unwrap!(sm.add(0, false, Address::default(), None, &TEST_DEV_DET));
1470        sess.set_local_sess_id(1);
1471        assert_eq!(sm.get_next_sess_id(), 2);
1472        sm.next_sess_id = 65534;
1473        assert_eq!(sm.get_next_sess_id(), 65534);
1474        assert_eq!(sm.get_next_sess_id(), 65535);
1475        assert_eq!(sm.get_next_sess_id(), 2);
1476    }
1477
1478    #[test]
1479    fn test_derive_group_session_id() {
1480        // Spec test vector:
1481        // Operational Group Key: a6:f5:30:6b:af:6d:05:0a:f2:3b:a4:bd:6b:9d:d9:60
1482        // Expected GroupSessionId: 0xB9F7 (47607)
1483        let op_key_bytes: [u8; 16] = [
1484            0xa6, 0xf5, 0x30, 0x6b, 0xaf, 0x6d, 0x05, 0x0a, 0xf2, 0x3b, 0xa4, 0xbd, 0x6b, 0x9d,
1485            0xd9, 0x60,
1486        ];
1487
1488        let mut op_key = AEAD_KEY_ZEROED;
1489        op_key.try_load_from_slice(&op_key_bytes).unwrap();
1490
1491        let crypto = test_only_crypto();
1492        let session_id = derive_group_session_id(&crypto, op_key.reference()).unwrap();
1493
1494        assert_eq!(
1495            session_id, 0xB9F7,
1496            "Group Session ID mismatch: got 0x{:04X}, expected 0xB9F7",
1497            session_id
1498        );
1499    }
1500}