| // Copyright 2020 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::network_config::{Credential, NetworkConfig, NetworkIdentifier}, |
| anyhow::{bail, format_err, Context, Error}, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_stash as fidl_stash, |
| fuchsia_component::client::connect_to_service, |
| serde_derive::{Deserialize, Serialize}, |
| std::collections::HashMap, |
| wlan_stash::{StashNode, NODE_SEPARATOR}, |
| }; |
| |
| const STASH_PREFIX: &str = "config"; |
| /// The name we store the persistent data of a network config under. The StashNode abstraction |
| /// requires that writing to a StashNode is done as a named field, so we will store the network |
| /// config's data under this name. |
| const DATA: &str = "data"; |
| |
| /// Manages access to the persistent storage or saved network configs through Stash |
| pub struct Stash { |
| root: StashNode, |
| } |
| |
| /// TODO(nmccracken) Remove this attribute when the stash is used by SavedNetworksManager. |
| #[allow(dead_code)] |
| impl Stash { |
| /// Initialize new Stash with the ID provided by the Saved Networks Manager. The ID will |
| /// identify stored values as being part of the same persistent storage. |
| pub fn new_with_id(id: &str) -> Result<Self, Error> { |
| let store_client = connect_to_service::<fidl_stash::SecureStoreMarker>() |
| .context("failed to connect to store")?; |
| store_client.identify(id).context("failed to identify client to store")?; |
| let (store, accessor_server) = create_proxy().context("failed to create accessor proxy")?; |
| store_client |
| .create_accessor(false, accessor_server) |
| .context("failed to create accessor")?; |
| let root = StashNode::root(store).child(STASH_PREFIX); |
| Ok(Stash { root }) |
| } |
| |
| /// Add or update network configs of a given network identifier to persistent storage. |
| pub fn write( |
| &self, |
| id: &NetworkIdentifier, |
| network_configs: &[NetworkConfig], |
| ) -> Result<(), Error> { |
| // write each config to a StashNode under the network identifier. The key of the StashNode |
| // will be STASH_PREFIX#<net_id>#<index> |
| let id_key = Self::serialize_key(id) |
| .map_err(|_| format_err!("failed to serialize network identifier"))?; |
| |
| // use a different number to separate each child network config |
| let mut config_index = 0; |
| let mut id_node = self.root.child(&id_key); |
| for network_config in network_configs { |
| let mut config_node = id_node.child(&config_index.to_string()); |
| write_config(&mut config_node, network_config)?; |
| config_index += 1; |
| } |
| id_node.commit() |
| } |
| |
| /// Make string value of NetworkIdentifier that will be the key for a config in the stash. |
| fn serialize_key(id: &NetworkIdentifier) -> Result<String, serde_json::error::Error> { |
| serde_json::to_string(id) |
| } |
| |
| /// Create the NetworkIdentifier described by the StashNode's key. The key must be in the |
| /// format of the root's key followed by a JSON representation of a NetworkIdentifier and then |
| /// a node separator. Everything after in the key will be ignored. |
| fn id_from_key(&self, stash_node: &StashNode) -> Result<NetworkIdentifier, Error> { |
| let key = stash_node.key(); |
| // Verify that the key begins with the root node's key and remove it. |
| if !key.starts_with(&self.root.key()) { |
| bail!("key is missing the beginning node separator"); |
| } |
| let prefix_len = self.root.key().len(); |
| let mut key_after_root = key[prefix_len..].to_string(); |
| if let Some(index) = key_after_root.find(NODE_SEPARATOR) { |
| key_after_root.truncate(index); |
| } else { |
| bail!("key is missing node separator after network identifier"); |
| } |
| // key_after_root should now just be the serialization of the NetworkIdentifier |
| serde_json::from_str(&key_after_root).map_err(|e| format_err!("{}", e)) |
| } |
| |
| /// Read persisting data of a given StashNode and use it to build a network config. |
| async fn read_config( |
| net_id: NetworkIdentifier, |
| stash_node: &StashNode, |
| ) -> Result<NetworkConfig, Error> { |
| let fields = stash_node.fields().await?; |
| let data = fields.get_str(DATA).ok_or_else(|| format_err!("failed to config's data"))?; |
| let data: PersistentData = serde_json::from_str(data).map_err(|e| format_err!("{}", e))?; |
| data.into_config_with_id(net_id) |
| } |
| |
| /// Load all saved network configs from stash. Will create HashMap of network configs by SSID |
| /// as saved in the stash |
| pub async fn load(&self) -> Result<HashMap<NetworkIdentifier, Vec<NetworkConfig>>, Error> { |
| // get all the children nodes of root, which represent the unique identifiers, |
| let id_nodes = self.root.children().await?; |
| let mut network_configs = HashMap::new(); |
| |
| // for each child representing a network config, read in values |
| for id_node in id_nodes { |
| let mut config_list = vec![]; |
| let net_id = self.id_from_key(&id_node)?; |
| for config_node in id_node.children().await? { |
| let network_config = Self::read_config(net_id.clone(), &config_node).await?; |
| config_list.push(network_config); |
| } |
| network_configs.insert(net_id, config_list); |
| } |
| |
| Ok(network_configs) |
| } |
| |
| /// Remove all saved values from the stash. It will delete everything under the root node, |
| /// and anything else in the same stash but not under the root node would be ignored. |
| pub fn clear(&mut self) -> Result<(), Error> { |
| self.root.delete(); |
| self.root.commit()?; |
| Ok(()) |
| } |
| } |
| |
| /// Write the persisting values (not including network ID) of a network config to the provided |
| /// stash node. |
| fn write_config(stash_node: &mut StashNode, config: &NetworkConfig) -> Result<(), Error> { |
| let data = PersistentData::new(config.credential.clone(), config.has_ever_connected); |
| let data_str = serde_json::to_string(&data).map_err(|e| format_err!("{}", e))?; |
| stash_node.write_str(DATA, data_str) |
| } |
| |
| /// The data that will be stored between reboots of a device. Used to convert the data between JSON |
| /// and network config |
| #[derive(Debug, Deserialize, PartialEq, Serialize)] |
| pub struct PersistentData { |
| credential: Credential, |
| has_ever_connected: bool, |
| } |
| |
| impl PersistentData { |
| fn new(credential: Credential, has_ever_connected: bool) -> Self { |
| Self { credential, has_ever_connected } |
| } |
| |
| /// Since Network Identifier is stored in the stash key and not in the stash value, we need |
| /// to combine network identifier with persistent data in order to make the network config. |
| fn into_config_with_id(self, network_id: NetworkIdentifier) -> Result<NetworkConfig, Error> { |
| let seen_in_passive = false; |
| NetworkConfig::new(network_id, self.credential, self.has_ever_connected, seen_in_passive) |
| .map_err(|_| format_err!("error creating network config from persistent data")) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::network_config::{Credential, NetworkIdentifier, SecurityType}, |
| fuchsia_async as fasync, |
| }; |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn write_and_read() { |
| let stash = new_stash("write_and_read"); |
| let cfg_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2); |
| let cfg = NetworkConfig::new( |
| cfg_id.clone(), |
| Credential::Password(b"password".to_vec()), |
| true, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| |
| // Save a network config to the stash |
| stash.write(&cfg_id, &vec![cfg.clone()]).expect("Failed writing to stash"); |
| |
| // Expect to read the same value back with the same key |
| let cfgs_from_stash = stash.load().await.expect("Failed reading from stash"); |
| assert_eq!(1, cfgs_from_stash.len()); |
| assert_eq!(Some(&vec![cfg.clone()]), cfgs_from_stash.get(&cfg_id)); |
| |
| // Overwrite the list of configs saved in stash |
| let cfg_2 = NetworkConfig::new( |
| NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2), |
| Credential::Password(b"other-password".to_vec()), |
| false, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| stash.write(&cfg_id, &vec![cfg.clone(), cfg_2.clone()]).expect("Failed writing to stash"); |
| |
| // Expect to read the saved value back with the same key |
| let cfgs_from_stash = stash.load().await.expect("Failed reading from stash"); |
| assert_eq!(1, cfgs_from_stash.len()); |
| let actual_configs = cfgs_from_stash.get(&cfg_id).unwrap(); |
| assert_eq!(2, actual_configs.len()); |
| assert!(actual_configs.contains(&cfg)); |
| assert!(actual_configs.contains(&cfg_2)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn write_read_security_types() { |
| let stash = new_stash("write_read_security_types"); |
| let password = Credential::Password(b"config-password".to_vec()); |
| |
| // create and write configs with each security type |
| let net_id_open = network_id("foo", SecurityType::None); |
| let net_id_wep = network_id("foo", SecurityType::Wep); |
| let net_id_wpa = network_id("foo", SecurityType::Wpa); |
| let net_id_wpa2 = network_id("foo", SecurityType::Wpa2); |
| let net_id_wpa3 = network_id("foo", SecurityType::Wpa3); |
| let cfg_open = new_config(net_id_open.clone(), Credential::None); |
| let cfg_wep = new_config(net_id_wep.clone(), password.clone()); |
| let cfg_wpa = new_config(net_id_wpa.clone(), password.clone()); |
| let cfg_wpa2 = new_config(net_id_wpa2.clone(), password.clone()); |
| let cfg_wpa3 = new_config(net_id_wpa3.clone(), password.clone()); |
| |
| stash.write(&net_id_open, &vec![cfg_open.clone()]).expect("failed to write config"); |
| stash.write(&net_id_wep, &vec![cfg_wep.clone()]).expect("failed to write config"); |
| stash.write(&net_id_wpa, &vec![cfg_wpa.clone()]).expect("failed to write config"); |
| stash.write(&net_id_wpa2, &vec![cfg_wpa2.clone()]).expect("failed to write config"); |
| stash.write(&net_id_wpa3, &vec![cfg_wpa3.clone()]).expect("failed to write config"); |
| |
| // load stash and expect each config that we wrote |
| let configs = stash.load().await.expect("failed loading from stash"); |
| assert_eq!(Some(&vec![cfg_open]), configs.get(&net_id_open)); |
| assert_eq!(Some(&vec![cfg_wep]), configs.get(&net_id_wep)); |
| assert_eq!(Some(&vec![cfg_wpa]), configs.get(&net_id_wpa)); |
| assert_eq!(Some(&vec![cfg_wpa2]), configs.get(&net_id_wpa2)); |
| assert_eq!(Some(&vec![cfg_wpa3]), configs.get(&net_id_wpa3)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn write_read_credentials() { |
| let stash = new_stash("write_read_credentials"); |
| |
| let net_id_none = network_id("bar-none", SecurityType::None); |
| let net_id_password = network_id("bar-password", SecurityType::Wpa2); |
| let net_id_psk = network_id("bar-psk", SecurityType::Wpa2); |
| |
| // create and write configs with each type credential |
| let password = Credential::Password(b"config-password".to_vec()); |
| let psk = Credential::Psk([65; 64].to_vec()); |
| |
| let cfg_none = new_config(net_id_none.clone(), Credential::None); |
| let cfg_password = new_config(net_id_password.clone(), password); |
| let cfg_psk = new_config(net_id_psk.clone(), psk); |
| |
| // write each config to stash, then check that we see them when we load |
| stash.write(&net_id_none, &vec![cfg_none.clone()]).expect("failed to write"); |
| stash.write(&net_id_password, &vec![cfg_password.clone()]).expect("failed to write"); |
| stash.write(&net_id_psk, &vec![cfg_psk.clone()]).expect("failed to write"); |
| |
| let configs = stash.load().await.expect("failed loading from stash"); |
| assert_eq!(Some(&vec![cfg_none]), configs.get(&net_id_none)); |
| assert_eq!(Some(&vec![cfg_password]), configs.get(&net_id_password)); |
| assert_eq!(Some(&vec![cfg_psk]), configs.get(&net_id_psk)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn write_persists() { |
| let stash_id = "write_persists"; |
| let stash = new_stash(stash_id); |
| let cfg_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2); |
| let cfg = NetworkConfig::new( |
| cfg_id.clone(), |
| Credential::Password(b"password".to_vec()), |
| true, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| |
| // Save a network config to the stash |
| stash.write(&cfg_id, &vec![cfg.clone()]).expect("Failed writing to stash"); |
| |
| //create the stash again with same id |
| let stash = Stash::new_with_id(stash_id).expect("Failed to create new stash"); |
| |
| // Expect to read the same value back with the same key, should exist in new stash |
| let cfgs_from_stash = stash.load().await.expect("Failed reading from stash"); |
| assert_eq!(1, cfgs_from_stash.len()); |
| assert_eq!(Some(&vec![cfg.clone()]), cfgs_from_stash.get(&cfg_id)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn load_stash() { |
| let store = new_stash("load_stash"); |
| let foo_net_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2); |
| let cfg_foo = NetworkConfig::new( |
| foo_net_id.clone(), |
| Credential::Password(b"12345678".to_vec()), |
| true, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| let bar_net_id = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2); |
| let cfg_bar = NetworkConfig::new( |
| bar_net_id.clone(), |
| Credential::Password(b"qwertyuiop".to_vec()), |
| true, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| |
| // Store two networks in our stash. |
| store.write(&foo_net_id, &vec![cfg_foo.clone()]).expect("Failed to save config to stash"); |
| store.write(&bar_net_id, &vec![cfg_bar.clone()]).expect("Failed to save config to stash"); |
| |
| // load should give us a hashmap with the two networks we saved |
| let mut expected_cfgs = HashMap::new(); |
| expected_cfgs.insert(foo_net_id.clone(), vec![cfg_foo]); |
| expected_cfgs.insert(bar_net_id.clone(), vec![cfg_bar]); |
| assert_eq!(expected_cfgs, store.load().await.expect("Failed to load configs from stash")); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn clear_stash() { |
| let stash_id = "clear_stash"; |
| let mut stash = new_stash(stash_id); |
| |
| // add some configs to the stash |
| let net_id_foo = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2); |
| let cfg_foo = NetworkConfig::new( |
| net_id_foo.clone(), |
| Credential::Password(b"qwertyuio".to_vec()), |
| true, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| let net_id_bar = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2); |
| let cfg_bar = NetworkConfig::new( |
| net_id_bar.clone(), |
| Credential::Password(b"12345678".to_vec()), |
| false, |
| false, |
| ) |
| .expect("Failed to create network config"); |
| stash.write(&net_id_foo, &vec![cfg_foo.clone()]).expect("Failed to write to stash"); |
| stash.write(&net_id_bar, &vec![cfg_bar.clone()]).expect("Failed to write to stash"); |
| |
| // verify that the configs are found in stash |
| let configs_from_stash = stash.load().await.expect("Failed to read"); |
| assert_eq!(2, configs_from_stash.len()); |
| assert_eq!(Some(&vec![cfg_foo.clone()]), configs_from_stash.get(&net_id_foo)); |
| assert_eq!(Some(&vec![cfg_bar.clone()]), configs_from_stash.get(&net_id_bar)); |
| |
| // clear the stash |
| stash.clear().expect("Failed to clear stash"); |
| // verify that the configs are no longer in the stash |
| let configs_from_stash = stash.load().await.expect("Failed to read"); |
| assert_eq!(0, configs_from_stash.len()); |
| |
| // recreate stash and verify that clearing the stash persists |
| let stash = Stash::new_with_id(stash_id).expect("Failed to create new stash"); |
| let configs_from_stash = stash.load().await.expect("Failed to read"); |
| assert_eq!(0, configs_from_stash.len()); |
| } |
| |
| // creates a new stash with the given ID and clears the values saved in the stash |
| fn new_stash(stash_id: &str) -> Stash { |
| let mut stash = Stash::new_with_id(stash_id).expect("Failed to create new stash"); |
| stash.root.delete(); |
| stash.root.commit().expect("Failed to commit clearing stash"); |
| stash |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_to_correct_stash_node() { |
| let stash = new_stash("write_to_correct_stash_node"); |
| |
| let net_id = network_id("foo", SecurityType::Wpa2); |
| let credential = Credential::Password(b"password".to_vec()); |
| let network_config = new_config(net_id.clone(), credential.clone()); |
| |
| // write to stash and check that the right thing is written under the right StashNode |
| stash.write(&net_id, &vec![network_config]).expect("failed to write to stash"); |
| let net_id_str = |
| Stash::serialize_key(&net_id).expect("failed to serialize network identifier"); |
| let expected_node = stash.root.child(&net_id_str).child(&format!("{}", 0)); |
| let fields = expected_node.fields().await.expect("failed to get fields"); |
| let data_actual = fields.get_str(&format!("{}", DATA)); |
| let data_expected = serde_json::to_string(&PersistentData::new(credential, false)) |
| .expect("failed to serialize data"); |
| assert_eq!(data_actual, Some(&data_expected)); |
| } |
| |
| fn network_id(ssid: impl Into<Vec<u8>>, security_type: SecurityType) -> NetworkIdentifier { |
| NetworkIdentifier::new(ssid.into(), security_type) |
| } |
| |
| fn new_config(network_id: NetworkIdentifier, credential: Credential) -> NetworkConfig { |
| NetworkConfig::new(network_id, credential, false, false).expect("failed to create config") |
| } |
| } |