wlan_storage/
policy.rs

1// Copyright 2020 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::stash_store::StashStore;
6use crate::storage_store::StorageStore;
7use anyhow::{format_err, Context, Error};
8use fidl_fuchsia_stash as fidl_stash;
9use fuchsia_component::client::connect_to_protocol;
10use wlan_metrics_registry::StashMigrationResultsMetricDimensionMigrationResult as MigrationResult;
11
12pub use wlan_storage_constants::{
13    self, Credential, NetworkIdentifier, PersistentData, PersistentStorageData, SecurityType,
14    POLICY_STORAGE_ID,
15};
16
17/// If the store ID is saved_networks, the file will be saved at /data/network-data.saved_networks
18const FILE_PATH_FORMAT: &str = "/data/network-data.";
19
20/// Manages access to the persistent storage. This layer on top of storage just migrates the legacy
21/// persisted data if it hasn't been migrated yet.
22pub struct PolicyStorage {
23    /// This is the actual store.
24    root: StorageStore,
25    /// This is used to get legacy data, and should only ever be used once, when migrating the data
26    /// the first time. The result is carried here because we only care about any errors if we end
27    /// up migrating the data.
28    legacy_stash: Result<StashStore, Error>,
29    cobalt_proxy: Option<fidl_fuchsia_metrics::MetricEventLoggerProxy>,
30}
31
32impl PolicyStorage {
33    /// Initialize new store with the ID provided by the Saved Networks Manager. The ID will
34    /// identify stored values as being part of the same persistent storage.
35    pub async fn new_with_id(id: &str) -> Self {
36        let path = format!("{FILE_PATH_FORMAT}{id}");
37        let root = StorageStore::new(&path);
38
39        let proxy = connect_to_protocol::<fidl_stash::SecureStoreMarker>();
40        let legacy_stash =
41            proxy.and_then(|p| StashStore::from_secure_store_proxy(id, p)).map_err(|e| e);
42
43        let cobalt_proxy = init_telemetry_channel()
44            .await
45            .inspect_err(|e| {
46                log::info!(
47                    "Error accessing telemetry. Stash migration metric will not be logged: {}",
48                    e
49                );
50            })
51            .ok();
52
53        Self { root, legacy_stash, cobalt_proxy }
54    }
55
56    /// Initializer for tests outside of this module
57    pub fn new_with_stash_proxy_and_id(
58        stash_proxy: fidl_stash::SecureStoreProxy,
59        id: &str,
60    ) -> Self {
61        let root = StorageStore::new(id);
62        let legacy_stash = StashStore::from_secure_store_proxy(id, stash_proxy);
63        let cobalt_proxy = None;
64        Self { root, legacy_stash, cobalt_proxy }
65    }
66
67    /// Initialize the storage wrapper and load all saved network configs from persistent storage.
68    /// If there is an error loading from local storage, that may mean stash data hasn't been
69    /// migrated yet. If so, the function will try to load from legacy stash data.
70    pub async fn load(&mut self) -> Result<Vec<PersistentStorageData>, Error> {
71        // If there is an error loading from the new version of storage, it means it hasn't been
72        // create and should be loaded from stash.
73        let load_err = match self.root.load() {
74            Ok(networks) => {
75                self.log_load_metric(MigrationResult::AlreadyMigrated).await;
76                return Ok(networks);
77            }
78            Err(e) => e,
79        };
80        let stash_store: &mut StashStore = if let Ok(stash) = self.legacy_stash.as_mut() {
81            stash
82        } else {
83            return Err(format_err!("error accessing stash"));
84        };
85        // Try and read from Stash since store doesn't exist yet
86        if let Ok(config) = stash_store.load().await {
87            // Read the stash data and convert it to a flattened list of config data.
88            let mut networks_list = Vec::new();
89            for (id, legacy_configs) in config.into_iter() {
90                let mut new_configs = legacy_configs
91                    .into_iter()
92                    .map(|c| PersistentStorageData::new_from_legacy_data(id.clone(), c));
93                networks_list.extend(&mut new_configs);
94            }
95
96            // Write the data to the new storage.
97            match self.root.write(networks_list.clone()) {
98                Ok(_) => {
99                    log::info!("Migrated saved networks from stash");
100                    // Delete from stash if writing was successful.
101                    let delete_result = stash_store.delete_store().await;
102                    match delete_result {
103                        Ok(()) => {
104                            self.log_load_metric(MigrationResult::Success).await;
105                        }
106                        Err(e) => {
107                            log::info!(
108                                "Failed to delete legacy stash data after migration: {:?}",
109                                e
110                            );
111                            self.log_load_metric(MigrationResult::MigratedButFailedToDeleteLegacy)
112                                .await;
113                        }
114                    }
115                }
116                Err(e) => {
117                    log::info!(e:?; "Failed to write migrated saved networks");
118                    self.log_load_metric(MigrationResult::FailedToWriteNewStore).await;
119                }
120            }
121            Ok(networks_list)
122        } else {
123            // The backing file is only actually created when a write happens, but we
124            // don't want to intentionally create a file if migrating stash fails since
125            // then we will never try to read from stash again.
126            log::info!(load_err:?; "Failed to read saved networks from file and legacy stash, new file will be created when a network is saved",);
127            self.log_load_metric(MigrationResult::FailedToLoadLegacyData).await;
128            Ok(Vec::new())
129        }
130    }
131
132    /// Update the network configs of a given network identifier to persistent storage, deleting
133    /// the key entirely if the new list of configs is empty.
134    pub fn write(&self, network_configs: Vec<PersistentStorageData>) -> Result<(), Error> {
135        self.root.write(network_configs)
136    }
137
138    /// Remove all saved values from the stash. It will delete everything under the root node,
139    /// and anything else in the same stash but not under the root node would be ignored.
140    pub fn clear(&mut self) -> Result<(), Error> {
141        self.root.empty_store()
142    }
143
144    async fn log_load_metric(&self, result_event_code: MigrationResult) {
145        // No need to log an error if the cobalt proxy is none, errors would have been logged on
146        // failed initialization of the channel.
147        let cobalt_proxy = match &self.cobalt_proxy {
148            Some(proxy) => proxy,
149            None => return,
150        };
151
152        let events = &[fidl_fuchsia_metrics::MetricEvent {
153            metric_id: wlan_metrics_registry::STASH_MIGRATION_RESULTS_METRIC_ID,
154            event_codes: vec![result_event_code as u32],
155            payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
156        }];
157
158        // The error type of this inner result is a fidl_fuchsia_metrics defined error.
159        match cobalt_proxy.log_metric_events(events).await {
160            Err(e) => {
161                log::info!(
162                    "Error logging metric {:?} for migration result: {:?}",
163                    result_event_code,
164                    e
165                );
166            }
167            Ok(Err(e)) => {
168                log::info!(
169                    "Error sending metric {:?} for migration result: {:?}",
170                    result_event_code,
171                    e
172                );
173            }
174            Ok(_) => (),
175        }
176    }
177}
178
179async fn init_telemetry_channel() -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, Error> {
180    // Get channel for logging cobalt 1.1 metrics.
181    let factory_proxy = fuchsia_component::client::connect_to_protocol::<
182        fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
183    >()?;
184
185    let (cobalt_proxy, cobalt_1dot1_server) =
186        fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
187
188    let project_spec = fidl_fuchsia_metrics::ProjectSpec {
189        customer_id: None, // defaults to fuchsia
190        project_id: Some(wlan_metrics_registry::PROJECT_ID),
191        ..Default::default()
192    };
193
194    factory_proxy
195        .create_metric_event_logger(&project_spec, cobalt_1dot1_server)
196        .await
197        .context("failed to create metrics event logger")?
198        .map_err(|e| format_err!("failed to create metrics event logger: {:?}", e))?;
199
200    Ok(cobalt_proxy)
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::tests::{network_id, rand_string};
207    use assert_matches::assert_matches;
208    use fidl::endpoints::create_request_stream;
209    use fidl_fuchsia_stash::{SecureStoreRequest, StoreAccessorRequest};
210    use fuchsia_async as fasync;
211    use futures::task::Poll;
212    use futures::{StreamExt, TryStreamExt};
213    use ieee80211::Ssid;
214    use std::convert::TryFrom;
215    use std::io::Write;
216    use std::sync::atomic::{AtomicBool, Ordering};
217    use std::sync::Arc;
218    use wlan_storage_constants::PersistentData;
219
220    /// The PSK provided must be the bytes form of the 64 hexadecimal character hash. This is a
221    /// duplicate of a definition in wlan/wlancfg/src, since I don't think there's a good way to
222    /// import just that constant.
223    pub const PSK_BYTE_LEN: usize = 32;
224
225    #[fuchsia::test]
226    async fn write_and_read() {
227        let mut store = new_storage(&rand_string()).await;
228        let cfg = PersistentStorageData {
229            ssid: Ssid::try_from("foo").unwrap().to_vec(),
230            security_type: SecurityType::Wpa2,
231            credential: Credential::Password(b"password".to_vec()),
232            has_ever_connected: true,
233        };
234
235        // Save a network config to storage
236        store.write(vec![cfg.clone()]).expect("Failed writing to storage");
237
238        // Expect to read the same value back with the same key
239        let cfgs_from_store = store.load().await.expect("Failed reading from storage");
240        assert_eq!(1, cfgs_from_store.len());
241        assert_eq!(vec![cfg.clone()], cfgs_from_store);
242
243        // Overwrite the list of configs saved in storage
244        let cfg_2 = PersistentStorageData {
245            ssid: Ssid::try_from("foo").unwrap().to_vec(),
246            security_type: SecurityType::Wpa2,
247            credential: Credential::Password(b"other-password".to_vec()),
248            has_ever_connected: false,
249        };
250        store.write(vec![cfg.clone(), cfg_2.clone()]).expect("Failed writing to storage");
251
252        // Expect to read the saved value back
253        let cfgs_from_store = store.load().await.expect("Failed reading from stash");
254        assert_eq!(2, cfgs_from_store.len());
255        assert!(cfgs_from_store.contains(&cfg));
256        assert!(cfgs_from_store.contains(&cfg_2));
257    }
258
259    #[fuchsia::test]
260    async fn write_read_security_types() {
261        let mut store = new_storage(&rand_string()).await;
262        let password = Credential::Password(b"config-password".to_vec());
263
264        // create and write configs with each security type
265        let cfg_open = PersistentStorageData {
266            ssid: Ssid::try_from("foo").unwrap().to_vec(),
267            security_type: SecurityType::None,
268            credential: Credential::None,
269            has_ever_connected: false,
270        };
271        let cfg_wep = PersistentStorageData {
272            ssid: Ssid::try_from("foo").unwrap().to_vec(),
273            security_type: SecurityType::Wep,
274            credential: password.clone(),
275            has_ever_connected: false,
276        };
277        let cfg_wpa = PersistentStorageData {
278            ssid: Ssid::try_from("foo").unwrap().to_vec(),
279            security_type: SecurityType::Wpa,
280            credential: password.clone(),
281            has_ever_connected: false,
282        };
283        let cfg_wpa2 = PersistentStorageData {
284            ssid: Ssid::try_from("foo").unwrap().to_vec(),
285            security_type: SecurityType::Wpa2,
286            credential: password.clone(),
287            has_ever_connected: false,
288        };
289        let cfg_wpa3 = PersistentStorageData {
290            ssid: Ssid::try_from("foo").unwrap().to_vec(),
291            security_type: SecurityType::Wpa3,
292            credential: password.clone(),
293            has_ever_connected: false,
294        };
295
296        // Write the saved network list as it would be each time a new network was added.
297        let mut saved_networks = vec![cfg_open.clone()];
298        store.write(saved_networks.clone()).expect("failed to write config");
299        saved_networks.push(cfg_wep.clone());
300        store.write(saved_networks.clone()).expect("failed to write config");
301        saved_networks.push(cfg_wpa.clone());
302        store.write(saved_networks.clone()).expect("failed to write config");
303        saved_networks.push(cfg_wpa2.clone());
304        store.write(saved_networks.clone()).expect("failed to write config");
305        saved_networks.push(cfg_wpa3.clone());
306        store.write(saved_networks.clone()).expect("failed to write config");
307
308        // Load storage and expect each config that we wrote.
309        let configs = store.load().await.expect("failed loading from storage");
310        assert_eq!(configs.len(), 5);
311        assert!(configs.contains(&cfg_open));
312        assert!(configs.contains(&cfg_wep));
313        assert!(configs.contains(&cfg_wpa));
314        assert!(configs.contains(&cfg_wpa2));
315        assert!(configs.contains(&cfg_wpa3));
316    }
317
318    #[fuchsia::test]
319    async fn write_read_credentials() {
320        let mut store = new_storage(&rand_string()).await;
321
322        // Create and write configs with each type credential.
323        let password = Credential::Password(b"config-password".to_vec());
324        let psk = Credential::Psk([65; PSK_BYTE_LEN].to_vec());
325
326        let cfg_none = PersistentStorageData {
327            ssid: b"bar-none".to_vec(),
328            security_type: SecurityType::None,
329            credential: Credential::None,
330            has_ever_connected: false,
331        };
332        let cfg_password = PersistentStorageData {
333            ssid: b"bar-password".to_vec(),
334            security_type: SecurityType::Wpa2,
335            credential: password,
336            has_ever_connected: false,
337        };
338        let cfg_psk = PersistentStorageData {
339            ssid: b"bar-psk".to_vec(),
340            security_type: SecurityType::Wpa2,
341            credential: psk,
342            has_ever_connected: false,
343        };
344
345        // Write configs to storage as they would be when saved.
346        let mut saved_networks = vec![cfg_none.clone()];
347        store.write(saved_networks.clone()).expect("failed to write");
348        saved_networks.push(cfg_password.clone());
349        store.write(saved_networks.clone()).expect("failed to write");
350        saved_networks.push(cfg_psk.clone());
351        store.write(saved_networks.clone()).expect("failed to write");
352
353        // Check that the configs are loaded correctly.
354        let configs = store.load().await.expect("failed loading from storage");
355        assert_eq!(3, configs.len());
356        assert!(configs.contains(&cfg_none));
357        assert!(configs.contains(&cfg_password));
358        assert!(configs.contains(&cfg_psk));
359    }
360
361    #[fuchsia::test]
362    async fn write_persists() {
363        let path = rand_string();
364        let store = new_storage(&path).await;
365        let cfg = PersistentStorageData {
366            ssid: Ssid::try_from("foo").unwrap().to_vec(),
367            security_type: SecurityType::Wpa2,
368            credential: Credential::Password(b"password".to_vec()),
369            has_ever_connected: true,
370        };
371
372        // Save a network config to the stash
373        store.write(vec![cfg.clone()]).expect("Failed writing to storage");
374
375        // Create the storage again with same id
376        let mut store = PolicyStorage::new_with_id(&path).await;
377
378        // Expect to read the same value back with the same key, should exist in new stash
379        let cfgs_from_store = store.load().await.expect("Failed reading from storage");
380        assert_eq!(1, cfgs_from_store.len());
381        assert!(cfgs_from_store.contains(&cfg));
382    }
383
384    #[fuchsia::test]
385    async fn load_storage() {
386        let mut store = new_storage(&rand_string()).await;
387        let cfg_foo = PersistentStorageData {
388            ssid: Ssid::try_from("foo").unwrap().to_vec(),
389            security_type: SecurityType::Wpa2,
390            credential: Credential::Password(b"12345678".to_vec()),
391            has_ever_connected: true,
392        };
393        let cfg_bar = PersistentStorageData {
394            ssid: Ssid::try_from("bar").unwrap().to_vec(),
395            security_type: SecurityType::Wpa2,
396            credential: Credential::Password(b"qwertyuiop".to_vec()),
397            has_ever_connected: true,
398        };
399
400        // Store two networks in our stash.
401        store
402            .write(vec![cfg_foo.clone(), cfg_bar.clone()])
403            .expect("Failed to save configs to stash");
404
405        // load should give us the two networks we saved
406        let expected_cfgs = vec![cfg_foo, cfg_bar];
407        assert_eq!(expected_cfgs, store.load().await.expect("Failed to load configs from stash"));
408    }
409
410    #[fuchsia::test]
411    async fn load_empty_storage_does_loads_empty_list() {
412        let store_id = &rand_string();
413        let mut store = new_storage(&store_id).await;
414
415        // write to storage an empty saved networks list
416        store.write(vec![]).expect("failed to write value");
417
418        // recreate the storage to load it
419        let loaded_configs = store.load().await.expect("failed to load store");
420        assert!(loaded_configs.is_empty());
421    }
422
423    #[fuchsia::test]
424    pub async fn load_no_file_creates_file() {
425        // Test what would happen if policy persistent storage is loaded twice - the first attempt
426        // should initialize the backing file. The second attempt should not attempt to load from
427        // the legacy stash.
428        let store_id = &rand_string();
429        let backing_file_path = format!("{}{}", FILE_PATH_FORMAT, store_id).to_string();
430        let mut store = PolicyStorage::new_with_id(store_id).await;
431
432        // The file should not exist yet, so reading it would give an error.
433        std::fs::read(&backing_file_path).expect_err("The file for the store should not exist yet");
434
435        // Load the store.
436        let loaded_configs = store.load().await.expect("failed to load store");
437        assert_eq!(loaded_configs, vec![]);
438
439        // Check that the file is created. It should have some JSON structure even though there
440        // are no saved networks.
441        let file_contents = std::fs::read(&backing_file_path).expect(
442            "Failed to read file that should have been created when loading non-existant file",
443        );
444        assert!(!file_contents.is_empty());
445
446        // Load the store again, but with some values in the legacy stash which should be ignored.
447        let cfg_id = NetworkIdentifier {
448            ssid: Ssid::try_from(rand_string().as_str()).unwrap().to_vec(),
449            security_type: SecurityType::Wpa2,
450        };
451        let cfg = PersistentData {
452            credential: Credential::Password(rand_string().as_bytes().to_vec()),
453            has_ever_connected: true,
454        };
455
456        match store.legacy_stash.as_mut() {
457            Ok(stash) => {
458                stash.write(&cfg_id, &[cfg]).await.expect("Failed writing to legacy stash");
459                stash.flush().await.expect("Failed to flush legacy stash");
460            }
461            Err(e) => {
462                panic!("error initializing legacy stash: {}", e);
463            }
464        }
465        let loaded_configs = store.load().await.expect("failed to load store");
466        assert!(loaded_configs.is_empty());
467
468        // The file should still exist.
469        let file_contents = std::fs::read(&backing_file_path).expect(
470            "Failed to read file that should have been created when loading non-existant file",
471        );
472        assert!(!file_contents.is_empty());
473    }
474
475    #[fuchsia::test]
476    async fn clear_storage() {
477        let storage_id = &rand_string();
478        let mut storage = new_storage(&storage_id).await;
479
480        // add some configs to the storage
481        let cfg_foo = PersistentStorageData {
482            ssid: Ssid::try_from("foo").unwrap().to_vec(),
483            security_type: SecurityType::Wpa2,
484            credential: Credential::Password(b"qwertyuio".to_vec()),
485            has_ever_connected: true,
486        };
487        let cfg_bar = PersistentStorageData {
488            ssid: Ssid::try_from("bar").unwrap().to_vec(),
489            security_type: SecurityType::Wpa2,
490            credential: Credential::Password(b"12345678".to_vec()),
491            has_ever_connected: false,
492        };
493        storage.write(vec![cfg_foo.clone(), cfg_bar.clone()]).expect("Failed to write to storage");
494
495        // verify that the configs are found in storage
496        let configs_from_storage = storage.load().await.expect("Failed to read");
497        assert_eq!(2, configs_from_storage.len());
498        assert!(configs_from_storage.contains(&cfg_foo));
499        assert!(configs_from_storage.contains(&cfg_bar));
500
501        // clear the storage
502        storage.clear().expect("Failed to clear storage");
503        // verify that the configs are no longer in the storage
504        let configs_from_storage = storage.load().await.expect("Failed to read");
505        assert_eq!(0, configs_from_storage.len());
506
507        // recreate storage and verify that clearing the storage persists
508        let mut storage = PolicyStorage::new_with_id(storage_id).await;
509        let configs_from_storage = storage.load().await.expect("Failed to read");
510        assert_eq!(0, configs_from_storage.len());
511    }
512
513    #[fuchsia::test]
514    async fn test_migration() {
515        let storage_id = rand_string();
516        let stash_client = connect_to_protocol::<fidl_stash::SecureStoreMarker>()
517            .expect("failed to connect to store");
518        let ssid = "foo";
519        let security_type = SecurityType::Wpa2;
520        let credential = Credential::Password(b"password".to_vec());
521        let has_ever_connected = false;
522        // This is the version used by the previous storage mechanism.
523        let network_id = network_id(ssid, security_type);
524        let previous_data = PersistentData { credential: credential.clone(), has_ever_connected };
525
526        // This is the version used by the new storage mechanism.
527        let network_config = vec![PersistentStorageData {
528            ssid: ssid.into(),
529            security_type: security_type,
530            credential: credential.clone(),
531            has_ever_connected,
532        }];
533
534        // Write the config to stash that storage will migrate from.
535        let stash = StashStore::from_secure_store_proxy(&storage_id, stash_client.clone())
536            .expect("failed to get stash proxy");
537        stash.write(&network_id, &vec![previous_data]).await.expect("write failed");
538
539        // Initialize storage, and give it the stash with the saved network data.
540        let mut storage = PolicyStorage::new_with_id(&storage_id).await;
541        storage.legacy_stash = Ok(stash);
542        assert_eq!(storage.load().await.expect("load failed"), network_config);
543
544        // The config should have been deleted from stash.
545        // The stash connection can't be reused, or the stash store will fail to access stash.
546        let stash_client = connect_to_protocol::<fidl_stash::SecureStoreMarker>()
547            .expect("failed to connect to store");
548        let stash = StashStore::from_secure_store_proxy(&storage_id, stash_client)
549            .expect("failed to get stash proxy");
550        assert!(stash.load().await.expect("load failed").is_empty());
551
552        // And once more, but this time there should be no migration.
553        let mut storage = PolicyStorage::new_with_id(&storage_id).await;
554        assert_eq!(storage.load().await.expect("load failed"), network_config);
555    }
556
557    #[fuchsia::test]
558    async fn test_migration_with_bad_stash() {
559        let store_id = rand_string();
560
561        let (client, mut request_stream) = create_request_stream::<fidl_stash::SecureStoreMarker>();
562
563        // This will be set to true if stash is accessed, so that the test can check whether stash
564        // was read by the migration code.
565        let read_from_stash = Arc::new(AtomicBool::new(false));
566
567        // This responds in the background to any stash requests for initializing the connection to
568        // stash (identify or create accessor), and responds to requests for reading data with an
569        // an error by dropping the responder.
570        let _task = {
571            let read_from_stash = read_from_stash.clone();
572            fasync::Task::spawn(async move {
573                while let Some(request) = request_stream.next().await {
574                    match request.unwrap() {
575                        SecureStoreRequest::Identify { .. } => {}
576                        SecureStoreRequest::CreateAccessor { accessor_request, .. } => {
577                            let read_from_stash = read_from_stash.clone();
578                            fuchsia_async::Task::spawn(async move {
579                                let mut request_stream = accessor_request.into_stream();
580                                while let Some(request) = request_stream.next().await {
581                                    match request.unwrap() {
582                                        StoreAccessorRequest::ListPrefix { .. } => {
583                                            read_from_stash.store(true, Ordering::Relaxed);
584                                            // If we just drop the iterator, it should trigger a
585                                            // read error.
586                                        }
587                                        _ => unreachable!(),
588                                    }
589                                }
590                            })
591                            .detach();
592                        }
593                    }
594                }
595            })
596        };
597
598        // Initialize the store but switch out with the stash we made to act corrupted.
599        let mut store = PolicyStorage::new_with_id(&store_id).await;
600        let proxy_fn = client.into_proxy();
601        store.legacy_stash = StashStore::from_secure_store_proxy(&store_id, proxy_fn);
602
603        // Try and load the config. It should provide empty config.
604        assert!(&store.load().await.expect("load failed").is_empty());
605
606        // Make sure there was an attempt to actually read from stash.
607        assert!(read_from_stash.load(Ordering::Relaxed));
608    }
609
610    /// Creates a new persistent storage with a file bath based on the given ID and clears any
611    /// values saved in the store.
612    pub async fn new_storage(id: &str) -> PolicyStorage {
613        let mut store = PolicyStorage::new_with_id(id).await;
614        store.clear().expect("failed to clear stash");
615        store
616    }
617
618    /// Metrics tests need to be able to control stash behavior and check what is sent to cobalt.
619    struct MetricsTestValues {
620        store: PolicyStorage,
621        cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
622        stash_stream: fidl_stash::SecureStoreRequestStream,
623    }
624
625    /// This initializes PolicyStorage with a cobalt channel and stash channel that is
626    /// controlled by the test. It is for tests that want to control stash responses and read
627    /// messages send to cobalt.
628    fn migration_metrics_test_values() -> MetricsTestValues {
629        let (cobalt_proxy, cobalt_stream) = fidl::endpoints::create_proxy_and_stream::<
630            fidl_fuchsia_metrics::MetricEventLoggerMarker,
631        >();
632
633        let (legacy_stash, stash_stream) = stash_for_test();
634        let root = StorageStore::new(format!("{FILE_PATH_FORMAT}{}", rand_string()));
635        let store = PolicyStorage { root, legacy_stash, cobalt_proxy: Some(cobalt_proxy) };
636
637        MetricsTestValues { store, cobalt_stream, stash_stream }
638    }
639
640    fn stash_for_test() -> (Result<StashStore, Error>, fidl_stash::SecureStoreRequestStream) {
641        let (client, stash_stream) = create_request_stream::<fidl_stash::SecureStoreMarker>();
642        let proxy_fn = client.into_proxy();
643        let id = rand_string();
644        let legacy_stash = StashStore::from_secure_store_proxy(&id, proxy_fn);
645
646        (legacy_stash, stash_stream)
647    }
648
649    // Checks that the metric event is correct and acks the metric event so that the load fut
650    // can continue.
651    fn check_load_metric(
652        logged_metric: fidl_fuchsia_metrics::MetricEventLoggerRequest,
653        expected_event: MigrationResult,
654    ) {
655        assert_matches!(logged_metric, fidl_fuchsia_metrics::MetricEventLoggerRequest::LogMetricEvents {
656            mut events, responder, ..
657        } => {
658            assert_eq!(events.len(), 1);
659            let event = events.pop().unwrap();
660            assert_matches!(event, fidl_fuchsia_metrics::MetricEvent { metric_id, event_codes, payload: _payload } => {
661                assert_eq!(metric_id, wlan_metrics_registry::STASH_MIGRATION_RESULTS_METRIC_ID);
662                assert_eq!(event_codes, [expected_event as u32]);
663            });
664
665            assert!(responder.send(Ok(())).is_ok());
666        });
667    }
668
669    fn process_init_stash(
670        exec: &mut fasync::TestExecutor,
671        mut stash_stream: fidl_stash::SecureStoreRequestStream,
672    ) -> fidl_stash::StoreAccessorRequestStream {
673        assert_matches!(
674            exec.run_until_stalled(&mut stash_stream.next()),
675            Poll::Ready(Some(Ok(SecureStoreRequest::Identify { .. })))
676        );
677
678        let accessor_req_stream = assert_matches!(
679            exec.run_until_stalled(&mut stash_stream.next()),
680            Poll::Ready(Some(Ok(SecureStoreRequest::CreateAccessor { accessor_request, .. }))) =>
681        {
682            accessor_request.into_stream()
683        });
684
685        accessor_req_stream
686    }
687
688    /// Respond to the ListPrefix request with empty data, which matches the scenario where nothing
689    /// is saved in stash.
690    fn respond_to_stash_list_prefix(
691        exec: &mut fasync::TestExecutor,
692        stash_server: &mut fidl_stash::StoreAccessorRequestStream,
693    ) {
694        let request = assert_matches!(exec.run_until_stalled(&mut stash_server.next()), Poll::Ready(req) => {
695            req.expect("ListPrefix stash request not recieved.")
696        });
697        match request.unwrap() {
698            StoreAccessorRequest::ListPrefix { it, .. } => {
699                let mut iter = it.into_stream();
700                assert_matches!(
701                    exec.run_until_stalled(&mut iter.try_next()),
702                    Poll::Ready(Ok(Some(fidl_stash::ListIteratorRequest::GetNext { responder }))) => {
703                        responder.send(&[]).expect("error sending stash response");
704                });
705            }
706            _ => unreachable!(),
707        }
708    }
709
710    fn process_stash_delete(
711        exec: &mut fasync::TestExecutor,
712        stash_server: &mut fidl_stash::StoreAccessorRequestStream,
713    ) {
714        // Respond to stash delete.
715        let request = assert_matches!(exec.run_until_stalled(&mut stash_server.next()), Poll::Ready(req) => {
716            req.expect("DeletePrefix stash request not recieved.")
717        });
718        match request.unwrap() {
719            StoreAccessorRequest::DeletePrefix { .. } => {}
720            _ => unreachable!(),
721        }
722
723        // Respond to stash flush.
724        assert_matches!(
725            exec.run_until_stalled(&mut stash_server.try_next()),
726            Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::Flush{responder}))) => {
727                responder.send(Ok(())).expect("failed to send stash response");
728            }
729        );
730    }
731
732    #[fuchsia::test]
733    pub fn test_load_logs_result_success_metric() {
734        let mut exec = fasync::TestExecutor::new();
735        // Use a PolicyStorage with the default stash proxy for this test, but switch out the
736        // cobalt proxy to intercept metric events.
737        let mut test_values = migration_metrics_test_values();
738
739        // Load for the first time successfully.
740        {
741            let load_fut = test_values.store.load();
742            futures::pin_mut!(load_fut);
743            assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
744
745            // Respond to stash initialization requests.
746            let mut accessor_req_stream = process_init_stash(&mut exec, test_values.stash_stream);
747            assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
748
749            // Respond to stash read requests with empty data.
750            respond_to_stash_list_prefix(&mut exec, &mut accessor_req_stream);
751            assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
752
753            // Process stash delete and flush.
754            process_stash_delete(&mut exec, &mut accessor_req_stream);
755            assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
756
757            let mut metric_fut = test_values.cobalt_stream.next();
758            assert_matches!(exec.run_until_stalled(&mut metric_fut), Poll::Ready(Some(Ok(logged_metric))) => {
759                check_load_metric(logged_metric, MigrationResult::Success);
760            });
761            assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Ready(Ok(_)));
762        }
763
764        // Load again, an AlreadyMigrated metric event code should be logged. Stash do not need
765        // handling because the stash wrapper internally stores the data.
766        let load_fut = test_values.store.load();
767        futures::pin_mut!(load_fut);
768        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
769
770        let mut metric_fut = test_values.cobalt_stream.next();
771        assert_matches!(exec.run_until_stalled(&mut metric_fut), Poll::Ready(Some(Ok(logged_metric))) => {
772            check_load_metric(logged_metric, MigrationResult::AlreadyMigrated);
773        });
774
775        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Ready(Ok(_)));
776
777        // Check that nothing else was logged
778        assert_matches!(exec.run_until_stalled(&mut metric_fut), Poll::Pending);
779    }
780
781    #[fuchsia::test]
782    pub fn test_load_failure_logs_result_metric() {
783        let mut exec = fuchsia_async::TestExecutor::new();
784        let mut test_values = migration_metrics_test_values();
785        let load_fut = test_values.store.load();
786        futures::pin_mut!(load_fut);
787
788        // Start running the future to load and trigger migration. It should halt waiting on a
789        // stash request.
790        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
791
792        // Respond to stash initialization requests.
793        let mut accessor_req_stream = process_init_stash(&mut exec, test_values.stash_stream);
794        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
795
796        // Drop the request to read stash so that loading stash fails.
797        let request = assert_matches!(exec.run_until_stalled(&mut accessor_req_stream.next()), Poll::Ready(req) => {
798            req.expect("ListPrefix stash request not recieved.")
799        });
800        match request.unwrap() {
801            StoreAccessorRequest::ListPrefix { it: _, .. } => {
802                // If we just drop the iterator without responding, it should trigger a read error.
803            }
804            _ => unreachable!(),
805        }
806
807        // Continue the load fut, it should wait on a response to sending a metric.
808        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
809
810        // Check for the correct metric and ack.
811        assert_matches!(exec.run_until_stalled(&mut test_values.cobalt_stream.next()), Poll::Ready(Some(Ok(metric))) => {
812            check_load_metric(metric, MigrationResult::FailedToLoadLegacyData);
813        });
814
815        // The load should finish this time.
816        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Ready(Ok(data)) => {
817            assert!(data.is_empty());
818        });
819
820        // Verify that nothing else was send through the cobalt channel.
821        assert_matches!(
822            exec.run_until_stalled(&mut test_values.cobalt_stream.next()),
823            Poll::Pending
824        );
825    }
826
827    #[fuchsia::test]
828    pub fn test_load_delete_stash_failure_logs_result_metric() {
829        let mut exec = fuchsia_async::TestExecutor::new();
830        let mut test_values = migration_metrics_test_values();
831        let load_fut = test_values.store.load();
832        futures::pin_mut!(load_fut);
833
834        // Start running the future to load and trigger migration. It should halt waiting on a
835        // stash request.
836        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
837
838        // Respond to stash initialization requests.
839        let mut accessor_req_stream = process_init_stash(&mut exec, test_values.stash_stream);
840        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
841
842        // Respond to stash requests as if loading empty stash data.
843        respond_to_stash_list_prefix(&mut exec, &mut accessor_req_stream);
844
845        // Drop the accessor request stream to trigger an error when deleting stash.
846        drop(accessor_req_stream);
847
848        // Continue the load fut, it should wait on a response to sending a metric.
849        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
850
851        // Check for the correct metric and ack.
852        assert_matches!(exec.run_until_stalled(&mut test_values.cobalt_stream.next()), Poll::Ready(Some(Ok(metric))) => {
853            check_load_metric(metric, MigrationResult::MigratedButFailedToDeleteLegacy);
854        });
855
856        // The load should finish this time.
857        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Ready(Ok(data)) => {
858            assert!(data.is_empty());
859        });
860
861        // Verify that nothing else was sent through the cobalt channel.
862        assert_matches!(
863            exec.run_until_stalled(&mut test_values.cobalt_stream.next()),
864            Poll::Pending
865        );
866    }
867
868    #[fuchsia::test]
869    pub fn test_load_logs_result_failed_to_write_metric() {
870        let mut exec = fasync::TestExecutor::new();
871        // Use a path that is invalid so that writing to it fails.
872        let store_id = "//";
873        let mut test_values = migration_metrics_test_values();
874
875        // Switch out StorageStore to one using the invalid path.
876        test_values.store.root = StorageStore::new(std::path::Path::new(store_id));
877
878        // Start loading to migrate stash data.
879        let load_fut = test_values.store.load();
880        futures::pin_mut!(load_fut);
881        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
882
883        // Respond to stash initialization requests.
884        let mut accessor_req_stream = process_init_stash(&mut exec, test_values.stash_stream);
885        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
886
887        // Respond to stash requests as if loading empty stash data.
888        respond_to_stash_list_prefix(&mut exec, &mut accessor_req_stream);
889        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Pending);
890
891        // Check that the metric is logged for the failure to write.
892        let mut metric_fut = test_values.cobalt_stream.next();
893        assert_matches!(exec.run_until_stalled(&mut metric_fut), Poll::Ready(Some(Ok(logged_metric))) => {
894            check_load_metric(logged_metric, MigrationResult::FailedToWriteNewStore);
895        });
896
897        assert_matches!(exec.run_until_stalled(&mut load_fut), Poll::Ready(Ok(_)));
898
899        // Verify that nothing else was sent through the cobalt channel.
900        assert_matches!(
901            exec.run_until_stalled(&mut test_values.cobalt_stream.next()),
902            Poll::Pending
903        );
904    }
905
906    #[fuchsia::test]
907    pub fn load_wpa2_and_open_network_golden_file_test() {
908        // This tests that this example persisted file with one WPA2 network and one open network
909        // can still be read by the code, even if the corresponding write logic changes.
910        // This test should NOT change even if the way data is saved changes, unless there has been
911        // a stepping stone version that migrates this format, since the purpose is to test that
912        // this format can be read after an update.
913
914        let file_contents =
915            "{\"saved_networks\":[\
916                {\
917                    \"ssid\":[115,111,109,101,45,110,101,116,119,111,114,107],\
918                    \"security_type\":\"Wpa2\",\
919                    \"credential\":{\"Password\":[115,111,109,101,45,112,97,115,115,119,111,114,100]},\
920                    \"has_ever_connected\":false
921                },\
922                {
923                    \"ssid\":[111,112,101,110,45,110,101,116,119,111,114,107],\
924                    \"security_type\":\"None\",\
925                    \"credential\":\"None\",\
926                    \"has_ever_connected\":true\
927                }],\
928                \"version\":1\
929            }"
930        .as_bytes();
931        let store_id = &rand_string();
932
933        let network_configs = vec![
934            PersistentStorageData {
935                ssid: vec![115, 111, 109, 101, 45, 110, 101, 116, 119, 111, 114, 107],
936                security_type: SecurityType::Wpa2,
937                credential: Credential::Password(vec![100, 100, 100, 100, 100, 100]),
938                has_ever_connected: false,
939            },
940            PersistentStorageData {
941                ssid: vec![111, 112, 101, 110, 45, 110, 101, 116, 119, 111, 114, 107],
942                security_type: SecurityType::None,
943                credential: Credential::None,
944                has_ever_connected: true,
945            },
946        ];
947
948        // Write the data to the StorageStore's backing file.
949        let path = format!("/data/config.{}", store_id);
950        let mut file = std::fs::File::create(&path).expect("failed to open file for writing");
951        assert_eq!(
952            file.write(&file_contents).expect("Failed to write to file"),
953            file_contents.len()
954        );
955        file.flush().expect("failed to flush contents of file");
956
957        // Load the file data and check that the expected networks are there.
958        let store = StorageStore::new(&path);
959        let loaded_configs = store.load().expect("load failed");
960        assert_eq!(loaded_configs.len(), network_configs.len());
961        for config in network_configs.iter() {
962            assert!(network_configs.contains(config));
963        }
964    }
965}