blob: 984a77530563893130303fdae842426444ad942d [file] [log] [blame]
// 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::{stash_store::StashStore, storage_store::StorageStore, Store},
anyhow::{Context, Error},
fidl_fuchsia_stash as fidl_stash,
fuchsia_component::client::connect_to_protocol,
std::collections::HashMap,
wlan_stash_constants::POLICY_STASH_PREFIX,
};
pub use wlan_stash_constants::{
Credential, NetworkIdentifier, PersistentData, SecurityType, POLICY_STASH_ID,
};
/// Manages access to the persistent storage or saved network configs through Stash
pub struct PolicyStorage {
root: Box<dyn Store>,
}
impl PolicyStorage {
/// Initialize new store with the ID provided by the Saved Networks Manager. The ID will
/// identify stored values as being part of the same persistent storage. `proxy_fn` is used to
/// connect to stash if necessary. If `only_stash` is true, only use a store backed by Stash
/// rather than a file under '/data'.
pub async fn new_with_id_and_proxy(
id: &str,
proxy_fn: impl FnOnce() -> Result<fidl_stash::SecureStoreProxy, Error>,
only_stash: bool,
) -> Result<Self, Error> {
let root = if only_stash {
Box::new(StashStore::from_secure_store_proxy(id, proxy_fn()?)?) as Box<dyn Store>
} else {
let path = format!("/data/network-data.{id}");
match StorageStore::new(&path) {
Ok(store) => Box::new(store),
Err(error) => {
// Try and read from Stash
let mut stash_store = StashStore::from_secure_store_proxy(id, proxy_fn()?)?;
let store = StorageStore::empty(&path);
if let Ok(config) = stash_store.load().await {
for (id, data) in &config {
store.write(id, data).await?;
}
if let Err(error) = store.flush().await {
tracing::info!(?error, "Failed to write migrated saved networks");
} else {
// Delete from stash.
stash_store
.delete_store()
.await
.context("Failed to delete stash store after migration")?;
tracing::info!("Migrated saved networks from stash");
}
} else {
tracing::info!(
?error,
"Failed to read saved networks from {path:?}, will create new"
);
}
Box::new(store)
}
}
};
Ok(Self { root })
}
/// Like `new_with_id_and_proxy` but provides a default `proxy_fn`.
pub async fn new_with_id(id: &str) -> Result<Self, Error> {
Self::new_with_id_and_proxy(
id,
default_stash_proxy,
// TODO(https://fxbug.dev/42086019): For now, always use stash.
/* only_stash: */
true,
)
.await
}
/// Initialize new Stash with a provided proxy in order to mock stash in unit tests.
pub fn new_with_stash(proxy: fidl_stash::StoreAccessorProxy) -> Self {
Self { root: Box::new(StashStore::new(proxy, POLICY_STASH_PREFIX)) }
}
/// Update the network configs of a given network identifier to persistent storage, deleting
/// the key entirely if the new list of configs is empty.
pub async fn write(
&self,
id: &NetworkIdentifier,
network_configs: &[PersistentData],
) -> Result<(), Error> {
self.root.write(id, network_configs).await?;
self.root.flush().await
}
/// Load all saved network configs from stash. Will create HashMap of network configs by SSID
/// as saved in the stash. If something in stash can't be interpreted, we ignore it.
pub async fn load(&self) -> Result<HashMap<NetworkIdentifier, Vec<PersistentData>>, Error> {
self.root.load().await
}
/// 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 async fn clear(&mut self) -> Result<(), Error> {
self.root.delete_store().await
}
}
fn default_stash_proxy() -> Result<fidl_stash::SecureStoreProxy, Error> {
connect_to_protocol::<fidl_stash::SecureStoreMarker>().map_err(|e| e.into())
}
#[cfg(test)]
mod tests {
#![allow(unused_variables)]
#![allow(unused_imports)]
use {
super::*,
crate::tests::{network_id, new_stash_id},
fidl::endpoints::{create_proxy, create_request_stream},
fidl_fuchsia_stash::{SecureStoreRequest, StoreAccessorRequest},
fuchsia_async as fasync,
futures::StreamExt,
ieee80211::Ssid,
rand::{
distributions::{Alphanumeric, DistString as _},
thread_rng,
},
std::{
convert::TryFrom,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
},
};
/// The PSK provided must be the bytes form of the 64 hexadecimal character hash. This is a
/// duplicate of a definition in wlan/wlancfg/src, since I don't think there's a good way to
/// import just that constant.
pub const PSK_BYTE_LEN: usize = 32;
#[fuchsia::test]
async fn write_and_read() {
let stash = new_stash(&new_stash_id()).await;
let cfg_id = NetworkIdentifier {
ssid: Ssid::try_from("foo").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg = PersistentData {
credential: Credential::Password(b"password".to_vec()),
has_ever_connected: true,
};
// Save a network config to the stash
stash.write(&cfg_id, &vec![cfg.clone()]).await.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 = PersistentData {
credential: Credential::Password(b"other-password".to_vec()),
has_ever_connected: false,
};
stash
.write(&cfg_id, &vec![cfg.clone(), cfg_2.clone()])
.await
.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::test]
async fn write_read_security_types() {
let stash = new_stash(&new_stash_id()).await;
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 = PersistentData { credential: Credential::None, has_ever_connected: false };
let cfg_wep = PersistentData { credential: password.clone(), has_ever_connected: false };
let cfg_wpa = PersistentData { credential: password.clone(), has_ever_connected: false };
let cfg_wpa2 = PersistentData { credential: password.clone(), has_ever_connected: false };
let cfg_wpa3 = PersistentData { credential: password.clone(), has_ever_connected: false };
stash.write(&net_id_open, &vec![cfg_open.clone()]).await.expect("failed to write config");
stash.write(&net_id_wep, &vec![cfg_wep.clone()]).await.expect("failed to write config");
stash.write(&net_id_wpa, &vec![cfg_wpa.clone()]).await.expect("failed to write config");
stash.write(&net_id_wpa2, &vec![cfg_wpa2.clone()]).await.expect("failed to write config");
stash.write(&net_id_wpa3, &vec![cfg_wpa3.clone()]).await.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::test]
async fn write_read_credentials() {
let stash = new_stash(&new_stash_id()).await;
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; PSK_BYTE_LEN].to_vec());
let cfg_none = PersistentData { credential: Credential::None, has_ever_connected: false };
let cfg_password = PersistentData { credential: password, has_ever_connected: false };
let cfg_psk = PersistentData { credential: psk, has_ever_connected: false };
// write each config to stash, then check that we see them when we load
stash.write(&net_id_none, &vec![cfg_none.clone()]).await.expect("failed to write");
stash.write(&net_id_password, &vec![cfg_password.clone()]).await.expect("failed to write");
stash.write(&net_id_psk, &vec![cfg_psk.clone()]).await.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::test]
async fn write_persists() {
let stash_id = &new_stash_id();
let stash = new_stash(&stash_id).await;
let cfg_id = NetworkIdentifier {
ssid: Ssid::try_from("foo").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg = PersistentData {
credential: Credential::Password(b"password".to_vec()),
has_ever_connected: true,
};
// Save a network config to the stash
stash.write(&cfg_id, &vec![cfg.clone()]).await.expect("Failed writing to stash");
// Create the stash again with same id
let stash = PolicyStorage::new_with_id(stash_id).await.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::test]
async fn load_stash() {
let store = new_stash(&new_stash_id()).await;
let foo_net_id = NetworkIdentifier {
ssid: Ssid::try_from("foo").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg_foo = PersistentData {
credential: Credential::Password(b"12345678".to_vec()),
has_ever_connected: true,
};
let bar_net_id = NetworkIdentifier {
ssid: Ssid::try_from("bar").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg_bar = PersistentData {
credential: Credential::Password(b"qwertyuiop".to_vec()),
has_ever_connected: true,
};
// Store two networks in our stash.
store
.write(&foo_net_id, &vec![cfg_foo.clone()])
.await
.expect("Failed to save config to stash");
store
.write(&bar_net_id, &vec![cfg_bar.clone()])
.await
.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::test]
async fn load_stash_does_not_load_empty_list() {
let stash_id = &new_stash_id();
let stash = new_stash(&stash_id).await;
// add a network identifier with no configs
let net_id = NetworkIdentifier {
ssid: Ssid::try_from(rand_string()).unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
stash.write(&net_id, &vec![]).await.expect("failed to write value");
// recreate the stash to load it
let stash = new_stash(&stash_id).await;
let loaded_configs = stash.load().await.expect("failed to load stash");
assert!(loaded_configs.is_empty());
}
#[fuchsia::test]
async fn clear_stash() {
let stash_id = &new_stash_id();
let mut stash = new_stash(&stash_id).await;
// add some configs to the stash
let net_id_foo = NetworkIdentifier {
ssid: Ssid::try_from("foo").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg_foo = PersistentData {
credential: Credential::Password(b"qwertyuio".to_vec()),
has_ever_connected: true,
};
let net_id_bar = NetworkIdentifier {
ssid: Ssid::try_from("bar").unwrap().to_vec(),
security_type: SecurityType::Wpa2,
};
let cfg_bar = PersistentData {
credential: Credential::Password(b"12345678".to_vec()),
has_ever_connected: false,
};
stash.write(&net_id_foo, &vec![cfg_foo.clone()]).await.expect("Failed to write to stash");
stash.write(&net_id_bar, &vec![cfg_bar.clone()]).await.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().await.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 = PolicyStorage::new_with_id(stash_id).await.expect("Failed to create new stash");
let configs_from_stash = stash.load().await.expect("Failed to read");
assert_eq!(0, configs_from_stash.len());
}
#[fuchsia::test]
async fn test_migration() {
let stash_id = new_stash_id();
let store_client = connect_to_protocol::<fidl_stash::SecureStoreMarker>()
.expect("failed to connect to store");
store_client.identify(&stash_id).expect("failed to identify client to store");
let (store_proxy, accessor_server) =
create_proxy().expect("failed to create accessor proxy");
store_client.create_accessor(false, accessor_server).expect("failed to create accessor");
let network_id = network_id("foo", SecurityType::Wpa2);
let network_config = vec![PersistentData {
credential: Credential::Password(b"password".to_vec()),
has_ever_connected: false,
}];
{
let stash = PolicyStorage::new_with_stash(store_proxy);
stash.write(&network_id, &network_config).await.expect("write failed");
}
let expected = HashMap::from([(network_id, network_config)]);
// Migrate the config.
{
let stash = PolicyStorage::new_with_id_and_proxy(
&stash_id,
default_stash_proxy,
/* only_stash: */ false,
)
.await
.expect("new_with_id failed");
assert_eq!(&stash.load().await.expect("load failed"), &expected);
}
// The config should have been deleted from Stash.
{
let (store_proxy, accessor_server) =
create_proxy().expect("failed to create accessor proxy");
store_client
.create_accessor(false, accessor_server)
.expect("failed to create accessor");
let stash = PolicyStorage::new_with_stash(store_proxy);
assert_eq!(stash.load().await.expect("load failed"), HashMap::new());
}
// And once more, but this time there should be no migration.
{
let stash = PolicyStorage::new_with_id_and_proxy(
&stash_id,
default_stash_proxy,
/* only_stash: */ false,
)
.await
.expect("new_with_id failed");
assert_eq!(&stash.load().await.expect("load failed"), &expected);
}
}
#[fuchsia::test]
async fn test_migration_with_bad_stash() {
let stash_id = new_stash_id();
let (client, mut request_stream) = create_request_stream::<fidl_stash::SecureStoreMarker>()
.expect("create_request_stream failed");
let read_from_stash = Arc::new(AtomicBool::new(false));
let _task = {
let read_from_stash = read_from_stash.clone();
fasync::Task::spawn(async move {
while let Some(request) = request_stream.next().await {
match request.unwrap() {
SecureStoreRequest::Identify { .. } => {}
SecureStoreRequest::CreateAccessor { accessor_request, .. } => {
let read_from_stash = read_from_stash.clone();
fuchsia_async::Task::spawn(async move {
let mut request_stream = accessor_request.into_stream().unwrap();
while let Some(request) = request_stream.next().await {
match request.unwrap() {
StoreAccessorRequest::ListPrefix { .. } => {
read_from_stash.store(true, Ordering::Relaxed);
// If we just drop the iterator, it should trigger a
// read error.
}
_ => unreachable!(),
}
}
})
.detach();
}
}
}
})
};
// Try and load the config. It should provide empty config.
let stash = PolicyStorage::new_with_id_and_proxy(
&stash_id,
|| Ok(client.into_proxy().unwrap()),
/* only_stash: */ false,
)
.await
.expect("new_with_id_and_proxy failed");
assert_eq!(&stash.load().await.expect("load failed"), &HashMap::new());
// Make sure there was an attempt to actually read from stash.
assert!(read_from_stash.load(Ordering::Relaxed));
}
/// Creates a new stash with the given ID and clears the values saved in the stash.
pub async fn new_stash(stash_id: &str) -> PolicyStorage {
let mut stash =
PolicyStorage::new_with_id(stash_id).await.expect("Failed to create new stash");
stash.clear().await.expect("failed to clear stash");
stash
}
fn rand_string() -> String {
Alphanumeric.sample_string(&mut thread_rng(), 20)
}
}