| // Copyright 2018 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 { |
| anyhow::{format_err, Error}, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_stash::{ |
| GetIteratorMarker, KeyValue, SecureStoreMarker, StoreAccessorMarker, StoreAccessorProxy, |
| Value, |
| }, |
| fuchsia_async as fasync, |
| fuchsia_bluetooth::{ |
| error::Error as BtError, |
| inspect::Inspectable, |
| types::{Address, BondingData, HostData, PeerId}, |
| }, |
| fuchsia_inspect, |
| futures::{ |
| channel::{mpsc, oneshot}, |
| future::{Future, FutureExt}, |
| stream::StreamExt, |
| }, |
| log::{error, info, warn}, |
| serde_json, |
| std::collections::HashMap, |
| }; |
| |
| #[cfg(test)] |
| use { |
| fuchsia_bluetooth::types::{LeBondData, OneOrBoth}, |
| fuchsia_inspect::testing::DiagnosticsHierarchyGetter, |
| std::collections::HashSet, |
| }; |
| |
| use crate::store::{ |
| keys::{ |
| bonding_data_key, host_data_key, host_id_from_key, BONDING_DATA_PREFIX, HOST_DATA_PREFIX, |
| }, |
| serde::{ |
| BondingDataDeserializer, BondingDataSerializer, HostDataDeserializer, HostDataSerializer, |
| }, |
| }; |
| |
| #[cfg(test)] |
| use crate::store::in_memory::InMemoryStore; |
| |
| /// These requests define the API surface for Stash. Each request signifies an atomic transaction that |
| /// the bt-gap stash can take |
| #[derive(Debug)] |
| pub(crate) enum Request { |
| /// Store 1 or more Bonds in the stash. |
| StoreBonds(Vec<BondingData>, oneshot::Sender<Result<(), Error>>), |
| |
| /// Completely remove a Peer and all its bonds from the stash. |
| RmPeer(PeerId, oneshot::Sender<Result<(), Error>>), |
| |
| /// Updates the host data for the host with the given identity address. |
| StoreHostData(Address, HostData, oneshot::Sender<Result<(), Error>>), |
| |
| /// Returns the local host data for the given local `address`. |
| GetHostData(Address, oneshot::Sender<Option<HostData>>), |
| |
| /// Returns an iterator over the bonding data entries for the local adapter with the given |
| /// `address`. Returns None if no such data exists. |
| ListBonds(Address, oneshot::Sender<Option<Vec<BondingData>>>), |
| } |
| |
| /// Size (in items) of the Stash Request channel buffer. It is possible for multiple items to be |
| /// queued at once, as the host-dispatcher can enqueue requests in response to both host activity |
| /// and also the activity of its fidl clients. Therefore we need >0 extra slots. 128 has been |
| /// un-scientifically chosen as a number which is estimated to be: |
| /// a) small enough that the size of the buffer will have negligible memory impact |
| /// b) large enough to prevent send blocking in all but the rarest cases |
| /// It is considered currently that further effort determining an optimum size will have little |
| /// value; if that changes that we should more empirically evaluate an effective buffer size |
| const STASH_MSG_QUEUE_CAPACITY: usize = 128; |
| |
| /// Clients interface with the Stash via the mechanism of a multiple-producer, single-consumer |
| /// queue. By handling all requests via this queue, we enforce linearization (and hence atomicity) |
| /// of stash updates |
| #[derive(Clone, Debug)] |
| pub struct Stash(mpsc::Sender<Request>); |
| |
| impl Stash { |
| pub fn store_bond(&mut self, bond: BondingData) -> impl Future<Output = Result<(), Error>> { |
| self.send_req(move |send| Request::StoreBonds(vec![bond], send)).map(|r| r.and_then(|r| r)) |
| } |
| pub fn store_bonds( |
| &mut self, |
| bonds: Vec<BondingData>, |
| ) -> impl Future<Output = Result<(), Error>> { |
| self.send_req(move |send| Request::StoreBonds(bonds, send)).map(|r| r.and_then(|r| r)) |
| } |
| pub fn rm_peer(&mut self, peer: PeerId) -> impl Future<Output = Result<(), Error>> { |
| self.send_req(move |send| Request::RmPeer(peer, send)).map(|r| r.and_then(|r| r)) |
| } |
| pub fn store_host_data( |
| &mut self, |
| local_address: Address, |
| data: HostData, |
| ) -> impl Future<Output = Result<(), Error>> { |
| self.send_req(move |send| Request::StoreHostData(local_address, data, send)) |
| .map(|r| r.and_then(|r| r)) |
| } |
| pub fn list_bonds( |
| &mut self, |
| local_address: Address, |
| ) -> impl Future<Output = Result<Option<Vec<BondingData>>, Error>> { |
| self.send_req(move |send| Request::ListBonds(local_address, send)) |
| } |
| pub fn get_host_data( |
| &mut self, |
| local_address: Address, |
| ) -> impl Future<Output = Result<Option<HostData>, Error>> { |
| self.send_req(move |send| Request::GetHostData(local_address, send)) |
| } |
| |
| /// Construct a Request with one half of a oneshot channel, and use the second half to await |
| /// the result of the request. This formulation ensures that the correct return type is used |
| fn send_req<T, F>(&mut self, build_request: F) -> impl Future<Output = Result<T, Error>> |
| where |
| F: FnOnce(oneshot::Sender<T>) -> Request, |
| { |
| let (send, recv) = oneshot::channel(); |
| let sent = self.0.try_send(build_request(send)); |
| async { |
| match sent { |
| Ok(_) => match recv.await { |
| Err(oneshot::Canceled) => { |
| return Err(format_err!("Response future was canceled")) |
| } |
| Ok(r) => Ok(r), |
| }, |
| Err(e) => Err(format_err!("Error communicating with bt-gap store: {}", e)), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| pub fn in_memory_mock() -> Stash { |
| let (sender, receiver) = mpsc::channel::<Request>(STASH_MSG_QUEUE_CAPACITY); |
| let mut store = InMemoryStore::default(); |
| fasync::Task::spawn( |
| receiver.for_each(move |request| futures::future::ready(store.handle_request(request))), |
| ) |
| .detach(); |
| Stash(sender) |
| } |
| } |
| |
| async fn run_stash(mut inbox: mpsc::Receiver<Request>, mut stash: StashInner) -> Result<(), Error> { |
| while let Some(event) = inbox.next().await { |
| match event { |
| Request::StoreBonds(bonds, signal) => { |
| let response = stash.store_bonds(bonds).await; |
| if let Err(_) = signal.send(response) { |
| return Err(format_err!("Failed to send response")); |
| } |
| } |
| Request::RmPeer(peer, signal) => { |
| let response = stash.rm_peer(peer).await; |
| if let Err(_) = signal.send(response) { |
| return Err(format_err!("Failed to send response")); |
| } |
| } |
| Request::StoreHostData(address, data, signal) => { |
| let response = stash.store_host_data(&address, data).await; |
| if let Err(_) = signal.send(response) { |
| return Err(format_err!("Failed to send response")); |
| } |
| } |
| Request::ListBonds(address, signal) => { |
| let response = stash.list_bonds(&address); |
| if let Err(_) = signal.send(response) { |
| return Err(format_err!("Failed to send response")); |
| }; |
| } |
| Request::GetHostData(address, signal) => { |
| let response = stash.get_host_data(&address); |
| if let Err(_) = signal.send(response) { |
| return Err(format_err!("Failed to send response")); |
| }; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Stash manages persistent data that is stored in bt-gap's component-specific storage. Data is |
| /// persisted in JSON format using the facilities provided by the serde library (see the |
| /// declarations in serde.rs for the description of the data format). |
| /// |
| /// The stash currently stores the following types of data: |
| /// |
| /// Bonding Data |
| /// ============ |
| /// Data for all bonded peers are each stored as a unique entry. The key for each bonding data |
| /// entry has the following format: |
| /// |
| /// "bonding-data:<device-id>" |
| /// |
| /// where <device-id> is a unique device identifier generated by the bt-host that has a bond with |
| /// the peer. The structure of the key allows all bonding data to be fetched from the stash by |
| /// requesting the "bonding-data:" prefix. Individual entries can be fetched and stored by providing |
| /// the complete key. |
| /// |
| /// Each bonding data entry contains the local bt-host identity address that it belongs to. |
| /// |
| /// Host Data |
| /// ========= |
| /// Data specific to a local bt-host identity are stored as a unique entry. The key for each host |
| /// data entry has the following format: |
| /// |
| /// "host-data:<host-identity-address>" |
| /// |
| /// where <host-identity-address> is a Bluetooth device address (e.g. |
| /// "host-data:01:02:03:04:05:06"). |
| #[derive(Debug)] |
| struct StashInner { |
| /// The proxy to the Fuchsia stash service. This is assumed to have been initialized as a |
| /// read/write capable accessor with the identity of the current component. |
| proxy: StoreAccessorProxy, |
| |
| /// In-memory state of the bonding data stash. Each entry is hierarchically indexed by a |
| /// local Bluetooth host identity and the resolved peer address. |
| bonding_data: HashMap<Address, HashMap<PeerId, Inspectable<BondingData>>>, |
| |
| /// Persisted data for a particular local Bluetooth host, indexed by local Bluetooth host |
| /// identity. |
| host_data: HashMap<Address, HostData>, |
| |
| /// Handle to inspect data |
| inspect: fuchsia_inspect::Node, |
| } |
| |
| fn bond_inspect_identifier(peer_id: PeerId) -> String { |
| format!("bond {}", peer_id) |
| } |
| |
| fn insert_inspectable_bonds( |
| data: &mut HashMap<Address, HashMap<PeerId, Inspectable<BondingData>>>, |
| inspect: &fuchsia_inspect::Node, |
| bonds: Vec<BondingData>, |
| ) { |
| for bond in bonds { |
| let (local_address, identifier) = (bond.local_address, bond.identifier); |
| let node = inspect.create_child(bond_inspect_identifier(identifier)); |
| let bond = Inspectable::new(bond, node); |
| // Update the in memory cache. |
| let host_bonds = data.entry(local_address).or_insert(HashMap::new()); |
| if host_bonds.insert(identifier, bond).is_some() { |
| warn!("Replaced bond data for {} peer id {}", local_address, identifier); |
| } |
| } |
| } |
| |
| /// Returns true if the underlying data in `lhs` is equivalent to `rhs`, aside from the |
| /// PeerId field, which is a Fuchsia-specific concept. |
| fn is_duplicate_bond(lhs: &BondingData, rhs: &BondingData) -> bool { |
| let rhs_with_lhs_id = BondingData { identifier: lhs.identifier.clone(), ..rhs.clone() }; |
| *lhs == rhs_with_lhs_id |
| } |
| |
| impl StashInner { |
| /// Updates the bonding data for a given device. Creates a new entry if one matching this |
| /// device does not exist. |
| async fn store_bonds(&mut self, bonds: Vec<BondingData>) -> Result<(), Error> { |
| for bond in bonds.iter() { |
| info!("storing bond (id: {})", bond.identifier); |
| // Persist the serialized blob. |
| let serialized = serde_json::to_string(&BondingDataSerializer::new(&bond))?; |
| self.proxy |
| .set_value(&bonding_data_key(bond.identifier), &mut Value::Stringval(serialized))?; |
| } |
| self.proxy.flush().await?.map_err(|e| format_err!("Failed to flush to stash: {:?}", e))?; |
| |
| insert_inspectable_bonds(&mut self.bonding_data, &self.inspect, bonds); |
| Ok(()) |
| } |
| |
| /// Returns an iterator over the bonding data entries for the local adapter with the given |
| /// `address`. Returns None if no such data exists. |
| fn list_bonds(&self, local_address: &Address) -> Option<Vec<BondingData>> { |
| Some( |
| self.bonding_data |
| .get(local_address)? |
| .values() |
| .into_iter() |
| .map(|bd| -> BondingData { (*bd).clone() }) |
| .collect(), |
| ) |
| } |
| |
| /// Removes persisted bond for a peer and removes its information from any adapters that have |
| /// it. Returns an error for failures but not if the peer isn't found. |
| async fn rm_peer(&mut self, peer_id: PeerId) -> Result<(), Error> { |
| info!("rm_peer (id: {})", peer_id); |
| |
| // Delete the persisted bond blob. |
| self.proxy.delete_value(&bonding_data_key(peer_id))?; |
| self.proxy.flush().await?.map_err(|e| format_err!("Failed to flush to stash: {:?}", e))?; |
| |
| // Delete peer from memory cache of all adapters. |
| self.bonding_data.values_mut().for_each(|m| m.retain(|k, _| *k != peer_id)); |
| Ok(()) |
| } |
| |
| /// Returns the local host data for the given local `address`. |
| fn get_host_data(&self, local_address: &Address) -> Option<HostData> { |
| self.host_data.get(local_address).cloned() |
| } |
| |
| /// Updates the host data for the host with the given identity address. |
| async fn store_host_data(&mut self, local_addr: &Address, data: HostData) -> Result<(), Error> { |
| info!("store_host_data (local address: {})", local_addr); |
| |
| // Persist the serialized blob. |
| let serialized = serde_json::to_string(&HostDataSerializer(&data.clone().into()))?; |
| self.proxy.set_value(&host_data_key(local_addr), &mut Value::Stringval(serialized))?; |
| self.proxy.flush().await?.map_err(|e| format_err!("Failed to flush to stash: {:?}", e))?; |
| |
| // Update the in memory cache. |
| let _ = self.host_data.insert(local_addr.clone(), data); |
| Ok(()) |
| } |
| |
| // Initializes the stash using the given `accessor`. This asynchronously loads existing |
| // stash data. Returns an error in case of failure. |
| async fn new( |
| accessor: StoreAccessorProxy, |
| inspect: fuchsia_inspect::Node, |
| ) -> Result<StashInner, Error> { |
| let bonding_data = StashInner::load_bonds(&accessor, &inspect).await?; |
| let host_data = StashInner::load_host_data(&accessor).await?; |
| Ok(StashInner { proxy: accessor, bonding_data, host_data, inspect }) |
| } |
| |
| fn deserialize_bonds( |
| raw_bonds: Vec<KeyValue>, |
| seen_addresses: &mut HashMap<(Address, Address), Vec<BondingData>>, |
| ) -> Result<(), Error> { |
| for key_value in raw_bonds { |
| let bond = if let Value::Stringval(json) = key_value.val { |
| BondingDataDeserializer::from_json(&json) |
| } else { |
| error!("stash malformed: bonding data should be a string"); |
| Err(format_err!("failed to initialize stash")) |
| }?; |
| let existing = seen_addresses.entry((bond.local_address, bond.address)).or_default(); |
| existing.push(bond); |
| } |
| Ok(()) |
| } |
| |
| async fn load_bonds<'a>( |
| accessor: &'a StoreAccessorProxy, |
| inspect: &'a fuchsia_inspect::Node, |
| ) -> Result<HashMap<Address, HashMap<PeerId, Inspectable<BondingData>>>, Error> { |
| // Obtain a list iterator for all cached bonding data. |
| let (iter, server_end) = create_proxy::<GetIteratorMarker>()?; |
| accessor.get_prefix(BONDING_DATA_PREFIX, server_end)?; |
| |
| let mut bonding_map = HashMap::new(); |
| let mut seen_addresses = HashMap::new(); |
| loop { |
| let next = iter.get_next().await?; |
| if next.is_empty() { |
| break; |
| } |
| Self::deserialize_bonds(next, &mut seen_addresses)?; |
| } |
| let mut bonds_to_store = Vec::new(); |
| for mut bonds in seen_addresses.into_values() { |
| let last = bonds.pop().ok_or(format_err!("unexpected empty bond list"))?; |
| // Generally, Fuchsia disallows restoration of multiple BondingDatas from the same local |
| // address to the same peer address. However, some system bootstrap flows cause the same |
| // underlying bond (i.e. security keys + local-peer address tuple) to be restored more |
| // than once under different Peer IDs. To be resilient to this flow, we deduplicate |
| // bonds which differ only in their PeerId from the Store as a special case. |
| if !bonds.iter().fold(true, |accum, b| accum && is_duplicate_bond(b, &last)) { |
| return Err(format_err!( |
| "multiple distinct bonds found for peer address {:?}, failing to load", |
| last.address |
| )); |
| } |
| for bond in bonds { |
| info!("removing duplicate bond for peer id {:?} from store", bond.identifier); |
| accessor.delete_value(&bonding_data_key(bond.identifier))?; |
| } |
| bonds_to_store.push(last); |
| } |
| accessor.flush().await?.map_err(|e| format_err!("Failed to flush to stash: {:?}", e))?; |
| |
| insert_inspectable_bonds(&mut bonding_map, &inspect, bonds_to_store); |
| Ok(bonding_map) |
| } |
| |
| async fn load_host_data( |
| accessor: &StoreAccessorProxy, |
| ) -> Result<HashMap<Address, HostData>, Error> { |
| // Obtain a list iterator for all cached host data. |
| let (iter, server_end) = create_proxy::<GetIteratorMarker>()?; |
| accessor.get_prefix(HOST_DATA_PREFIX, server_end)?; |
| |
| let mut host_data_map = HashMap::new(); |
| loop { |
| let next = iter.get_next().await?; |
| if next.is_empty() { |
| break; |
| } |
| for key_value in next { |
| let host_address = host_id_from_key(&key_value.key)?; |
| let host_address = Address::public_from_str(&host_address)?; |
| if let Value::Stringval(json) = key_value.val { |
| let host_data = HostDataDeserializer::from_json(&json)?; |
| if host_data_map.insert(host_address, host_data.into()).is_some() { |
| warn!("Replaced host data for {} while loading", host_address); |
| } |
| } else { |
| error!("stash malformed: host data should be a string"); |
| return Err(BtError::new("failed to initialize stash").into()); |
| } |
| } |
| } |
| Ok(host_data_map) |
| } |
| } |
| |
| /// Connects to the stash service and initializes a Stash object. This function obtains |
| /// read/write capability to the component-specific storage identified by `component_id`. |
| pub async fn init_stash( |
| component_id: &str, |
| inspect: fuchsia_inspect::Node, |
| ) -> Result<Stash, Error> { |
| let stash_svc = fuchsia_component::client::connect_to_protocol::<SecureStoreMarker>()?; |
| stash_svc.identify(component_id)?; |
| |
| let (proxy, server_end) = create_proxy::<StoreAccessorMarker>()?; |
| stash_svc.create_accessor(false, server_end)?; |
| |
| let inner = StashInner::new(proxy, inspect).await?; |
| let (stash, stash_run) = build_stash(inner); |
| fasync::Task::spawn(stash_run.map(|r| { |
| if let Err(e) = r { |
| error!("Error running stash: {}", e); |
| } |
| })) |
| .detach(); |
| Ok(stash) |
| } |
| |
| fn build_stash(inner: StashInner) -> (Stash, impl Future<Output = Result<(), Error>>) { |
| let (sender, receiver) = mpsc::channel::<Request>(STASH_MSG_QUEUE_CAPACITY); |
| (Stash(sender), run_stash(receiver, inner)) |
| } |
| |
| // These tests access stash in a hermetic environment and thus it's ok for state to leak between |
| // test runs, regardless of test failure. Each test clears out the state in stash before performing |
| // its test logic. |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use { |
| core::hash::Hash, fidl_fuchsia_bluetooth_sys::Key, |
| fuchsia_component::client::connect_to_protocol, futures::select, pin_utils::pin_mut, |
| }; |
| |
| static TEST_INSPECT_ROOT: &'static str = "test"; |
| // create_stash_accessor will create a new accessor to stash scoped under the given test name. |
| // All preexisting data in stash under this identity is deleted before the accessor is |
| // returned. |
| async fn create_stash_accessor(test_name: &str) -> Result<StoreAccessorProxy, Error> { |
| let stashserver = connect_to_protocol::<SecureStoreMarker>()?; |
| |
| // Identify |
| stashserver.identify(&(BONDING_DATA_PREFIX.to_owned() + test_name))?; |
| |
| // Create an accessor |
| let (acc, server_end) = create_proxy()?; |
| stashserver.create_accessor(false, server_end)?; |
| |
| // Clear all data in stash under our identity |
| acc.delete_prefix("")?; |
| acc.flush().await?.map_err(|e| format_err!("Failed to flush to stash: {:?}", e))?; |
| |
| Ok(acc) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_succeeds_with_empty_values() { |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| |
| // Create a Stash service interface. |
| let accessor = create_stash_accessor("new_stash_succeeds_with_empty_values") |
| .await |
| .expect("failed to create StashAccessor"); |
| let stash = StashInner::new(accessor, inspect).await.expect("expected Stash to initialize"); |
| |
| // The stash should be initialized with no data. |
| assert!(stash.bonding_data.is_empty()); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_fails_with_malformed_key_value_entry() { |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| |
| // Create a Stash service interface. |
| let accessor = create_stash_accessor("new_stash_fails_with_malformed_key_value_entry") |
| .await |
| .expect("failed to create StashAccessor"); |
| |
| // Set a key/value that contains a non-string value. |
| accessor |
| .set_value("bonding-data:test1234", &mut Value::Intval(5)) |
| .expect("failed to set a bonding data value"); |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| |
| // The stash should fail to initialize. |
| assert!(StashInner::new(accessor, inspect).await.is_err()); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_fails_with_malformed_json() { |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| |
| // Create a mock Stash service interface. |
| let accessor = create_stash_accessor("new_stash_fails_with_malformed_json") |
| .await |
| .expect("failed to create StashAccessor"); |
| |
| // Set a vector that contains a malformed JSON value |
| accessor |
| .set_value("bonding-data:test1234", &mut Value::Stringval("{0}".to_string())) |
| .expect("failed to set a bonding data value"); |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| |
| // The stash should fail to initialize. |
| assert!(StashInner::new(accessor, inspect).await.is_err()); |
| } |
| |
| fn host_data_1() -> HostData { |
| HostData { |
| irk: Some(Key { value: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] }), |
| } |
| } |
| |
| fn host_data_2() -> HostData { |
| HostData { |
| irk: Some(Key { value: [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] }), |
| } |
| } |
| |
| fn host_text_1() -> Value { |
| Value::Stringval( |
| "{\"irk\":{\"value\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]}}".to_string(), |
| ) |
| } |
| |
| fn host_text_2() -> Value { |
| Value::Stringval( |
| "{\"irk\":{\"value\":[16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]}}".to_string(), |
| ) |
| } |
| |
| fn default_le_data() -> LeBondData { |
| LeBondData { |
| connection_parameters: None, |
| services: vec![], |
| peer_ltk: None, |
| local_ltk: None, |
| irk: None, |
| csrk: None, |
| } |
| } |
| |
| fn bond_data_1() -> BondingData { |
| BondingData { |
| identifier: PeerId(1), |
| address: Address::Random([3, 0, 0, 0, 0, 0]), |
| local_address: Address::Public([1, 0, 0, 0, 0, 0]), |
| name: Some("Test Device 1".to_string()), |
| data: OneOrBoth::Left(default_le_data()), |
| } |
| } |
| fn bond_data_2() -> BondingData { |
| BondingData { |
| identifier: PeerId(2), |
| address: Address::Random([4, 0, 0, 0, 0, 0]), |
| local_address: Address::Public([1, 0, 0, 0, 0, 0]), |
| name: Some("Test Device 2".to_string()), |
| data: OneOrBoth::Left(default_le_data()), |
| } |
| } |
| |
| fn bond_data_3() -> BondingData { |
| BondingData { |
| identifier: PeerId(3), |
| address: Address::Random([3, 0, 0, 0, 0, 0]), |
| local_address: Address::Public([2, 0, 0, 0, 0, 0]), |
| name: None, |
| data: OneOrBoth::Left(default_le_data()), |
| } |
| } |
| |
| fn bond_data_4_dupes_3() -> BondingData { |
| BondingData { |
| identifier: PeerId(4), |
| address: Address::Random([3, 0, 0, 0, 0, 0]), |
| local_address: Address::Public([2, 0, 0, 0, 0, 0]), |
| name: None, |
| data: OneOrBoth::Left(default_le_data()), |
| } |
| } |
| |
| #[rustfmt::skip] |
| fn bond_entry_1() -> Value { |
| Value::Stringval( |
| "{\ |
| \"identifier\":1,\ |
| \"address\":{\ |
| \"type\":\"random\",\ |
| \"value\":[3,0,0,0,0,0]\ |
| },\ |
| \"hostAddress\":{\ |
| \"type\":\"public\",\ |
| \"value\":[1,0,0,0,0,0]\ |
| },\ |
| \"name\":\"Test Device 1\",\ |
| \"le\":{\ |
| \"connectionParameters\":null,\ |
| \"peerLtk\":null,\ |
| \"localLtk\":null,\ |
| \"irk\":null,\ |
| \"csrk\":null\ |
| },\ |
| \"bredr\":null\ |
| }" |
| .to_string(), |
| ) |
| } |
| |
| fn bond_entry_2() -> Value { |
| Value::Stringval( |
| r#" |
| { |
| "identifier": 2, |
| "hostAddress": { |
| "type": "public", |
| "value": [1,0,0,0,0,0] |
| }, |
| "address": { |
| "type": "random", |
| "value": [4,0,0,0,0,0] |
| }, |
| "name": "Test Device 2", |
| "le": { |
| "connectionParameters": null, |
| "peerLtk": null, |
| "localLtk": null, |
| "irk": null, |
| "csrk": null |
| }, |
| "bredr": null |
| }"# |
| .to_string(), |
| ) |
| } |
| |
| fn bond_entry_3() -> Value { |
| Value::Stringval( |
| r#" |
| { |
| "identifier": 3, |
| "hostAddress": { |
| "type": "public", |
| "value": [2,0,0,0,0,0] |
| }, |
| "address": { |
| "type": "random", |
| "value": [3,0,0,0,0,0] |
| }, |
| "name": null, |
| "le": { |
| "connectionParameters": null, |
| "peerLtk": null, |
| "localLtk": null, |
| "irk": null, |
| "csrk": null |
| }, |
| "bredr": null |
| }"# |
| .to_string(), |
| ) |
| } |
| |
| fn bond_entry_4_dupes_3() -> Value { |
| Value::Stringval( |
| r#" |
| { |
| "identifier": 4, |
| "hostAddress": { |
| "type": "public", |
| "value": [2,0,0,0,0,0] |
| }, |
| "address": { |
| "type": "random", |
| "value": [3,0,0,0,0,0] |
| }, |
| "name": null, |
| "le": { |
| "connectionParameters": null, |
| "peerLtk": null, |
| "localLtk": null, |
| "irk": null, |
| "csrk": null |
| }, |
| "bredr": null |
| }"# |
| .to_string(), |
| ) |
| } |
| |
| // This entry has the same hostAddress and address fields as entry 3, but populates the BR/EDR |
| // bond data field instead of the LE bond data field. |
| fn bond_entry_5_same_addrs_3() -> Value { |
| Value::Stringval( |
| r#" |
| { |
| "identifier": 5, |
| "hostAddress": { |
| "type": "public", |
| "value": [2,0,0,0,0,0] |
| }, |
| "address": { |
| "type": "random", |
| "value": [3,0,0,0,0,0] |
| }, |
| "name": null, |
| "le": null, |
| "bredr": { |
| "rolePreference": null, |
| "services": [], |
| "linkKey": null |
| } |
| }"# |
| .to_string(), |
| ) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_succeeds_with_values() { |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| |
| // Create a Stash service interface. |
| let accessor = create_stash_accessor("new_stash_succeeds_with_values") |
| .await |
| .expect("failed to create StashAccessor"); |
| |
| // Insert values into stash that contain bonding data for several devices. |
| accessor.set_value("bonding-data:1", &mut bond_entry_1()).expect("failed to set value"); |
| accessor.set_value("bonding-data:2", &mut bond_entry_2()).expect("failed to set value"); |
| accessor.set_value("bonding-data:3", &mut bond_entry_3()).expect("failed to set value"); |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| |
| // The stash should initialize with bonding data stored in stash |
| let stash = StashInner::new(accessor, inspect).await.expect("stash failed to initialize"); |
| |
| // There should be devices registered for two local addresses. |
| assert_eq!(2, stash.bonding_data.len()); |
| |
| // The first local address should have two devices associated with it. |
| let local = stash |
| .bonding_data |
| .get(&Address::Public([1, 0, 0, 0, 0, 0])) |
| .expect("could not find local address entries"); |
| assert_eq!(2, local.len()); |
| let bond: &BondingData = &*local.get(&PeerId(1)).expect("could not find device"); |
| assert_eq!(&bond_data_1(), bond); |
| let bond: &BondingData = &*local.get(&PeerId(2)).expect("could not find device"); |
| assert_eq!(&bond_data_2(), bond); |
| |
| // The second local address should have one device associated with it. |
| let local = stash |
| .bonding_data |
| .get(&Address::Public([2, 0, 0, 0, 0, 0])) |
| .expect("could not find local address entries"); |
| assert_eq!(1, local.len()); |
| let bond: &BondingData = &*local.get(&PeerId(3)).expect("could not find device"); |
| assert_eq!(&bond_data_3(), bond); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_filters_duplicate_bonds() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let inspect = inspector.root().create_child(TEST_INSPECT_ROOT); |
| // Create a Stash service interface. |
| let accessor = create_stash_accessor("new_stash_filters_duplicate_bonds") |
| .await |
| .expect("failed to create StashAccessor"); |
| |
| // Insert values into stash that contain bonding data for several devices. Other tests use |
| // simpler identifiers (e.g. `bonding-data:X`), but these cause issues when verifying Stash |
| // interactions with the store, as Stash uses the full 16-bit, zero-padded fmt::Display |
| // PeerId impl to create identifiers for the store. |
| let (id_3_key, id_4_key) = ( |
| bonding_data_key(bond_data_3().identifier), |
| bonding_data_key(bond_data_4_dupes_3().identifier), |
| ); |
| accessor.set_value(&id_3_key, &mut bond_entry_3()).expect("failed to set value"); |
| accessor.set_value(&id_4_key, &mut bond_entry_4_dupes_3()).expect("failed to set value"); |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| |
| // The stash should initialize with bonding data stored in stash |
| let stash = |
| StashInner::new(accessor.clone(), inspect).await.expect("stash failed to initialize"); |
| |
| // Although we added two bond entries for local host address [2, 0, ...] with distinct peer |
| // IDs, they use the same address, so they should be deduplicated in the store, with no |
| // guarantees about which bond is retained. |
| let local = stash |
| .bonding_data |
| .get(&Address::Public([2, 0, 0, 0, 0, 0])) |
| .expect("could not find local address entries"); |
| assert_eq!(1, local.len()); |
| |
| // The duplicate should also be removed from the store so that the store matches what's in |
| // memory, leaving only one entry. |
| let (iter, server_end) = create_proxy::<GetIteratorMarker>().unwrap(); |
| accessor.get_prefix(BONDING_DATA_PREFIX, server_end).expect("failed to fetch bond data"); |
| let res = iter.get_next().await.unwrap(); |
| assert_eq!(1, res.len()); |
| assert!(iter.get_next().await.unwrap().is_empty()); |
| |
| // The inspect hierarchy should contain exactly one bond node, deduplicated from the two |
| // in the original store. |
| let inspect_hierarchy = inspector.get_diagnostics_hierarchy(); |
| let test_hierarchy = |
| inspect_hierarchy.get_child(TEST_INSPECT_ROOT).expect("missing test hierarchy node"); |
| let bond_3_record = |
| test_hierarchy.get_child(&bond_inspect_identifier(bond_data_3().identifier)); |
| let bond_4_record = |
| test_hierarchy.get_child(&bond_inspect_identifier(bond_data_4_dupes_3().identifier)); |
| if bond_3_record.is_some() { |
| assert!( |
| !bond_4_record.is_some(), |
| "expected one deduplicated bond in Inspect, found both" |
| ); |
| } else { |
| assert!(bond_4_record.is_some(), "expected one bond record in Inspect, found none"); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn new_stash_fails_loading_same_addrs_different_bond() { |
| fuchsia_syslog::init().unwrap(); |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| |
| // Create a Stash service interface. |
| let accessor = create_stash_accessor("new_stash_fails_loading_same_addrs_different_bond") |
| .await |
| .expect("failed to create StashAccessor"); |
| |
| accessor.set_value("bonding-data:3", &mut bond_entry_3()).expect("failed to set value"); |
| accessor |
| .set_value(&"bonding-data:5", &mut bond_entry_5_same_addrs_3()) |
| .expect("failed to set value"); |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| |
| // Bond entry 5 uses the same local and peer addresses as bond entry 3, but the security |
| // data itself differs between the entries. This indicates that the store is in an invalid |
| // state, so we expect to fail initialization of the Stash. |
| assert!(StashInner::new(accessor.clone(), inspect).await.is_err()); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn store_bond_commits_entry() { |
| let mut stash = setup_stash("store_bond_commits_entry", vec![]).await; |
| let accessor = stash.proxy.clone(); |
| |
| assert!(stash.store_bonds(vec![bond_data_1()]).await.is_ok()); |
| |
| // Make sure that the in-memory cache has been updated. |
| assert_eq!(1, stash.bonding_data.len()); |
| let bond: &BondingData = &*stash |
| .bonding_data |
| .get(&Address::Public([1, 0, 0, 0, 0, 0])) |
| .unwrap() |
| .get(&PeerId(1)) |
| .unwrap(); |
| assert_eq!(&bond_data_1(), bond); |
| |
| // The new data should be accessible over FIDL. |
| let result = accessor.get_value("bonding-data:0000000000000001").await; |
| let bond_data = result.expect("failed to get value").map(|x| *x); |
| assert_eq!(bond_data, Some(bond_entry_1())); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn list_bonds() { |
| let initial_data = |
| vec![("bonding-data:1", bond_entry_1()), ("bonding-data:2", bond_entry_2())]; |
| let stash = setup_stash("list_bonds", initial_data).await; |
| |
| // Should return None for unknown address. |
| assert_eq!(stash.list_bonds(&Address::Public([0, 0, 0, 0, 0, 0])), None); |
| |
| let bonds = stash |
| .list_bonds(&Address::Public([1, 0, 0, 0, 0, 0])) |
| .expect("expected to find address"); |
| let ids: HashSet<PeerId> = bonds.iter().map(|bond| bond.identifier).collect(); |
| assert_eq!(ids, set_of(vec![PeerId(1), PeerId(2)])); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn get_host_data() { |
| let initial_data = vec![ |
| ("host-data:00:00:00:00:00:01", host_text_1()), |
| ("host-data:00:00:00:00:00:02", host_text_2()), |
| ]; |
| let stash = setup_stash("get_host_data", initial_data).await; |
| |
| // Should return None for unknown identity address. |
| assert!(stash.get_host_data(&Address::Public([0, 0, 0, 0, 0, 0])).is_none()); |
| |
| let host_data = stash |
| .get_host_data(&Address::Public([1, 0, 0, 0, 0, 0])) |
| .expect("expected to find HostData"); |
| assert_eq!(host_data_1(), host_data); |
| |
| let host_data = stash |
| .get_host_data(&Address::Public([2, 0, 0, 0, 0, 0])) |
| .expect("expected to find HostData"); |
| assert_eq!(host_data_2(), host_data); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn rm_peer() { |
| let initial_data = |
| vec![("bonding-data:1", bond_entry_1()), ("bonding-data:2", bond_entry_2())]; |
| let mut stash = setup_stash("rm_peer", initial_data).await; |
| |
| // OK to remove some unknown peer... |
| assert!(stash.rm_peer(PeerId(0)).await.is_ok()); |
| |
| // ...or known peer. |
| assert!(stash.rm_peer(PeerId(1)).await.is_ok()); |
| |
| let local = stash |
| .bonding_data |
| .get(&Address::Public([1, 0, 0, 0, 0, 0])) |
| .expect("could not find local address entries"); |
| assert_eq!(1, local.len()); |
| assert!(local.get(&PeerId(1)).is_none()); |
| let bond: &BondingData = &*(local.get(&PeerId(2)).expect("could not find device")); |
| assert_eq!(&bond_data_2(), bond); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn store_host_data() { |
| let host_address = Address::Public([1, 0, 0, 0, 0, 0]); |
| let mut stash = setup_stash("store_host_data", vec![]).await; |
| let accessor = stash.proxy.clone(); |
| |
| assert!(stash.store_host_data(&host_address, host_data_1()).await.is_ok()); |
| |
| // Make sure the in-memory cache has been updated. |
| assert_eq!(Some(&host_data_1()), stash.host_data.get(&host_address)); |
| assert_eq!(1, stash.host_data.len()); |
| |
| // The new data should be accessible over FIDL. |
| let host_text = accessor.get_value("host-data:00:00:00:00:00:01").await; |
| let host_text = host_text.expect("failed to get value").map(|x| *x); |
| assert_eq!(host_text, Some(host_text_1())); |
| |
| // It should be possible to overwrite the IRK. |
| assert!(stash.store_host_data(&host_address, host_data_2()).await.is_ok()); |
| |
| // Make sure the in-memory cache has been updated. |
| assert_eq!(Some(&host_data_2()), stash.host_data.get(&host_address)); |
| assert_eq!(1, stash.host_data.len()); |
| |
| // The new data should be accessible over FIDL. |
| let host_text = accessor.get_value("host-data:00:00:00:00:00:01").await; |
| let host_text = host_text.expect("failed to get value").map(|x| *x); |
| assert_eq!(host_text, Some(host_text_2())); |
| } |
| |
| async fn setup_stash(name: &'static str, entries: Vec<(&'static str, Value)>) -> StashInner { |
| let inspect = fuchsia_inspect::Inspector::new().root().create_child(TEST_INSPECT_ROOT); |
| let accessor = create_stash_accessor(name).await.expect("failed to create StashAccessor"); |
| |
| // Insert intial bonding data values into stash |
| for (id, mut entry) in entries { |
| accessor.set_value(id, &mut entry).expect("failed to set value"); |
| } |
| accessor |
| .flush() |
| .await |
| .expect("failed to flush a bonding data value") |
| .expect("failed to flush a bonding data value"); |
| StashInner::new(accessor, inspect).await.expect("stash failed to initialize") |
| } |
| |
| fn set_of<I>(elems: I) -> HashSet<I::Item> |
| where |
| I: IntoIterator, |
| I::Item: Eq + Hash, |
| { |
| elems.into_iter().collect() |
| } |
| |
| async fn run_with_stash<F, T, Fut>(inner: StashInner, f: F) -> Result<T, Error> |
| where |
| F: FnOnce(Stash) -> Fut, |
| Fut: Future<Output = Result<T, Error>>, |
| { |
| let (stash, run_stash) = build_stash(inner); |
| let run_fn = f(stash); |
| |
| pin_mut!(run_stash); |
| pin_mut!(run_fn); |
| select! { |
| result = run_fn.fuse() => result, |
| run = run_stash.fuse() => match run { |
| Ok(_) => return Err(format_err!("Stash receiver stopped unexpectedly")), |
| Err(e) => Err(e) |
| } |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn request_list_bonds() -> Result<(), Error> { |
| let initial_data = |
| vec![("bonding-data:1", bond_entry_1()), ("bonding-data:2", bond_entry_2())]; |
| let stash = setup_stash("request_list_bonds", initial_data).await; |
| |
| run_with_stash(stash, move |mut s: Stash| { |
| async move { |
| // Should return None for unknown address. |
| let bonds = s.list_bonds(Address::Public([0, 0, 0, 0, 0, 0])).await?; |
| assert_eq!(bonds, None); |
| |
| // Should return expected elements for known address |
| let bonds = s.list_bonds(Address::Public([1, 0, 0, 0, 0, 0])).await?; |
| let ids = |
| bonds.expect("expected to find address").iter().map(|b| b.identifier).collect(); |
| assert_eq!(set_of(vec![PeerId(1), PeerId(2)]), ids); |
| Ok(()) |
| } |
| }) |
| .await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn request_store_bonds() -> Result<(), Error> { |
| let stash = setup_stash("request_store_bonds", vec![]).await; |
| let accessor = stash.proxy.clone(); |
| |
| run_with_stash(stash, move |mut s: Stash| { |
| async move { |
| s.store_bond(bond_data_1()).await?; |
| |
| // The new data should be accessible over FIDL. |
| let result = accessor.get_value("bonding-data:0000000000000001").await; |
| let bond_data = result.expect("failed to get value").map(|x| *x); |
| assert_eq!(bond_data, Some(bond_entry_1())); |
| Ok(()) |
| } |
| }) |
| .await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn request_rm_peer() -> Result<(), Error> { |
| let initial_data = |
| vec![("bonding-data:1", bond_entry_1()), ("bonding-data:2", bond_entry_2())]; |
| let stash = setup_stash("request_rm_peer", initial_data).await; |
| |
| run_with_stash(stash, move |mut s: Stash| { |
| async move { |
| // OK to remove some unknown peer... |
| s.rm_peer(PeerId(0)).await?; |
| |
| // ...or known peer. |
| s.rm_peer(PeerId(1)).await?; |
| |
| // Should return only non-removed element for known address |
| let bonds = s.list_bonds(bond_data_2().local_address).await?; |
| let bonds = bonds.expect("expected to find address"); |
| assert_eq!(bonds, vec![bond_data_2()]); |
| Ok(()) |
| } |
| }) |
| .await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn request_get_host_data() -> Result<(), Error> { |
| let initial_data = vec![ |
| ("host-data:00:00:00:00:00:01", host_text_1()), |
| ("host-data:00:00:00:00:00:02", host_text_2()), |
| ]; |
| let stash = setup_stash("request_get_host_data", initial_data).await; |
| run_with_stash(stash, move |mut s: Stash| { |
| async move { |
| // Should return None for unknown identity address. |
| assert!(s.get_host_data(Address::Public([0, 0, 0, 0, 0, 0])).await?.is_none()); |
| |
| let host_data = s.get_host_data(Address::Public([1, 0, 0, 0, 0, 0])).await?; |
| assert_eq!(Some(host_data_1()), host_data); |
| |
| let host_data = s.get_host_data(Address::Public([2, 0, 0, 0, 0, 0])).await?; |
| assert_eq!(Some(host_data_2()), host_data); |
| Ok(()) |
| } |
| }) |
| .await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn request_store_host_data() -> Result<(), Error> { |
| let stash = setup_stash("request_store_host_data", vec![]).await; |
| let accessor = stash.proxy.clone(); |
| run_with_stash(stash, move |mut s: Stash| { |
| async move { |
| let host_address = Address::Public([1, 0, 0, 0, 0, 0]); |
| |
| let host_data = host_data_1(); |
| assert!(s.store_host_data(host_address, host_data).await.is_ok()); |
| |
| // Make sure the in-memory cache has been updated. |
| let host_data = s.get_host_data(host_address).await?; |
| assert_eq!(Some(host_data_1()), host_data); |
| |
| // The new data should be accessible over FIDL. |
| let host_text = accessor.get_value("host-data:00:00:00:00:00:01").await; |
| let host_text = host_text.expect("failed to get value").map(|x| *x); |
| assert_eq!(host_text, Some(host_text_1())); |
| |
| // It should be possible to overwrite the IRK. |
| let host_data = host_data_2(); |
| assert!(s.store_host_data(host_address, host_data).await.is_ok()); |
| |
| // Make sure the in-memory cache has been updated. |
| let host_data = s.get_host_data(host_address).await?; |
| assert_eq!(Some(host_data_2()), host_data); |
| |
| // The new data should be accessible over FIDL. |
| let host_text = accessor.get_value("host-data:00:00:00:00:00:01").await; |
| let host_text = host_text.expect("failed to get value").map(|x| *x); |
| assert_eq!(host_text, Some(host_text_2())); |
| Ok(()) |
| } |
| }) |
| .await |
| } |
| } |