Skip to main content

matter_controller/
group.rs

1//! `GroupKeyManagement` (0x003F) + `Groups` (0x0004) controller support: provisioning
2//! types and the pure Value codecs the group verbs compose. Decoder-agnostic
3//! (hand-built Value); the generated matter-clusters encoders are the byte-parity
4//! oracle. M9-E1.
5
6use matter_codec::{Tag, Value};
7
8/// Cluster ID for `GroupKeyManagement` (Matter §11.2).
9// Not yet wired to a caller outside tests; will be used by the group verbs task.
10#[allow(dead_code)]
11pub(crate) const GROUP_KEY_MANAGEMENT_CLUSTER: u32 = 0x003F;
12/// Cluster ID for `Groups` (Matter §1.3).
13#[allow(dead_code)]
14pub(crate) const GROUPS_CLUSTER: u32 = 0x0004;
15/// `KeySetWrite` command ID (`GroupKeyManagement`).
16#[allow(dead_code)]
17pub(crate) const CMD_KEY_SET_WRITE: u32 = 0x00;
18/// `KeySetRemove` command ID (`GroupKeyManagement`).
19#[allow(dead_code)]
20pub(crate) const CMD_KEY_SET_REMOVE: u32 = 0x03;
21/// `GroupKeyMap` attribute ID (`GroupKeyManagement`).
22#[allow(dead_code)]
23pub(crate) const ATTR_GROUP_KEY_MAP: u32 = 0x0000;
24/// `AddGroup` command ID (`Groups`).
25#[allow(dead_code)]
26pub(crate) const CMD_ADD_GROUP: u32 = 0x00;
27/// `RemoveGroup` command ID (`Groups`).
28#[allow(dead_code)]
29pub(crate) const CMD_REMOVE_GROUP: u32 = 0x03;
30/// `TrustFirst` group key security policy (the only one we provision).
31#[allow(dead_code)]
32pub(crate) const SECURITY_POLICY_TRUST_FIRST: u64 = 0;
33
34/// A group key set to provision via `KeySetWrite` (Matter §11.2.6.1).
35///
36/// Only a single epoch key (key0) is populated; keys 1 and 2 are left as
37/// `Null` per the single-epoch provisioning pattern used by most controllers.
38#[derive(Clone, Debug, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct GroupKeySet {
41    /// Key set id (must be ≥ 1; 0 is reserved for the IPK).
42    pub key_set_id: u16,
43    /// 16-byte epoch key (the group's symmetric key material).
44    pub epoch_key: Vec<u8>,
45    /// Epoch start time (Matter epoch microseconds).
46    pub epoch_start_time: u64,
47}
48
49impl GroupKeySet {
50    /// Construct a group key set. `epoch_key` must be 16 bytes.
51    #[must_use]
52    pub fn new(key_set_id: u16, epoch_key: Vec<u8>, epoch_start_time: u64) -> Self {
53        Self {
54            key_set_id,
55            epoch_key,
56            epoch_start_time,
57        }
58    }
59}
60
61/// One `GroupKeyMap` entry binding a group id to a key set (Matter §11.2.6.x).
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct GroupKeyMapEntry {
65    /// Group id.
66    pub group_id: u16,
67    /// Key set id this group uses.
68    pub group_key_set_id: u16,
69}
70
71impl GroupKeyMapEntry {
72    /// Construct a group-key-map entry.
73    #[must_use]
74    pub fn new(group_id: u16, group_key_set_id: u16) -> Self {
75        Self {
76            group_id,
77            group_key_set_id,
78        }
79    }
80}
81
82/// Build the `KeySetWrite` command fields (one field at tag 0 = the `GroupKeySet`
83/// struct). `epochKey1`/2 + `startTime1`/2 are emitted as `Null` (single-key set).
84#[allow(dead_code)]
85pub(crate) fn key_set_write_fields(set: &GroupKeySet) -> Value {
86    let key_set = Value::Structure(vec![
87        (Tag::Context(0), Value::Uint(u64::from(set.key_set_id))),
88        (Tag::Context(1), Value::Uint(SECURITY_POLICY_TRUST_FIRST)),
89        (Tag::Context(2), Value::Bytes(set.epoch_key.clone())),
90        (Tag::Context(3), Value::Uint(set.epoch_start_time)),
91        (Tag::Context(4), Value::Null),
92        (Tag::Context(5), Value::Null),
93        (Tag::Context(6), Value::Null),
94        (Tag::Context(7), Value::Null),
95    ]);
96    Value::Structure(vec![(Tag::Context(0), key_set)])
97}
98
99/// Build one `GroupKeyMapStruct` element (`group_id` t1, `key_set_id` t2). `fabric_index`
100/// (t254) is omitted on write — the device assigns the accessing fabric.
101#[allow(dead_code)]
102pub(crate) fn group_key_map_entry_value(e: GroupKeyMapEntry) -> Value {
103    Value::Structure(vec![
104        (Tag::Context(1), Value::Uint(u64::from(e.group_id))),
105        (Tag::Context(2), Value::Uint(u64::from(e.group_key_set_id))),
106    ])
107}
108
109/// Build the `AddGroup` command fields (`group_id` t0, `group_name` t1).
110#[allow(dead_code)]
111pub(crate) fn add_group_fields(group_id: u16, name: &str) -> Value {
112    Value::Structure(vec![
113        (Tag::Context(0), Value::Uint(u64::from(group_id))),
114        (Tag::Context(1), Value::Utf8(name.to_string())),
115    ])
116}
117
118/// Build the `RemoveGroup` command fields (`group_id` t0).
119#[allow(dead_code)]
120pub(crate) fn remove_group_fields(group_id: u16) -> Value {
121    Value::Structure(vec![(Tag::Context(0), Value::Uint(u64::from(group_id)))])
122}
123
124/// Parse the `status` (context tag 0) from a `Groups` response-command fields struct.
125///
126/// Returns `u8::MAX` if absent/malformed (treated as a non-success rejection).
127#[allow(dead_code)]
128pub(crate) fn parse_group_status(fields: &Value) -> u8 {
129    let members = match fields {
130        Value::Structure(m) | Value::List(m) => m.as_slice(),
131        _ => return u8::MAX,
132    };
133    members
134        .iter()
135        .find(|(t, _)| *t == Tag::Context(0))
136        .and_then(|(_, v)| {
137            if let Value::Uint(n) = v {
138                u8::try_from(*n).ok()
139            } else {
140                None
141            }
142        })
143        .unwrap_or(u8::MAX)
144}
145
146#[cfg(test)]
147mod tests {
148    use matter_codec::{TlvReader, TlvWriter};
149
150    use super::*;
151
152    fn enc(v: &Value) -> Vec<u8> {
153        let mut b = Vec::new();
154        #[allow(clippy::unwrap_used)] // test: Vec writer is infallible
155        TlvWriter::new(&mut b)
156            .write_value(Tag::Anonymous, v)
157            .unwrap();
158        b
159    }
160
161    #[test]
162    fn key_set_write_matches_generated_encoder() {
163        use matter_clusters::gen::group_key_management::{
164            encode_key_set_write, GroupKeySecurityPolicyEnum, GroupKeySetStruct,
165        };
166        use matter_clusters::types::Nullable;
167        let epoch = vec![0xABu8; 16];
168        let ours = enc(&key_set_write_fields(&GroupKeySet::new(
169            42,
170            epoch.clone(),
171            0,
172        )));
173        let theirs = encode_key_set_write(GroupKeySetStruct {
174            group_key_set_id: 42,
175            group_key_security_policy: GroupKeySecurityPolicyEnum::TrustFirst,
176            epoch_key0: Nullable::Value(epoch),
177            epoch_start_time0: Nullable::Value(0),
178            epoch_key1: Nullable::Null,
179            epoch_start_time1: Nullable::Null,
180            epoch_key2: Nullable::Null,
181            epoch_start_time2: Nullable::Null,
182            group_key_multicast_policy: None,
183            fabric_index: None,
184        });
185        assert_eq!(
186            ours, theirs,
187            "KeySetWrite fields must byte-match the generated encoder"
188        );
189    }
190
191    #[test]
192    fn group_key_map_entry_matches_generated_encoder() {
193        // `GroupKeyMapStruct` is `#[non_exhaustive]` — can't construct externally.
194        // Verify our builder produces a valid structure with the correct t1/t2 members
195        // by round-tripping through TLV and confirming `group_id` and `key_set_id`.
196        let ours = enc(&group_key_map_entry_value(GroupKeyMapEntry::new(7, 42)));
197        // Parse back and check the two shared fields (t1 = group_id, t2 = group_key_set_id).
198        #[allow(clippy::unwrap_used)] // test: known-good bytes
199        let (_tag, val) = TlvReader::new(&ours).read_value().unwrap();
200        let Value::Structure(members) = val else {
201            panic!("expected structure")
202        };
203        let group_id = members.iter().find(|(t, _)| *t == Tag::Context(1));
204        let key_set_id = members.iter().find(|(t, _)| *t == Tag::Context(2));
205        assert_eq!(group_id, Some(&(Tag::Context(1), Value::Uint(7))));
206        assert_eq!(key_set_id, Some(&(Tag::Context(2), Value::Uint(42))));
207        // `fabric_index` (t254) must NOT be present on write.
208        let fabric_index = members.iter().find(|(t, _)| *t == Tag::Context(254));
209        assert!(fabric_index.is_none(), "write must not emit fabric_index");
210    }
211
212    #[test]
213    fn add_group_and_status() {
214        let f = add_group_fields(7, "kitchen");
215        let Value::Structure(m) = f else { panic!() };
216        assert_eq!(m[0], (Tag::Context(0), Value::Uint(7)));
217        assert_eq!(m[1], (Tag::Context(1), Value::Utf8("kitchen".into())));
218        let resp = Value::Structure(vec![
219            (Tag::Context(0), Value::Uint(0)),
220            (Tag::Context(1), Value::Uint(7)),
221        ]);
222        assert_eq!(parse_group_status(&resp), 0);
223        assert_eq!(
224            parse_group_status(&Value::Structure(vec![(Tag::Context(0), Value::Uint(137))])),
225            137
226        );
227    }
228}