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
87#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
89#[cfg_attr(feature = "defmt", derive(defmt::Format))]
90pub enum CommWindowState {
91 Closed,
93 Open {
95 opener: Option<CommWindowOpener>,
98 },
99}
100
101impl CommWindowState {
102 pub const fn is_open(&self) -> bool {
107 matches!(self, Self::Open { .. })
108 }
109
110 pub const fn is_open_on_all_transports(&self) -> bool {
113 matches!(self, Self::Open { opener: None })
114 }
115}
116
117pub struct CommWindow {
119 mdns_id: u64,
121 discriminator: u16,
123 pub(crate) verifier: Spake2pVerifierData,
125 opener: Option<CommWindowOpener>,
127 window_expiry: Instant,
129 pake_failures: u8,
133}
134
135impl CommWindow {
136 fn init_with_pw<'a>(
147 mdns_id: u64,
148 password: Spake2pVerifierPasswordRef<'a>,
149 salt: &'a [u8],
150 discriminator: u16,
151 opener: Option<CommWindowOpener>,
152 window_expiry: Instant,
153 ) -> impl Init<Self> + 'a {
154 init!(Self {
155 mdns_id,
156 discriminator,
157 verifier <- Spake2pVerifierData::init_with_pw(password, salt),
158 opener,
159 window_expiry,
160 pake_failures: 0,
161 })
162 }
163
164 fn init<'a>(
174 mdns_id: u64,
175 verifier: Spake2pVerifierStrRef<'a>,
176 salt: &'a [u8],
177 count: u32,
178 discriminator: u16,
179 opener: Option<CommWindowOpener>,
180 window_expiry: Instant,
181 ) -> impl Init<Self> + 'a {
182 init!(Self {
183 mdns_id,
184 discriminator,
185 verifier <- Spake2pVerifierData::init(verifier, salt, count),
186 opener,
187 window_expiry,
188 pake_failures: 0,
189 })
190 }
191
192 pub fn comm_window_type(&self) -> CommWindowType {
194 if self.verifier.password.is_some() {
195 CommWindowType::Basic
196 } else {
197 CommWindowType::Enhanced
198 }
199 }
200
201 pub fn opener(&self) -> Option<CommWindowOpener> {
203 self.opener
204 }
205
206 pub fn mdns_service(&self) -> MatterLocalService {
208 MatterLocalService::Commissionable {
209 id: self.mdns_id,
210 discriminator: self.discriminator,
211 enhanced: matches!(self.comm_window_type(), CommWindowType::Enhanced),
212 }
213 }
214}
215
216pub struct Pase {
218 comm_window: Maybe<CommWindow>,
220 pub(crate) session_timeout: Option<SessionEstTimeout>,
223}
224
225impl Pase {
226 #[inline(always)]
228 pub const fn new() -> Self {
229 Self {
230 comm_window: Maybe::none(),
231 session_timeout: None,
232 }
233 }
234
235 pub fn init() -> impl Init<Self> {
237 init!(Self {
238 comm_window <- Maybe::init_none(),
239 session_timeout: None,
240 })
241 }
242
243 pub fn check_comm_window_timeout(
248 &mut self,
249 notify_mdns: impl FnMut(),
250 notify_change: impl FnMut(EndptId, ClusterId),
251 ) -> Result<bool, Error> {
252 let expired = self
253 .comm_window
254 .as_opt_ref()
255 .map(|comm_window| Instant::now() > comm_window.window_expiry)
256 .unwrap_or(false);
257
258 if expired {
259 warn!("PASE Commissioning Window expired, closing");
260
261 self.close_comm_window(notify_mdns, notify_change)?;
262
263 Ok(true)
264 } else {
265 Ok(false)
266 }
267 }
268
269 pub fn comm_window(&self) -> Option<&CommWindow> {
271 self.comm_window.as_opt_ref()
272 }
273
274 fn validate_salt_len(salt: &[u8]) -> Result<(), Error> {
277 if !(SPAKE2P_VERIFIER_SALT_MIN_LEN..=SPAKE2P_VERIFIER_SALT_LEN).contains(&salt.len()) {
278 Err(ErrorCode::ConstraintError)?;
279 }
280
281 Ok(())
282 }
283
284 pub fn comm_window_state(&self) -> CommWindowState {
287 match self.comm_window() {
288 Some(comm_window) => CommWindowState::Open {
289 opener: comm_window.opener(),
290 },
291 None => CommWindowState::Closed,
292 }
293 }
294
295 #[allow(clippy::too_many_arguments)]
312 pub fn open_basic_comm_window(
313 &mut self,
314 mdns_id: u64,
315 salt: &[u8],
316 password: Spake2pVerifierPasswordRef<'_>,
317 discriminator: u16,
318 timeout_secs: u16,
319 opener: Option<CommWindowOpener>,
320 mut notify_mdns: impl FnMut(),
321 mut notify_change: impl FnMut(EndptId, ClusterId),
322 ) -> Result<(), Error> {
323 if self.comm_window.is_some() {
324 Err(ErrorCode::Busy)?;
325 }
326
327 if !(MIN_COMM_WINDOW_TIMEOUT_SECS..=MAX_COMM_WINDOW_TIMEOUT_SECS).contains(&timeout_secs) {
328 Err(ErrorCode::InvalidCommand)?;
329 }
330
331 Self::validate_salt_len(salt)?;
332
333 let window_expiry = Instant::now().saturating_add(Duration::from_secs(timeout_secs as _));
334
335 self.comm_window
336 .reinit(Maybe::init_some(CommWindow::init_with_pw(
337 mdns_id,
338 password,
339 salt,
340 discriminator,
341 opener,
342 window_expiry,
343 )));
344
345 notify_mdns();
346 notify_adm_comm_window_attrs_changed(&mut notify_change);
347
348 info!("PASE Basic Commissioning Window opened");
349
350 Ok(())
351 }
352
353 #[allow(clippy::too_many_arguments)]
371 pub fn open_comm_window(
372 &mut self,
373 mdns_id: u64,
374 verifier: Spake2pVerifierStrRef<'_>,
375 salt: &[u8],
376 count: u32,
377 discriminator: u16,
378 timeout_secs: u16,
379 opener: Option<CommWindowOpener>,
380 mut notify_mdns: impl FnMut(),
381 mut notify_change: impl FnMut(EndptId, ClusterId),
382 ) -> Result<(), Error> {
383 if self.comm_window.is_some() {
384 Err(ErrorCode::Busy)?;
385 }
386
387 if !(MIN_COMM_WINDOW_TIMEOUT_SECS..=MAX_COMM_WINDOW_TIMEOUT_SECS).contains(&timeout_secs) {
388 Err(ErrorCode::InvalidCommand)?;
389 }
390
391 Self::validate_salt_len(salt)?;
392
393 let window_expiry = Instant::now().saturating_add(Duration::from_secs(timeout_secs as _));
394
395 self.comm_window.reinit(Maybe::init_some(CommWindow::init(
396 mdns_id,
397 verifier,
398 salt,
399 count,
400 discriminator,
401 opener,
402 window_expiry,
403 )));
404
405 notify_mdns();
406 notify_adm_comm_window_attrs_changed(&mut notify_change);
407
408 info!("PASE Commissioning Window opened");
409
410 Ok(())
411 }
412
413 pub fn record_pake_failure(
427 &mut self,
428 notify_mdns: impl FnMut(),
429 notify_change: impl FnMut(EndptId, ClusterId),
430 ) -> Result<(), Error> {
431 const MAX_PAKE_FAILURES: u8 = 20;
432
433 self.session_timeout = None;
434
435 let revoke = if let Some(window) = self.comm_window.as_opt_mut() {
436 window.pake_failures = window.pake_failures.saturating_add(1);
437 warn!(
438 "PASE Commissioning Window: PAKE failure {} of {}",
439 window.pake_failures, MAX_PAKE_FAILURES
440 );
441 window.pake_failures >= MAX_PAKE_FAILURES
442 } else {
443 false
444 };
445
446 if revoke {
447 warn!("PASE Commissioning Window revoked after too many failed PAKE attempts");
448 self.close_comm_window(notify_mdns, notify_change)?;
449 }
450
451 Ok(())
452 }
453
454 pub fn close_comm_window(
463 &mut self,
464 mut notify_mdns: impl FnMut(),
465 mut notify_change: impl FnMut(EndptId, ClusterId),
466 ) -> Result<bool, Error> {
467 if self.comm_window.is_some() {
468 self.comm_window.clear();
469
470 notify_mdns();
471 notify_adm_comm_window_attrs_changed(&mut notify_change);
472
473 info!("PASE Commissioning Window closed");
474
475 Ok(true)
476 } else {
477 warn!("No PASE Commissioning Window to close");
478
479 Ok(false)
480 }
481 }
482}
483
484impl Default for Pase {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490const PASE_SESSION_EST_TIMEOUT_SECS: Duration = Duration::from_secs(60);
492
493pub(crate) const SPAKE2_SESSION_KEYS_INFO: &[u8] = b"SessionKeys";
495
496pub(crate) struct SessionEstTimeout {
498 session_est_expiry: Instant,
500 pub(crate) exch_id: ExchangeId,
502}
503
504impl SessionEstTimeout {
505 pub(crate) fn new(exchange: &Exchange) -> Self {
507 Self {
508 session_est_expiry: Instant::now().saturating_add(PASE_SESSION_EST_TIMEOUT_SECS),
509 exch_id: exchange.id(),
510 }
511 }
512
513 pub(crate) fn is_sess_expired(&self) -> bool {
515 Instant::now() > self.session_est_expiry
516 }
517}
518
519#[derive(FromTLV, ToTLV, Debug)]
521#[cfg_attr(feature = "defmt", derive(defmt::Format))]
522#[tlvargs(lifetime = "'a", start = 1)]
523pub(crate) struct PBKDFParamReq<'a> {
524 pub initiator_random: OctetStr<'a>,
526 pub initiator_ssid: u16,
528 pub passcode_id: u16,
530 pub has_params: bool,
532 pub session_parameters: Option<SessionParameters>,
534}
535
536#[derive(FromTLV, ToTLV, Debug)]
538#[cfg_attr(feature = "defmt", derive(defmt::Format))]
539#[tlvargs(lifetime = "'a", start = 1)]
540pub(crate) struct PBKDFParamResp<'a> {
541 pub initiator_random: OctetStr<'a>,
543 pub responder_random: OctetStr<'a>,
545 pub responder_ssid: u16,
547 pub params: Option<PBKDFParamRespParams<'a>>,
549 pub session_parameters: Option<crate::sc::SessionParameters>,
551}
552
553#[derive(FromTLV, ToTLV, Debug)]
555#[cfg_attr(feature = "defmt", derive(defmt::Format))]
556#[tlvargs(lifetime = "'a", start = 1)]
557pub(crate) struct PBKDFParamRespParams<'a> {
558 pub iterations: u32,
560 pub salt: OctetStr<'a>,
562}
563
564#[derive(FromTLV, ToTLV, Debug)]
566#[cfg_attr(feature = "defmt", derive(defmt::Format))]
567#[tlvargs(lifetime = "'a", start = 1)]
568pub(crate) struct Pake1<'a> {
569 pub pa: OctetStr<'a>,
571}
572
573#[derive(FromTLV, ToTLV, Debug)]
575#[cfg_attr(feature = "defmt", derive(defmt::Format))]
576#[tlvargs(lifetime = "'a", start = 1)]
577pub(crate) struct Pake2<'a> {
578 pub pb: OctetStr<'a>,
580 pub cb: OctetStr<'a>,
582}
583
584#[derive(FromTLV, ToTLV, Debug)]
586#[cfg_attr(feature = "defmt", derive(defmt::Format))]
587#[tlvargs(lifetime = "'a", start = 1)]
588pub(crate) struct Pake3<'a> {
589 pub ca: OctetStr<'a>,
591}