use heapless::Vec;
use panweave_types::{Duration, ExtendedAddress, Instant};
use crate::auth::Level;
use crate::tlv::Psk;
pub const MIN_NEW_ZDD_PROVISIONING_TIMEOUT: Duration = Duration::from_secs(60);
pub const SESSIONS: usize = 4;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Interface {
OpenToBeProvisioned {
until: Instant,
},
Off,
OpenToConnect,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Refusal {
InterfaceOff,
NotProvisioned,
NetworkClosed,
AnonymousJoinExpired,
AuthorizationSessionActive,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Transition {
Advertise,
StopAdvertising,
}
#[derive(Clone, Debug)]
pub struct SecurityState {
interface: Interface,
provisioned: bool,
timeout: Duration,
provisioning_open: bool,
sessions: Vec<(ExtendedAddress, Level), SESSIONS>,
}
impl SecurityState {
pub fn new(timeout: Duration) -> Self {
let timeout = if timeout.as_millis() < MIN_NEW_ZDD_PROVISIONING_TIMEOUT.as_millis() {
MIN_NEW_ZDD_PROVISIONING_TIMEOUT
} else {
timeout
};
SecurityState {
interface: Interface::Off,
provisioned: false,
timeout,
provisioning_open: false,
sessions: Vec::new(),
}
}
pub const fn interface(&self) -> Interface {
self.interface
}
pub const fn is_provisioned(&self) -> bool {
self.provisioned
}
pub const fn advertising(&self) -> bool {
!matches!(self.interface, Interface::Off)
}
pub fn power_up(&mut self, now: Instant, provisioned: bool) -> Transition {
self.provisioned = provisioned;
self.provisioning_open = false;
self.sessions.clear();
self.interface = if provisioned {
Interface::OpenToConnect
} else {
Interface::OpenToBeProvisioned {
until: now + self.timeout,
}
};
Transition::Advertise
}
pub fn on_joined(&mut self) -> Option<Transition> {
self.provisioned = true;
self.provisioning_open = false;
let was_off = !self.advertising();
self.interface = Interface::OpenToConnect;
was_off.then_some(Transition::Advertise)
}
pub fn on_left(&mut self, now: Instant) -> Transition {
self.power_up(now, false)
}
pub fn on_interface_configured(&mut self, enabled: bool) -> Option<Transition> {
if !self.provisioned {
return None;
}
match (enabled, self.advertising()) {
(true, false) => {
self.interface = Interface::OpenToConnect;
Some(Transition::Advertise)
}
(false, true) => {
self.interface = Interface::Off;
self.sessions.clear();
Some(Transition::StopAdvertising)
}
_ => None,
}
}
pub fn poll(&mut self, now: Instant) -> Option<Transition> {
if let Interface::OpenToBeProvisioned { until } = self.interface
&& !self.provisioning_open
&& now.has_reached(until)
{
self.interface = Interface::Off;
return Some(Transition::StopAdvertising);
}
None
}
pub fn admit(
&self,
peer: ExtendedAddress,
psk: Psk,
network_open: bool,
anonymous_allowed: bool,
) -> Result<Level, Refusal> {
if !self.advertising() {
return Err(Refusal::InterfaceOff);
}
match psk {
Psk::BasicAuthorization | Psk::AdminAuthorization => {
if !self.provisioned {
return Err(Refusal::NotProvisioned);
}
Ok(if psk == Psk::AdminAuthorization {
Level::Admin
} else {
Level::Basic
})
}
Psk::SymmetricToken | Psk::InstallCode | Psk::Passcode | Psk::Anonymous => {
if self
.sessions
.iter()
.any(|(p, l)| *p == peer && *l != Level::Provisioning)
{
return Err(Refusal::AuthorizationSessionActive);
}
if self.provisioned {
if !network_open {
return Err(Refusal::NetworkClosed);
}
if psk == Psk::Anonymous && !anonymous_allowed {
return Err(Refusal::AnonymousJoinExpired);
}
}
Ok(Level::Provisioning)
}
}
}
pub fn session_opened(&mut self, peer: ExtendedAddress, level: Level) {
self.sessions.retain(|(p, _)| *p != peer);
if self.sessions.is_full() {
self.sessions.remove(0);
}
let _ = self.sessions.push((peer, level));
if level == Level::Provisioning && !self.provisioned {
self.provisioning_open = true;
}
}
pub fn session_closed(&mut self, peer: ExtendedAddress, now: Instant) {
self.sessions.retain(|(p, _)| *p != peer);
if self.provisioning_open && !self.sessions.iter().any(|(_, l)| *l == Level::Provisioning) {
self.provisioning_open = false;
if !self.provisioned && self.advertising() {
self.interface = Interface::OpenToBeProvisioned {
until: now + self.timeout,
};
}
}
}
pub fn session_level(&self, peer: ExtendedAddress) -> Option<Level> {
self.sessions
.iter()
.find(|(p, _)| *p == peer)
.map(|(_, l)| *l)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ZVD: ExtendedAddress = ExtendedAddress(0x00AA_0000_0000_0001);
const T0: Instant = Instant::from_millis(0);
#[test]
fn an_unprovisioned_zdd_times_out_unless_provisioning_starts() {
let mut s = SecurityState::new(Duration::from_secs(10));
assert_eq!(s.power_up(T0, false), Transition::Advertise);
assert_eq!(
s.interface(),
Interface::OpenToBeProvisioned {
until: T0 + Duration::from_secs(60)
}
);
assert_eq!(
s.admit(ZVD, Psk::BasicAuthorization, false, false),
Err(Refusal::NotProvisioned)
);
assert_eq!(
s.admit(ZVD, Psk::Anonymous, false, false),
Ok(Level::Provisioning)
);
assert_eq!(s.poll(T0 + Duration::from_secs(59)), None);
s.session_opened(ZVD, Level::Provisioning);
assert_eq!(s.poll(T0 + Duration::from_secs(120)), None);
s.session_closed(ZVD, T0 + Duration::from_secs(120));
assert_eq!(s.poll(T0 + Duration::from_secs(150)), None);
assert_eq!(
s.poll(T0 + Duration::from_secs(180)),
Some(Transition::StopAdvertising)
);
assert_eq!(s.interface(), Interface::Off);
assert_eq!(
s.admit(ZVD, Psk::InstallCode, true, true),
Err(Refusal::InterfaceOff)
);
assert_eq!(
s.power_up(T0 + Duration::from_secs(200), false),
Transition::Advertise
);
assert!(s.advertising());
}
#[test]
fn a_provisioned_zdd_admits_by_the_network_state_and_the_cluster() {
let mut s = SecurityState::new(Duration::from_secs(60));
s.power_up(T0, false);
s.session_opened(ZVD, Level::Provisioning);
assert_eq!(s.on_joined(), None);
assert_eq!(s.interface(), Interface::OpenToConnect);
assert!(s.is_provisioned());
assert_eq!(
s.admit(ZVD, Psk::AdminAuthorization, false, false),
Ok(Level::Admin)
);
assert_eq!(
s.admit(ZVD, Psk::BasicAuthorization, false, false),
Ok(Level::Basic)
);
assert_eq!(
s.admit(ZVD, Psk::InstallCode, false, true),
Err(Refusal::NetworkClosed)
);
assert_eq!(
s.admit(ZVD, Psk::InstallCode, true, false),
Ok(Level::Provisioning)
);
assert_eq!(
s.admit(ZVD, Psk::Anonymous, true, false),
Err(Refusal::AnonymousJoinExpired)
);
assert_eq!(
s.admit(ZVD, Psk::Anonymous, true, true),
Ok(Level::Provisioning)
);
s.session_opened(ZVD, Level::Basic);
assert_eq!(
s.admit(ZVD, Psk::Anonymous, true, true),
Err(Refusal::AuthorizationSessionActive)
);
assert_eq!(
s.admit(
ExtendedAddress(0x00AA_0000_0000_0002),
Psk::Anonymous,
true,
true
),
Ok(Level::Provisioning)
);
s.session_closed(ZVD, T0);
assert_eq!(
s.admit(ZVD, Psk::Anonymous, true, true),
Ok(Level::Provisioning)
);
assert_eq!(
s.on_interface_configured(false),
Some(Transition::StopAdvertising)
);
assert_eq!(
s.admit(ZVD, Psk::BasicAuthorization, true, true),
Err(Refusal::InterfaceOff)
);
assert_eq!(s.on_interface_configured(false), None);
assert_eq!(s.on_interface_configured(true), Some(Transition::Advertise));
assert_eq!(s.poll(T0 + Duration::from_secs(3600)), None);
assert_eq!(
s.on_left(T0 + Duration::from_secs(10)),
Transition::Advertise
);
assert!(!s.is_provisioned());
assert_eq!(
s.interface(),
Interface::OpenToBeProvisioned {
until: T0 + Duration::from_secs(70)
}
);
}
}