use rand_core::RngCore;
use crate::crypto::Crypto;
use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext};
use crate::error::{Error, ErrorCode};
use crate::sc::pase::spake2p::SPAKE2P_VERIFIER_SALT_ZEROED;
use crate::sc::pase::{CommWindowOpener, CommWindowType};
use crate::tlv::Nullable;
use crate::MatterState;
pub use crate::dm::clusters::decl::administrator_commissioning::*;
use crate::transport::exchange::ExchangeId;
use crate::transport::session::SessionMode;
const MIN_PBKDF_ITERATIONS: u32 = 1000;
const MAX_PBKDF_ITERATIONS: u32 = 100_000;
const MIN_PAKE_SALT_LEN: usize = 16;
const MAX_PAKE_SALT_LEN: usize = 32;
const PAKE_VERIFIER_LEN: usize = 97;
fn cluster_status_err(ctx: &impl InvokeContext, status: StatusCode) -> Error {
ctx.cmd().set_cluster_status(status as u8);
Error::new(ErrorCode::Failure)
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AdminCommHandler {
dataver: Dataver,
}
impl AdminCommHandler {
pub const fn new(dataver: Dataver) -> Self {
Self { dataver }
}
pub const fn adapt(self) -> HandlerAdaptor<Self> {
HandlerAdaptor(self)
}
fn current_window_opener(state: &mut MatterState, id: &ExchangeId) -> Option<CommWindowOpener> {
let session = id.session(&mut state.sessions);
match session.get_session_mode() {
SessionMode::Case { fab_idx, .. } => Some(CommWindowOpener {
fab_idx: *fab_idx,
vendor_id: unwrap!(state.fabrics.get(*fab_idx)).vendor_id(),
}),
_ => None,
}
}
}
impl ClusterHandler for AdminCommHandler {
const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_features(Feature::BASIC.bits());
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
fn window_status(&self, ctx: impl ReadContext) -> Result<CommissioningWindowStatusEnum, Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
ctx.exchange().with_state(|state| {
state
.pase
.check_comm_window_timeout(notify_mdns, notify_change)?;
let comm_window = state.pase.comm_window();
let window_type = comm_window.map(|comm_window| comm_window.comm_window_type());
Ok(match window_type {
Some(CommWindowType::Basic) => CommissioningWindowStatusEnum::BasicWindowOpen,
Some(CommWindowType::Enhanced) => CommissioningWindowStatusEnum::EnhancedWindowOpen,
None => CommissioningWindowStatusEnum::WindowNotOpen,
})
})
}
fn admin_fabric_index(&self, ctx: impl ReadContext) -> Result<Nullable<u8>, Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
ctx.exchange().with_state(|state| {
state
.pase
.check_comm_window_timeout(notify_mdns, notify_change)?;
let comm_window = state.pase.comm_window();
if let Some(opener) = comm_window.and_then(|comm_window| comm_window.opener()) {
if state.fabrics.get(opener.fab_idx).is_some() {
return Ok(Nullable::some(opener.fab_idx.get()));
}
}
Ok(Nullable::none())
})
}
fn admin_vendor_id(&self, ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
ctx.exchange().with_state(|state| {
state
.pase
.check_comm_window_timeout(notify_mdns, notify_change)?;
let comm_window = state.pase.comm_window();
Ok(Nullable::new(
comm_window
.and_then(|comm_window| comm_window.opener())
.map(|opener| opener.vendor_id),
))
})
}
fn handle_open_commissioning_window(
&self,
ctx: impl InvokeContext,
request: OpenCommissioningWindowRequest<'_>,
) -> Result<(), Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
let iterations = request.iterations()?;
if !(MIN_PBKDF_ITERATIONS..=MAX_PBKDF_ITERATIONS).contains(&iterations) {
return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
}
let salt = request.salt()?;
if !(MIN_PAKE_SALT_LEN..=MAX_PAKE_SALT_LEN).contains(&salt.0.len()) {
return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
}
let verifier = request.pake_passcode_verifier()?;
if verifier.0.len() != PAKE_VERIFIER_LEN {
return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
}
ctx.exchange().with_state(|state| {
state
.pase
.check_comm_window_timeout(notify_mdns, notify_change)?;
let opener = Self::current_window_opener(state, &ctx.exchange().id());
let mdns_id = ctx.crypto().rand()?.next_u64();
state
.pase
.open_comm_window(
mdns_id,
verifier.0.try_into()?,
salt.0,
iterations,
request.discriminator()?,
request.commissioning_timeout()?,
opener,
notify_mdns,
notify_change,
)
.map_err(|err| map_open_window_err(&ctx, err))
})
}
fn handle_open_basic_commissioning_window(
&self,
ctx: impl InvokeContext,
request: OpenBasicCommissioningWindowRequest<'_>,
) -> Result<(), Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
ctx.exchange().with_state(|state| {
state
.pase
.check_comm_window_timeout(notify_mdns, notify_change)?;
let opener = Self::current_window_opener(state, &ctx.exchange().id());
let dev_comm = ctx.exchange().matter().dev_comm();
let crypto = ctx.crypto();
let mut rand = crypto.rand()?;
let mdns_id = rand.next_u64();
let mut salt = SPAKE2P_VERIFIER_SALT_ZEROED;
rand.fill_bytes(salt.access_mut());
state
.pase
.open_basic_comm_window(
mdns_id,
salt.access(),
dev_comm.password.reference(),
dev_comm.discriminator,
request.commissioning_timeout()?,
opener,
notify_mdns,
notify_change,
)
.map_err(|err| map_open_window_err(&ctx, err))
})
}
fn handle_revoke_commissioning(&self, ctx: impl InvokeContext) -> Result<(), Error> {
let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
let notify_change = |_, _| ctx.notify_own_cluster_changed();
ctx.exchange().with_state(|state| {
let sess = ctx.exchange().id().session(&mut state.sessions);
let expire_sess_id = matches!(
sess.get_session_mode(),
crate::transport::session::SessionMode::Pase { .. }
)
.then(|| sess.id());
state.failsafe.expire(
&mut state.fabrics,
&mut state.sessions,
expire_sess_id,
ctx.networks(),
ctx.kv(),
notify_mdns,
notify_change,
)?;
ctx.exchange().matter().transport().notify_session_removed();
Ok::<_, Error>(())
})?;
ctx.exchange().with_state(|state| {
state
.pase
.close_comm_window(notify_mdns, notify_change)
.map_err(|err| map_revoke_err(&ctx, err))
})?;
Ok(())
}
}
fn map_open_window_err(ctx: &impl InvokeContext, err: Error) -> Error {
if err.code() == ErrorCode::Busy {
cluster_status_err(ctx, StatusCode::Busy)
} else {
err
}
}
fn map_revoke_err(ctx: &impl InvokeContext, err: Error) -> Error {
if matches!(err.code(), ErrorCode::Invalid) {
cluster_status_err(ctx, StatusCode::WindowNotOpen)
} else {
err
}
}