1use matter_codec::{Tag, Value};
5use matter_interaction::AttributePath;
6
7use crate::error::Error;
8
9pub(crate) const ADMIN_COMMISSIONING_CLUSTER: u32 = 0x003C;
11pub(crate) const CMD_OPEN_COMMISSIONING_WINDOW: u32 = 0x00;
13pub(crate) const CMD_OPEN_BASIC_COMMISSIONING_WINDOW: u32 = 0x01;
15pub(crate) const CMD_REVOKE_COMMISSIONING: u32 = 0x02;
17pub(crate) const ATTR_WINDOW_STATUS: u32 = 0x0000;
19pub(crate) const ATTR_ADMIN_FABRIC_INDEX: u32 = 0x0001;
21pub(crate) const ATTR_ADMIN_VENDOR_ID: u32 = 0x0002;
23pub const DEFAULT_WINDOW_ITERATIONS: u32 = 1000;
25pub const DEFAULT_WINDOW_TIMEOUT_S: u16 = 180;
27
28#[derive(Clone, Debug)]
30#[non_exhaustive]
31pub struct OpenWindowOpts {
32 pub timeout_s: u16,
34 pub iterations: u32,
36 pub vendor_id: Option<u16>,
39 pub product_id: Option<u16>,
41}
42
43impl Default for OpenWindowOpts {
44 fn default() -> Self {
45 Self {
46 timeout_s: DEFAULT_WINDOW_TIMEOUT_S,
47 iterations: DEFAULT_WINDOW_ITERATIONS,
48 vendor_id: None,
49 product_id: None,
50 }
51 }
52}
53
54#[derive(Clone, Debug)]
57#[non_exhaustive]
58pub struct CommissioningWindow {
59 pub passcode: u32,
61 pub discriminator: u16,
63 pub iterations: u32,
65 pub salt: Vec<u8>,
67 pub manual_code: String,
69 pub qr_code: Option<String>,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum CommissioningWindowStatus {
77 WindowNotOpen,
79 EnhancedWindowOpen,
81 BasicWindowOpen,
83 Unknown(u8),
85}
86
87impl CommissioningWindowStatus {
88 fn from_u8(v: u8) -> Self {
89 match v {
90 0 => Self::WindowNotOpen,
91 1 => Self::EnhancedWindowOpen,
92 2 => Self::BasicWindowOpen,
93 other => Self::Unknown(other),
94 }
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100#[non_exhaustive]
101pub struct WindowStatus {
102 pub status: CommissioningWindowStatus,
104 pub admin_fabric_index: Option<u8>,
106 pub admin_vendor_id: Option<u16>,
108}
109
110pub(crate) fn open_window_fields(
119 timeout_s: u16,
120 verifier: &[u8],
121 discriminator: u16,
122 iterations: u32,
123 salt: &[u8],
124) -> Value {
125 Value::Structure(vec![
126 (Tag::Context(0), Value::Uint(u64::from(timeout_s))),
127 (Tag::Context(1), Value::Bytes(verifier.to_vec())),
128 (Tag::Context(2), Value::Uint(u64::from(discriminator))),
129 (Tag::Context(3), Value::Uint(u64::from(iterations))),
130 (Tag::Context(4), Value::Bytes(salt.to_vec())),
131 ])
132}
133
134pub(crate) fn onboarding_payload(
141 passcode: u32,
142 discriminator: u16,
143 vendor_id: Option<u16>,
144 product_id: Option<u16>,
145) -> Result<(String, Option<String>), Error> {
146 use matter_commissioning::setup::{
147 encode_manual_code, encode_qr, CommissioningFlow, DiscoveryCapabilities, Discriminator,
148 Passcode, SetupPayload,
149 };
150 let map = |e: matter_commissioning::setup::Error| Error::SetupCode(e.to_string());
151 let base = SetupPayload {
152 version: 0,
153 vendor_id: None,
154 product_id: None,
155 commissioning_flow: CommissioningFlow::Standard,
156 discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
157 discriminator: Discriminator::new(discriminator).map_err(map)?,
158 passcode: Passcode::new(passcode).map_err(map)?,
159 };
160 let manual_code = encode_manual_code(&base);
162 let qr_code = match (vendor_id, product_id) {
163 (Some(v), Some(p)) => {
164 let qr = SetupPayload {
165 vendor_id: Some(v),
166 product_id: Some(p),
167 ..base
168 };
169 Some(encode_qr(&qr).map_err(map)?)
170 }
171 _ => None,
172 };
173 Ok((manual_code, qr_code))
174}
175
176pub(crate) fn random_window_secrets() -> Result<(u32, [u8; 32], u16), Error> {
185 use matter_commissioning::setup::Passcode;
186 let rng = |buf: &mut [u8]| {
187 matter_crypto::random_bytes(buf).map_err(|e| Error::Operational(format!("rng: {e}")))
188 };
189 let mut salt = [0u8; 32];
190 rng(&mut salt)?;
191 let mut db = [0u8; 2];
192 rng(&mut db)?;
193 let discriminator = u16::from_le_bytes(db) & 0x0FFF;
194 for _ in 0..64 {
197 let mut pb = [0u8; 4];
198 rng(&mut pb)?;
199 let candidate = u32::from_le_bytes(pb) & 0x07FF_FFFF; if Passcode::new(candidate).is_ok() {
201 return Ok((candidate, salt, discriminator));
202 }
203 }
204 Err(Error::Operational(
205 "could not generate a valid passcode".into(),
206 ))
207}
208
209pub(crate) fn parse_window_status(reports: &[(AttributePath, Value)]) -> WindowStatus {
211 let mut status = CommissioningWindowStatus::WindowNotOpen;
212 let mut admin_fabric_index: Option<u8> = None;
213 let mut admin_vendor_id: Option<u16> = None;
214 for (path, value) in reports {
215 match path.attribute {
216 ATTR_WINDOW_STATUS => {
217 if let Value::Uint(v) = value {
218 #[allow(clippy::cast_possible_truncation)]
219 {
221 status = CommissioningWindowStatus::from_u8(*v as u8);
222 }
223 }
224 }
225 ATTR_ADMIN_FABRIC_INDEX => {
226 if let Value::Uint(v) = value {
227 #[allow(clippy::cast_possible_truncation)]
228 {
230 admin_fabric_index = Some(*v as u8);
231 }
232 }
233 }
234 ATTR_ADMIN_VENDOR_ID => {
235 if let Value::Uint(v) = value {
236 #[allow(clippy::cast_possible_truncation)]
237 {
239 admin_vendor_id = Some(*v as u16);
240 }
241 }
242 }
243 _ => {}
244 }
245 }
246 WindowStatus {
247 status,
248 admin_fabric_index,
249 admin_vendor_id,
250 }
251}
252
253#[cfg(test)]
254#[allow(clippy::unwrap_used)] mod tests {
256 use super::*;
257
258 #[test]
259 fn open_window_fields_uses_spec_tags() {
260 let v = open_window_fields(180, &[0xAA; 97], 0xABC, 1000, &[0x01; 32]);
261 let Value::Structure(members) = v else {
262 panic!("expected struct")
263 };
264 assert_eq!(members.len(), 5);
265 assert_eq!(members[0].0, Tag::Context(0));
266 assert_eq!(members[0].1, Value::Uint(180));
267 assert_eq!(members[2].1, Value::Uint(0xABC));
268 assert_eq!(members[3].1, Value::Uint(1000));
269 assert!(matches!(&members[1].1, Value::Bytes(b) if b.len() == 97));
270 assert!(matches!(&members[4].1, Value::Bytes(b) if b.len() == 32));
271 }
272
273 #[test]
274 fn onboarding_payload_manual_always_qr_only_with_vidpid() {
275 let (manual, qr) = onboarding_payload(20_202_021, 3840, None, None).unwrap();
276 assert_eq!(manual.len(), 11);
277 assert!(qr.is_none());
278 let (_m, qr2) = onboarding_payload(20_202_021, 3840, Some(0xFFF1), Some(0x8000)).unwrap();
279 assert!(qr2.unwrap().starts_with("MT:"));
280 }
281
282 #[test]
283 fn random_window_secrets_are_valid_and_vary() {
284 use matter_commissioning::setup::{Discriminator, Passcode};
285 let (p1, s1, d1) = random_window_secrets().unwrap();
286 let (p2, _s2, _d2) = random_window_secrets().unwrap();
287 Passcode::new(p1).unwrap();
289 Discriminator::new(d1).unwrap();
290 assert_eq!(s1.len(), 32);
291 assert_ne!(s1, [0u8; 32]);
292 assert_ne!(p1, p2); }
294
295 #[test]
296 fn parse_window_status_reads_three_attrs() {
297 let ap = |a: u32| AttributePath {
298 endpoint: 0,
299 cluster: ADMIN_COMMISSIONING_CLUSTER,
300 attribute: a,
301 };
302 let reports = vec![
303 (ap(ATTR_WINDOW_STATUS), Value::Uint(1)),
304 (ap(ATTR_ADMIN_FABRIC_INDEX), Value::Uint(2)),
305 (ap(ATTR_ADMIN_VENDOR_ID), Value::Null),
306 ];
307 let ws = parse_window_status(&reports);
308 assert_eq!(ws.status, CommissioningWindowStatus::EnhancedWindowOpen);
309 assert_eq!(ws.admin_fabric_index, Some(2));
310 assert_eq!(ws.admin_vendor_id, None);
311 }
312}