Skip to main content

wlancfg_lib/config_management/
network_config.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::client::types as client_types;
6use crate::util::historical_list::{HistoricalList, Timestamped};
7use arbitrary::Arbitrary;
8#[cfg(test)]
9use fidl_fuchsia_wlan_common_security as fidl_security;
10use std::cmp::Reverse;
11use std::collections::{HashMap, HashSet};
12use std::fmt::{self, Debug};
13use wlan_common::security::wep::WepKey;
14use wlan_common::security::wpa::WpaDescriptor;
15use wlan_common::security::wpa::credential::{Passphrase, Psk};
16use wlan_common::security::{SecurityAuthenticator, SecurityDescriptor};
17use {fidl_fuchsia_wlan_policy as fidl_policy, fuchsia_async as fasync};
18
19/// The max number of connection results we will store per BSS at a time. For now, this number is
20/// chosen arbitartily.
21const NUM_CONNECTION_RESULTS_PER_BSS: usize = 10;
22/// constants for the constraints on valid credential values
23const WEP_40_ASCII_LEN: usize = 5;
24const WEP_40_HEX_LEN: usize = 10;
25const WEP_104_ASCII_LEN: usize = 13;
26const WEP_104_HEX_LEN: usize = 26;
27const WPA_MIN_PASSWORD_LEN: usize = 8;
28const WPA_MAX_PASSWORD_LEN: usize = 63;
29pub const WPA_PSK_BYTE_LEN: usize = 32;
30/// If we have seen a network in a passive scan, we will rarely actively scan for it.
31pub const PROB_HIDDEN_IF_SEEN_PASSIVE: f32 = 0.05;
32/// If we have connected to a network from a passive scan, we will never scan for it.
33pub const PROB_HIDDEN_IF_CONNECT_PASSIVE: f32 = 0.0;
34/// If we connected to a network after we had to scan actively to find it, it is likely hidden.
35pub const PROB_HIDDEN_IF_CONNECT_ACTIVE: f32 = 0.95;
36/// Default probability that we will actively scan for the network if we haven't seen it in any
37/// passive scan.
38pub const PROB_HIDDEN_DEFAULT: f32 = 0.9;
39/// The lowest we will set the probability for actively scanning for a network.
40pub const PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE: f32 = 0.25;
41/// How much we will lower the probability of scanning for an active network if we don't see the
42/// network in an active scan.
43pub const PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE: f32 = 0.14;
44/// Threshold for saying that a network has "high probability of being hidden".
45// Implementation detail: this is set to PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE
46// to allow for a newly saved network to be scanned at least twice before falling out of the
47// "HIDDEN_PROBABILITY_HIGH" range.
48pub const HIDDEN_PROBABILITY_HIGH: f32 =
49    PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
50// The probability at which we decisively claim a network to be hidden. Implementation detail: we
51// assume a network to be hidden iff we connect only after observing the network in an active scan,
52// not a passive scan.
53pub const PROB_IS_HIDDEN: f32 = PROB_HIDDEN_IF_CONNECT_ACTIVE;
54pub const NUM_SCANS_TO_DECIDE_LIKELY_SINGLE_BSS: usize = 4;
55
56pub type SaveError = fidl_policy::NetworkConfigChangeError;
57
58/// In-memory history of things that we need to know to calculated hidden network probability.
59#[derive(Clone, Debug, PartialEq)]
60struct HiddenProbabilityStats {
61    pub connected_active: bool,
62}
63
64impl HiddenProbabilityStats {
65    fn new() -> Self {
66        HiddenProbabilityStats { connected_active: false }
67    }
68}
69
70/// History of connects, disconnects, and connection strength to estimate whether we can establish
71/// and maintain connection with a network and if it is weakening. Used in choosing best network.
72#[derive(Clone, Debug, PartialEq)]
73pub struct PerformanceStats {
74    pub connect_failures: HistoricalListsByBssid<ConnectFailure>,
75    pub past_connections: HistoricalListsByBssid<PastConnectionData>,
76}
77
78impl Default for PerformanceStats {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl PerformanceStats {
85    pub fn new() -> Self {
86        Self {
87            connect_failures: HistoricalListsByBssid::new(),
88            past_connections: HistoricalListsByBssid::new(),
89        }
90    }
91}
92
93/// Data about scans involving this network. It is used to determine whether or not a network is
94/// likely multi-BSS or single-BSS, since one BSS could be missed, off, or out of range.
95#[derive(Clone, Debug, PartialEq)]
96struct ScanStats {
97    pub have_seen_multi_bss: bool,
98    /// The number of scans that have been performed for this metric.
99    pub num_scans: usize,
100}
101
102impl ScanStats {
103    pub fn new() -> Self {
104        Self { have_seen_multi_bss: false, num_scans: 0 }
105    }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub enum FailureReason {
110    // Failed to join because the authenticator did not accept the credentials provided.
111    CredentialRejected,
112    // Failed to join for other reason, mapped from SME ConnectResultCode::Failed
113    GeneralFailure,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq)]
117pub struct ConnectFailure {
118    /// For determining whether this connection failure is still relevant
119    pub time: fasync::MonotonicInstant,
120    /// The reason that connection failed
121    pub reason: FailureReason,
122    /// The BSSID that we failed to connect to
123    pub bssid: client_types::Bssid,
124}
125
126impl Timestamped for ConnectFailure {
127    fn time(&self) -> fasync::MonotonicInstant {
128        self.time
129    }
130}
131
132/// Data points related to historical connection
133#[derive(Clone, Copy, Debug, PartialEq)]
134pub struct PastConnectionData {
135    pub bssid: client_types::Bssid,
136    /// Time at which the connection was ended
137    pub disconnect_time: fasync::MonotonicInstant,
138    /// The time that the connection was up - from established to disconnected.
139    pub connection_uptime: zx::MonotonicDuration,
140    /// Cause of disconnect or failure to connect
141    pub disconnect_reason: client_types::DisconnectReason,
142    /// Final signal strength measure before disconnect
143    pub signal_at_disconnect: client_types::Signal,
144    /// Average phy rate over connection duration
145    pub average_tx_rate: u32,
146}
147
148impl PastConnectionData {
149    pub fn new(
150        bssid: client_types::Bssid,
151        disconnect_time: fasync::MonotonicInstant,
152        connection_uptime: zx::MonotonicDuration,
153        disconnect_reason: client_types::DisconnectReason,
154        signal_at_disconnect: client_types::Signal,
155        average_tx_rate: u32,
156    ) -> Self {
157        Self {
158            bssid,
159            disconnect_time,
160            connection_uptime,
161            disconnect_reason,
162            signal_at_disconnect,
163            average_tx_rate,
164        }
165    }
166}
167
168impl Timestamped for PastConnectionData {
169    fn time(&self) -> fasync::MonotonicInstant {
170        self.disconnect_time
171    }
172}
173
174/// Data structures for storing historical connection information for a BSS.
175pub type PastConnectionList = HistoricalList<PastConnectionData>;
176impl Default for PastConnectionList {
177    fn default() -> Self {
178        Self::new(NUM_CONNECTION_RESULTS_PER_BSS)
179    }
180}
181
182/// Struct for map from BSSID to HistoricalList
183#[derive(Clone, Debug, PartialEq)]
184pub struct HistoricalListsByBssid<T: Timestamped>(HashMap<client_types::Bssid, HistoricalList<T>>);
185
186impl<T> Default for HistoricalListsByBssid<T>
187where
188    T: Timestamped + Clone,
189{
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl<T> HistoricalListsByBssid<T>
196where
197    T: Timestamped + Clone,
198{
199    pub fn new() -> Self {
200        Self(HashMap::new())
201    }
202
203    pub fn add(&mut self, bssid: client_types::Bssid, data: T) {
204        self.0
205            .entry(bssid)
206            .or_insert_with(|| HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS))
207            .add(data);
208    }
209
210    /// Retrieve list of Data entries to any BSS with a time more recent than earliest_time, sorted
211    /// from oldest to newest. May be empty.
212    pub fn get_recent_for_network(&self, earliest_time: fasync::MonotonicInstant) -> Vec<T> {
213        let mut recents: Vec<T> = vec![];
214        for bssid in self.0.keys() {
215            recents.append(&mut self.get_list_for_bss(bssid).get_recent(earliest_time));
216        }
217        recents.sort_by_key(|a| a.time());
218        recents
219    }
220
221    /// Retrieve List for a particular BSS, in order to retrieve BSS specific Data entries.
222    pub fn get_list_for_bss(&self, bssid: &client_types::Bssid) -> HistoricalList<T> {
223        self.0
224            .get(bssid)
225            .cloned()
226            .unwrap_or_else(|| HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS))
227    }
228}
229
230/// Used to allow hidden probability calculations to make use of what happened most recently
231#[derive(Clone, Copy)]
232pub enum HiddenProbEvent {
233    /// We just saw the network in a passive scan
234    SeenPassive,
235    /// We just connected to the network using passive scan results
236    ConnectPassive,
237    /// We just connected to the network after needing an active scan to see it.
238    ConnectActive,
239    /// We just actively scanned for the network and did not see it.
240    NotSeenActive,
241}
242
243/// Saved data for networks, to remember how to connect to a network and determine if we should.
244#[derive(Clone, Debug, PartialEq)]
245pub struct NetworkConfig {
246    /// (persist) SSID and security type to identify a network.
247    pub ssid: client_types::Ssid,
248    pub security_type: SecurityType,
249    /// (persist) Credential to connect to a protected network or None if the network is open.
250    pub credential: Credential,
251    /// (persist) Remember whether our network indentifier and credential work.
252    pub has_ever_connected: bool,
253    /// How confident we are that this network is hidden, between 0 and 1. We will use
254    /// this number to probabilistically perform an active scan for the network. This is persisted
255    /// to maintain consistent behavior between reboots. 0 means not hidden.
256    pub hidden_probability: f32,
257    /// Data that we use to calculate hidden_probability.
258    hidden_probability_stats: HiddenProbabilityStats,
259    /// Used to estimate quality to determine whether we want to choose this network.
260    pub perf_stats: PerformanceStats,
261    /// Used to determine whether the BSS is likely a single-BSS network, so that roam scans
262    /// happen much less if it is single-BSS.
263    scan_stats: ScanStats,
264}
265
266impl NetworkConfig {
267    /// A new network config is created by loading from persistent storage on boot or when a new
268    /// network is saved.
269    pub fn new(
270        id: NetworkIdentifier,
271        credential: Credential,
272        has_ever_connected: bool,
273    ) -> Result<Self, NetworkConfigError> {
274        check_config_errors(&id.ssid, &id.security_type, &credential)?;
275
276        Ok(Self {
277            ssid: id.ssid,
278            security_type: id.security_type,
279            credential,
280            has_ever_connected,
281            hidden_probability: PROB_HIDDEN_DEFAULT,
282            hidden_probability_stats: HiddenProbabilityStats::new(),
283            perf_stats: PerformanceStats::new(),
284            scan_stats: ScanStats::new(),
285        })
286    }
287
288    // Update the network config's probability that we will actively scan for the network.
289    // If a network has been both seen in a passive scan and connected to after an active scan,
290    // we will determine probability based on what happened most recently.
291    // TODO(63306) Add metric to see if we see conflicting passive/active events.
292    pub fn update_hidden_prob(&mut self, event: HiddenProbEvent) {
293        match event {
294            HiddenProbEvent::ConnectPassive => {
295                self.hidden_probability = PROB_HIDDEN_IF_CONNECT_PASSIVE;
296            }
297            HiddenProbEvent::SeenPassive => {
298                // If the probability hidden is lower from connecting to the network after a
299                // passive scan, don't change.
300                if self.hidden_probability > PROB_HIDDEN_IF_SEEN_PASSIVE {
301                    self.hidden_probability = PROB_HIDDEN_IF_SEEN_PASSIVE;
302                }
303            }
304            HiddenProbEvent::ConnectActive => {
305                self.hidden_probability_stats.connected_active = true;
306                self.hidden_probability = PROB_HIDDEN_IF_CONNECT_ACTIVE;
307            }
308            HiddenProbEvent::NotSeenActive => {
309                // If we have previously required an active scan to connect this network, we are
310                // confident that it is hidden and don't care about this event.
311                if self.hidden_probability_stats.connected_active {
312                    return;
313                }
314                // The probability will not be changed if already lower than the threshold.
315                if self.hidden_probability <= PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE {
316                    return;
317                }
318                // If we failed to find the network in an active scan, lower the probability but
319                // not below a certain threshold.
320                let new_prob = self.hidden_probability - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
321                self.hidden_probability = new_prob.max(PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
322            }
323        }
324    }
325
326    pub fn is_hidden(&self) -> bool {
327        self.hidden_probability >= PROB_IS_HIDDEN
328    }
329
330    #[allow(clippy::assign_op_pattern, reason = "mass allow for https://fxbug.dev/381896734")]
331    pub fn update_seen_multiple_bss(&mut self, multi_bss: bool) {
332        self.scan_stats.have_seen_multi_bss = self.scan_stats.have_seen_multi_bss || multi_bss;
333        self.scan_stats.num_scans = self.scan_stats.num_scans + 1;
334    }
335
336    #[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
337    /// We say that a BSS is likely a single-BSS network if only 1 BSS has ever been seen at a time
338    /// for the network and there have been at least some number of scans for the network.
339    pub fn is_likely_single_bss(&self) -> bool {
340        return !self.scan_stats.have_seen_multi_bss
341            && self.scan_stats.num_scans > NUM_SCANS_TO_DECIDE_LIKELY_SINGLE_BSS;
342    }
343}
344
345impl From<&NetworkConfig> for fidl_policy::NetworkConfig {
346    fn from(network_config: &NetworkConfig) -> Self {
347        let network_id = fidl_policy::NetworkIdentifier {
348            ssid: network_config.ssid.to_vec(),
349            type_: network_config.security_type.into(),
350        };
351        let credential = network_config.credential.clone().into();
352        fidl_policy::NetworkConfig {
353            id: Some(network_id),
354            credential: Some(credential),
355            ..Default::default()
356        }
357    }
358}
359
360/// The credential of a network connection. It mirrors the fidl_fuchsia_wlan_policy Credential
361#[derive(Arbitrary)] // Derive Arbitrary for fuzzer
362#[derive(Clone, Debug, PartialEq)]
363pub enum Credential {
364    None,
365    Password(Vec<u8>),
366    Psk(Vec<u8>),
367}
368
369impl Credential {
370    /// Returns:
371    /// - an Open-Credential instance iff `bytes` is empty,
372    /// - a Password-Credential in all other cases.
373    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
374    /// This function does not support reading PSK from bytes because the PSK byte length overlaps
375    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
376    /// with a valid password length. This function should only be used to load legacy data, where
377    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
378    /// PSK was not supported.
379    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
380    /// Note: This function is of temporary nature to support legacy code.
381    pub fn from_bytes(bytes: impl AsRef<[u8]> + Into<Vec<u8>>) -> Self {
382        match bytes.as_ref().len() {
383            0 => Credential::None,
384            _ => Credential::Password(bytes.into()),
385        }
386    }
387
388    /// Transform credential into the bytes that represent the credential, dropping the information
389    /// of the type. This is used to support the legacy storage method.
390    pub fn into_bytes(self) -> Vec<u8> {
391        match self {
392            Credential::Password(pwd) => pwd,
393            Credential::Psk(psk) => psk,
394            Credential::None => vec![],
395        }
396    }
397
398    /// Choose a security type that fits the credential while we don't actually know the security type
399    /// of the saved networks. This should only be used if we don't have a specified security type.
400    pub fn derived_security_type(&self) -> SecurityType {
401        match self {
402            Credential::None => SecurityType::None,
403            _ => SecurityType::Wpa2,
404        }
405    }
406
407    pub fn type_str(&self) -> &str {
408        match self {
409            Credential::None => "None",
410            Credential::Password(_) => "Password",
411            Credential::Psk(_) => "PSK",
412        }
413    }
414}
415
416impl TryFrom<fidl_policy::Credential> for Credential {
417    type Error = NetworkConfigError;
418    /// Create a Credential from a fidl Crednetial value.
419    fn try_from(credential: fidl_policy::Credential) -> Result<Self, Self::Error> {
420        match credential {
421            fidl_policy::Credential::None(fidl_policy::Empty {}) => Ok(Self::None),
422            fidl_policy::Credential::Password(pwd) => Ok(Self::Password(pwd)),
423            fidl_policy::Credential::Psk(psk) => Ok(Self::Psk(psk)),
424            _ => Err(NetworkConfigError::CredentialTypeInvalid),
425        }
426    }
427}
428
429impl From<Credential> for fidl_policy::Credential {
430    fn from(credential: Credential) -> Self {
431        match credential {
432            Credential::Password(pwd) => fidl_policy::Credential::Password(pwd),
433            Credential::Psk(psk) => fidl_policy::Credential::Psk(psk),
434            Credential::None => fidl_policy::Credential::None(fidl_policy::Empty),
435        }
436    }
437}
438
439// TODO(https://fxbug.dev/42053561): Remove this operator implementation. Once calls to
440//                         `select_authentication_method` are removed from the state machine, there
441//                         will instead be an `Authentication` (or `SecurityAuthenticator`) field
442//                         in `ScannedCandidate` which can be more directly compared to SME
443//                         `ConnectRequest`s in tests.
444#[cfg(test)]
445impl PartialEq<Option<fidl_security::Credentials>> for Credential {
446    fn eq(&self, credentials: &Option<fidl_security::Credentials>) -> bool {
447        use fidl_security::{Credentials, WepCredentials, WpaCredentials};
448
449        match credentials {
450            None => matches!(self, Credential::None),
451            Some(Credentials::Wep(WepCredentials { key })) => {
452                if let Credential::Password(unparsed) = self {
453                    // `Credential::Password` is used for both WEP and WPA. The encoding of WEP
454                    // keys is unspecified and may be either binary (unencoded) or ASCII-encoded
455                    // hexadecimal. To compare, this WEP key must be parsed.
456                    WepKey::parse(unparsed).is_ok_and(|parsed| &Vec::from(parsed) == key)
457                } else {
458                    false
459                }
460            }
461            Some(Credentials::Wpa(credentials)) => match credentials {
462                WpaCredentials::Passphrase(passphrase) => {
463                    if let Credential::Password(unparsed) = self {
464                        unparsed == &passphrase.clone()
465                    } else {
466                        false
467                    }
468                }
469                WpaCredentials::Psk(psk) => {
470                    if let Credential::Psk(unparsed) = self {
471                        unparsed == &Vec::from(*psk)
472                    } else {
473                        false
474                    }
475                }
476                _ => panic!("unrecognized FIDL variant"),
477            },
478            Some(_) => panic!("unrecognized FIDL variant"),
479        }
480    }
481}
482
483// TODO(https://fxbug.dev/42053561): Remove this operator implementation. See the similar conversion above.
484#[cfg(test)]
485impl PartialEq<Option<Box<fidl_security::Credentials>>> for Credential {
486    fn eq(&self, credentials: &Option<Box<fidl_security::Credentials>>) -> bool {
487        self.eq(&credentials.as_ref().map(|credentials| *credentials.clone()))
488    }
489}
490
491#[derive(Arbitrary)] // Derive Arbitrary for fuzzer
492#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
493pub enum SecurityType {
494    None,
495    Wep,
496    Wpa,
497    Wpa2,
498    Wpa3,
499}
500
501impl From<SecurityDescriptor> for SecurityType {
502    fn from(descriptor: SecurityDescriptor) -> Self {
503        match descriptor {
504            SecurityDescriptor::Open => SecurityType::None,
505            // TODO(https://fxbug.dev/458136222): Introduce SecurityType::Owe
506            SecurityDescriptor::Owe => SecurityType::None,
507            SecurityDescriptor::Wep => SecurityType::Wep,
508            SecurityDescriptor::Wpa(wpa) => match wpa {
509                WpaDescriptor::Wpa1 { .. } => SecurityType::Wpa,
510                WpaDescriptor::Wpa2 { .. } => SecurityType::Wpa2,
511                WpaDescriptor::Wpa3 { .. } => SecurityType::Wpa3,
512            },
513        }
514    }
515}
516
517impl From<fidl_policy::SecurityType> for SecurityType {
518    fn from(security: fidl_policy::SecurityType) -> Self {
519        match security {
520            fidl_policy::SecurityType::None => SecurityType::None,
521            fidl_policy::SecurityType::Wep => SecurityType::Wep,
522            fidl_policy::SecurityType::Wpa => SecurityType::Wpa,
523            fidl_policy::SecurityType::Wpa2 => SecurityType::Wpa2,
524            fidl_policy::SecurityType::Wpa3 => SecurityType::Wpa3,
525        }
526    }
527}
528
529impl From<SecurityType> for fidl_policy::SecurityType {
530    fn from(security_type: SecurityType) -> Self {
531        match security_type {
532            SecurityType::None => fidl_policy::SecurityType::None,
533            SecurityType::Wep => fidl_policy::SecurityType::Wep,
534            SecurityType::Wpa => fidl_policy::SecurityType::Wpa,
535            SecurityType::Wpa2 => fidl_policy::SecurityType::Wpa2,
536            SecurityType::Wpa3 => fidl_policy::SecurityType::Wpa3,
537        }
538    }
539}
540
541impl SecurityType {
542    /// List all security type variants.
543    pub fn list_variants() -> Vec<Self> {
544        vec![
545            SecurityType::None,
546            SecurityType::Wep,
547            SecurityType::Wpa,
548            SecurityType::Wpa2,
549            SecurityType::Wpa3,
550        ]
551    }
552
553    /// Return whether or not this saved security type can be used to connect scan results with
554    /// this detailed security type.
555    pub fn is_compatible_with_scanned_type(
556        &self,
557        scanned_type: &client_types::SecurityTypeDetailed,
558    ) -> bool {
559        match self {
560            SecurityType::None => {
561                // return true if the scanned security is open, or false otherwise.
562                scanned_type == &client_types::SecurityTypeDetailed::Open
563            }
564            SecurityType::Wep => scanned_type == &client_types::SecurityTypeDetailed::Wep,
565            SecurityType::Wpa => {
566                scanned_type == &client_types::SecurityTypeDetailed::Wpa1
567                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2Personal
568                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly
569                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Personal
570                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2PersonalTkipOnly
571            }
572            SecurityType::Wpa2 => {
573                scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2Personal
574                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly
575                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Personal
576                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2PersonalTkipOnly
577                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Wpa3Personal
578                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa3Personal
579            }
580            SecurityType::Wpa3 => {
581                scanned_type == &client_types::SecurityTypeDetailed::Wpa2Wpa3Personal
582                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa3Personal
583            }
584        }
585    }
586}
587
588/// The network identifier is the SSID and security policy of the network, and it is used to
589/// distinguish networks. It mirrors the NetworkIdentifier in fidl_fuchsia_wlan_policy.
590#[derive(Arbitrary)]
591// Derive Arbitrary for fuzzer
592// To avoid printing PII, only allow Debug in tests, runtime logging should use Display
593#[derive(Clone, Eq, Hash, PartialEq)]
594#[cfg_attr(test, derive(Debug))]
595pub struct NetworkIdentifier {
596    pub ssid: client_types::Ssid,
597    pub security_type: SecurityType,
598}
599
600impl fmt::Display for NetworkIdentifier {
601    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
602        write!(f, "NetworkIdentifier: {}, {:?}", self.ssid, self.security_type)
603    }
604}
605
606impl NetworkIdentifier {
607    pub fn new(ssid: client_types::Ssid, security_type: SecurityType) -> Self {
608        NetworkIdentifier { ssid, security_type }
609    }
610
611    #[cfg(test)]
612    pub fn try_from(ssid: &str, security_type: SecurityType) -> Result<Self, anyhow::Error> {
613        Ok(NetworkIdentifier { ssid: client_types::Ssid::try_from(ssid)?, security_type })
614    }
615}
616
617impl From<fidl_policy::NetworkIdentifier> for NetworkIdentifier {
618    fn from(id: fidl_policy::NetworkIdentifier) -> Self {
619        Self::new(client_types::Ssid::from_bytes_unchecked(id.ssid), id.type_.into())
620    }
621}
622
623impl From<NetworkIdentifier> for fidl_policy::NetworkIdentifier {
624    fn from(id: NetworkIdentifier) -> Self {
625        fidl_policy::NetworkIdentifier { ssid: id.ssid.into(), type_: id.security_type.into() }
626    }
627}
628
629impl From<NetworkConfig> for fidl_policy::NetworkConfig {
630    fn from(config: NetworkConfig) -> Self {
631        let network_id = NetworkIdentifier::new(config.ssid, config.security_type);
632        fidl_policy::NetworkConfig {
633            id: Some(fidl_policy::NetworkIdentifier::from(network_id)),
634            credential: Some(fidl_policy::Credential::from(config.credential)),
635            ..Default::default()
636        }
637    }
638}
639
640/// Returns an error if the input network values are not valid or none if the values are valid.
641/// For example it is an error if the network is Open (no password) but a password is supplied.
642/// TODO(nmccracken) - Specific errors need to be added to the API and returned here
643fn check_config_errors(
644    ssid: &client_types::Ssid,
645    security_type: &SecurityType,
646    credential: &Credential,
647) -> Result<(), NetworkConfigError> {
648    // Verify SSID has at least 1 byte.
649    if ssid.is_empty() {
650        return Err(NetworkConfigError::SsidEmpty);
651    }
652    // Verify that credentials match the security type. This code only inspects the lengths of
653    // passphrases and PSKs; the underlying data is considered opaque here.
654    match security_type {
655        SecurityType::None => {
656            if let Credential::Psk(_) | Credential::Password(_) = credential {
657                return Err(NetworkConfigError::OpenNetworkPassword);
658            }
659        }
660        // Note that some vendors allow WEP passphrase and PSK lengths that are not described by
661        // IEEE 802.11. These lengths are unsupported. See also the `wep_deprecated` crate.
662        SecurityType::Wep => match credential {
663            Credential::Password(password) => match password.len() {
664                // ASCII encoding.
665                WEP_40_ASCII_LEN | WEP_104_ASCII_LEN => {}
666                // Hexadecimal encoding.
667                WEP_40_HEX_LEN | WEP_104_HEX_LEN => {}
668                _ => {
669                    return Err(NetworkConfigError::PasswordLen);
670                }
671            },
672            _ => {
673                return Err(NetworkConfigError::MissingPasswordPsk);
674            }
675        },
676        SecurityType::Wpa | SecurityType::Wpa2 | SecurityType::Wpa3 => match credential {
677            Credential::Password(pwd) => {
678                if pwd.len() < WPA_MIN_PASSWORD_LEN || pwd.len() > WPA_MAX_PASSWORD_LEN {
679                    return Err(NetworkConfigError::PasswordLen);
680                }
681            }
682            Credential::Psk(psk) => {
683                if security_type == &SecurityType::Wpa3 {
684                    return Err(NetworkConfigError::Wpa3Psk);
685                }
686                if psk.len() != WPA_PSK_BYTE_LEN {
687                    return Err(NetworkConfigError::PskLen);
688                }
689            }
690            _ => {
691                return Err(NetworkConfigError::MissingPasswordPsk);
692            }
693        },
694    }
695    Ok(())
696}
697
698/// Error codes representing problems in trying to save a network config, such as errors saving
699/// or removing a network config, or for invalid values when trying to create a network config.
700#[derive(Hash, PartialEq, Eq)]
701pub enum NetworkConfigError {
702    OpenNetworkPassword,
703    Wpa3Psk,
704    PasswordLen,
705    PskLen,
706    SsidEmpty,
707    MissingPasswordPsk,
708    ConfigMissingId,
709    ConfigMissingCredential,
710    CredentialTypeInvalid,
711    FileWriteError,
712    LegacyWriteError,
713}
714
715impl Debug for NetworkConfigError {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
717        match self {
718            NetworkConfigError::OpenNetworkPassword => {
719                write!(f, "can't have an open network with a password or PSK")
720            }
721            NetworkConfigError::Wpa3Psk => {
722                write!(f, "can't use a PSK to connect to a WPA3 network")
723            }
724            NetworkConfigError::PasswordLen => write!(f, "invalid password length"),
725            NetworkConfigError::PskLen => write!(f, "invalid PSK length"),
726            NetworkConfigError::SsidEmpty => {
727                write!(f, "SSID must have a non-zero length")
728            }
729            NetworkConfigError::MissingPasswordPsk => {
730                write!(f, "no password or PSK provided but required by security type")
731            }
732            NetworkConfigError::ConfigMissingId => {
733                write!(f, "cannot create network config, network id is None")
734            }
735            NetworkConfigError::ConfigMissingCredential => {
736                write!(f, "cannot create network config, no credential is given")
737            }
738            NetworkConfigError::CredentialTypeInvalid => {
739                write!(f, "cannot convert fidl Credential, unknown variant")
740            }
741            NetworkConfigError::FileWriteError => {
742                write!(f, "error writing network config to file")
743            }
744            NetworkConfigError::LegacyWriteError => {
745                write!(f, "error writing network config to legacy storage")
746            }
747        }
748    }
749}
750
751impl From<NetworkConfigError> for fidl_policy::NetworkConfigChangeError {
752    fn from(err: NetworkConfigError) -> Self {
753        match err {
754            NetworkConfigError::OpenNetworkPassword
755            | NetworkConfigError::MissingPasswordPsk
756            | NetworkConfigError::Wpa3Psk => {
757                fidl_policy::NetworkConfigChangeError::InvalidSecurityCredentialError
758            }
759            NetworkConfigError::PasswordLen | NetworkConfigError::PskLen => {
760                fidl_policy::NetworkConfigChangeError::CredentialLenError
761            }
762            NetworkConfigError::SsidEmpty => fidl_policy::NetworkConfigChangeError::SsidEmptyError,
763            NetworkConfigError::ConfigMissingId | NetworkConfigError::ConfigMissingCredential => {
764                fidl_policy::NetworkConfigChangeError::NetworkConfigMissingFieldError
765            }
766            NetworkConfigError::CredentialTypeInvalid => {
767                fidl_policy::NetworkConfigChangeError::UnsupportedCredentialError
768            }
769            NetworkConfigError::FileWriteError | NetworkConfigError::LegacyWriteError => {
770                fidl_policy::NetworkConfigChangeError::NetworkConfigWriteError
771            }
772        }
773    }
774}
775
776/// Binds a credential to a security protocol.
777///
778/// Binding constructs a `SecurityAuthenticator` that can be used to construct an SME
779/// `ConnectRequest`. This function is similar to `SecurityDescriptor::bind`, but operates on the
780/// Policy `Credential` type, which requires some additional logic to determine how the credential
781/// data is interpreted.
782///
783/// Returns `None` if the given protocol is incompatible with the given credential.
784fn bind_credential_to_protocol(
785    protocol: SecurityDescriptor,
786    credential: &Credential,
787) -> Option<SecurityAuthenticator> {
788    match protocol {
789        SecurityDescriptor::Open => match credential {
790            Credential::None => protocol.bind(None).ok(),
791            _ => None,
792        },
793        SecurityDescriptor::Owe => match credential {
794            Credential::None => protocol.bind(None).ok(),
795            _ => None,
796        },
797        SecurityDescriptor::Wep => match credential {
798            Credential::Password(key) => {
799                WepKey::parse(key).ok().and_then(|key| protocol.bind(Some(key.into())).ok())
800            }
801            _ => None,
802        },
803        SecurityDescriptor::Wpa(wpa) => match wpa {
804            WpaDescriptor::Wpa1 { .. } | WpaDescriptor::Wpa2 { .. } => match credential {
805                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
806                    .ok()
807                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
808                Credential::Psk(psk) => {
809                    Psk::parse(psk).ok().and_then(|psk| protocol.bind(Some(psk.into())).ok())
810                }
811                _ => None,
812            },
813            WpaDescriptor::Wpa3 { .. } => match credential {
814                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
815                    .ok()
816                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
817                _ => None,
818            },
819        },
820    }
821}
822
823/// Creates a security authenticator based on supported security protocols and credentials.
824///
825/// The authentication method is chosen based on the general strength of each mutually supported
826/// security protocol (the protocols supported by both the local and remote stations) and the
827/// compatibility of those protocols with the given credentials.
828///
829/// Returns `None` if no appropriate authentication method can be selected for the given protocols
830/// and credentials.
831pub fn select_authentication_method(
832    mutual_security_protocols: HashSet<SecurityDescriptor>,
833    credential: &Credential,
834) -> Option<SecurityAuthenticator> {
835    let mut protocols: Vec<_> = mutual_security_protocols.into_iter().collect();
836    protocols.sort_by_key(|protocol| {
837        Reverse(match protocol {
838            SecurityDescriptor::Open => 0,
839            SecurityDescriptor::Owe => 3,
840            SecurityDescriptor::Wep => 1,
841            SecurityDescriptor::Wpa(wpa) => match wpa {
842                WpaDescriptor::Wpa1 { .. } => 2,
843                WpaDescriptor::Wpa2 { .. } => 4,
844                WpaDescriptor::Wpa3 { .. } => 5,
845            },
846        })
847    });
848    protocols
849        .into_iter()
850        .flat_map(|protocol| bind_credential_to_protocol(protocol, credential))
851        .next()
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857    use crate::util::testing::{generate_string, random_connection_data};
858    use assert_matches::assert_matches;
859    use std::collections::VecDeque;
860    use test_case::test_case;
861    use wlan_common::security::wep::WepAuthenticator;
862    use wlan_common::security::wpa::{
863        Authentication, Wpa1Credentials, Wpa2PersonalCredentials, Wpa3PersonalCredentials,
864        WpaAuthenticator,
865    };
866
867    #[fuchsia::test]
868    fn new_network_config_none_credential() {
869        let credential = Credential::None;
870        let network_config = NetworkConfig::new(
871            NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(),
872            credential.clone(),
873            false,
874        )
875        .expect("Error creating network config for foo");
876
877        assert_eq!(
878            network_config,
879            NetworkConfig {
880                ssid: client_types::Ssid::try_from("foo").unwrap(),
881                security_type: SecurityType::None,
882                credential,
883                has_ever_connected: false,
884                hidden_probability: PROB_HIDDEN_DEFAULT,
885                hidden_probability_stats: HiddenProbabilityStats::new(),
886                perf_stats: PerformanceStats::new(),
887                scan_stats: ScanStats::new(),
888            }
889        );
890    }
891
892    #[fuchsia::test]
893    fn new_network_config_password_credential() {
894        let credential = Credential::Password(b"foo-password".to_vec());
895
896        let network_config = NetworkConfig::new(
897            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
898            credential.clone(),
899            false,
900        )
901        .expect("Error creating network config for foo");
902
903        assert_eq!(
904            network_config,
905            NetworkConfig {
906                ssid: client_types::Ssid::try_from("foo").unwrap(),
907                security_type: SecurityType::Wpa2,
908                credential,
909                has_ever_connected: false,
910                hidden_probability: PROB_HIDDEN_DEFAULT,
911                hidden_probability_stats: HiddenProbabilityStats::new(),
912                perf_stats: PerformanceStats::new(),
913                scan_stats: ScanStats::new(),
914            }
915        );
916        assert!(network_config.perf_stats.connect_failures.0.is_empty());
917    }
918
919    #[fuchsia::test]
920    fn new_network_config_psk_credential() {
921        let credential = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
922
923        let network_config = NetworkConfig::new(
924            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
925            credential.clone(),
926            false,
927        )
928        .expect("Error creating network config for foo");
929
930        assert_eq!(
931            network_config,
932            NetworkConfig {
933                ssid: client_types::Ssid::try_from("foo").unwrap(),
934                security_type: SecurityType::Wpa2,
935                credential,
936                has_ever_connected: false,
937                hidden_probability: PROB_HIDDEN_DEFAULT,
938                hidden_probability_stats: HiddenProbabilityStats::new(),
939                perf_stats: PerformanceStats::new(),
940                scan_stats: ScanStats::new(),
941            }
942        );
943    }
944
945    #[fuchsia::test]
946    fn new_network_config_invalid_password() {
947        let credential = Credential::Password([1; 64].to_vec());
948
949        let config_result = NetworkConfig::new(
950            NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(),
951            credential,
952            false,
953        );
954
955        assert_matches!(config_result, Err(NetworkConfigError::PasswordLen));
956    }
957
958    #[fuchsia::test]
959    fn new_network_config_invalid_psk() {
960        let credential = Credential::Psk(b"bar".to_vec());
961
962        let config_result = NetworkConfig::new(
963            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
964            credential,
965            false,
966        );
967
968        assert_matches!(config_result, Err(NetworkConfigError::PskLen));
969    }
970
971    #[fuchsia::test]
972    fn check_config_errors_invalid_wep_password() {
973        // Unsupported length (7).
974        let password = Credential::Password(b"1234567".to_vec());
975        assert_matches!(
976            check_config_errors(
977                &client_types::Ssid::try_from("valid_ssid").unwrap(),
978                &SecurityType::Wep,
979                &password
980            ),
981            Err(NetworkConfigError::PasswordLen)
982        );
983    }
984
985    #[fuchsia::test]
986    fn check_config_errors_invalid_wpa_password() {
987        // password too short
988        let short_password = Credential::Password(b"1234567".to_vec());
989        assert_matches!(
990            check_config_errors(
991                &client_types::Ssid::try_from("valid_ssid").unwrap(),
992                &SecurityType::Wpa2,
993                &short_password
994            ),
995            Err(NetworkConfigError::PasswordLen)
996        );
997
998        // password too long
999        let long_password = Credential::Password([5, 65].to_vec());
1000        assert_matches!(
1001            check_config_errors(
1002                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1003                &SecurityType::Wpa2,
1004                &long_password
1005            ),
1006            Err(NetworkConfigError::PasswordLen)
1007        );
1008    }
1009
1010    #[fuchsia::test]
1011    fn check_config_errors_invalid_wep_credential_variant() {
1012        // Unsupported variant (`Psk`).
1013        let psk = Credential::Psk(b"12345".to_vec());
1014        assert_matches!(
1015            check_config_errors(
1016                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1017                &SecurityType::Wep,
1018                &psk
1019            ),
1020            Err(NetworkConfigError::MissingPasswordPsk)
1021        );
1022    }
1023
1024    #[fuchsia::test]
1025    fn check_config_errors_invalid_wpa_psk() {
1026        // PSK length not 32 characters
1027        let short_psk = Credential::Psk([6; WPA_PSK_BYTE_LEN - 1].to_vec());
1028
1029        assert_matches!(
1030            check_config_errors(
1031                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1032                &SecurityType::Wpa2,
1033                &short_psk
1034            ),
1035            Err(NetworkConfigError::PskLen)
1036        );
1037
1038        let long_psk = Credential::Psk([7; WPA_PSK_BYTE_LEN + 1].to_vec());
1039        assert_matches!(
1040            check_config_errors(
1041                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1042                &SecurityType::Wpa2,
1043                &long_psk
1044            ),
1045            Err(NetworkConfigError::PskLen)
1046        );
1047    }
1048
1049    #[fuchsia::test]
1050    fn check_config_errors_invalid_security_credential() {
1051        // Use a password with open network.
1052        let password = Credential::Password(b"password".to_vec());
1053        assert_matches!(
1054            check_config_errors(
1055                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1056                &SecurityType::None,
1057                &password
1058            ),
1059            Err(NetworkConfigError::OpenNetworkPassword)
1060        );
1061
1062        let psk = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
1063        assert_matches!(
1064            check_config_errors(
1065                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1066                &SecurityType::None,
1067                &psk
1068            ),
1069            Err(NetworkConfigError::OpenNetworkPassword)
1070        );
1071        // Use no password with a protected network.
1072        let password = Credential::None;
1073        assert_matches!(
1074            check_config_errors(
1075                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1076                &SecurityType::Wpa,
1077                &password
1078            ),
1079            Err(NetworkConfigError::MissingPasswordPsk)
1080        );
1081
1082        assert_matches!(
1083            check_config_errors(
1084                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1085                &SecurityType::Wpa2,
1086                &password
1087            ),
1088            Err(NetworkConfigError::MissingPasswordPsk)
1089        );
1090
1091        assert_matches!(
1092            check_config_errors(
1093                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1094                &SecurityType::Wpa3,
1095                &password
1096            ),
1097            Err(NetworkConfigError::MissingPasswordPsk)
1098        );
1099
1100        assert_matches!(
1101            check_config_errors(
1102                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1103                &SecurityType::Wpa3,
1104                &psk
1105            ),
1106            Err(NetworkConfigError::Wpa3Psk)
1107        );
1108    }
1109
1110    #[fuchsia::test]
1111    fn check_config_errors_ssid_empty() {
1112        assert_matches!(
1113            check_config_errors(
1114                &client_types::Ssid::empty(),
1115                &SecurityType::None,
1116                &Credential::None
1117            ),
1118            Err(NetworkConfigError::SsidEmpty)
1119        );
1120    }
1121
1122    #[fasync::run_singlethreaded(test)]
1123    async fn test_connect_failures_by_bssid_add_and_get() {
1124        let mut connect_failures = HistoricalListsByBssid::new();
1125        let curr_time = fasync::MonotonicInstant::now();
1126
1127        // Add two failures for BSSID_1
1128        let bssid_1 = client_types::Bssid::from([1; 6]);
1129        let failure_1_bssid_1 = ConnectFailure {
1130            time: curr_time - zx::MonotonicDuration::from_seconds(10),
1131            bssid: bssid_1,
1132            reason: FailureReason::GeneralFailure,
1133        };
1134        connect_failures.add(bssid_1, failure_1_bssid_1);
1135
1136        let failure_2_bssid_1 = ConnectFailure {
1137            time: curr_time - zx::MonotonicDuration::from_seconds(5),
1138            bssid: bssid_1,
1139            reason: FailureReason::CredentialRejected,
1140        };
1141        connect_failures.add(bssid_1, failure_2_bssid_1);
1142
1143        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1144        assert_eq!(
1145            connect_failures
1146                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1147            vec![failure_1_bssid_1, failure_2_bssid_1]
1148        );
1149
1150        // Add one failure for BSSID_2
1151        let bssid_2 = client_types::Bssid::from([2; 6]);
1152        let failure_1_bssid_2 = ConnectFailure {
1153            time: curr_time - zx::MonotonicDuration::from_seconds(3),
1154            bssid: bssid_2,
1155            reason: FailureReason::GeneralFailure,
1156        };
1157        connect_failures.add(bssid_2, failure_1_bssid_2);
1158
1159        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1160        assert_eq!(
1161            connect_failures
1162                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1163            vec![failure_1_bssid_1, failure_2_bssid_1, failure_1_bssid_2]
1164        );
1165
1166        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1167        assert_eq!(
1168            connect_failures
1169                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1170            vec![failure_2_bssid_1, failure_1_bssid_2]
1171        );
1172
1173        // Verify get_recent_for_network(curr_time) is empty
1174        assert_eq!(connect_failures.get_recent_for_network(curr_time), vec![]);
1175
1176        // Verify get_list_for_bss retrieves correct connect failures
1177        assert_eq!(
1178            connect_failures.get_list_for_bss(&bssid_1),
1179            HistoricalList(VecDeque::from_iter([failure_1_bssid_1, failure_2_bssid_1]))
1180        );
1181
1182        assert_eq!(
1183            connect_failures.get_list_for_bss(&bssid_2),
1184            HistoricalList(VecDeque::from_iter([failure_1_bssid_2]))
1185        );
1186    }
1187
1188    #[fasync::run_singlethreaded(test)]
1189    async fn failure_list_add_and_get() {
1190        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1191
1192        // Get time before adding so we can get back everything we added.
1193        let curr_time = fasync::MonotonicInstant::now();
1194        assert!(connect_failures.get_recent(curr_time).is_empty());
1195        let bssid = client_types::Bssid::from([1; 6]);
1196        let failure =
1197            ConnectFailure { time: curr_time, bssid, reason: FailureReason::GeneralFailure };
1198        connect_failures.add(failure);
1199
1200        let result_list = connect_failures.get_recent(curr_time);
1201        assert_eq!(1, result_list.len());
1202        assert_eq!(FailureReason::GeneralFailure, result_list[0].reason);
1203        assert_eq!(bssid, result_list[0].bssid);
1204        // Should not get any results if we request denials older than the specified time.
1205        let later_time = fasync::MonotonicInstant::now();
1206        assert!(connect_failures.get_recent(later_time).is_empty());
1207    }
1208
1209    #[fasync::run_singlethreaded(test)]
1210    async fn test_failure_list_add_when_full() {
1211        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1212        let curr_time = fasync::MonotonicInstant::now();
1213
1214        // Add to list, exceeding the capacity by one entry
1215        for i in 0..connect_failures.0.capacity() + 1 {
1216            connect_failures.add(ConnectFailure {
1217                time: curr_time + zx::MonotonicDuration::from_seconds(i as i64),
1218                reason: FailureReason::GeneralFailure,
1219                bssid: client_types::Bssid::from([1; 6]),
1220            })
1221        }
1222
1223        // Validate entry with time = curr_time was evicted.
1224        for (i, e) in connect_failures.0.iter().enumerate() {
1225            assert_eq!(e.time, curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1));
1226        }
1227    }
1228
1229    #[fasync::run_singlethreaded(test)]
1230    async fn test_past_connections_by_bssid_add_and_get() {
1231        let mut past_connections_list = HistoricalListsByBssid::new();
1232        let curr_time = fasync::MonotonicInstant::now();
1233
1234        // Add two past_connections for BSSID_1
1235        let mut data_1_bssid_1 = random_connection_data();
1236        let bssid_1 = data_1_bssid_1.bssid;
1237        data_1_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(10);
1238
1239        past_connections_list.add(bssid_1, data_1_bssid_1);
1240
1241        let mut data_2_bssid_1 = random_connection_data();
1242        data_2_bssid_1.bssid = bssid_1;
1243        data_2_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(5);
1244        past_connections_list.add(bssid_1, data_2_bssid_1);
1245
1246        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1247        assert_eq!(
1248            past_connections_list
1249                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1250            vec![data_1_bssid_1, data_2_bssid_1]
1251        );
1252
1253        // Add one past_connection for BSSID_2
1254        let mut data_1_bssid_2 = random_connection_data();
1255        let bssid_2 = data_1_bssid_2.bssid;
1256        data_1_bssid_2.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(3);
1257        past_connections_list.add(bssid_2, data_1_bssid_2);
1258
1259        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1260        assert_eq!(
1261            past_connections_list
1262                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1263            vec![data_1_bssid_1, data_2_bssid_1, data_1_bssid_2]
1264        );
1265
1266        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1267        assert_eq!(
1268            past_connections_list
1269                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1270            vec![data_2_bssid_1, data_1_bssid_2]
1271        );
1272
1273        // Verify get_recent_for_network(curr_time) is empty
1274        assert_eq!(past_connections_list.get_recent_for_network(curr_time), vec![]);
1275
1276        // Verify get_list_for_bss retrieves correct PastConnectionLists
1277        assert_eq!(
1278            past_connections_list.get_list_for_bss(&bssid_1),
1279            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_1, data_2_bssid_1]) }
1280        );
1281
1282        assert_eq!(
1283            past_connections_list.get_list_for_bss(&bssid_2),
1284            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_2]) }
1285        );
1286    }
1287
1288    #[fasync::run_singlethreaded(test)]
1289    async fn test_past_connections_list_add_when_full() {
1290        let mut past_connections_list = PastConnectionList::default();
1291        let curr_time = fasync::MonotonicInstant::now();
1292
1293        // Add to list, exceeding the capacity by one entry
1294        for i in 0..past_connections_list.0.capacity() + 1 {
1295            let mut data = random_connection_data();
1296            data.bssid = client_types::Bssid::from([1; 6]);
1297            data.disconnect_time = curr_time + zx::MonotonicDuration::from_seconds(i as i64);
1298            past_connections_list.add(data);
1299        }
1300
1301        // Validate entry with time = curr_time was evicted.
1302        for (i, e) in past_connections_list.0.iter().enumerate() {
1303            assert_eq!(
1304                e.disconnect_time,
1305                curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1)
1306            );
1307        }
1308    }
1309
1310    #[fasync::run_singlethreaded(test)]
1311    async fn test_past_connections_list_add_and_get() {
1312        let mut past_connections_list = PastConnectionList::default();
1313        let curr_time = fasync::MonotonicInstant::now();
1314        assert!(past_connections_list.get_recent(curr_time).is_empty());
1315
1316        let mut past_connection_data = random_connection_data();
1317        past_connection_data.disconnect_time = curr_time;
1318        // Add a past connection
1319        past_connections_list.add(past_connection_data);
1320
1321        // We should get back the added data when specifying the same or an earlier time.
1322        assert_eq!(past_connections_list.get_recent(curr_time).len(), 1);
1323        assert_matches!(past_connections_list.get_recent(curr_time).as_slice(), [data] => {
1324            assert_eq!(data, &past_connection_data.clone());
1325        });
1326        let earlier_time = curr_time - zx::MonotonicDuration::from_seconds(1);
1327        assert_matches!(past_connections_list.get_recent(earlier_time).as_slice(), [data] => {
1328            assert_eq!(data, &data.clone());
1329        });
1330        // The results should be empty if the requested time is after the latest past connection's
1331        // time.
1332        let later_time = curr_time + zx::MonotonicDuration::from_seconds(1);
1333        assert!(past_connections_list.get_recent(later_time).is_empty());
1334    }
1335
1336    #[fuchsia::test]
1337    fn test_credential_from_bytes() {
1338        assert_eq!(Credential::from_bytes(vec![1]), Credential::Password(vec![1]));
1339        assert_eq!(Credential::from_bytes(vec![2; 63]), Credential::Password(vec![2; 63]));
1340        // credential from bytes should only be used to load legacy data, so PSK won't be supported
1341        assert_eq!(
1342            Credential::from_bytes(vec![2; WPA_PSK_BYTE_LEN]),
1343            Credential::Password(vec![2; WPA_PSK_BYTE_LEN])
1344        );
1345        assert_eq!(Credential::from_bytes(vec![]), Credential::None);
1346    }
1347
1348    #[fuchsia::test]
1349    fn test_derived_security_type_from_credential() {
1350        let password = Credential::Password(b"password".to_vec());
1351        let psk = Credential::Psk(b"psk-type".to_vec());
1352        let none = Credential::None;
1353
1354        assert_eq!(SecurityType::Wpa2, password.derived_security_type());
1355        assert_eq!(SecurityType::Wpa2, psk.derived_security_type());
1356        assert_eq!(SecurityType::None, none.derived_security_type());
1357    }
1358
1359    #[fuchsia::test]
1360    fn test_hidden_prob_calculation() {
1361        let mut network_config = NetworkConfig::new(
1362            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1363            Credential::None,
1364            false,
1365        )
1366        .expect("Failed to create network config");
1367        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_DEFAULT);
1368
1369        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1370        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1371
1372        network_config.update_hidden_prob(HiddenProbEvent::ConnectPassive);
1373        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1374
1375        // Hidden probability shouldn't go back up after seeing a network in a passive
1376        // scan again after connecting with a passive scan
1377        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1378        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1379    }
1380
1381    #[fuchsia::test]
1382    fn test_hidden_prob_calc_active_connect() {
1383        let mut network_config = NetworkConfig::new(
1384            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1385            Credential::None,
1386            false,
1387        )
1388        .expect("Failed to create network config");
1389
1390        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1391        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1392
1393        // If we see a network in a passive scan after connecting from an active scan,
1394        // we won't care that we previously needed an active scan.
1395        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1396        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1397
1398        // If we require an active scan to connect to a network, raise probability as if the
1399        // network has become hidden.
1400        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1401        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1402    }
1403
1404    #[fuchsia::test]
1405    fn test_hidden_prob_calc_not_seen_in_active_scan_lowers_prob() {
1406        // Test that updating hidden probability after not seeing the network in a directed active
1407        // scan lowers the hidden probability
1408        let mut network_config = NetworkConfig::new(
1409            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1410            Credential::None,
1411            false,
1412        )
1413        .expect("Failed to create network config");
1414
1415        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1416        let expected_prob = PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1417        assert_eq!(network_config.hidden_probability, expected_prob);
1418
1419        // If we update hidden probability again, the probability should lower again.
1420        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1421        let expected_prob = expected_prob - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1422        assert_eq!(network_config.hidden_probability, expected_prob);
1423    }
1424
1425    #[fuchsia::test]
1426    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_lower_past_threshold() {
1427        let mut network_config = NetworkConfig::new(
1428            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1429            Credential::None,
1430            false,
1431        )
1432        .expect("Failed to create network config");
1433
1434        // If hidden probability is slightly above the minimum from not seing the network in an
1435        // active scan, it should not be lowered past the minimum.
1436        network_config.hidden_probability = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE + 0.01;
1437        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1438        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1439
1440        // If hidden probability is at the minimum, it should not be lowered.
1441        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1442        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1443    }
1444
1445    #[fuchsia::test]
1446    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_change_if_lower_than_threshold() {
1447        let mut network_config = NetworkConfig::new(
1448            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1449            Credential::None,
1450            false,
1451        )
1452        .expect("Failed to create network config");
1453
1454        // If the hidden probability is lower than the minimum of not seeing the network in an,
1455        // active scan, which could happen after seeing it in a passive scan, the hidden
1456        // probability will not lower from this event.
1457        let prob_before_update = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE - 0.1;
1458        network_config.hidden_probability = prob_before_update;
1459        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1460        assert_eq!(network_config.hidden_probability, prob_before_update);
1461    }
1462
1463    #[fuchsia::test]
1464    fn test_hidden_prob_calc_not_seen_active_after_active_connect() {
1465        // Test the specific case where we fail to see the network in an active scan after we
1466        // previously connected to the network after an active scan was required.
1467        let mut network_config = NetworkConfig::new(
1468            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1469            Credential::None,
1470            false,
1471        )
1472        .expect("Failed to create network config");
1473
1474        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1475        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1476
1477        // If we update the probability after a not-seen-in-active-scan, the probability should
1478        // still reflect that we think the network is hidden after the connect.
1479        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1480        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1481    }
1482
1483    #[fuchsia::test]
1484    fn test_is_hidden_implementation() {
1485        let mut config = NetworkConfig::new(
1486            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
1487            policy_wpa_password(),
1488            false,
1489        )
1490        .expect("Error creating network config for foo");
1491        config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1492        assert!(config.is_hidden());
1493    }
1494
1495    fn policy_wep_key() -> Credential {
1496        Credential::Password("abcdef0000".as_bytes().to_vec())
1497    }
1498
1499    fn common_wep_key() -> WepKey {
1500        WepKey::parse("abcdef0000").unwrap()
1501    }
1502
1503    fn policy_wpa_password() -> Credential {
1504        Credential::Password("password".as_bytes().to_vec())
1505    }
1506
1507    fn common_wpa_password() -> Passphrase {
1508        Passphrase::try_from("password").unwrap()
1509    }
1510
1511    fn policy_wpa_psk() -> Credential {
1512        Credential::Psk(vec![0u8; WPA_PSK_BYTE_LEN])
1513    }
1514
1515    fn common_wpa_psk() -> Psk {
1516        Psk::from([0u8; WPA_PSK_BYTE_LEN])
1517    }
1518
1519    // Expect successful mapping in the following cases.
1520    #[test_case(
1521        [SecurityDescriptor::OPEN],
1522        Credential::None
1523        =>
1524        Some(SecurityAuthenticator::Open)
1525    )]
1526    #[test_case(
1527        [SecurityDescriptor::OWE],
1528        Credential::None
1529        =>
1530        Some(SecurityAuthenticator::Owe)
1531    )]
1532    #[test_case(
1533        [SecurityDescriptor::WEP],
1534        policy_wep_key()
1535        =>
1536        Some(SecurityAuthenticator::Wep(WepAuthenticator {
1537            key: common_wep_key(),
1538        }))
1539    )]
1540    #[test_case(
1541        [SecurityDescriptor::WPA1],
1542        policy_wpa_password()
1543        =>
1544        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa1 {
1545            credentials: Wpa1Credentials::Passphrase(common_wpa_password()),
1546        }))
1547    )]
1548    #[test_case(
1549        [SecurityDescriptor::OPEN, SecurityDescriptor::OWE],
1550        Credential::None
1551        =>
1552        Some(SecurityAuthenticator::Owe)
1553    )]
1554    #[test_case(
1555        [SecurityDescriptor::WPA1, SecurityDescriptor::WPA2_PERSONAL],
1556        policy_wpa_psk()
1557        =>
1558        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1559            cipher: None,
1560            authentication: Authentication::Personal(
1561                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1562            ),
1563        }))
1564    )]
1565    #[test_case(
1566        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1567        policy_wpa_password()
1568        =>
1569        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa3 {
1570            cipher: None,
1571            authentication: Authentication::Personal(
1572                Wpa3PersonalCredentials::Passphrase(common_wpa_password())
1573            ),
1574        }))
1575    )]
1576    #[test_case(
1577        [SecurityDescriptor::WPA2_PERSONAL],
1578        policy_wpa_password()
1579        =>
1580        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1581            cipher: None,
1582            authentication: Authentication::Personal(
1583                Wpa2PersonalCredentials::Passphrase(common_wpa_password())
1584            ),
1585        }))
1586    )]
1587    #[test_case(
1588        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1589        policy_wpa_psk()
1590        =>
1591        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1592            cipher: None,
1593            authentication: Authentication::Personal(
1594                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1595            ),
1596        }))
1597    )]
1598    // Expect failed mapping in the following cases.
1599    #[test_case(
1600        [SecurityDescriptor::WPA3_PERSONAL],
1601        policy_wpa_psk()
1602        =>
1603        None
1604    )]
1605    #[fuchsia::test(add_test_attr = false)]
1606    fn select_authentication_method_matrix(
1607        mutual_security_protocols: impl IntoIterator<Item = SecurityDescriptor>,
1608        credential: Credential,
1609    ) -> Option<SecurityAuthenticator> {
1610        super::select_authentication_method(
1611            mutual_security_protocols.into_iter().collect(),
1612            &credential,
1613        )
1614    }
1615
1616    #[test_case(SecurityType::None)]
1617    #[test_case(SecurityType::Wep)]
1618    #[test_case(SecurityType::Wpa)]
1619    #[test_case(SecurityType::Wpa2)]
1620    #[test_case(SecurityType::Wpa3)]
1621    fn test_security_type_list_includes_type(security: SecurityType) {
1622        let types = SecurityType::list_variants();
1623        assert!(types.contains(&security));
1624    }
1625
1626    // If this test doesn't compile, add the security type to this test and list_security_types().
1627    #[fuchsia::test]
1628    fn test_security_type_list_completeness() {
1629        // Any variant works here.
1630        let security = SecurityType::Wpa;
1631        // This will not compile if a new variant is added until this test is updated. Do not
1632        // a wildcard branch.
1633        match security {
1634            SecurityType::None => {}
1635            SecurityType::Wep => {}
1636            SecurityType::Wpa => {}
1637            SecurityType::Wpa2 => {}
1638            SecurityType::Wpa3 => {}
1639        }
1640    }
1641
1642    #[fuchsia::test]
1643    fn test_is_likely_single_bss() {
1644        let ssid = generate_string();
1645        let mut network_config = NetworkConfig::new(
1646            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1647            Credential::None,
1648            false,
1649        )
1650        .expect("Failed to create network config");
1651
1652        // Record that the network was seen with 1 BSS a few times.
1653        for _ in 0..5 {
1654            network_config.update_seen_multiple_bss(false);
1655        }
1656
1657        // Verify the network is considered single BSS.
1658        assert!(network_config.is_likely_single_bss());
1659    }
1660
1661    #[fuchsia::test]
1662    fn test_is_not_single_bss() {
1663        let ssid = generate_string();
1664        let mut network_config = NetworkConfig::new(
1665            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1666            Credential::None,
1667            false,
1668        )
1669        .expect("Failed to create network config");
1670
1671        // Record that the network was seen with multiple BSS a few times then one BSS once.
1672        for _ in 0..5 {
1673            network_config.update_seen_multiple_bss(true);
1674        }
1675        network_config.update_seen_multiple_bss(false);
1676
1677        // Verify the network is not considered single BSS.
1678        assert!(!network_config.is_likely_single_bss());
1679    }
1680
1681    #[fuchsia::test]
1682    fn test_cannot_yet_determine_single_bss() {
1683        let ssid = generate_string();
1684        let mut network_config = NetworkConfig::new(
1685            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1686            Credential::None,
1687            false,
1688        )
1689        .expect("Failed to create network config");
1690
1691        // Record that the network was seen with a single BSS only a couple times.
1692        network_config.update_seen_multiple_bss(false);
1693        network_config.update_seen_multiple_bss(false);
1694
1695        // Verify the network is not considered likely single BSS.
1696        assert!(!network_config.is_likely_single_bss());
1697    }
1698}