| // 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::{Key, StorageManager}; |
| use account_common::{AccountManagerError, ResultExt}; |
| use anyhow::format_err; |
| use async_trait::async_trait; |
| use fidl_fuchsia_identity_account::Error as ApiError; |
| use fidl_fuchsia_io::{DirectoryProxy, OPEN_FLAG_CREATE, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE}; |
| use files_async::{DirEntry, DirentKind}; |
| use fuchsia_zircon as zx; |
| use futures::lock::Mutex; |
| use lazy_static::lazy_static; |
| use std::path::Path; |
| |
| /// Path to the file containing the expected key. |
| const KEY_FILE_PATH: &str = "keyfile"; |
| /// The subdirectory `InsecureKeyDirectoryStorageManager` serves via `get_root_dir`. |
| const CLIENT_ROOT_PATH: &str = "root"; |
| |
| lazy_static! { |
| /// Expected `DirEntry`s for a `InsecureKeyDirectoryStorageManager` in the |
| /// locked or available state. |
| static ref EXPECTED_DIRECTORY_ENTRIES: Vec<DirEntry> = { |
| let mut entries = vec![ |
| DirEntry { name: CLIENT_ROOT_PATH.to_string(), kind: DirentKind::Directory }, |
| DirEntry { name: KEY_FILE_PATH.to_string(), kind: DirentKind::File } |
| ]; |
| // files_async::readdir sorts results, so by sorting the entries we can |
| // do a direct comparison with the readdir results. |
| entries.sort(); |
| entries |
| }; |
| } |
| |
| /// An enumeration of the different internal states a |
| /// `InsecureKeyDirectoryStorageManager` may be in. |
| #[derive(Debug)] |
| enum StorageManagerState { |
| /// The underlying directory is not initialized. |
| Uninitialized, |
| /// The underlying directory is initialized, but the manager is not giving |
| /// out handles to it. |
| Locked, |
| /// The underlying directory is initialized and the manager is giving out |
| /// handles via `get_root_directory`. |
| Available, |
| } |
| |
| /// A `StorageManager` that manages access to a directory it is given at |
| /// creation time. `InsecureKeyDirectoryStorageManager` emulates, but does not |
| /// provide, secure storage accessed with a locking `Key`. It is intended only |
| /// for use during testing. |
| /// |
| /// `InsecureKeyDirectoryStorageManager` maintains a `root` subdirectory |
| /// in the managed directory for which it gives out handles through |
| /// `get_root_dir`. It also maintains a `keyfile` file which contains the |
| /// correct key. |
| /// |
| /// The contents of `managed_dir` appear as follows in each state: |
| /// * *Uninitialized* - `managed_dir` is empty |
| /// * *Locked* - `managed_dir` contains a `root` subdirectory and a `keyfile` |
| /// file that contains the correct key. |
| /// * *Available* - `managed_dir` appears the same as in the *Locked* state. |
| /// |
| /// Note: The directory provided to `InsecureKeyDirectoryStorageManager` does |
| /// not need to be mounted to the component's namespace, the only requirement |
| /// is that it is accessible via a `DirectoryProxy`. |
| #[derive(Debug)] |
| pub struct InsecureKeyDirectoryStorageManager { |
| /// The current state of the `InsecureKeyDirectoryStorageManager`. |
| state: Mutex<StorageManagerState>, |
| /// A handle to the root of the managed directory. |
| managed_dir: DirectoryProxy, |
| } |
| |
| #[async_trait(?Send)] |
| impl StorageManager for InsecureKeyDirectoryStorageManager { |
| async fn provision(&self, key: &Key) -> Result<(), AccountManagerError> { |
| let mut state_lock = self.state.lock().await; |
| match *state_lock { |
| StorageManagerState::Uninitialized => { |
| self.store_correct_key(key).await?; |
| io_util::create_sub_directories(&self.managed_dir, &Path::new(CLIENT_ROOT_PATH)) |
| .map_err(|e| { |
| AccountManagerError::new(ApiError::Resource).with_cause(format_err!( |
| "Failed to create client root directory: {:?}", |
| e |
| )) |
| })?; |
| *state_lock = StorageManagerState::Available; |
| Ok(()) |
| } |
| ref invalid_state @ _ => Err(AccountManagerError::new(ApiError::FailedPrecondition) |
| .with_cause(format_err!( |
| "StorageManager provision called in the {:?} state", |
| invalid_state |
| ))), |
| } |
| } |
| |
| async fn unlock(&self, key: &Key) -> Result<(), AccountManagerError> { |
| let mut state_lock = self.state.lock().await; |
| match *state_lock { |
| StorageManagerState::Locked => { |
| self.check_unlock_key(key).await?; |
| *state_lock = StorageManagerState::Available; |
| Ok(()) |
| } |
| ref invalid_state @ _ => Err(AccountManagerError::new(ApiError::FailedPrecondition) |
| .with_cause(format_err!( |
| "StorageManager unlock called in the {:?} state", |
| invalid_state |
| ))), |
| } |
| } |
| |
| async fn lock(&self) -> Result<(), AccountManagerError> { |
| let mut state_lock = self.state.lock().await; |
| match *state_lock { |
| StorageManagerState::Available => { |
| *state_lock = StorageManagerState::Locked; |
| Ok(()) |
| } |
| ref invalid_state @ _ => Err(AccountManagerError::new(ApiError::FailedPrecondition) |
| .with_cause(format_err!( |
| "StorageManager lock called in the {:?} state", |
| invalid_state |
| ))), |
| } |
| } |
| |
| async fn destroy(&self) -> Result<(), AccountManagerError> { |
| let mut state_lock = self.state.lock().await; |
| match *state_lock { |
| StorageManagerState::Locked => { |
| files_async::remove_dir_recursive(&self.managed_dir, CLIENT_ROOT_PATH) |
| .await |
| .map_err(|e| { |
| AccountManagerError::new(ApiError::Unknown).with_cause(format_err!( |
| "Failed to destroy client root directory: {:?}", |
| e |
| )) |
| })?; |
| |
| let remove_status = self.managed_dir.unlink(KEY_FILE_PATH).await.map_err(|e| { |
| AccountManagerError::new(ApiError::Resource) |
| .with_cause(format_err!("Failed to destroy keyfile: {:?}", e)) |
| })?; |
| if remove_status != zx::Status::OK.into_raw() { |
| return Err(AccountManagerError::new(ApiError::Resource).with_cause( |
| format_err!("Failed to destroy keyfile: {:?}", remove_status), |
| )); |
| } |
| |
| *state_lock = StorageManagerState::Uninitialized; |
| Ok(()) |
| } |
| ref invalid_state @ _ => Err(AccountManagerError::new(ApiError::FailedPrecondition) |
| .with_cause(format_err!( |
| "StorageManager destroy called in the {:?} state", |
| invalid_state |
| ))), |
| } |
| } |
| |
| async fn get_root_dir(&self) -> Result<DirectoryProxy, AccountManagerError> { |
| let state_lock = self.state.lock().await; |
| match *state_lock { |
| StorageManagerState::Available => io_util::open_directory( |
| &self.managed_dir, |
| Path::new(CLIENT_ROOT_PATH), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| ) |
| .account_manager_error(ApiError::Resource), |
| ref invalid_state @ _ => Err(AccountManagerError::new(ApiError::FailedPrecondition) |
| .with_cause(format_err!( |
| "StorageManager get_root_dir called in the {:?} state", |
| invalid_state |
| ))), |
| } |
| } |
| } |
| |
| impl InsecureKeyDirectoryStorageManager { |
| /// Create a new `InsecureKeyDirectoryStorageManager` that manages the given |
| /// directory. The directory must be either empty or previously managed |
| /// by a `InsecureKeyDirectoryStorageManager`. |
| #[allow(dead_code)] |
| pub async fn new(managed_dir: DirectoryProxy) -> Result<Self, AccountManagerError> { |
| // check internal state of filesystem to derive state |
| let dir_entries = |
| files_async::readdir(&managed_dir).await.account_manager_error(ApiError::Resource)?; |
| |
| let state = if dir_entries.as_slice() == EXPECTED_DIRECTORY_ENTRIES.as_slice() { |
| StorageManagerState::Locked |
| } else if dir_entries.is_empty() { |
| StorageManagerState::Uninitialized |
| } else { |
| return Err(AccountManagerError::new(ApiError::Internal) |
| .with_cause(format_err!("Cannot determine StorageManager state"))); |
| }; |
| Ok(InsecureKeyDirectoryStorageManager { state: Mutex::new(state), managed_dir }) |
| } |
| |
| /// Stores the correct key. |
| async fn store_correct_key(&self, key: &Key) -> Result<(), AccountManagerError> { |
| let serialized_key = serde_json::to_string(key).map_err(|e| { |
| AccountManagerError::new(ApiError::Internal) |
| .with_cause(format_err!("Failed to serialize correct key: {:?}", e)) |
| })?; |
| let key_file = io_util::open_file( |
| &self.managed_dir, |
| &Path::new(KEY_FILE_PATH), |
| OPEN_FLAG_CREATE | OPEN_RIGHT_WRITABLE, |
| ) |
| .map_err(|e| { |
| AccountManagerError::new(ApiError::Resource) |
| .with_cause(format_err!("Failed to open keyfile while saving key: {:?}", e)) |
| })?; |
| io_util::write_file(&key_file, &serialized_key).await.map_err(|e| { |
| AccountManagerError::new(ApiError::Resource) |
| .with_cause(format_err!("Failed to write key to keyfile: {:?}", e)) |
| }) |
| } |
| |
| /// Verify if the given key is the correct key needed for unlock. |
| async fn check_unlock_key(&self, key: &Key) -> Result<(), AccountManagerError> { |
| let file_proxy = |
| io_util::open_file(&self.managed_dir, &Path::new(KEY_FILE_PATH), OPEN_RIGHT_READABLE) |
| .map_err(|e| { |
| AccountManagerError::new(ApiError::Resource) |
| .with_cause(format_err!("Failed to open keyfile: {:?}", e)) |
| })?; |
| let serialized_correct_key = io_util::read_file(&file_proxy).await.map_err(|e| { |
| AccountManagerError::new(ApiError::Resource) |
| .with_cause(format_err!("Failed to read keyfile: {:?}", e)) |
| })?; |
| let correct_key = serde_json::from_str(&serialized_correct_key).map_err(|e| { |
| AccountManagerError::new(ApiError::Internal) |
| .with_cause(format_err!("Failed to deserialize correct key: {:?}", e)) |
| })?; |
| if key == &correct_key { |
| Ok(()) |
| } else { |
| Err(AccountManagerError::new(ApiError::FailedAuthentication)) |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use fuchsia_async as fasync; |
| use futures::prelude::*; |
| use tempfile::TempDir; |
| |
| lazy_static! { |
| static ref CUSTOM_KEY_CONTENTS: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7]; |
| static ref CUSTOM_KEY: Key = Key::CustomKey(CUSTOM_KEY_CONTENTS.clone()); |
| } |
| |
| /// Creates a TempDir and DirectoryProxy handle to it. The TempDir must |
| /// be kept in scope for the duration that DirectoryProxy is used. |
| fn create_temp_directory() -> (TempDir, DirectoryProxy) { |
| let temp_dir = TempDir::new().unwrap(); |
| let dir_proxy = io_util::open_directory_in_namespace( |
| temp_dir.path().to_str().unwrap(), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| (temp_dir, dir_proxy) |
| } |
| |
| async fn create_file_with_content(dir: &DirectoryProxy, path: &str, content: &str) { |
| let file = io_util::open_file(dir, Path::new(path), OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE) |
| .unwrap(); |
| io_util::write_file(&file, content).await.unwrap(); |
| } |
| |
| async fn assert_directory_empty(dir: &DirectoryProxy) { |
| let dir_entries = files_async::readdir(dir).await.unwrap(); |
| assert!(dir_entries.is_empty()); |
| } |
| |
| async fn assert_file_contents(dir: &DirectoryProxy, path: &str, content: &str) { |
| let file = io_util::open_file(dir, Path::new(path), OPEN_RIGHT_READABLE).unwrap(); |
| let file_content = io_util::read_file(&file).await.unwrap(); |
| assert_eq!(content, &file_content); |
| } |
| |
| /// Runs a test multiple times - once for each valid Key variation. |
| async fn run_with_key_variations<F, Fut>(test_fn: F) |
| where |
| F: Fn(Key) -> Fut, |
| Fut: Future<Output = ()>, |
| { |
| test_fn(Key::NoCustomKey).await; |
| test_fn(Key::CustomKey(CUSTOM_KEY_CONTENTS.clone())).await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_directory_storage_manager_destroy_removes_files() { |
| run_with_key_variations(|key| async move { |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&key).await.unwrap(); |
| |
| // Write some data to directories |
| let data_dir = manager.get_root_dir().await.unwrap(); |
| create_file_with_content(&data_dir, "test-data-file", "test-data-content").await; |
| // Drop the data dir reference - this is a precondition of calling lock(). |
| std::mem::drop(data_dir); |
| |
| manager.lock().await.unwrap(); |
| manager.destroy().await.unwrap(); |
| manager.provision(&key).await.unwrap(); |
| |
| // Files should no longer exist. |
| let data_dir = manager.get_root_dir().await.unwrap(); |
| assert_directory_empty(&data_dir).await; |
| }) |
| .await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_directory_storage_manager_files_persist_across_lock() { |
| run_with_key_variations(|key| async move { |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&key).await.unwrap(); |
| |
| // Write some data to directories |
| let data_dir = manager.get_root_dir().await.unwrap(); |
| create_file_with_content(&data_dir, "test-data-file", "test-data-content").await; |
| std::mem::drop(data_dir); |
| |
| manager.lock().await.unwrap(); |
| manager.unlock(&key).await.unwrap(); |
| |
| // Files should still exist. |
| let data_dir = manager.get_root_dir().await.unwrap(); |
| assert_file_contents(&data_dir, "test-data-file", "test-data-content").await; |
| }) |
| .await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_directory_storage_manager_files_persist_across_instances() { |
| run_with_key_variations(|key| async move { |
| let (dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&key).await.unwrap(); |
| |
| // Write some data to directories |
| let data_dir = manager.get_root_dir().await.unwrap(); |
| create_file_with_content(&data_dir, "test-data-file", "test-data-content").await; |
| std::mem::drop(data_dir); |
| std::mem::drop(manager); |
| |
| // Create a new manager with the same directory. |
| let new_dir_proxy = io_util::open_directory_in_namespace( |
| dir.path().to_str().unwrap(), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| let new_manager = InsecureKeyDirectoryStorageManager::new(new_dir_proxy).await.unwrap(); |
| new_manager.unlock(&key).await.unwrap(); |
| |
| // Files should still exist. |
| let data_dir = new_manager.get_root_dir().await.unwrap(); |
| assert_file_contents(&data_dir, "test-data-file", "test-data-content").await; |
| }) |
| .await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_directory_storage_manager_unlock_failed_authentication() { |
| // CustomKey given when NoCustomKey expected |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&Key::NoCustomKey).await.unwrap(); |
| manager.lock().await.unwrap(); |
| assert_eq!( |
| manager.unlock(&*CUSTOM_KEY).await.unwrap_err().api_error, |
| ApiError::FailedAuthentication |
| ); |
| |
| // NoCustomKey given when CustomKey expected |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&*CUSTOM_KEY).await.unwrap(); |
| manager.lock().await.unwrap(); |
| assert_eq!( |
| manager.unlock(&Key::NoCustomKey).await.unwrap_err().api_error, |
| ApiError::FailedAuthentication |
| ); |
| |
| // Wrong CustomKey given |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&*CUSTOM_KEY).await.unwrap(); |
| manager.lock().await.unwrap(); |
| let wrong_key = vec![99]; |
| assert_eq!( |
| manager.unlock(&Key::CustomKey(wrong_key)).await.unwrap_err().api_error, |
| ApiError::FailedAuthentication |
| ); |
| |
| // Key of length 0 given when NoCustomKey expected |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&Key::NoCustomKey).await.unwrap(); |
| manager.lock().await.unwrap(); |
| assert_eq!( |
| manager.unlock(&Key::CustomKey(vec![])).await.unwrap_err().api_error, |
| ApiError::FailedAuthentication |
| ); |
| |
| // NoCustomKey given when key of length 0 expected |
| let (_dir, dir_proxy) = create_temp_directory(); |
| let manager = InsecureKeyDirectoryStorageManager::new(dir_proxy).await.unwrap(); |
| manager.provision(&Key::CustomKey(vec![])).await.unwrap(); |
| manager.lock().await.unwrap(); |
| assert_eq!( |
| manager.unlock(&Key::NoCustomKey).await.unwrap_err().api_error, |
| ApiError::FailedAuthentication |
| ); |
| } |
| } |