Skip to main content

homecore_hap/
session.rs

1//! Per-connection HAP authentication state.
2#![cfg_attr(not(feature = "hap-server"), allow(dead_code))]
3
4use crate::crypto::SessionKeys;
5use crate::error::HapError;
6
7/// Authentication phase of one TCP connection.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum SessionState {
10    Connected,
11    PairSetup,
12    PairVerify,
13    Authenticated { controller_id: String, admin: bool },
14    Closing,
15}
16
17impl SessionState {
18    fn name(&self) -> &'static str {
19        match self {
20            Self::Connected => "connected",
21            Self::PairSetup => "pair-setup",
22            Self::PairVerify => "pair-verify",
23            Self::Authenticated { .. } => "authenticated",
24            Self::Closing => "closing",
25        }
26    }
27
28    pub fn is_authenticated(&self) -> bool {
29        matches!(self, Self::Authenticated { .. })
30    }
31}
32
33/// Fail-closed state machine for one HAP connection.
34pub struct Session {
35    state: SessionState,
36    pending_keys: Option<SessionKeys>,
37}
38
39impl Default for Session {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl Session {
46    pub fn new() -> Self {
47        Self {
48            state: SessionState::Connected,
49            pending_keys: None,
50        }
51    }
52
53    pub fn state(&self) -> &SessionState {
54        &self.state
55    }
56
57    pub fn begin_pair_setup(&mut self) -> Result<(), HapError> {
58        self.transition(SessionState::PairSetup)
59    }
60
61    pub fn begin_pair_verify(&mut self) -> Result<(), HapError> {
62        self.transition(SessionState::PairVerify)
63    }
64
65    pub(crate) fn authenticate(
66        &mut self,
67        controller_id: String,
68        admin: bool,
69        keys: SessionKeys,
70    ) -> Result<(), HapError> {
71        if !matches!(self.state, SessionState::PairVerify) {
72            return Err(HapError::InvalidSessionTransition {
73                from: self.state.name(),
74                to: "authenticated",
75            });
76        }
77        self.state = SessionState::Authenticated {
78            controller_id,
79            admin,
80        };
81        self.pending_keys = Some(keys);
82        Ok(())
83    }
84
85    pub(crate) fn take_session_keys(&mut self) -> Option<SessionKeys> {
86        self.pending_keys.take()
87    }
88
89    pub fn reset_pairing(&mut self) -> Result<(), HapError> {
90        match self.state {
91            SessionState::PairSetup | SessionState::PairVerify => {
92                self.state = SessionState::Connected;
93                self.pending_keys = None;
94                Ok(())
95            }
96            _ => Err(HapError::InvalidSessionTransition {
97                from: self.state.name(),
98                to: "connected",
99            }),
100        }
101    }
102
103    pub fn close(&mut self) {
104        self.pending_keys = None;
105        self.state = SessionState::Closing;
106    }
107
108    pub(crate) fn controller_id(&self) -> Option<&str> {
109        match &self.state {
110            SessionState::Authenticated { controller_id, .. } => Some(controller_id),
111            _ => None,
112        }
113    }
114
115    #[cfg(all(test, feature = "hap-server"))]
116    pub(crate) fn authenticated_for_test(admin: bool) -> Self {
117        Self {
118            state: SessionState::Authenticated {
119                controller_id: "test-controller".into(),
120                admin,
121            },
122            pending_keys: None,
123        }
124    }
125
126    fn transition(&mut self, next: SessionState) -> Result<(), HapError> {
127        if matches!(self.state, SessionState::Connected) {
128            self.state = next;
129            Ok(())
130        } else {
131            Err(HapError::InvalidSessionTransition {
132                from: self.state.name(),
133                to: next.name(),
134            })
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn authentication_requires_pair_verify_and_yields_keys_once() {
145        let mut session = Session::new();
146        let keys = SessionKeys::derive(&[7; 32]).unwrap();
147        assert!(session
148            .authenticate("controller".into(), true, keys)
149            .is_err());
150        session.begin_pair_verify().unwrap();
151        session
152            .authenticate(
153                "controller".into(),
154                true,
155                SessionKeys::derive(&[7; 32]).unwrap(),
156            )
157            .unwrap();
158        assert!(session.state().is_authenticated());
159        assert!(session.take_session_keys().is_some());
160        assert!(session.take_session_keys().is_none());
161    }
162
163    #[test]
164    fn pairing_phases_cannot_overlap() {
165        let mut session = Session::new();
166        session.begin_pair_setup().unwrap();
167        assert!(session.begin_pair_verify().is_err());
168        session.reset_pairing().unwrap();
169        session.begin_pair_verify().unwrap();
170    }
171}