matter_controller/icd.rs
1//! ICD (Intermittently Connected Device) client registration state + the
2//! `IcdManagement` (0x0046) `RegisterClient` command `Value`.
3//!
4//! When the controller registers as a check-in client with an ICD, it generates
5//! a 16-byte symmetric key and records a [`IcdRegistration`] so the check-in
6//! listener can later decrypt + verify that device's unsolicited Check-In
7//! messages (see [`crate::icd_listener`]).
8
9use matter_codec::{Tag, Value};
10
11/// ICD Management cluster id.
12pub(crate) const ICD_MANAGEMENT_CLUSTER: u32 = 0x0046;
13
14/// `IcdManagement.ClientTypeEnum` โ whether the registration is permanent or
15/// ephemeral (Matter Core ยง9.17).
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum IcdClientType {
19 /// The client stays registered across the device's reboots.
20 Permanent,
21 /// The registration is dropped when the device reboots.
22 Ephemeral,
23}
24
25impl IcdClientType {
26 fn to_u8(self) -> u8 {
27 match self {
28 Self::Permanent => 0,
29 Self::Ephemeral => 1,
30 }
31 }
32}
33
34/// A persisted ICD client registration โ the controller registered itself as a
35/// check-in client with the device `node_id`, and holds the shared key needed to
36/// verify that device's Check-In messages.
37#[derive(Clone, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct IcdRegistration {
40 /// The ICD device this registration is with.
41 pub node_id: u64,
42 /// The node id the ICD sends Check-Ins to (our commissioner node id).
43 pub check_in_node_id: u64,
44 /// The subject the ICD monitors on our behalf (typically our node id).
45 pub monitored_subject: u64,
46 /// The 16-byte symmetric key protecting this device's Check-In messages.
47 pub key: [u8; 16],
48 /// The device's `ICDCounter` at registration time โ the floor for the
49 /// monotonicity (replay) check on inbound Check-Ins.
50 pub start_counter: u32,
51}
52
53impl IcdRegistration {
54 /// Construct an [`IcdRegistration`] (the struct is `#[non_exhaustive]`).
55 #[must_use]
56 pub fn new(
57 node_id: u64,
58 check_in_node_id: u64,
59 monitored_subject: u64,
60 key: [u8; 16],
61 start_counter: u32,
62 ) -> Self {
63 Self {
64 node_id,
65 check_in_node_id,
66 monitored_subject,
67 key,
68 start_counter,
69 }
70 }
71}
72
73/// Build the `RegisterClient` command `Value` (0x0046 cmd 0x00): ctx0
74/// `CheckInNodeID`, ctx1 `MonitoredSubject`, ctx2 `Key`, ctx4 `ClientType`. The
75/// optional `VerificationKey` (ctx3) is omitted (only needed to re-key an
76/// existing registration, which is deferred).
77pub(crate) fn register_client_fields(
78 check_in_node_id: u64,
79 monitored_subject: u64,
80 key: &[u8; 16],
81 client_type: IcdClientType,
82) -> Value {
83 Value::Structure(vec![
84 (Tag::Context(0), Value::Uint(check_in_node_id)),
85 (Tag::Context(1), Value::Uint(monitored_subject)),
86 (Tag::Context(2), Value::Bytes(key.to_vec())),
87 (Tag::Context(4), Value::Uint(u64::from(client_type.to_u8()))),
88 ])
89}
90
91#[cfg(test)]
92mod tests {
93 #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md carve-out.
94 use super::*;
95
96 #[test]
97 fn register_client_fields_has_expected_tags() {
98 let key = [0xABu8; 16];
99 let v = register_client_fields(1, 2, &key, IcdClientType::Permanent);
100 let Value::Structure(m) = v else {
101 panic!("expected struct")
102 };
103 assert_eq!(m[0], (Tag::Context(0), Value::Uint(1)));
104 assert_eq!(m[1], (Tag::Context(1), Value::Uint(2)));
105 assert_eq!(m[2], (Tag::Context(2), Value::Bytes(key.to_vec())));
106 assert_eq!(m[3], (Tag::Context(4), Value::Uint(0)));
107 }
108}