1use matter_cert::MatterCertificate;
32use matter_codec::{Tag, TlvWriter, Value};
33
34use crate::error::Error;
35use crate::state::{
36 CommissionerIdentity, ControllerState, DeviceEntry, FabricEntry, GroupKeySetConfig,
37};
38
39pub const SNAPSHOT_VERSION: u8 = 1;
41
42pub fn serialize(state: &ControllerState) -> Result<Vec<u8>, Error> {
49 let mut fabrics = Vec::with_capacity(state.fabrics.len());
50 for f in &state.fabrics {
51 fabrics.push(fabric_to_value(f)?);
52 }
53 let root = Value::Structure(vec![
54 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
55 (Tag::Context(1), Value::Array(fabrics)),
56 ]);
57
58 let mut out = Vec::new();
59 let mut w = TlvWriter::new(&mut out);
60 w.write_value(Tag::Anonymous, &root)?;
61 Ok(out)
62}
63
64fn fabric_to_value(f: &FabricEntry) -> Result<Value, Error> {
65 let devices = f.devices.iter().map(device_to_value).collect();
66 let group_keys: Vec<Value> = f.group_keys.iter().map(group_key_to_value).collect();
67 let icd_clients: Vec<Value> = f
68 .icd_clients
69 .iter()
70 .map(icd_registration_to_value)
71 .collect();
72 Ok(Value::Structure(vec![
73 (Tag::Context(0), Value::Uint(f.fabric_id)),
74 (Tag::Context(1), Value::Bytes(f.ipk.to_vec())),
75 (Tag::Context(2), Value::Bytes(f.rcac_cert.to_tlv()?)),
76 (Tag::Context(3), Value::Bytes(f.rcac_pkcs8.clone())),
77 (Tag::Context(4), commissioner_to_value(&f.commissioner)?),
78 (Tag::Context(5), Value::Array(devices)),
79 (Tag::Context(6), Value::Array(group_keys)),
80 (
81 Tag::Context(7),
82 Value::Uint(u64::from(f.outbound_group_counter)),
83 ),
84 (Tag::Context(8), Value::Array(icd_clients)),
85 ]))
86}
87
88fn icd_registration_to_value(r: &crate::icd::IcdRegistration) -> Value {
89 Value::Structure(vec![
90 (Tag::Context(0), Value::Uint(r.node_id)),
91 (Tag::Context(1), Value::Uint(r.check_in_node_id)),
92 (Tag::Context(2), Value::Uint(r.monitored_subject)),
93 (Tag::Context(3), Value::Bytes(r.key.to_vec())),
94 (Tag::Context(4), Value::Uint(u64::from(r.start_counter))),
95 ])
96}
97
98fn group_key_to_value(k: &GroupKeySetConfig) -> Value {
99 Value::Structure(vec![
100 (Tag::Context(0), Value::Uint(u64::from(k.key_set_id))),
101 (Tag::Context(1), Value::Bytes(k.epoch_key.to_vec())),
102 (Tag::Context(2), Value::Uint(k.epoch_start_time)),
103 ])
104}
105
106fn commissioner_to_value(c: &CommissionerIdentity) -> Result<Value, Error> {
107 Ok(Value::Structure(vec![
108 (Tag::Context(0), Value::Uint(c.node_id)),
109 (Tag::Context(1), Value::Bytes(c.operational_pkcs8.clone())),
110 (Tag::Context(2), Value::Bytes(c.noc.to_tlv()?)),
111 ]))
112}
113
114fn device_to_value(d: &DeviceEntry) -> Value {
115 let mut members = vec![
116 (Tag::Context(0), Value::Uint(d.node_id)),
117 (
118 Tag::Context(1),
119 Value::Bytes(d.peer_noc_public_key.to_vec()),
120 ),
121 ];
122 if let Some(rr) = &d.resumption_record {
123 members.push((Tag::Context(2), Value::Bytes(rr.clone())));
124 }
125 if let Some(addr) = &d.last_known_addr {
126 members.push((Tag::Context(3), Value::Utf8(addr.clone())));
127 }
128 Value::Structure(members)
129}
130
131pub fn deserialize(bytes: &[u8]) -> Result<ControllerState, Error> {
139 use matter_codec::TlvReader;
140
141 let mut r = TlvReader::new(bytes);
142 let (_tag, value) = r.read_value()?;
143 let root = as_struct(&value)?;
144
145 let version = get_uint(root, 0)?;
146 if version != u64::from(SNAPSHOT_VERSION) {
147 return Err(Error::Snapshot(format!(
148 "unsupported snapshot version {version}"
149 )));
150 }
151
152 let fabrics_val =
153 get(root, 1).ok_or_else(|| Error::Snapshot("missing fabrics array".into()))?;
154 let mut fabrics = Vec::new();
155 for fv in as_array(fabrics_val)? {
156 fabrics.push(fabric_from_value(fv)?);
157 }
158 Ok(ControllerState { fabrics })
159}
160
161fn fabric_from_value(v: &Value) -> Result<FabricEntry, Error> {
162 let m = as_struct(v)?;
163 let commissioner_val =
164 get(m, 4).ok_or_else(|| Error::Snapshot("missing commissioner".into()))?;
165 let devices_val = get(m, 5).ok_or_else(|| Error::Snapshot("missing devices array".into()))?;
166 let mut devices = Vec::new();
167 for dv in as_array(devices_val)? {
168 devices.push(device_from_value(dv)?);
169 }
170
171 let group_keys = match get(m, 6) {
175 Some(arr) => {
176 let mut keys = Vec::new();
177 for kv in as_array(arr)? {
178 keys.push(group_key_from_value(kv)?);
179 }
180 keys
181 }
182 None => Vec::new(),
183 };
184 let outbound_group_counter = match get(m, 7) {
185 Some(Value::Uint(n)) => u32::try_from(*n)
186 .map_err(|_| Error::Snapshot("outbound_group_counter exceeds u32 range".into()))?,
187 _ => 0,
188 };
189 let icd_clients = match get(m, 8) {
192 Some(arr) => {
193 let mut regs = Vec::new();
194 for rv in as_array(arr)? {
195 regs.push(icd_registration_from_value(rv)?);
196 }
197 regs
198 }
199 None => Vec::new(),
200 };
201
202 Ok(FabricEntry {
203 fabric_id: get_uint(m, 0)?,
204 ipk: byte_array::<16>(get_bytes(m, 1)?, "ipk")?,
205 rcac_cert: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
206 rcac_pkcs8: get_bytes(m, 3)?.to_vec(),
207 commissioner: commissioner_from_value(commissioner_val)?,
208 devices,
209 group_keys,
210 outbound_group_counter,
211 icd_clients,
212 })
213}
214
215fn icd_registration_from_value(v: &Value) -> Result<crate::icd::IcdRegistration, Error> {
216 let m = as_struct(v)?;
217 let start_counter = u32::try_from(get_uint(m, 4)?)
218 .map_err(|_| Error::Snapshot("icd start_counter exceeds u32 range".into()))?;
219 Ok(crate::icd::IcdRegistration::new(
220 get_uint(m, 0)?,
221 get_uint(m, 1)?,
222 get_uint(m, 2)?,
223 byte_array::<16>(get_bytes(m, 3)?, "icd key")?,
224 start_counter,
225 ))
226}
227
228fn group_key_from_value(v: &Value) -> Result<GroupKeySetConfig, Error> {
229 let m = as_struct(v)?;
230 let key_set_id = u16::try_from(get_uint(m, 0)?)
231 .map_err(|_| Error::Snapshot("key_set_id exceeds u16 range".into()))?;
232 let epoch_key = byte_array::<16>(get_bytes(m, 1)?, "epoch_key")?;
233 let epoch_start_time = get_uint(m, 2)?;
234 Ok(GroupKeySetConfig::new(
235 key_set_id,
236 epoch_key,
237 epoch_start_time,
238 ))
239}
240
241fn commissioner_from_value(v: &Value) -> Result<CommissionerIdentity, Error> {
242 let m = as_struct(v)?;
243 Ok(CommissionerIdentity {
244 node_id: get_uint(m, 0)?,
245 operational_pkcs8: get_bytes(m, 1)?.to_vec(),
246 noc: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
247 })
248}
249
250fn device_from_value(v: &Value) -> Result<DeviceEntry, Error> {
251 let m = as_struct(v)?;
252 let resumption_record = match get(m, 2) {
253 Some(Value::Bytes(b)) => Some(b.clone()),
254 _ => None,
255 };
256 let last_known_addr = match get(m, 3) {
257 Some(Value::Utf8(s)) => Some(s.clone()),
258 _ => None,
259 };
260 Ok(DeviceEntry {
261 node_id: get_uint(m, 0)?,
262 peer_noc_public_key: byte_array::<65>(get_bytes(m, 1)?, "peer_noc_public_key")?,
263 resumption_record,
264 last_known_addr,
265 })
266}
267
268fn as_struct(v: &Value) -> Result<&[(Tag, Value)], Error> {
271 match v {
272 Value::Structure(members) => Ok(members),
273 _ => Err(Error::Snapshot("expected structure".into())),
274 }
275}
276
277fn as_array(v: &Value) -> Result<&[Value], Error> {
278 match v {
279 Value::Array(items) => Ok(items),
280 _ => Err(Error::Snapshot("expected array".into())),
281 }
282}
283
284fn get(members: &[(Tag, Value)], ctx: u8) -> Option<&Value> {
285 members
286 .iter()
287 .find(|(t, _)| *t == Tag::Context(ctx))
288 .map(|(_, v)| v)
289}
290
291fn get_uint(members: &[(Tag, Value)], ctx: u8) -> Result<u64, Error> {
292 match get(members, ctx) {
293 Some(Value::Uint(n)) => Ok(*n),
294 _ => Err(Error::Snapshot(format!(
295 "missing or non-uint at context {ctx}"
296 ))),
297 }
298}
299
300fn get_bytes(members: &[(Tag, Value)], ctx: u8) -> Result<&[u8], Error> {
301 match get(members, ctx) {
302 Some(Value::Bytes(b)) => Ok(b),
303 _ => Err(Error::Snapshot(format!(
304 "missing or non-bytes at context {ctx}"
305 ))),
306 }
307}
308
309fn byte_array<const N: usize>(b: &[u8], field: &str) -> Result<[u8; N], Error> {
310 b.try_into()
311 .map_err(|_| Error::Snapshot(format!("{field}: expected {N} bytes, got {}", b.len())))
312}
313
314#[cfg(test)]
315#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
317 use super::*;
318 use crate::fabric::{create_fabric, FabricConfig};
319 use matter_cert::MatterTime;
320 use matter_commissioning::SystemNocRng;
321
322 fn sample_state() -> ControllerState {
323 let cfg = FabricConfig {
324 fabric_id: 0x1122_3344_5566_7788,
325 rcac_id: 1,
326 commissioner_node_id: 0x0000_0000_0000_0001,
327 validity: (
328 MatterTime::from_unix_secs(1_700_000_000),
329 MatterTime::NO_EXPIRY,
330 ),
331 };
332 let mut fabric = create_fabric(&cfg, &SystemNocRng).expect("create_fabric");
333 fabric.devices.push(DeviceEntry {
334 node_id: 0xABCD,
335 peer_noc_public_key: [0x04; 65],
336 resumption_record: Some(vec![1, 2, 3, 4]),
337 last_known_addr: Some("[fe80::1]:5540".to_string()),
338 });
339 fabric.devices.push(DeviceEntry {
340 node_id: 0xBEEF,
341 peer_noc_public_key: [0x04; 65],
342 resumption_record: None,
343 last_known_addr: None,
344 });
345 ControllerState {
346 fabrics: vec![fabric],
347 }
348 }
349
350 #[test]
351 fn round_trips_a_full_state() {
352 let state = sample_state();
353 let bytes = serialize(&state).expect("serialize");
354 let back = deserialize(&bytes).expect("deserialize");
355
356 assert_eq!(back.fabrics.len(), 1);
357 let (a, b) = (&state.fabrics[0], &back.fabrics[0]);
358 assert_eq!(a.fabric_id, b.fabric_id);
359 assert_eq!(a.ipk, b.ipk);
360 assert_eq!(a.rcac_pkcs8, b.rcac_pkcs8);
361 assert_eq!(a.rcac_cert.to_tlv().unwrap(), b.rcac_cert.to_tlv().unwrap());
362 assert_eq!(a.commissioner.node_id, b.commissioner.node_id);
363 assert_eq!(
364 a.commissioner.operational_pkcs8,
365 b.commissioner.operational_pkcs8
366 );
367 assert_eq!(
368 a.commissioner.noc.to_tlv().unwrap(),
369 b.commissioner.noc.to_tlv().unwrap()
370 );
371 assert_eq!(a.devices.len(), b.devices.len());
372 assert_eq!(a.devices[0].node_id, b.devices[0].node_id);
373 assert_eq!(
374 a.devices[0].resumption_record,
375 b.devices[0].resumption_record
376 );
377 assert_eq!(a.devices[0].last_known_addr, b.devices[0].last_known_addr);
378 assert_eq!(a.devices[1].resumption_record, None);
379 assert_eq!(a.devices[1].last_known_addr, None);
380 }
381
382 #[test]
383 fn empty_state_round_trips() {
384 let bytes = serialize(&ControllerState::default()).expect("serialize");
385 assert!(deserialize(&bytes).expect("deserialize").fabrics.is_empty());
386 }
387
388 #[test]
389 fn rejects_unknown_version() {
390 let root = Value::Structure(vec![
392 (Tag::Context(0), Value::Uint(99)),
393 (Tag::Context(1), Value::Array(vec![])),
394 ]);
395 let mut out = Vec::new();
396 let mut w = TlvWriter::new(&mut out);
397 w.write_value(Tag::Anonymous, &root).unwrap();
398 let err = deserialize(&out).expect_err("must reject");
399 assert!(matches!(err, Error::Snapshot(_)));
400 }
401
402 use proptest::prelude::*;
405 use std::sync::OnceLock;
406
407 fn shared_fabric() -> &'static FabricEntry {
410 static FABRIC: OnceLock<FabricEntry> = OnceLock::new();
411 FABRIC.get_or_init(|| {
412 let cfg = FabricConfig {
413 fabric_id: 0x0102_0304_0506_0708,
414 rcac_id: 1,
415 commissioner_node_id: 0x0000_0000_0000_0001,
416 validity: (
417 MatterTime::from_unix_secs(1_700_000_000),
418 MatterTime::NO_EXPIRY,
419 ),
420 };
421 create_fabric(&cfg, &SystemNocRng).expect("mint shared fabric")
422 })
423 }
424
425 prop_compose! {
426 fn arb_device()(
427 node_id in any::<u64>(),
428 pk in prop::collection::vec(any::<u8>(), 65),
429 rr in prop::option::of(prop::collection::vec(any::<u8>(), 0..40)),
430 addr in prop::option::of("[ -~]{0,32}"),
431 ) -> DeviceEntry {
432 let mut peer_noc_public_key = [0u8; 65];
433 peer_noc_public_key.copy_from_slice(&pk);
434 DeviceEntry { node_id, peer_noc_public_key, resumption_record: rr, last_known_addr: addr }
435 }
436 }
437
438 proptest! {
439 #[test]
441 fn snapshot_round_trips(devices in prop::collection::vec(arb_device(), 0..6)) {
442 let mut fabric = shared_fabric().clone();
443 fabric.devices = devices.clone();
444 let state = ControllerState { fabrics: vec![fabric] };
445
446 let bytes = serialize(&state).expect("serialize");
447 let back = deserialize(&bytes).expect("deserialize");
448
449 prop_assert_eq!(back.fabrics.len(), 1);
450 let dev_back = &back.fabrics[0].devices;
451 prop_assert_eq!(dev_back.len(), devices.len());
452 for (a, b) in devices.iter().zip(dev_back.iter()) {
453 prop_assert_eq!(a.node_id, b.node_id);
454 prop_assert_eq!(a.peer_noc_public_key, b.peer_noc_public_key);
455 prop_assert_eq!(&a.resumption_record, &b.resumption_record);
456 prop_assert_eq!(&a.last_known_addr, &b.last_known_addr);
457 }
458 }
459 }
460
461 #[test]
464 fn group_keys_round_trip() {
465 let mut fabric = shared_fabric().clone();
468 fabric.group_keys = vec![
469 GroupKeySetConfig::new(0x0001, [0xAA; 16], 1_700_000_000),
470 GroupKeySetConfig::new(0x0002, [0xBB; 16], 1_700_100_000),
471 ];
472 fabric.outbound_group_counter = 42;
473 let state = ControllerState {
474 fabrics: vec![fabric.clone()],
475 };
476
477 let bytes = serialize(&state).expect("serialize");
478 let back = deserialize(&bytes).expect("deserialize");
479
480 assert_eq!(back.fabrics.len(), 1);
481 let f = &back.fabrics[0];
482 assert_eq!(f.outbound_group_counter, 42);
483 assert_eq!(f.group_keys.len(), 2);
484 assert_eq!(f.group_keys[0].key_set_id, 0x0001);
485 assert_eq!(f.group_keys[0].epoch_key, [0xAA; 16]);
486 assert_eq!(f.group_keys[0].epoch_start_time, 1_700_000_000);
487 assert_eq!(f.group_keys[1].key_set_id, 0x0002);
488 assert_eq!(f.group_keys[1].epoch_key, [0xBB; 16]);
489 assert_eq!(f.group_keys[1].epoch_start_time, 1_700_100_000);
490 }
491
492 #[test]
493 fn icd_clients_round_trip() {
494 let mut fabric = shared_fabric().clone();
497 fabric.icd_clients = vec![
498 crate::icd::IcdRegistration::new(0x0042, 1, 1, [0xCC; 16], 7),
499 crate::icd::IcdRegistration::new(0x0043, 1, 2, [0xDD; 16], 99),
500 ];
501 let state = ControllerState {
502 fabrics: vec![fabric.clone()],
503 };
504 let bytes = serialize(&state).expect("serialize");
505 let back = deserialize(&bytes).expect("deserialize");
506 assert_eq!(back.fabrics[0].icd_clients, fabric.icd_clients);
507 }
508
509 #[test]
510 fn old_snapshot_without_t6_t7_loads_with_defaults() {
511 let fabric = shared_fabric().clone();
515
516 let old_fabric_val = Value::Structure(vec![
518 (Tag::Context(0), Value::Uint(fabric.fabric_id)),
519 (Tag::Context(1), Value::Bytes(fabric.ipk.to_vec())),
520 (
521 Tag::Context(2),
522 Value::Bytes(fabric.rcac_cert.to_tlv().expect("rcac tlv")),
523 ),
524 (Tag::Context(3), Value::Bytes(fabric.rcac_pkcs8.clone())),
525 (
526 Tag::Context(4),
527 commissioner_to_value(&fabric.commissioner).expect("commissioner"),
528 ),
529 (Tag::Context(5), Value::Array(vec![])), ]);
531
532 let root = Value::Structure(vec![
533 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
534 (Tag::Context(1), Value::Array(vec![old_fabric_val])),
535 ]);
536
537 let mut out = Vec::new();
538 let mut w = TlvWriter::new(&mut out);
539 w.write_value(Tag::Anonymous, &root).unwrap();
540
541 let back = deserialize(&out).expect("old snapshot must load without error");
542 assert_eq!(back.fabrics.len(), 1);
543 let f = &back.fabrics[0];
544 assert!(
545 f.group_keys.is_empty(),
546 "group_keys must default to empty for old snapshot"
547 );
548 assert_eq!(
549 f.outbound_group_counter, 0,
550 "outbound_group_counter must default to 0 for old snapshot"
551 );
552 }
553}