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