1use core::num::NonZeroU8;
24
25use embassy_time::{Duration, Instant};
26
27use crate::dm::clusters::adm_comm::{self};
28use crate::dm::endpoints::ROOT_ENDPOINT_ID;
29use crate::error::{Error, ErrorCode};
30use crate::im::{ClusterId, EndptId};
31use crate::sc::pase::spake2p::{
32 Spake2pVerifierData, Spake2pVerifierStrRef, SPAKE2P_VERIFIER_SALT_LEN,
33 SPAKE2P_VERIFIER_SALT_MIN_LEN,
34};
35use crate::sc::SessionParameters;
36use crate::tlv::{FromTLV, OctetStr, ToTLV};
37use crate::transport::exchange::{Exchange, ExchangeId};
38use crate::utils::init::{init, Init};
39use crate::utils::maybe::Maybe;
40use crate::MatterLocalService;
41
42pub use initiator::PaseInitiator;
43pub use responder::PaseResponder;
44pub use spake2p::{
45 Spake2pVerifierPassword, Spake2pVerifierPasswordRef, SPAKE2P_VERIFIER_PASSWORD_LEN,
46 SPAKE2P_VERIFIER_PASSWORD_ZEROED,
47};
48
49mod initiator;
50mod responder;
51pub(crate) mod spake2p;
52
53pub const MIN_COMM_WINDOW_TIMEOUT_SECS: u16 = 3 * 60;
55pub const MAX_COMM_WINDOW_TIMEOUT_SECS: u16 = 15 * 60;
57
58fn notify_adm_comm_window_attrs_changed(notify_change: &mut impl FnMut(EndptId, ClusterId)) {
64 notify_change(ROOT_ENDPOINT_ID, adm_comm::FULL_CLUSTER.id);
65}
66
67#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70pub enum CommWindowType {
71 Basic,
73 Enhanced,
75}
76
77#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
79#[cfg_attr(feature = "defmt", derive(defmt::Format))]
80pub struct CommWindowOpener {
81 pub fab_idx: NonZeroU8,
83 pub vendor_id: u16,
85}
86
87pub struct CommWindow {
89 mdns_id: u64,
91 discriminator: u16,
93 pub(crate) verifier: Spake2pVerifierData,
95 opener: Option<CommWindowOpener>,
97 window_expiry: Instant,
99 pake_failures: u8,
103}
104
105impl CommWindow {
106 fn init_with_pw<'a>(
117 mdns_id: u64,
118 password: Spake2pVerifierPasswordRef<'a>,
119 salt: &'a [u8],
120 discriminator: u16,
121 opener: Option<CommWindowOpener>,
122 window_expiry: Instant,
123 ) -> impl Init<Self> + 'a {
124 init!(Self {
125 mdns_id,
126 discriminator,
127 verifier <- Spake2pVerifierData::init_with_pw(password, salt),
128 opener,
129 window_expiry,
130 pake_failures: 0,
131 })
132 }
133
134 fn init<'a>(
144 mdns_id: u64,
145 verifier: Spake2pVerifierStrRef<'a>,
146 salt: &'a [u8],
147 count: u32,
148 discriminator: u16,
149 opener: Option<CommWindowOpener>,
150 window_expiry: Instant,
151 ) -> impl Init<Self> + 'a {
152 init!(Self {
153 mdns_id,
154 discriminator,
155 verifier <- Spake2pVerifierData::init(verifier, salt, count),
156 opener,
157 window_expiry,
158 pake_failures: 0,
159 })
160 }
161
162 pub fn comm_window_type(&self) -> CommWindowType {
164 if self.verifier.password.is_some() {
165 CommWindowType::Basic
166 } else {
167 CommWindowType::Enhanced
168 }
169 }
170
171 pub fn opener(&self) -> Option<CommWindowOpener> {
173 self.opener
174 }
175
176 pub fn mdns_service(&self) -> MatterLocalService {
178 MatterLocalService::Commissionable {
179 id: self.mdns_id,
180 discriminator: self.discriminator,
181 enhanced: matches!(self.comm_window_type(), CommWindowType::Enhanced),
182 }
183 }
184}
185
186pub struct Pase {
188 comm_window: Maybe<CommWindow>,
190 pub(crate) session_timeout: Option<SessionEstTimeout>,
193}
194
195impl Pase {
196 #[inline(always)]
198 pub const fn new() -> Self {
199 Self {
200 comm_window: Maybe::none(),
201 session_timeout: None,
202 }
203 }
204
205 pub fn init() -> impl Init<Self> {
207 init!(Self {
208 comm_window <- Maybe::init_none(),
209 session_timeout: None,
210 })
211 }
212
213 pub fn check_comm_window_timeout(
218 &mut self,
219 notify_mdns: impl FnMut(),
220 notify_change: impl FnMut(EndptId, ClusterId),
221 ) -> Result<bool, Error> {
222 let expired = self
223 .comm_window
224 .as_opt_ref()
225 .map(|comm_window| Instant::now() > comm_window.window_expiry)
226 .unwrap_or(false);
227
228 if expired {
229 warn!("PASE Commissioning Window expired, closing");
230
231 self.close_comm_window(notify_mdns, notify_change)?;
232
233 Ok(true)
234 } else {
235 Ok(false)
236 }
237 }
238
239 pub fn comm_window(&self) -> Option<&CommWindow> {
241 self.comm_window.as_opt_ref()
242 }
243
244 fn validate_salt_len(salt: &[u8]) -> Result<(), Error> {
247 if !(SPAKE2P_VERIFIER_SALT_MIN_LEN..=SPAKE2P_VERIFIER_SALT_LEN).contains(&salt.len()) {
248 Err(ErrorCode::ConstraintError)?;
249 }
250
251 Ok(())
252 }
253
254 #[allow(clippy::too_many_arguments)]
271 pub fn open_basic_comm_window(
272 &mut self,
273 mdns_id: u64,
274 salt: &[u8],
275 password: Spake2pVerifierPasswordRef<'_>,
276 discriminator: u16,
277 timeout_secs: u16,
278 opener: Option<CommWindowOpener>,
279 mut notify_mdns: impl FnMut(),
280 mut notify_change: impl FnMut(EndptId, ClusterId),
281 ) -> Result<(), Error> {
282 if self.comm_window.is_some() {
283 Err(ErrorCode::Busy)?;
284 }
285
286 if !(MIN_COMM_WINDOW_TIMEOUT_SECS..=MAX_COMM_WINDOW_TIMEOUT_SECS).contains(&timeout_secs) {
287 Err(ErrorCode::InvalidCommand)?;
288 }
289
290 Self::validate_salt_len(salt)?;
291
292 let window_expiry = Instant::now().saturating_add(Duration::from_secs(timeout_secs as _));
293
294 self.comm_window
295 .reinit(Maybe::init_some(CommWindow::init_with_pw(
296 mdns_id,
297 password,
298 salt,
299 discriminator,
300 opener,
301 window_expiry,
302 )));
303
304 notify_mdns();
305 notify_adm_comm_window_attrs_changed(&mut notify_change);
306
307 info!("PASE Basic Commissioning Window opened");
308
309 Ok(())
310 }
311
312 #[allow(clippy::too_many_arguments)]
330 pub fn open_comm_window(
331 &mut self,
332 mdns_id: u64,
333 verifier: Spake2pVerifierStrRef<'_>,
334 salt: &[u8],
335 count: u32,
336 discriminator: u16,
337 timeout_secs: u16,
338 opener: Option<CommWindowOpener>,
339 mut notify_mdns: impl FnMut(),
340 mut notify_change: impl FnMut(EndptId, ClusterId),
341 ) -> Result<(), Error> {
342 if self.comm_window.is_some() {
343 Err(ErrorCode::Busy)?;
344 }
345
346 if !(MIN_COMM_WINDOW_TIMEOUT_SECS..=MAX_COMM_WINDOW_TIMEOUT_SECS).contains(&timeout_secs) {
347 Err(ErrorCode::InvalidCommand)?;
348 }
349
350 Self::validate_salt_len(salt)?;
351
352 let window_expiry = Instant::now().saturating_add(Duration::from_secs(timeout_secs as _));
353
354 self.comm_window.reinit(Maybe::init_some(CommWindow::init(
355 mdns_id,
356 verifier,
357 salt,
358 count,
359 discriminator,
360 opener,
361 window_expiry,
362 )));
363
364 notify_mdns();
365 notify_adm_comm_window_attrs_changed(&mut notify_change);
366
367 info!("PASE Commissioning Window opened");
368
369 Ok(())
370 }
371
372 pub fn record_pake_failure(
386 &mut self,
387 notify_mdns: impl FnMut(),
388 notify_change: impl FnMut(EndptId, ClusterId),
389 ) -> Result<(), Error> {
390 const MAX_PAKE_FAILURES: u8 = 20;
391
392 self.session_timeout = None;
393
394 let revoke = if let Some(window) = self.comm_window.as_opt_mut() {
395 window.pake_failures = window.pake_failures.saturating_add(1);
396 warn!(
397 "PASE Commissioning Window: PAKE failure {} of {}",
398 window.pake_failures, MAX_PAKE_FAILURES
399 );
400 window.pake_failures >= MAX_PAKE_FAILURES
401 } else {
402 false
403 };
404
405 if revoke {
406 warn!("PASE Commissioning Window revoked after too many failed PAKE attempts");
407 self.close_comm_window(notify_mdns, notify_change)?;
408 }
409
410 Ok(())
411 }
412
413 pub fn close_comm_window(
422 &mut self,
423 mut notify_mdns: impl FnMut(),
424 mut notify_change: impl FnMut(EndptId, ClusterId),
425 ) -> Result<bool, Error> {
426 if self.comm_window.is_some() {
427 self.comm_window.clear();
428
429 notify_mdns();
430 notify_adm_comm_window_attrs_changed(&mut notify_change);
431
432 info!("PASE Commissioning Window closed");
433
434 Ok(true)
435 } else {
436 warn!("No PASE Commissioning Window to close");
437
438 Ok(false)
439 }
440 }
441}
442
443impl Default for Pase {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449const PASE_SESSION_EST_TIMEOUT_SECS: Duration = Duration::from_secs(60);
451
452pub(crate) const SPAKE2_SESSION_KEYS_INFO: &[u8] = b"SessionKeys";
454
455pub(crate) struct SessionEstTimeout {
457 session_est_expiry: Instant,
459 pub(crate) exch_id: ExchangeId,
461}
462
463impl SessionEstTimeout {
464 pub(crate) fn new(exchange: &Exchange) -> Self {
466 Self {
467 session_est_expiry: Instant::now().saturating_add(PASE_SESSION_EST_TIMEOUT_SECS),
468 exch_id: exchange.id(),
469 }
470 }
471
472 pub(crate) fn is_sess_expired(&self) -> bool {
474 Instant::now() > self.session_est_expiry
475 }
476}
477
478#[derive(FromTLV, ToTLV, Debug)]
480#[cfg_attr(feature = "defmt", derive(defmt::Format))]
481#[tlvargs(lifetime = "'a", start = 1)]
482pub(crate) struct PBKDFParamReq<'a> {
483 pub initiator_random: OctetStr<'a>,
485 pub initiator_ssid: u16,
487 pub passcode_id: u16,
489 pub has_params: bool,
491 pub session_parameters: Option<SessionParameters>,
493}
494
495#[derive(FromTLV, ToTLV, Debug)]
497#[cfg_attr(feature = "defmt", derive(defmt::Format))]
498#[tlvargs(lifetime = "'a", start = 1)]
499pub(crate) struct PBKDFParamResp<'a> {
500 pub initiator_random: OctetStr<'a>,
502 pub responder_random: OctetStr<'a>,
504 pub responder_ssid: u16,
506 pub params: Option<PBKDFParamRespParams<'a>>,
508 pub session_parameters: Option<crate::sc::SessionParameters>,
510}
511
512#[derive(FromTLV, ToTLV, Debug)]
514#[cfg_attr(feature = "defmt", derive(defmt::Format))]
515#[tlvargs(lifetime = "'a", start = 1)]
516pub(crate) struct PBKDFParamRespParams<'a> {
517 pub iterations: u32,
519 pub salt: OctetStr<'a>,
521}
522
523#[derive(FromTLV, ToTLV, Debug)]
525#[cfg_attr(feature = "defmt", derive(defmt::Format))]
526#[tlvargs(lifetime = "'a", start = 1)]
527pub(crate) struct Pake1<'a> {
528 pub pa: OctetStr<'a>,
530}
531
532#[derive(FromTLV, ToTLV, Debug)]
534#[cfg_attr(feature = "defmt", derive(defmt::Format))]
535#[tlvargs(lifetime = "'a", start = 1)]
536pub(crate) struct Pake2<'a> {
537 pub pb: OctetStr<'a>,
539 pub cb: OctetStr<'a>,
541}
542
543#[derive(FromTLV, ToTLV, Debug)]
545#[cfg_attr(feature = "defmt", derive(defmt::Format))]
546#[tlvargs(lifetime = "'a", start = 1)]
547pub(crate) struct Pake3<'a> {
548 pub ca: OctetStr<'a>,
550}