blob: b20e2c369953c248f0838d92a82bed880ca40a47 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::{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
);
}
}