| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{ |
| ie::{self, rsn::suite_filter}, |
| mac::CapabilityInfo, |
| }, |
| anyhow::format_err, |
| fidl_fuchsia_wlan_mlme as fidl_mlme, fidl_fuchsia_wlan_sme as fidl_sme, |
| std::{collections::HashMap, fmt, hash::Hash}, |
| }; |
| |
| // TODO(fxbug.dev/29885): Represent this as bitfield instead. |
| #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] |
| pub enum Protection { |
| /// Higher number on Protection enum indicates a more preferred protection type for our SME. |
| /// TODO(fxbug.dev/29877): Move all ordering logic to SME. |
| Unknown = 0, |
| Open = 1, |
| Wep = 2, |
| Wpa1 = 3, |
| Wpa1Wpa2Personal = 4, |
| Wpa2Personal = 5, |
| Wpa2Wpa3Personal = 6, |
| Wpa3Personal = 7, |
| Wpa2Enterprise = 8, |
| /// WPA3 Enterprise 192-bit mode. WPA3 spec specifies an optional 192-bit mode but says nothing |
| /// about a non 192-bit version. Thus, colloquially, it's likely that the term WPA3 Enterprise |
| /// will be used to refer to WPA3 Enterprise 192-bit mode. |
| Wpa3Enterprise = 9, |
| } |
| |
| impl From<Protection> for fidl_sme::Protection { |
| fn from(protection: Protection) -> fidl_sme::Protection { |
| match protection { |
| Protection::Unknown => fidl_sme::Protection::Unknown, |
| Protection::Open => fidl_sme::Protection::Open, |
| Protection::Wep => fidl_sme::Protection::Wep, |
| Protection::Wpa1 => fidl_sme::Protection::Wpa1, |
| Protection::Wpa1Wpa2Personal => fidl_sme::Protection::Wpa1Wpa2Personal, |
| Protection::Wpa2Personal => fidl_sme::Protection::Wpa2Personal, |
| Protection::Wpa2Wpa3Personal => fidl_sme::Protection::Wpa2Wpa3Personal, |
| Protection::Wpa3Personal => fidl_sme::Protection::Wpa3Personal, |
| Protection::Wpa2Enterprise => fidl_sme::Protection::Wpa2Enterprise, |
| Protection::Wpa3Enterprise => fidl_sme::Protection::Wpa3Enterprise, |
| } |
| } |
| } |
| |
| impl fmt::Display for Protection { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| Protection::Unknown => write!(f, "{}", "Unknown"), |
| Protection::Open => write!(f, "{}", "Open"), |
| Protection::Wep => write!(f, "{}", "Wep"), |
| Protection::Wpa1 => write!(f, "{}", "Wpa1"), |
| Protection::Wpa1Wpa2Personal => write!(f, "{}", "Wpa1Wpa2Personal"), |
| Protection::Wpa2Personal => write!(f, "{}", "Wpa2Personal"), |
| Protection::Wpa2Wpa3Personal => write!(f, "{}", "Wpa2Wpa3Personal"), |
| Protection::Wpa3Personal => write!(f, "{}", "Wpa3Personal"), |
| Protection::Wpa2Enterprise => write!(f, "{}", "Wpa2Enterprise"), |
| Protection::Wpa3Enterprise => write!(f, "{}", "Wpa3Enterprise"), |
| } |
| } |
| } |
| |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| pub enum Standard { |
| Dot11A, |
| Dot11B, |
| Dot11G, |
| Dot11N, |
| Dot11Ac, |
| } |
| |
| pub trait BssDescriptionExt { |
| /// Return bool on whether BSS is protected. |
| fn is_protected(&self) -> bool { |
| self.get_protection() != Protection::Open |
| } |
| /// Return bool on whether BSS has security type that would require exchanging EAPOL frames. |
| fn needs_eapol_exchange(&self) -> bool { |
| match self.get_protection() { |
| Protection::Unknown | Protection::Open | Protection::Wep => false, |
| _ => true, |
| } |
| } |
| /// Categorize BSS on what protection it supports. |
| fn get_protection(&self) -> Protection; |
| /// Get the latest WLAN standard that the BSS supports. |
| fn get_latest_standard(&self) -> Standard; |
| /// Search for vendor-specific Info Element for WPA. If found, return the body. |
| fn find_wpa_ie(&self) -> Option<&[u8]>; |
| /// Search for WPA Info Element and parse it. If no WPA Info Element is found, or a WPA Info |
| /// Element is found but is not valid, return an error. |
| fn get_wpa_ie(&self) -> Result<ie::wpa::WpaIe, anyhow::Error>; |
| /// Search for the WiFi Simple Configuration Info Element. If found, return the body. |
| fn find_wsc_ie(&self) -> Option<&[u8]>; |
| /// Return true if an attribute is present in the WiFi Simple Configuration element. |
| /// If the element or the attribute does not exist, return false. |
| fn has_wsc_attr(&self, id: ie::wsc::Id) -> bool; |
| } |
| |
| impl BssDescriptionExt for fidl_mlme::BssDescription { |
| fn get_protection(&self) -> Protection { |
| let supports_wpa_1 = self |
| .get_wpa_ie() |
| .map(|wpa_ie| { |
| let rsne = ie::rsn::rsne::Rsne { |
| group_data_cipher_suite: Some(wpa_ie.multicast_cipher), |
| pairwise_cipher_suites: wpa_ie.unicast_cipher_list, |
| akm_suites: wpa_ie.akm_list, |
| ..Default::default() |
| }; |
| suite_filter::WPA1_PERSONAL.is_satisfied(&rsne) |
| }) |
| .unwrap_or(false); |
| |
| let rsne = match self.rsne.as_ref() { |
| Some(rsne) => match ie::rsn::rsne::from_bytes(rsne) { |
| Ok((_, rsne)) => rsne, |
| Err(_e) => return Protection::Unknown, |
| }, |
| None if !CapabilityInfo(self.cap).privacy() => return Protection::Open, |
| None if self.find_wpa_ie().is_some() => { |
| if supports_wpa_1 { |
| return Protection::Wpa1; |
| } else { |
| return Protection::Unknown; |
| } |
| } |
| None => return Protection::Wep, |
| }; |
| |
| let rsn_caps = rsne.rsn_capabilities.as_ref().unwrap_or(&ie::rsn::rsne::RsnCapabilities(0)); |
| let mfp_req = rsn_caps.mgmt_frame_protection_req(); |
| let mfp_cap = rsn_caps.mgmt_frame_protection_cap(); |
| |
| if suite_filter::WPA3_PERSONAL.is_satisfied(&rsne) { |
| if suite_filter::WPA2_PERSONAL.is_satisfied(&rsne) { |
| if mfp_cap && !mfp_req { |
| return Protection::Wpa2Wpa3Personal; |
| } |
| } else if mfp_cap && mfp_req { |
| return Protection::Wpa3Personal; |
| } |
| } else if suite_filter::WPA2_PERSONAL.is_satisfied(&rsne) { |
| if supports_wpa_1 || suite_filter::WPA2_LEGACY.is_satisfied(&rsne) { |
| return Protection::Wpa1Wpa2Personal; |
| } else { |
| return Protection::Wpa2Personal; |
| } |
| } else if supports_wpa_1 { |
| return Protection::Wpa1; |
| } else if suite_filter::WPA3_ENTERPRISE_192_BIT.is_satisfied(&rsne) { |
| if mfp_cap && mfp_req { |
| return Protection::Wpa3Enterprise; |
| } |
| } else if suite_filter::WPA2_ENTERPRISE.is_satisfied(&rsne) { |
| return Protection::Wpa2Enterprise; |
| } |
| Protection::Unknown |
| } |
| |
| fn get_latest_standard(&self) -> Standard { |
| if self.vht_cap.is_some() && self.vht_op.is_some() { |
| Standard::Dot11Ac |
| } else if self.ht_cap.is_some() && self.ht_op.is_some() { |
| Standard::Dot11N |
| } else if self.chan.primary <= 14 { |
| if self.rates.iter().any(|r| match ie::SupportedRate(*r).rate() { |
| 12 | 18 | 24 | 36 | 48 | 72 | 96 | 108 => true, |
| _ => false, |
| }) { |
| Standard::Dot11G |
| } else { |
| Standard::Dot11B |
| } |
| } else { |
| Standard::Dot11A |
| } |
| } |
| |
| fn find_wpa_ie(&self) -> Option<&[u8]> { |
| let ies = self.vendor_ies.as_ref()?; |
| ie::Reader::new(&ies[..]) |
| .filter_map(|(id, ie)| match id { |
| ie::Id::VENDOR_SPECIFIC => match ie::parse_vendor_ie(ie) { |
| Ok(ie::VendorIe::MsftLegacyWpa(body)) => Some(&body[..]), |
| _ => None, |
| }, |
| _ => None, |
| }) |
| .next() |
| } |
| |
| fn get_wpa_ie(&self) -> Result<ie::wpa::WpaIe, anyhow::Error> { |
| ie::parse_wpa_ie(self.find_wpa_ie().ok_or(format_err!("no wpa ie found"))?) |
| .map_err(|e| e.into()) |
| } |
| |
| fn find_wsc_ie(&self) -> Option<&[u8]> { |
| let ies = self.vendor_ies.as_ref()?; |
| ie::Reader::new(&ies[..]) |
| .filter_map(|(id, ie)| match id { |
| ie::Id::VENDOR_SPECIFIC => match ie::parse_vendor_ie(ie) { |
| Ok(ie::VendorIe::Wsc(body)) => Some(&body[..]), |
| _ => None, |
| }, |
| _ => None, |
| }) |
| .next() |
| } |
| |
| fn has_wsc_attr(&self, id: ie::wsc::Id) -> bool { |
| match self.find_wsc_ie() { |
| Some(bytes) => ie::wsc::Reader::new(bytes).find(|attr| id == attr.0).is_some(), |
| None => false, |
| } |
| } |
| } |
| |
| /// Given a list of BssDescription, categorize each one based on the latest PHY standard it |
| /// supports and return a mapping from Standard to number of BSS. |
| pub fn get_phy_standard_map(bss_list: &Vec<fidl_mlme::BssDescription>) -> HashMap<Standard, usize> { |
| get_info_map(bss_list, |bss| bss.get_latest_standard()) |
| } |
| |
| /// Given a list of BssDescription, return a mapping from channel to the number of BSS using |
| /// that channel. |
| pub fn get_channel_map(bss_list: &Vec<fidl_mlme::BssDescription>) -> HashMap<u8, usize> { |
| get_info_map(bss_list, |bss| bss.chan.primary) |
| } |
| |
| fn get_info_map<F, T>(bss_list: &Vec<fidl_mlme::BssDescription>, f: F) -> HashMap<T, usize> |
| where |
| T: Eq + Hash, |
| F: Fn(&fidl_mlme::BssDescription) -> T, |
| { |
| let mut info_map: HashMap<T, usize> = HashMap::new(); |
| for bss in bss_list { |
| *info_map.entry(f(&bss)).or_insert(0) += 1 |
| } |
| info_map |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::fake_bss, |
| crate::test_utils::fake_frames::{ |
| fake_unknown_rsne, fake_wpa1_ie_body, fake_wpa2_legacy_rsne, invalid_wpa2_wpa3_rsne, |
| invalid_wpa3_enterprise_192_bit_rsne, invalid_wpa3_rsne, |
| }, |
| fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_mlme as fidl_mlme, |
| }; |
| |
| #[test] |
| fn test_get_known_protection() { |
| assert_eq!(Protection::Open, fake_bss!(Open).get_protection()); |
| assert_eq!(Protection::Wep, fake_bss!(Wep).get_protection()); |
| assert_eq!(Protection::Wpa1, fake_bss!(Wpa1).get_protection()); |
| assert_eq!(Protection::Wpa1, fake_bss!(Wpa1Enhanced).get_protection()); |
| assert_eq!(Protection::Wpa1Wpa2Personal, fake_bss!(Wpa1Wpa2).get_protection()); |
| assert_eq!(Protection::Wpa1Wpa2Personal, fake_bss!(Wpa2Mixed).get_protection()); |
| assert_eq!(Protection::Wpa2Personal, fake_bss!(Wpa2).get_protection()); |
| assert_eq!(Protection::Wpa2Wpa3Personal, fake_bss!(Wpa2Wpa3).get_protection()); |
| assert_eq!(Protection::Wpa3Personal, fake_bss!(Wpa3).get_protection()); |
| assert_eq!(Protection::Wpa2Enterprise, fake_bss!(Wpa2Enterprise).get_protection()); |
| assert_eq!(Protection::Wpa3Enterprise, fake_bss!(Wpa3Enterprise).get_protection()); |
| } |
| |
| #[test] |
| fn test_get_unknown_protection() { |
| let mut bss = fake_bss!(Wpa2); |
| bss.rsne = Some(fake_unknown_rsne()); |
| assert_eq!(Protection::Unknown, bss.get_protection()); |
| |
| bss.rsne = Some(invalid_wpa2_wpa3_rsne()); |
| assert_eq!(Protection::Unknown, bss.get_protection()); |
| |
| bss.rsne = Some(invalid_wpa3_rsne()); |
| assert_eq!(Protection::Unknown, bss.get_protection()); |
| |
| bss.rsne = Some(invalid_wpa3_enterprise_192_bit_rsne()); |
| assert_eq!(Protection::Unknown, bss.get_protection()); |
| |
| bss.rsne = Some(fake_wpa2_legacy_rsne()); |
| assert_eq!(Protection::Unknown, bss.get_protection()); |
| } |
| |
| #[test] |
| fn test_needs_eapol_exchange() { |
| assert!(fake_bss!(Wpa1).needs_eapol_exchange()); |
| assert!(fake_bss!(Wpa2).needs_eapol_exchange()); |
| |
| assert!(!fake_bss!(Open).needs_eapol_exchange()); |
| assert!(!fake_bss!(Wep).needs_eapol_exchange()); |
| } |
| |
| #[test] |
| fn test_get_wpa_ie() { |
| let mut buf = vec![]; |
| fake_bss!(Wpa1) |
| .get_wpa_ie() |
| .expect("failed to find WPA1 IE") |
| .write_into(&mut buf) |
| .expect("failed to serialize WPA1 IE"); |
| assert_eq!(fake_wpa1_ie_body(false), buf); |
| fake_bss!(Wpa2).get_wpa_ie().expect_err("found unexpected WPA1 IE"); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_ac() { |
| let bss = fake_bss!(Open, |
| vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities { bytes: Default::default() })), |
| vht_op: Some(Box::new(fidl_mlme::VhtOperation { bytes: Default::default() })) |
| ); |
| assert_eq!(Standard::Dot11Ac, bss.get_latest_standard()); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_n() { |
| let bss = fake_bss!(Open, |
| vht_cap: None, |
| vht_op: None, |
| ht_cap: Some(Box::new(fidl_mlme::HtCapabilities { bytes: Default::default() })), |
| ht_op: Some(Box::new(fidl_mlme::HtOperation { bytes: Default::default() })), |
| ); |
| assert_eq!(Standard::Dot11N, bss.get_latest_standard()); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_g() { |
| let bss = fake_bss!(Open, |
| vht_cap: None, |
| vht_op: None, |
| ht_cap: None, |
| ht_op: None, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| rates: vec![12], |
| ); |
| assert_eq!(Standard::Dot11G, bss.get_latest_standard()); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_b() { |
| let bss = fake_bss!(Open, |
| vht_cap: None, |
| vht_op: None, |
| ht_cap: None, |
| ht_op: None, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| rates: vec![2], |
| ); |
| assert_eq!(Standard::Dot11B, bss.get_latest_standard()); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_b_with_basic() { |
| let bss = fake_bss!(Open, |
| vht_cap: None, |
| vht_op: None, |
| ht_cap: None, |
| ht_op: None, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| rates: vec![ie::SupportedRate(2).with_basic(true).0], |
| ); |
| assert_eq!(Standard::Dot11B, bss.get_latest_standard()); |
| } |
| |
| #[test] |
| fn test_get_latest_standard_a() { |
| let bss = fake_bss!(Open, |
| vht_cap: None, |
| vht_op: None, |
| ht_cap: None, |
| ht_op: None, |
| chan: fidl_common::WlanChan { |
| primary: 36, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| rates: vec![48], |
| ); |
| assert_eq!(Standard::Dot11A, bss.get_latest_standard()); |
| } |
| } |