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 NetworkIdentifier {
601    pub fn new(ssid: client_types::Ssid, security_type: SecurityType) -> Self {
602        NetworkIdentifier { ssid, security_type }
603    }
604
605    #[cfg(test)]
606    pub fn try_from(ssid: &str, security_type: SecurityType) -> Result<Self, anyhow::Error> {
607        Ok(NetworkIdentifier { ssid: client_types::Ssid::try_from(ssid)?, security_type })
608    }
609}
610
611impl From<fidl_policy::NetworkIdentifier> for NetworkIdentifier {
612    fn from(id: fidl_policy::NetworkIdentifier) -> Self {
613        Self::new(client_types::Ssid::from_bytes_unchecked(id.ssid), id.type_.into())
614    }
615}
616
617impl From<NetworkIdentifier> for fidl_policy::NetworkIdentifier {
618    fn from(id: NetworkIdentifier) -> Self {
619        fidl_policy::NetworkIdentifier { ssid: id.ssid.into(), type_: id.security_type.into() }
620    }
621}
622
623impl From<NetworkConfig> for fidl_policy::NetworkConfig {
624    fn from(config: NetworkConfig) -> Self {
625        let network_id = NetworkIdentifier::new(config.ssid, config.security_type);
626        fidl_policy::NetworkConfig {
627            id: Some(fidl_policy::NetworkIdentifier::from(network_id)),
628            credential: Some(fidl_policy::Credential::from(config.credential)),
629            ..Default::default()
630        }
631    }
632}
633
634/// Returns an error if the input network values are not valid or none if the values are valid.
635/// For example it is an error if the network is Open (no password) but a password is supplied.
636/// TODO(nmccracken) - Specific errors need to be added to the API and returned here
637fn check_config_errors(
638    ssid: &client_types::Ssid,
639    security_type: &SecurityType,
640    credential: &Credential,
641) -> Result<(), NetworkConfigError> {
642    // Verify SSID has at least 1 byte.
643    if ssid.is_empty() {
644        return Err(NetworkConfigError::SsidEmpty);
645    }
646    // Verify that credentials match the security type. This code only inspects the lengths of
647    // passphrases and PSKs; the underlying data is considered opaque here.
648    match security_type {
649        SecurityType::None => {
650            if let Credential::Psk(_) | Credential::Password(_) = credential {
651                return Err(NetworkConfigError::OpenNetworkPassword);
652            }
653        }
654        // Note that some vendors allow WEP passphrase and PSK lengths that are not described by
655        // IEEE 802.11. These lengths are unsupported. See also the `wep_deprecated` crate.
656        SecurityType::Wep => match credential {
657            Credential::Password(password) => match password.len() {
658                // ASCII encoding.
659                WEP_40_ASCII_LEN | WEP_104_ASCII_LEN => {}
660                // Hexadecimal encoding.
661                WEP_40_HEX_LEN | WEP_104_HEX_LEN => {}
662                _ => {
663                    return Err(NetworkConfigError::PasswordLen);
664                }
665            },
666            _ => {
667                return Err(NetworkConfigError::MissingPasswordPsk);
668            }
669        },
670        SecurityType::Wpa | SecurityType::Wpa2 | SecurityType::Wpa3 => match credential {
671            Credential::Password(pwd) => {
672                if pwd.len() < WPA_MIN_PASSWORD_LEN || pwd.len() > WPA_MAX_PASSWORD_LEN {
673                    return Err(NetworkConfigError::PasswordLen);
674                }
675            }
676            Credential::Psk(psk) => {
677                if security_type == &SecurityType::Wpa3 {
678                    return Err(NetworkConfigError::Wpa3Psk);
679                }
680                if psk.len() != WPA_PSK_BYTE_LEN {
681                    return Err(NetworkConfigError::PskLen);
682                }
683            }
684            _ => {
685                return Err(NetworkConfigError::MissingPasswordPsk);
686            }
687        },
688    }
689    Ok(())
690}
691
692/// Error codes representing problems in trying to save a network config, such as errors saving
693/// or removing a network config, or for invalid values when trying to create a network config.
694#[derive(Hash, PartialEq, Eq)]
695pub enum NetworkConfigError {
696    OpenNetworkPassword,
697    Wpa3Psk,
698    PasswordLen,
699    PskLen,
700    SsidEmpty,
701    MissingPasswordPsk,
702    ConfigMissingId,
703    ConfigMissingCredential,
704    CredentialTypeInvalid,
705    FileWriteError,
706    LegacyWriteError,
707}
708
709impl Debug for NetworkConfigError {
710    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
711        match self {
712            NetworkConfigError::OpenNetworkPassword => {
713                write!(f, "can't have an open network with a password or PSK")
714            }
715            NetworkConfigError::Wpa3Psk => {
716                write!(f, "can't use a PSK to connect to a WPA3 network")
717            }
718            NetworkConfigError::PasswordLen => write!(f, "invalid password length"),
719            NetworkConfigError::PskLen => write!(f, "invalid PSK length"),
720            NetworkConfigError::SsidEmpty => {
721                write!(f, "SSID must have a non-zero length")
722            }
723            NetworkConfigError::MissingPasswordPsk => {
724                write!(f, "no password or PSK provided but required by security type")
725            }
726            NetworkConfigError::ConfigMissingId => {
727                write!(f, "cannot create network config, network id is None")
728            }
729            NetworkConfigError::ConfigMissingCredential => {
730                write!(f, "cannot create network config, no credential is given")
731            }
732            NetworkConfigError::CredentialTypeInvalid => {
733                write!(f, "cannot convert fidl Credential, unknown variant")
734            }
735            NetworkConfigError::FileWriteError => {
736                write!(f, "error writing network config to file")
737            }
738            NetworkConfigError::LegacyWriteError => {
739                write!(f, "error writing network config to legacy storage")
740            }
741        }
742    }
743}
744
745impl From<NetworkConfigError> for fidl_policy::NetworkConfigChangeError {
746    fn from(err: NetworkConfigError) -> Self {
747        match err {
748            NetworkConfigError::OpenNetworkPassword
749            | NetworkConfigError::MissingPasswordPsk
750            | NetworkConfigError::Wpa3Psk => {
751                fidl_policy::NetworkConfigChangeError::InvalidSecurityCredentialError
752            }
753            NetworkConfigError::PasswordLen | NetworkConfigError::PskLen => {
754                fidl_policy::NetworkConfigChangeError::CredentialLenError
755            }
756            NetworkConfigError::SsidEmpty => fidl_policy::NetworkConfigChangeError::SsidEmptyError,
757            NetworkConfigError::ConfigMissingId | NetworkConfigError::ConfigMissingCredential => {
758                fidl_policy::NetworkConfigChangeError::NetworkConfigMissingFieldError
759            }
760            NetworkConfigError::CredentialTypeInvalid => {
761                fidl_policy::NetworkConfigChangeError::UnsupportedCredentialError
762            }
763            NetworkConfigError::FileWriteError | NetworkConfigError::LegacyWriteError => {
764                fidl_policy::NetworkConfigChangeError::NetworkConfigWriteError
765            }
766        }
767    }
768}
769
770/// Binds a credential to a security protocol.
771///
772/// Binding constructs a `SecurityAuthenticator` that can be used to construct an SME
773/// `ConnectRequest`. This function is similar to `SecurityDescriptor::bind`, but operates on the
774/// Policy `Credential` type, which requires some additional logic to determine how the credential
775/// data is interpreted.
776///
777/// Returns `None` if the given protocol is incompatible with the given credential.
778fn bind_credential_to_protocol(
779    protocol: SecurityDescriptor,
780    credential: &Credential,
781) -> Option<SecurityAuthenticator> {
782    match protocol {
783        SecurityDescriptor::Open => match credential {
784            Credential::None => protocol.bind(None).ok(),
785            _ => None,
786        },
787        SecurityDescriptor::Owe => match credential {
788            Credential::None => protocol.bind(None).ok(),
789            _ => None,
790        },
791        SecurityDescriptor::Wep => match credential {
792            Credential::Password(key) => {
793                WepKey::parse(key).ok().and_then(|key| protocol.bind(Some(key.into())).ok())
794            }
795            _ => None,
796        },
797        SecurityDescriptor::Wpa(wpa) => match wpa {
798            WpaDescriptor::Wpa1 { .. } | WpaDescriptor::Wpa2 { .. } => match credential {
799                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
800                    .ok()
801                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
802                Credential::Psk(psk) => {
803                    Psk::parse(psk).ok().and_then(|psk| protocol.bind(Some(psk.into())).ok())
804                }
805                _ => None,
806            },
807            WpaDescriptor::Wpa3 { .. } => match credential {
808                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
809                    .ok()
810                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
811                _ => None,
812            },
813        },
814    }
815}
816
817/// Creates a security authenticator based on supported security protocols and credentials.
818///
819/// The authentication method is chosen based on the general strength of each mutually supported
820/// security protocol (the protocols supported by both the local and remote stations) and the
821/// compatibility of those protocols with the given credentials.
822///
823/// Returns `None` if no appropriate authentication method can be selected for the given protocols
824/// and credentials.
825pub fn select_authentication_method(
826    mutual_security_protocols: HashSet<SecurityDescriptor>,
827    credential: &Credential,
828) -> Option<SecurityAuthenticator> {
829    let mut protocols: Vec<_> = mutual_security_protocols.into_iter().collect();
830    protocols.sort_by_key(|protocol| {
831        Reverse(match protocol {
832            SecurityDescriptor::Open => 0,
833            SecurityDescriptor::Owe => 3,
834            SecurityDescriptor::Wep => 1,
835            SecurityDescriptor::Wpa(wpa) => match wpa {
836                WpaDescriptor::Wpa1 { .. } => 2,
837                WpaDescriptor::Wpa2 { .. } => 4,
838                WpaDescriptor::Wpa3 { .. } => 5,
839            },
840        })
841    });
842    protocols
843        .into_iter()
844        .flat_map(|protocol| bind_credential_to_protocol(protocol, credential))
845        .next()
846}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851    use crate::util::testing::{generate_string, random_connection_data};
852    use assert_matches::assert_matches;
853    use std::collections::VecDeque;
854    use test_case::test_case;
855    use wlan_common::security::wep::WepAuthenticator;
856    use wlan_common::security::wpa::{
857        Authentication, Wpa1Credentials, Wpa2PersonalCredentials, Wpa3PersonalCredentials,
858        WpaAuthenticator,
859    };
860
861    #[fuchsia::test]
862    fn new_network_config_none_credential() {
863        let credential = Credential::None;
864        let network_config = NetworkConfig::new(
865            NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(),
866            credential.clone(),
867            false,
868        )
869        .expect("Error creating network config for foo");
870
871        assert_eq!(
872            network_config,
873            NetworkConfig {
874                ssid: client_types::Ssid::try_from("foo").unwrap(),
875                security_type: SecurityType::None,
876                credential,
877                has_ever_connected: false,
878                hidden_probability: PROB_HIDDEN_DEFAULT,
879                hidden_probability_stats: HiddenProbabilityStats::new(),
880                perf_stats: PerformanceStats::new(),
881                scan_stats: ScanStats::new(),
882            }
883        );
884    }
885
886    #[fuchsia::test]
887    fn new_network_config_password_credential() {
888        let credential = Credential::Password(b"foo-password".to_vec());
889
890        let network_config = NetworkConfig::new(
891            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
892            credential.clone(),
893            false,
894        )
895        .expect("Error creating network config for foo");
896
897        assert_eq!(
898            network_config,
899            NetworkConfig {
900                ssid: client_types::Ssid::try_from("foo").unwrap(),
901                security_type: SecurityType::Wpa2,
902                credential,
903                has_ever_connected: false,
904                hidden_probability: PROB_HIDDEN_DEFAULT,
905                hidden_probability_stats: HiddenProbabilityStats::new(),
906                perf_stats: PerformanceStats::new(),
907                scan_stats: ScanStats::new(),
908            }
909        );
910        assert!(network_config.perf_stats.connect_failures.0.is_empty());
911    }
912
913    #[fuchsia::test]
914    fn new_network_config_psk_credential() {
915        let credential = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
916
917        let network_config = NetworkConfig::new(
918            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
919            credential.clone(),
920            false,
921        )
922        .expect("Error creating network config for foo");
923
924        assert_eq!(
925            network_config,
926            NetworkConfig {
927                ssid: client_types::Ssid::try_from("foo").unwrap(),
928                security_type: SecurityType::Wpa2,
929                credential,
930                has_ever_connected: false,
931                hidden_probability: PROB_HIDDEN_DEFAULT,
932                hidden_probability_stats: HiddenProbabilityStats::new(),
933                perf_stats: PerformanceStats::new(),
934                scan_stats: ScanStats::new(),
935            }
936        );
937    }
938
939    #[fuchsia::test]
940    fn new_network_config_invalid_password() {
941        let credential = Credential::Password([1; 64].to_vec());
942
943        let config_result = NetworkConfig::new(
944            NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(),
945            credential,
946            false,
947        );
948
949        assert_matches!(config_result, Err(NetworkConfigError::PasswordLen));
950    }
951
952    #[fuchsia::test]
953    fn new_network_config_invalid_psk() {
954        let credential = Credential::Psk(b"bar".to_vec());
955
956        let config_result = NetworkConfig::new(
957            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
958            credential,
959            false,
960        );
961
962        assert_matches!(config_result, Err(NetworkConfigError::PskLen));
963    }
964
965    #[fuchsia::test]
966    fn check_config_errors_invalid_wep_password() {
967        // Unsupported length (7).
968        let password = Credential::Password(b"1234567".to_vec());
969        assert_matches!(
970            check_config_errors(
971                &client_types::Ssid::try_from("valid_ssid").unwrap(),
972                &SecurityType::Wep,
973                &password
974            ),
975            Err(NetworkConfigError::PasswordLen)
976        );
977    }
978
979    #[fuchsia::test]
980    fn check_config_errors_invalid_wpa_password() {
981        // password too short
982        let short_password = Credential::Password(b"1234567".to_vec());
983        assert_matches!(
984            check_config_errors(
985                &client_types::Ssid::try_from("valid_ssid").unwrap(),
986                &SecurityType::Wpa2,
987                &short_password
988            ),
989            Err(NetworkConfigError::PasswordLen)
990        );
991
992        // password too long
993        let long_password = Credential::Password([5, 65].to_vec());
994        assert_matches!(
995            check_config_errors(
996                &client_types::Ssid::try_from("valid_ssid").unwrap(),
997                &SecurityType::Wpa2,
998                &long_password
999            ),
1000            Err(NetworkConfigError::PasswordLen)
1001        );
1002    }
1003
1004    #[fuchsia::test]
1005    fn check_config_errors_invalid_wep_credential_variant() {
1006        // Unsupported variant (`Psk`).
1007        let psk = Credential::Psk(b"12345".to_vec());
1008        assert_matches!(
1009            check_config_errors(
1010                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1011                &SecurityType::Wep,
1012                &psk
1013            ),
1014            Err(NetworkConfigError::MissingPasswordPsk)
1015        );
1016    }
1017
1018    #[fuchsia::test]
1019    fn check_config_errors_invalid_wpa_psk() {
1020        // PSK length not 32 characters
1021        let short_psk = Credential::Psk([6; WPA_PSK_BYTE_LEN - 1].to_vec());
1022
1023        assert_matches!(
1024            check_config_errors(
1025                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1026                &SecurityType::Wpa2,
1027                &short_psk
1028            ),
1029            Err(NetworkConfigError::PskLen)
1030        );
1031
1032        let long_psk = Credential::Psk([7; WPA_PSK_BYTE_LEN + 1].to_vec());
1033        assert_matches!(
1034            check_config_errors(
1035                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1036                &SecurityType::Wpa2,
1037                &long_psk
1038            ),
1039            Err(NetworkConfigError::PskLen)
1040        );
1041    }
1042
1043    #[fuchsia::test]
1044    fn check_config_errors_invalid_security_credential() {
1045        // Use a password with open network.
1046        let password = Credential::Password(b"password".to_vec());
1047        assert_matches!(
1048            check_config_errors(
1049                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1050                &SecurityType::None,
1051                &password
1052            ),
1053            Err(NetworkConfigError::OpenNetworkPassword)
1054        );
1055
1056        let psk = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
1057        assert_matches!(
1058            check_config_errors(
1059                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1060                &SecurityType::None,
1061                &psk
1062            ),
1063            Err(NetworkConfigError::OpenNetworkPassword)
1064        );
1065        // Use no password with a protected network.
1066        let password = Credential::None;
1067        assert_matches!(
1068            check_config_errors(
1069                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1070                &SecurityType::Wpa,
1071                &password
1072            ),
1073            Err(NetworkConfigError::MissingPasswordPsk)
1074        );
1075
1076        assert_matches!(
1077            check_config_errors(
1078                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1079                &SecurityType::Wpa2,
1080                &password
1081            ),
1082            Err(NetworkConfigError::MissingPasswordPsk)
1083        );
1084
1085        assert_matches!(
1086            check_config_errors(
1087                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1088                &SecurityType::Wpa3,
1089                &password
1090            ),
1091            Err(NetworkConfigError::MissingPasswordPsk)
1092        );
1093
1094        assert_matches!(
1095            check_config_errors(
1096                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1097                &SecurityType::Wpa3,
1098                &psk
1099            ),
1100            Err(NetworkConfigError::Wpa3Psk)
1101        );
1102    }
1103
1104    #[fuchsia::test]
1105    fn check_config_errors_ssid_empty() {
1106        assert_matches!(
1107            check_config_errors(
1108                &client_types::Ssid::empty(),
1109                &SecurityType::None,
1110                &Credential::None
1111            ),
1112            Err(NetworkConfigError::SsidEmpty)
1113        );
1114    }
1115
1116    #[fasync::run_singlethreaded(test)]
1117    async fn test_connect_failures_by_bssid_add_and_get() {
1118        let mut connect_failures = HistoricalListsByBssid::new();
1119        let curr_time = fasync::MonotonicInstant::now();
1120
1121        // Add two failures for BSSID_1
1122        let bssid_1 = client_types::Bssid::from([1; 6]);
1123        let failure_1_bssid_1 = ConnectFailure {
1124            time: curr_time - zx::MonotonicDuration::from_seconds(10),
1125            bssid: bssid_1,
1126            reason: FailureReason::GeneralFailure,
1127        };
1128        connect_failures.add(bssid_1, failure_1_bssid_1);
1129
1130        let failure_2_bssid_1 = ConnectFailure {
1131            time: curr_time - zx::MonotonicDuration::from_seconds(5),
1132            bssid: bssid_1,
1133            reason: FailureReason::CredentialRejected,
1134        };
1135        connect_failures.add(bssid_1, failure_2_bssid_1);
1136
1137        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1138        assert_eq!(
1139            connect_failures
1140                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1141            vec![failure_1_bssid_1, failure_2_bssid_1]
1142        );
1143
1144        // Add one failure for BSSID_2
1145        let bssid_2 = client_types::Bssid::from([2; 6]);
1146        let failure_1_bssid_2 = ConnectFailure {
1147            time: curr_time - zx::MonotonicDuration::from_seconds(3),
1148            bssid: bssid_2,
1149            reason: FailureReason::GeneralFailure,
1150        };
1151        connect_failures.add(bssid_2, failure_1_bssid_2);
1152
1153        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1154        assert_eq!(
1155            connect_failures
1156                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1157            vec![failure_1_bssid_1, failure_2_bssid_1, failure_1_bssid_2]
1158        );
1159
1160        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1161        assert_eq!(
1162            connect_failures
1163                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1164            vec![failure_2_bssid_1, failure_1_bssid_2]
1165        );
1166
1167        // Verify get_recent_for_network(curr_time) is empty
1168        assert_eq!(connect_failures.get_recent_for_network(curr_time), vec![]);
1169
1170        // Verify get_list_for_bss retrieves correct connect failures
1171        assert_eq!(
1172            connect_failures.get_list_for_bss(&bssid_1),
1173            HistoricalList(VecDeque::from_iter([failure_1_bssid_1, failure_2_bssid_1]))
1174        );
1175
1176        assert_eq!(
1177            connect_failures.get_list_for_bss(&bssid_2),
1178            HistoricalList(VecDeque::from_iter([failure_1_bssid_2]))
1179        );
1180    }
1181
1182    #[fasync::run_singlethreaded(test)]
1183    async fn failure_list_add_and_get() {
1184        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1185
1186        // Get time before adding so we can get back everything we added.
1187        let curr_time = fasync::MonotonicInstant::now();
1188        assert!(connect_failures.get_recent(curr_time).is_empty());
1189        let bssid = client_types::Bssid::from([1; 6]);
1190        let failure =
1191            ConnectFailure { time: curr_time, bssid, reason: FailureReason::GeneralFailure };
1192        connect_failures.add(failure);
1193
1194        let result_list = connect_failures.get_recent(curr_time);
1195        assert_eq!(1, result_list.len());
1196        assert_eq!(FailureReason::GeneralFailure, result_list[0].reason);
1197        assert_eq!(bssid, result_list[0].bssid);
1198        // Should not get any results if we request denials older than the specified time.
1199        let later_time = fasync::MonotonicInstant::now();
1200        assert!(connect_failures.get_recent(later_time).is_empty());
1201    }
1202
1203    #[fasync::run_singlethreaded(test)]
1204    async fn test_failure_list_add_when_full() {
1205        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1206        let curr_time = fasync::MonotonicInstant::now();
1207
1208        // Add to list, exceeding the capacity by one entry
1209        for i in 0..connect_failures.0.capacity() + 1 {
1210            connect_failures.add(ConnectFailure {
1211                time: curr_time + zx::MonotonicDuration::from_seconds(i as i64),
1212                reason: FailureReason::GeneralFailure,
1213                bssid: client_types::Bssid::from([1; 6]),
1214            })
1215        }
1216
1217        // Validate entry with time = curr_time was evicted.
1218        for (i, e) in connect_failures.0.iter().enumerate() {
1219            assert_eq!(e.time, curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1));
1220        }
1221    }
1222
1223    #[fasync::run_singlethreaded(test)]
1224    async fn test_past_connections_by_bssid_add_and_get() {
1225        let mut past_connections_list = HistoricalListsByBssid::new();
1226        let curr_time = fasync::MonotonicInstant::now();
1227
1228        // Add two past_connections for BSSID_1
1229        let mut data_1_bssid_1 = random_connection_data();
1230        let bssid_1 = data_1_bssid_1.bssid;
1231        data_1_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(10);
1232
1233        past_connections_list.add(bssid_1, data_1_bssid_1);
1234
1235        let mut data_2_bssid_1 = random_connection_data();
1236        data_2_bssid_1.bssid = bssid_1;
1237        data_2_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(5);
1238        past_connections_list.add(bssid_1, data_2_bssid_1);
1239
1240        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1241        assert_eq!(
1242            past_connections_list
1243                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1244            vec![data_1_bssid_1, data_2_bssid_1]
1245        );
1246
1247        // Add one past_connection for BSSID_2
1248        let mut data_1_bssid_2 = random_connection_data();
1249        let bssid_2 = data_1_bssid_2.bssid;
1250        data_1_bssid_2.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(3);
1251        past_connections_list.add(bssid_2, data_1_bssid_2);
1252
1253        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1254        assert_eq!(
1255            past_connections_list
1256                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1257            vec![data_1_bssid_1, data_2_bssid_1, data_1_bssid_2]
1258        );
1259
1260        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1261        assert_eq!(
1262            past_connections_list
1263                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1264            vec![data_2_bssid_1, data_1_bssid_2]
1265        );
1266
1267        // Verify get_recent_for_network(curr_time) is empty
1268        assert_eq!(past_connections_list.get_recent_for_network(curr_time), vec![]);
1269
1270        // Verify get_list_for_bss retrieves correct PastConnectionLists
1271        assert_eq!(
1272            past_connections_list.get_list_for_bss(&bssid_1),
1273            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_1, data_2_bssid_1]) }
1274        );
1275
1276        assert_eq!(
1277            past_connections_list.get_list_for_bss(&bssid_2),
1278            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_2]) }
1279        );
1280    }
1281
1282    #[fasync::run_singlethreaded(test)]
1283    async fn test_past_connections_list_add_when_full() {
1284        let mut past_connections_list = PastConnectionList::default();
1285        let curr_time = fasync::MonotonicInstant::now();
1286
1287        // Add to list, exceeding the capacity by one entry
1288        for i in 0..past_connections_list.0.capacity() + 1 {
1289            let mut data = random_connection_data();
1290            data.bssid = client_types::Bssid::from([1; 6]);
1291            data.disconnect_time = curr_time + zx::MonotonicDuration::from_seconds(i as i64);
1292            past_connections_list.add(data);
1293        }
1294
1295        // Validate entry with time = curr_time was evicted.
1296        for (i, e) in past_connections_list.0.iter().enumerate() {
1297            assert_eq!(
1298                e.disconnect_time,
1299                curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1)
1300            );
1301        }
1302    }
1303
1304    #[fasync::run_singlethreaded(test)]
1305    async fn test_past_connections_list_add_and_get() {
1306        let mut past_connections_list = PastConnectionList::default();
1307        let curr_time = fasync::MonotonicInstant::now();
1308        assert!(past_connections_list.get_recent(curr_time).is_empty());
1309
1310        let mut past_connection_data = random_connection_data();
1311        past_connection_data.disconnect_time = curr_time;
1312        // Add a past connection
1313        past_connections_list.add(past_connection_data);
1314
1315        // We should get back the added data when specifying the same or an earlier time.
1316        assert_eq!(past_connections_list.get_recent(curr_time).len(), 1);
1317        assert_matches!(past_connections_list.get_recent(curr_time).as_slice(), [data] => {
1318            assert_eq!(data, &past_connection_data.clone());
1319        });
1320        let earlier_time = curr_time - zx::MonotonicDuration::from_seconds(1);
1321        assert_matches!(past_connections_list.get_recent(earlier_time).as_slice(), [data] => {
1322            assert_eq!(data, &data.clone());
1323        });
1324        // The results should be empty if the requested time is after the latest past connection's
1325        // time.
1326        let later_time = curr_time + zx::MonotonicDuration::from_seconds(1);
1327        assert!(past_connections_list.get_recent(later_time).is_empty());
1328    }
1329
1330    #[fuchsia::test]
1331    fn test_credential_from_bytes() {
1332        assert_eq!(Credential::from_bytes(vec![1]), Credential::Password(vec![1]));
1333        assert_eq!(Credential::from_bytes(vec![2; 63]), Credential::Password(vec![2; 63]));
1334        // credential from bytes should only be used to load legacy data, so PSK won't be supported
1335        assert_eq!(
1336            Credential::from_bytes(vec![2; WPA_PSK_BYTE_LEN]),
1337            Credential::Password(vec![2; WPA_PSK_BYTE_LEN])
1338        );
1339        assert_eq!(Credential::from_bytes(vec![]), Credential::None);
1340    }
1341
1342    #[fuchsia::test]
1343    fn test_derived_security_type_from_credential() {
1344        let password = Credential::Password(b"password".to_vec());
1345        let psk = Credential::Psk(b"psk-type".to_vec());
1346        let none = Credential::None;
1347
1348        assert_eq!(SecurityType::Wpa2, password.derived_security_type());
1349        assert_eq!(SecurityType::Wpa2, psk.derived_security_type());
1350        assert_eq!(SecurityType::None, none.derived_security_type());
1351    }
1352
1353    #[fuchsia::test]
1354    fn test_hidden_prob_calculation() {
1355        let mut network_config = NetworkConfig::new(
1356            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1357            Credential::None,
1358            false,
1359        )
1360        .expect("Failed to create network config");
1361        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_DEFAULT);
1362
1363        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1364        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1365
1366        network_config.update_hidden_prob(HiddenProbEvent::ConnectPassive);
1367        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1368
1369        // Hidden probability shouldn't go back up after seeing a network in a passive
1370        // scan again after connecting with a passive scan
1371        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1372        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1373    }
1374
1375    #[fuchsia::test]
1376    fn test_hidden_prob_calc_active_connect() {
1377        let mut network_config = NetworkConfig::new(
1378            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1379            Credential::None,
1380            false,
1381        )
1382        .expect("Failed to create network config");
1383
1384        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1385        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1386
1387        // If we see a network in a passive scan after connecting from an active scan,
1388        // we won't care that we previously needed an active scan.
1389        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1390        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1391
1392        // If we require an active scan to connect to a network, raise probability as if the
1393        // network has become hidden.
1394        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1395        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1396    }
1397
1398    #[fuchsia::test]
1399    fn test_hidden_prob_calc_not_seen_in_active_scan_lowers_prob() {
1400        // Test that updating hidden probability after not seeing the network in a directed active
1401        // scan lowers the hidden probability
1402        let mut network_config = NetworkConfig::new(
1403            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1404            Credential::None,
1405            false,
1406        )
1407        .expect("Failed to create network config");
1408
1409        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1410        let expected_prob = PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1411        assert_eq!(network_config.hidden_probability, expected_prob);
1412
1413        // If we update hidden probability again, the probability should lower again.
1414        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1415        let expected_prob = expected_prob - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1416        assert_eq!(network_config.hidden_probability, expected_prob);
1417    }
1418
1419    #[fuchsia::test]
1420    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_lower_past_threshold() {
1421        let mut network_config = NetworkConfig::new(
1422            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1423            Credential::None,
1424            false,
1425        )
1426        .expect("Failed to create network config");
1427
1428        // If hidden probability is slightly above the minimum from not seing the network in an
1429        // active scan, it should not be lowered past the minimum.
1430        network_config.hidden_probability = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE + 0.01;
1431        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1432        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1433
1434        // If hidden probability is at the minimum, it should not be lowered.
1435        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1436        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1437    }
1438
1439    #[fuchsia::test]
1440    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_change_if_lower_than_threshold() {
1441        let mut network_config = NetworkConfig::new(
1442            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1443            Credential::None,
1444            false,
1445        )
1446        .expect("Failed to create network config");
1447
1448        // If the hidden probability is lower than the minimum of not seeing the network in an,
1449        // active scan, which could happen after seeing it in a passive scan, the hidden
1450        // probability will not lower from this event.
1451        let prob_before_update = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE - 0.1;
1452        network_config.hidden_probability = prob_before_update;
1453        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1454        assert_eq!(network_config.hidden_probability, prob_before_update);
1455    }
1456
1457    #[fuchsia::test]
1458    fn test_hidden_prob_calc_not_seen_active_after_active_connect() {
1459        // Test the specific case where we fail to see the network in an active scan after we
1460        // previously connected to the network after an active scan was required.
1461        let mut network_config = NetworkConfig::new(
1462            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1463            Credential::None,
1464            false,
1465        )
1466        .expect("Failed to create network config");
1467
1468        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1469        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1470
1471        // If we update the probability after a not-seen-in-active-scan, the probability should
1472        // still reflect that we think the network is hidden after the connect.
1473        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1474        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1475    }
1476
1477    #[fuchsia::test]
1478    fn test_is_hidden_implementation() {
1479        let mut config = NetworkConfig::new(
1480            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
1481            policy_wpa_password(),
1482            false,
1483        )
1484        .expect("Error creating network config for foo");
1485        config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1486        assert!(config.is_hidden());
1487    }
1488
1489    fn policy_wep_key() -> Credential {
1490        Credential::Password("abcdef0000".as_bytes().to_vec())
1491    }
1492
1493    fn common_wep_key() -> WepKey {
1494        WepKey::parse("abcdef0000").unwrap()
1495    }
1496
1497    fn policy_wpa_password() -> Credential {
1498        Credential::Password("password".as_bytes().to_vec())
1499    }
1500
1501    fn common_wpa_password() -> Passphrase {
1502        Passphrase::try_from("password").unwrap()
1503    }
1504
1505    fn policy_wpa_psk() -> Credential {
1506        Credential::Psk(vec![0u8; WPA_PSK_BYTE_LEN])
1507    }
1508
1509    fn common_wpa_psk() -> Psk {
1510        Psk::from([0u8; WPA_PSK_BYTE_LEN])
1511    }
1512
1513    // Expect successful mapping in the following cases.
1514    #[test_case(
1515        [SecurityDescriptor::OPEN],
1516        Credential::None
1517        =>
1518        Some(SecurityAuthenticator::Open)
1519    )]
1520    #[test_case(
1521        [SecurityDescriptor::OWE],
1522        Credential::None
1523        =>
1524        Some(SecurityAuthenticator::Owe)
1525    )]
1526    #[test_case(
1527        [SecurityDescriptor::WEP],
1528        policy_wep_key()
1529        =>
1530        Some(SecurityAuthenticator::Wep(WepAuthenticator {
1531            key: common_wep_key(),
1532        }))
1533    )]
1534    #[test_case(
1535        [SecurityDescriptor::WPA1],
1536        policy_wpa_password()
1537        =>
1538        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa1 {
1539            credentials: Wpa1Credentials::Passphrase(common_wpa_password()),
1540        }))
1541    )]
1542    #[test_case(
1543        [SecurityDescriptor::OPEN, SecurityDescriptor::OWE],
1544        Credential::None
1545        =>
1546        Some(SecurityAuthenticator::Owe)
1547    )]
1548    #[test_case(
1549        [SecurityDescriptor::WPA1, SecurityDescriptor::WPA2_PERSONAL],
1550        policy_wpa_psk()
1551        =>
1552        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1553            cipher: None,
1554            authentication: Authentication::Personal(
1555                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1556            ),
1557        }))
1558    )]
1559    #[test_case(
1560        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1561        policy_wpa_password()
1562        =>
1563        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa3 {
1564            cipher: None,
1565            authentication: Authentication::Personal(
1566                Wpa3PersonalCredentials::Passphrase(common_wpa_password())
1567            ),
1568        }))
1569    )]
1570    #[test_case(
1571        [SecurityDescriptor::WPA2_PERSONAL],
1572        policy_wpa_password()
1573        =>
1574        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1575            cipher: None,
1576            authentication: Authentication::Personal(
1577                Wpa2PersonalCredentials::Passphrase(common_wpa_password())
1578            ),
1579        }))
1580    )]
1581    #[test_case(
1582        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1583        policy_wpa_psk()
1584        =>
1585        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1586            cipher: None,
1587            authentication: Authentication::Personal(
1588                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1589            ),
1590        }))
1591    )]
1592    // Expect failed mapping in the following cases.
1593    #[test_case(
1594        [SecurityDescriptor::WPA3_PERSONAL],
1595        policy_wpa_psk()
1596        =>
1597        None
1598    )]
1599    #[fuchsia::test(add_test_attr = false)]
1600    fn select_authentication_method_matrix(
1601        mutual_security_protocols: impl IntoIterator<Item = SecurityDescriptor>,
1602        credential: Credential,
1603    ) -> Option<SecurityAuthenticator> {
1604        super::select_authentication_method(
1605            mutual_security_protocols.into_iter().collect(),
1606            &credential,
1607        )
1608    }
1609
1610    #[test_case(SecurityType::None)]
1611    #[test_case(SecurityType::Wep)]
1612    #[test_case(SecurityType::Wpa)]
1613    #[test_case(SecurityType::Wpa2)]
1614    #[test_case(SecurityType::Wpa3)]
1615    fn test_security_type_list_includes_type(security: SecurityType) {
1616        let types = SecurityType::list_variants();
1617        assert!(types.contains(&security));
1618    }
1619
1620    // If this test doesn't compile, add the security type to this test and list_security_types().
1621    #[fuchsia::test]
1622    fn test_security_type_list_completeness() {
1623        // Any variant works here.
1624        let security = SecurityType::Wpa;
1625        // This will not compile if a new variant is added until this test is updated. Do not
1626        // a wildcard branch.
1627        match security {
1628            SecurityType::None => {}
1629            SecurityType::Wep => {}
1630            SecurityType::Wpa => {}
1631            SecurityType::Wpa2 => {}
1632            SecurityType::Wpa3 => {}
1633        }
1634    }
1635
1636    #[fuchsia::test]
1637    fn test_is_likely_single_bss() {
1638        let ssid = generate_string();
1639        let mut network_config = NetworkConfig::new(
1640            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1641            Credential::None,
1642            false,
1643        )
1644        .expect("Failed to create network config");
1645
1646        // Record that the network was seen with 1 BSS a few times.
1647        for _ in 0..5 {
1648            network_config.update_seen_multiple_bss(false);
1649        }
1650
1651        // Verify the network is considered single BSS.
1652        assert!(network_config.is_likely_single_bss());
1653    }
1654
1655    #[fuchsia::test]
1656    fn test_is_not_single_bss() {
1657        let ssid = generate_string();
1658        let mut network_config = NetworkConfig::new(
1659            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1660            Credential::None,
1661            false,
1662        )
1663        .expect("Failed to create network config");
1664
1665        // Record that the network was seen with multiple BSS a few times then one BSS once.
1666        for _ in 0..5 {
1667            network_config.update_seen_multiple_bss(true);
1668        }
1669        network_config.update_seen_multiple_bss(false);
1670
1671        // Verify the network is not considered single BSS.
1672        assert!(!network_config.is_likely_single_bss());
1673    }
1674
1675    #[fuchsia::test]
1676    fn test_cannot_yet_determine_single_bss() {
1677        let ssid = generate_string();
1678        let mut network_config = NetworkConfig::new(
1679            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1680            Credential::None,
1681            false,
1682        )
1683        .expect("Failed to create network config");
1684
1685        // Record that the network was seen with a single BSS only a couple times.
1686        network_config.update_seen_multiple_bss(false);
1687        network_config.update_seen_multiple_bss(false);
1688
1689        // Verify the network is not considered likely single BSS.
1690        assert!(!network_config.is_likely_single_bss());
1691    }
1692}