umsh_ulcp_runtime/
ble_security.rs1pub const MAX_PAIRING_FAILURES: u8 = 3;
5
6pub const fn pairing_enabled(pairing_mode: bool, pin_configured: bool, locked_out: bool) -> bool {
23 pairing_mode && (!pin_configured || !locked_out)
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum PairingFailureClass {
28 ConfirmValue,
29 DhKeyCheck,
30 Other,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct PairingRuntime {
41 pub pairing_mode: bool,
42 pub failures: u8,
43 pub locked_out: bool,
44}
45
46impl PairingRuntime {
47 pub const fn record_failure(mut self, failure: PairingFailureClass) -> Self {
48 let (failures, locked_out) = record_pairing_failure(self.failures, failure);
49 self.failures = failures;
50 self.locked_out = locked_out;
51 self
52 }
53
54 pub const fn pairing_succeeded(mut self) -> Self {
58 self.pairing_mode = false;
59 self.failures = 0;
60 self.locked_out = false;
61 self
62 }
63
64 pub const fn bonded_reconnect(mut self) -> Self {
67 self.pairing_mode = false;
68 self
69 }
70}
71
72impl PairingFailureClass {
73 pub const fn counts_toward_lockout(self) -> bool {
74 matches!(self, Self::ConfirmValue | Self::DhKeyCheck)
75 }
76}
77
78pub const fn record_pairing_failure(current: u8, failure: PairingFailureClass) -> (u8, bool) {
84 if !failure.counts_toward_lockout() {
85 return (current, current >= MAX_PAIRING_FAILURES);
86 }
87
88 let failures = current.saturating_add(1);
89 (failures, failures >= MAX_PAIRING_FAILURES)
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn only_authentication_failures_advance_lockout() {
98 assert_eq!(
99 record_pairing_failure(0, PairingFailureClass::Other),
100 (0, false)
101 );
102 assert!(PairingFailureClass::ConfirmValue.counts_toward_lockout());
103 assert!(PairingFailureClass::DhKeyCheck.counts_toward_lockout());
104 assert!(!PairingFailureClass::Other.counts_toward_lockout());
105 }
106
107 #[test]
108 fn third_authentication_failure_locks_pairing() {
109 assert_eq!(
110 record_pairing_failure(0, PairingFailureClass::ConfirmValue),
111 (1, false)
112 );
113 assert_eq!(
114 record_pairing_failure(1, PairingFailureClass::DhKeyCheck),
115 (2, false)
116 );
117 assert_eq!(
118 record_pairing_failure(2, PairingFailureClass::ConfirmValue),
119 (3, true)
120 );
121 }
122
123 #[test]
124 fn locked_counter_saturates() {
125 assert_eq!(
126 record_pairing_failure(3, PairingFailureClass::DhKeyCheck),
127 (4, true)
128 );
129 assert_eq!(
130 record_pairing_failure(u8::MAX, PairingFailureClass::ConfirmValue),
131 (u8::MAX, true)
132 );
133 assert_eq!(
134 record_pairing_failure(u8::MAX, PairingFailureClass::Other),
135 (u8::MAX, true)
136 );
137 }
138
139 #[test]
140 fn pairing_requires_pairing_mode_even_with_a_configured_pin() {
141 assert!(!pairing_enabled(false, true, false));
143 assert!(!pairing_enabled(false, true, true));
144 assert!(!pairing_enabled(false, false, false));
145
146 assert!(pairing_enabled(true, false, false));
147 assert!(pairing_enabled(true, true, false));
148 }
149
150 #[test]
151 fn lockout_applies_only_where_a_passkey_can_be_probed() {
152 assert!(!pairing_enabled(true, true, true));
155 assert!(pairing_enabled(true, false, true));
158 }
159
160 #[test]
161 fn successful_pairing_clears_failures_even_without_event_bond() {
162 let state = PairingRuntime {
163 pairing_mode: true,
164 failures: 2,
165 locked_out: false,
166 }
167 .pairing_succeeded();
168
169 assert_eq!(
170 state,
171 PairingRuntime {
172 pairing_mode: false,
173 failures: 0,
174 locked_out: false,
175 }
176 );
177 }
178
179 #[test]
180 fn bonded_reconnect_closes_pairing_mode_without_changing_failures() {
181 let state = PairingRuntime {
182 pairing_mode: true,
183 failures: 1,
184 locked_out: false,
185 }
186 .bonded_reconnect();
187
188 assert!(!state.pairing_mode);
189 assert_eq!(state.failures, 1);
190 assert!(!state.locked_out);
191 }
192}