use matter_codec::{Tag, Value};
use matter_interaction::AttributePath;
use crate::error::Error;
pub(crate) const ADMIN_COMMISSIONING_CLUSTER: u32 = 0x003C;
pub(crate) const CMD_OPEN_COMMISSIONING_WINDOW: u32 = 0x00;
pub(crate) const CMD_OPEN_BASIC_COMMISSIONING_WINDOW: u32 = 0x01;
pub(crate) const CMD_REVOKE_COMMISSIONING: u32 = 0x02;
pub(crate) const ATTR_WINDOW_STATUS: u32 = 0x0000;
pub(crate) const ATTR_ADMIN_FABRIC_INDEX: u32 = 0x0001;
pub(crate) const ATTR_ADMIN_VENDOR_ID: u32 = 0x0002;
pub const DEFAULT_WINDOW_ITERATIONS: u32 = 1000;
pub const DEFAULT_WINDOW_TIMEOUT_S: u16 = 180;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct OpenWindowOpts {
pub timeout_s: u16,
pub iterations: u32,
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
}
impl Default for OpenWindowOpts {
fn default() -> Self {
Self {
timeout_s: DEFAULT_WINDOW_TIMEOUT_S,
iterations: DEFAULT_WINDOW_ITERATIONS,
vendor_id: None,
product_id: None,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CommissioningWindow {
pub passcode: u32,
pub discriminator: u16,
pub iterations: u32,
pub salt: Vec<u8>,
pub manual_code: String,
pub qr_code: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommissioningWindowStatus {
WindowNotOpen,
EnhancedWindowOpen,
BasicWindowOpen,
Unknown(u8),
}
impl CommissioningWindowStatus {
fn from_u8(v: u8) -> Self {
match v {
0 => Self::WindowNotOpen,
1 => Self::EnhancedWindowOpen,
2 => Self::BasicWindowOpen,
other => Self::Unknown(other),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct WindowStatus {
pub status: CommissioningWindowStatus,
pub admin_fabric_index: Option<u8>,
pub admin_vendor_id: Option<u16>,
}
pub(crate) fn open_window_fields(
timeout_s: u16,
verifier: &[u8],
discriminator: u16,
iterations: u32,
salt: &[u8],
) -> Value {
Value::Structure(vec![
(Tag::Context(0), Value::Uint(u64::from(timeout_s))),
(Tag::Context(1), Value::Bytes(verifier.to_vec())),
(Tag::Context(2), Value::Uint(u64::from(discriminator))),
(Tag::Context(3), Value::Uint(u64::from(iterations))),
(Tag::Context(4), Value::Bytes(salt.to_vec())),
])
}
pub(crate) fn onboarding_payload(
passcode: u32,
discriminator: u16,
vendor_id: Option<u16>,
product_id: Option<u16>,
) -> Result<(String, Option<String>), Error> {
use matter_commissioning::setup::{
encode_manual_code, encode_qr, CommissioningFlow, DiscoveryCapabilities, Discriminator,
Passcode, SetupPayload,
};
let map = |e: matter_commissioning::setup::Error| Error::SetupCode(e.to_string());
let base = SetupPayload {
version: 0,
vendor_id: None,
product_id: None,
commissioning_flow: CommissioningFlow::Standard,
discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
discriminator: Discriminator::new(discriminator).map_err(map)?,
passcode: Passcode::new(passcode).map_err(map)?,
};
let manual_code = encode_manual_code(&base);
let qr_code = match (vendor_id, product_id) {
(Some(v), Some(p)) => {
let qr = SetupPayload {
vendor_id: Some(v),
product_id: Some(p),
..base
};
Some(encode_qr(&qr).map_err(map)?)
}
_ => None,
};
Ok((manual_code, qr_code))
}
pub(crate) fn random_window_secrets() -> Result<(u32, [u8; 32], u16), Error> {
use matter_commissioning::setup::Passcode;
let rng = |buf: &mut [u8]| {
matter_crypto::random_bytes(buf).map_err(|e| Error::Operational(format!("rng: {e}")))
};
let mut salt = [0u8; 32];
rng(&mut salt)?;
let mut db = [0u8; 2];
rng(&mut db)?;
let discriminator = u16::from_le_bytes(db) & 0x0FFF;
for _ in 0..64 {
let mut pb = [0u8; 4];
rng(&mut pb)?;
let candidate = u32::from_le_bytes(pb) & 0x07FF_FFFF; if Passcode::new(candidate).is_ok() {
return Ok((candidate, salt, discriminator));
}
}
Err(Error::Operational(
"could not generate a valid passcode".into(),
))
}
pub(crate) fn parse_window_status(reports: &[(AttributePath, Value)]) -> WindowStatus {
let mut status = CommissioningWindowStatus::WindowNotOpen;
let mut admin_fabric_index: Option<u8> = None;
let mut admin_vendor_id: Option<u16> = None;
for (path, value) in reports {
match path.attribute {
ATTR_WINDOW_STATUS => {
if let Value::Uint(v) = value {
#[allow(clippy::cast_possible_truncation)]
{
status = CommissioningWindowStatus::from_u8(*v as u8);
}
}
}
ATTR_ADMIN_FABRIC_INDEX => {
if let Value::Uint(v) = value {
#[allow(clippy::cast_possible_truncation)]
{
admin_fabric_index = Some(*v as u8);
}
}
}
ATTR_ADMIN_VENDOR_ID => {
if let Value::Uint(v) = value {
#[allow(clippy::cast_possible_truncation)]
{
admin_vendor_id = Some(*v as u16);
}
}
}
_ => {}
}
}
WindowStatus {
status,
admin_fabric_index,
admin_vendor_id,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn open_window_fields_uses_spec_tags() {
let v = open_window_fields(180, &[0xAA; 97], 0xABC, 1000, &[0x01; 32]);
let Value::Structure(members) = v else {
panic!("expected struct")
};
assert_eq!(members.len(), 5);
assert_eq!(members[0].0, Tag::Context(0));
assert_eq!(members[0].1, Value::Uint(180));
assert_eq!(members[2].1, Value::Uint(0xABC));
assert_eq!(members[3].1, Value::Uint(1000));
assert!(matches!(&members[1].1, Value::Bytes(b) if b.len() == 97));
assert!(matches!(&members[4].1, Value::Bytes(b) if b.len() == 32));
}
#[test]
fn onboarding_payload_manual_always_qr_only_with_vidpid() {
let (manual, qr) = onboarding_payload(20_202_021, 3840, None, None).unwrap();
assert_eq!(manual.len(), 11);
assert!(qr.is_none());
let (_m, qr2) = onboarding_payload(20_202_021, 3840, Some(0xFFF1), Some(0x8000)).unwrap();
assert!(qr2.unwrap().starts_with("MT:"));
}
#[test]
fn random_window_secrets_are_valid_and_vary() {
use matter_commissioning::setup::{Discriminator, Passcode};
let (p1, s1, d1) = random_window_secrets().unwrap();
let (p2, _s2, _d2) = random_window_secrets().unwrap();
Passcode::new(p1).unwrap();
Discriminator::new(d1).unwrap();
assert_eq!(s1.len(), 32);
assert_ne!(s1, [0u8; 32]);
assert_ne!(p1, p2); }
#[test]
fn parse_window_status_reads_three_attrs() {
let ap = |a: u32| AttributePath {
endpoint: 0,
cluster: ADMIN_COMMISSIONING_CLUSTER,
attribute: a,
};
let reports = vec![
(ap(ATTR_WINDOW_STATUS), Value::Uint(1)),
(ap(ATTR_ADMIN_FABRIC_INDEX), Value::Uint(2)),
(ap(ATTR_ADMIN_VENDOR_ID), Value::Null),
];
let ws = parse_window_status(&reports);
assert_eq!(ws.status, CommissioningWindowStatus::EnhancedWindowOpen);
assert_eq!(ws.admin_fabric_index, Some(2));
assert_eq!(ws.admin_vendor_id, None);
}
}