1#![cfg_attr(not(feature = "hap-server"), allow(dead_code))]
3
4use crate::crypto::SessionKeys;
5use crate::error::HapError;
6
7#[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
33pub 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}