file storage

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2024-09-10 17:56:57 -04:00
parent d729823f33
commit 4f86e9604f
13 changed files with 727 additions and 358 deletions

View File

@@ -0,0 +1,176 @@
use eframe::Result;
use enostr::{Keypair, Pubkey, SerializableKeypair};
use crate::Error;
use super::{
file_storage::{delete_file, write_file, Directory},
key_storage_impl::{KeyStorageError, KeyStorageResponse},
};
static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey";
/// An OS agnostic file key storage implementation
#[derive(Debug, PartialEq)]
pub struct FileKeyStorage {
keys_directory: Directory,
selected_key_directory: Directory,
}
impl FileKeyStorage {
pub fn new(keys_directory: Directory, selected_key_directory: Directory) -> Self {
Self {
keys_directory,
selected_key_directory,
}
}
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
write_file(
&self.keys_directory.file_path,
key.pubkey.hex(),
&serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
.map_err(|e| KeyStorageError::Addition(Error::Generic(e.to_string())))?,
)
.map_err(KeyStorageError::Addition)
}
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
let keys = self
.keys_directory
.get_files()
.map_err(KeyStorageError::Retrieval)?
.values()
.filter_map(|str_key| serde_json::from_str::<SerializableKeypair>(str_key).ok())
.map(|serializable_keypair| serializable_keypair.to_keypair(""))
.collect();
Ok(keys)
}
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
delete_file(&self.keys_directory.file_path, key.pubkey.hex())
.map_err(KeyStorageError::Removal)
}
fn get_selected_pubkey(&self) -> Result<Option<Pubkey>, KeyStorageError> {
let pubkey_str = self
.selected_key_directory
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
.map_err(KeyStorageError::Selection)?;
serde_json::from_str(&pubkey_str)
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))
}
fn select_pubkey(&self, pubkey: Option<Pubkey>) -> Result<(), KeyStorageError> {
if let Some(pubkey) = pubkey {
write_file(
&self.selected_key_directory.file_path,
SELECTED_PUBKEY_FILE_NAME.to_owned(),
&serde_json::to_string(&pubkey.hex())
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))?,
)
.map_err(KeyStorageError::Selection)
} else if self
.selected_key_directory
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
.is_ok()
{
// Case where user chose to have no selected pubkey, but one already exists
delete_file(
&self.selected_key_directory.file_path,
SELECTED_PUBKEY_FILE_NAME.to_owned(),
)
.map_err(KeyStorageError::Selection)
} else {
Ok(())
}
}
}
impl FileKeyStorage {
pub fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
}
pub fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
}
pub fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
}
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
KeyStorageResponse::ReceivedResult(self.get_selected_pubkey())
}
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.select_pubkey(key))
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use enostr::Keypair;
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
impl FileKeyStorage {
fn mock() -> Result<Self, Error> {
Ok(Self {
keys_directory: Directory::new(CREATE_TMP_DIR()?),
selected_key_directory: Directory::new(CREATE_TMP_DIR()?),
})
}
}
#[test]
fn test_basic() {
let kp = enostr::FullKeypair::generate().to_keypair();
let storage = FileKeyStorage::mock().unwrap();
let resp = storage.add_key(&kp);
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
assert_num_storage(&storage.get_keys(), 1);
assert_eq!(
storage.remove_key(&kp),
KeyStorageResponse::ReceivedResult(Ok(()))
);
assert_num_storage(&storage.get_keys(), 0);
}
fn assert_num_storage(keys_response: &KeyStorageResponse<Vec<Keypair>>, n: usize) {
match keys_response {
KeyStorageResponse::ReceivedResult(Ok(keys)) => {
assert_eq!(keys.len(), n);
}
KeyStorageResponse::ReceivedResult(Err(_e)) => {
panic!("could not get keys");
}
KeyStorageResponse::Waiting => {
panic!("did not receive result");
}
}
}
#[test]
fn test_select_key() {
let kp = enostr::FullKeypair::generate().to_keypair();
let storage = FileKeyStorage::mock().unwrap();
let _ = storage.add_key(&kp);
assert_num_storage(&storage.get_keys(), 1);
let resp = storage.select_pubkey(Some(kp.pubkey));
assert!(resp.is_ok());
let resp = storage.get_selected_pubkey();
assert!(resp.is_ok());
}
}

259
src/storage/file_storage.rs Normal file
View File

@@ -0,0 +1,259 @@
use std::{
collections::{HashMap, VecDeque},
fs::{self, File},
io::{self, BufRead},
path::{Path, PathBuf},
time::SystemTime,
};
use crate::Error;
pub enum DataPaths {
Log,
Setting,
Keys,
SelectedKey,
}
impl DataPaths {
pub fn get_path(&self) -> Result<PathBuf, Error> {
let base_path = match self {
DataPaths::Log => dirs::data_local_dir(),
DataPaths::Setting | DataPaths::Keys | DataPaths::SelectedKey => {
dirs::config_local_dir()
}
}
.ok_or(Error::Generic(
"Could not open well known OS directory".to_owned(),
))?;
let specific_path = match self {
DataPaths::Log => PathBuf::from("logs"),
DataPaths::Setting => PathBuf::from("settings"),
DataPaths::Keys => PathBuf::from("storage").join("accounts"),
DataPaths::SelectedKey => PathBuf::from("storage").join("selected_account"),
};
Ok(base_path.join("notedeck").join(specific_path))
}
}
#[derive(Debug, PartialEq)]
pub struct Directory {
pub file_path: PathBuf,
}
impl Directory {
pub fn new(file_path: PathBuf) -> Self {
Self { file_path }
}
/// Get the files in the current directory where the key is the file name and the value is the file contents
pub fn get_files(&self) -> Result<HashMap<String, String>, Error> {
let dir = fs::read_dir(self.file_path.clone())?;
let map = dir
.filter_map(|f| f.ok())
.filter(|f| f.path().is_file())
.filter_map(|f| {
let file_name = f.file_name().into_string().ok()?;
let contents = fs::read_to_string(f.path()).ok()?;
Some((file_name, contents))
})
.collect();
Ok(map)
}
pub fn get_file_names(&self) -> Result<Vec<String>, Error> {
let dir = fs::read_dir(self.file_path.clone())?;
let names = dir
.filter_map(|f| f.ok())
.filter(|f| f.path().is_file())
.filter_map(|f| f.file_name().into_string().ok())
.collect();
Ok(names)
}
pub fn get_file(&self, file_name: String) -> Result<String, Error> {
let filepath = self.file_path.clone().join(file_name.clone());
if filepath.exists() && filepath.is_file() {
let filepath_str = filepath
.to_str()
.ok_or_else(|| Error::Generic("Could not turn path to string".to_owned()))?;
Ok(fs::read_to_string(filepath_str)?)
} else {
Err(Error::Generic(format!(
"Requested file was not found: {}",
file_name
)))
}
}
pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result<FileResult, Error> {
let filepath = self.file_path.clone().join(file_name.clone());
if filepath.exists() && filepath.is_file() {
let file = File::open(&filepath)?;
let reader = io::BufReader::new(file);
let mut queue: VecDeque<String> = VecDeque::with_capacity(n);
let mut total_lines_in_file = 0;
for line in reader.lines() {
let line = line?;
queue.push_back(line);
if queue.len() > n {
queue.pop_front();
}
total_lines_in_file += 1;
}
let output_num_lines = queue.len();
let output = queue.into_iter().collect::<Vec<String>>().join("\n");
Ok(FileResult {
output,
output_num_lines,
total_lines_in_file,
})
} else {
Err(Error::Generic(format!(
"Requested file was not found: {}",
file_name
)))
}
}
/// Get the file name which is most recently modified in the directory
pub fn get_most_recent(&self) -> Result<Option<String>, Error> {
let mut most_recent: Option<(SystemTime, String)> = None;
for entry in fs::read_dir(&self.file_path)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_file() {
let modified = metadata.modified()?;
let file_name = entry.file_name().to_string_lossy().to_string();
match most_recent {
Some((last_modified, _)) if modified > last_modified => {
most_recent = Some((modified, file_name));
}
None => {
most_recent = Some((modified, file_name));
}
_ => {}
}
}
}
Ok(most_recent.map(|(_, file_name)| file_name))
}
}
pub struct FileResult {
pub output: String,
pub output_num_lines: usize,
pub total_lines_in_file: usize,
}
/// Write the file to the directory
pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> {
if !directory.exists() {
fs::create_dir_all(directory)?
}
std::fs::write(directory.join(file_name), data)?;
Ok(())
}
pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> {
let file_to_delete = directory.join(file_name.clone());
if file_to_delete.exists() && file_to_delete.is_file() {
fs::remove_file(file_to_delete).map_err(Error::Io)
} else {
Err(Error::Generic(format!(
"Requested file to delete was not found: {}",
file_name
)))
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{
storage::file_storage::{delete_file, write_file},
Error,
};
use super::Directory;
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
#[test]
fn test_add_get_delete() {
if let Ok(path) = CREATE_TMP_DIR() {
let directory = Directory::new(path);
let file_name = "file_test_name.txt".to_string();
let file_contents = "test";
let write_res = write_file(&directory.file_path, file_name.clone(), file_contents);
assert!(write_res.is_ok());
if let Ok(asserted_file_contents) = directory.get_file(file_name.clone()) {
assert_eq!(asserted_file_contents, file_contents);
} else {
panic!("File not found");
}
let delete_res = delete_file(&directory.file_path, file_name);
assert!(delete_res.is_ok());
} else {
panic!("could not get interactor")
}
}
#[test]
fn test_get_multiple() {
if let Ok(path) = CREATE_TMP_DIR() {
let directory = Directory::new(path);
for i in 0..10 {
let file_name = format!("file{}.txt", i);
let write_res = write_file(&directory.file_path, file_name, "test");
assert!(write_res.is_ok());
}
if let Ok(files) = directory.get_files() {
for i in 0..10 {
let file_name = format!("file{}.txt", i);
assert!(files.contains_key(&file_name));
assert_eq!(files.get(&file_name).unwrap(), "test");
}
} else {
panic!("Files not found");
}
if let Ok(file_names) = directory.get_file_names() {
for i in 0..10 {
let file_name = format!("file{}.txt", i);
assert!(file_names.contains(&file_name));
}
} else {
panic!("File names not found");
}
for i in 0..10 {
let file_name = format!("file{}.txt", i);
assert!(delete_file(&directory.file_path, file_name).is_ok());
}
} else {
panic!("could not get interactor")
}
}
}

View File

@@ -0,0 +1,112 @@
use enostr::{Keypair, Pubkey};
use super::file_key_storage::FileKeyStorage;
use crate::Error;
#[cfg(target_os = "macos")]
use super::security_framework_key_storage::SecurityFrameworkKeyStorage;
#[derive(Debug, PartialEq)]
pub enum KeyStorageType {
None,
FileSystem(FileKeyStorage),
#[cfg(target_os = "macos")]
SecurityFramework(SecurityFrameworkKeyStorage),
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum KeyStorageResponse<R> {
Waiting,
ReceivedResult(Result<R, KeyStorageError>),
}
impl<R: PartialEq> PartialEq for KeyStorageResponse<R> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(KeyStorageResponse::Waiting, KeyStorageResponse::Waiting) => true,
(
KeyStorageResponse::ReceivedResult(Ok(r1)),
KeyStorageResponse::ReceivedResult(Ok(r2)),
) => r1 == r2,
(
KeyStorageResponse::ReceivedResult(Err(_)),
KeyStorageResponse::ReceivedResult(Err(_)),
) => true,
_ => false,
}
}
}
impl KeyStorageType {
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
Self::FileSystem(f) => f.get_keys(),
#[cfg(target_os = "macos")]
Self::SecurityFramework(f) => f.get_keys(),
}
}
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
let _ = key;
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
Self::FileSystem(f) => f.add_key(key),
#[cfg(target_os = "macos")]
Self::SecurityFramework(f) => f.add_key(key),
}
}
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
let _ = key;
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
Self::FileSystem(f) => f.remove_key(key),
#[cfg(target_os = "macos")]
Self::SecurityFramework(f) => f.remove_key(key),
}
}
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(None)),
Self::FileSystem(f) => f.get_selected_key(),
#[cfg(target_os = "macos")]
Self::SecurityFramework(_) => unimplemented!(),
}
}
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
Self::FileSystem(f) => f.select_key(key),
#[cfg(target_os = "macos")]
Self::SecurityFramework(_) => unimplemented!(),
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum KeyStorageError {
Retrieval(Error),
Addition(Error),
Selection(Error),
Removal(Error),
OSError(Error),
}
impl std::fmt::Display for KeyStorageError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e),
Self::Addition(key) => write!(f, "Failed to add key: {:?}", key),
Self::Selection(pubkey) => write!(f, "Failed to select key: {:?}", pubkey),
Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key),
Self::OSError(e) => write!(f, "OS had an error: {:?}", e),
}
}
}
impl std::error::Error for KeyStorageError {}

14
src/storage/mod.rs Normal file
View File

@@ -0,0 +1,14 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod file_key_storage;
mod file_storage;
pub use file_key_storage::FileKeyStorage;
pub use file_storage::write_file;
pub use file_storage::DataPaths;
pub use file_storage::Directory;
#[cfg(target_os = "macos")]
mod security_framework_key_storage;
pub mod key_storage_impl;
pub use key_storage_impl::{KeyStorageResponse, KeyStorageType};

View File

@@ -0,0 +1,198 @@
use std::borrow::Cow;
use enostr::{Keypair, Pubkey, SecretKey};
use security_framework::{
item::{ItemClass, ItemSearchOptions, Limit, SearchResult},
passwords::{delete_generic_password, set_generic_password},
};
use tracing::error;
use crate::Error;
use super::{key_storage_impl::KeyStorageError, KeyStorageResponse};
#[derive(Debug, PartialEq)]
pub struct SecurityFrameworkKeyStorage {
pub service_name: Cow<'static, str>,
}
impl SecurityFrameworkKeyStorage {
pub fn new(service_name: String) -> Self {
SecurityFrameworkKeyStorage {
service_name: Cow::Owned(service_name),
}
}
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
match set_generic_password(
&self.service_name,
key.pubkey.hex().as_str(),
key.secret_key
.as_ref()
.map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()),
) {
Ok(_) => Ok(()),
Err(e) => Err(KeyStorageError::Addition(Error::Generic(e.to_string()))),
}
}
fn get_pubkey_strings(&self) -> Vec<String> {
let search_results = ItemSearchOptions::new()
.class(ItemClass::generic_password())
.service(&self.service_name)
.load_attributes(true)
.limit(Limit::All)
.search();
let mut accounts = Vec::new();
if let Ok(search_results) = search_results {
for result in search_results {
if let Some(map) = result.simplify_dict() {
if let Some(val) = map.get("acct") {
accounts.push(val.clone());
}
}
}
}
accounts
}
fn get_pubkeys(&self) -> Vec<Pubkey> {
self.get_pubkey_strings()
.iter_mut()
.filter_map(|pubkey_str| Pubkey::from_hex(pubkey_str.as_str()).ok())
.collect()
}
fn get_privkey_bytes_for(&self, account: &str) -> Option<Vec<u8>> {
let search_result = ItemSearchOptions::new()
.class(ItemClass::generic_password())
.service(&self.service_name)
.load_data(true)
.account(account)
.search();
if let Ok(results) = search_result {
if let Some(SearchResult::Data(vec)) = results.first() {
return Some(vec.clone());
}
}
None
}
fn get_secret_key_for_pubkey(&self, pubkey: &Pubkey) -> Option<SecretKey> {
if let Some(bytes) = self.get_privkey_bytes_for(pubkey.hex().as_str()) {
SecretKey::from_slice(bytes.as_slice()).ok()
} else {
None
}
}
fn get_all_keypairs(&self) -> Vec<Keypair> {
self.get_pubkeys()
.iter()
.map(|pubkey| {
let maybe_secret = self.get_secret_key_for_pubkey(pubkey);
Keypair::new(*pubkey, maybe_secret)
})
.collect()
}
fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> {
match delete_generic_password(&self.service_name, pubkey.hex().as_str()) {
Ok(_) => Ok(()),
Err(e) => {
error!("delete key error {}", e);
Err(KeyStorageError::Removal(Error::Generic(e.to_string())))
}
}
}
}
impl SecurityFrameworkKeyStorage {
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
}
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs()))
}
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey))
}
}
#[cfg(test)]
mod tests {
use super::*;
use enostr::FullKeypair;
static TEST_SERVICE_NAME: &str = "NOTEDECKTEST";
static STORAGE: SecurityFrameworkKeyStorage = SecurityFrameworkKeyStorage {
service_name: Cow::Borrowed(TEST_SERVICE_NAME),
};
// individual tests are ignored so test runner doesn't run them all concurrently
// TODO: a way to run them all serially should be devised
#[test]
#[ignore]
fn add_and_remove_test_pubkey_only() {
let num_keys_before_test = STORAGE.get_pubkeys().len();
let keypair = FullKeypair::generate().to_keypair();
let add_result = STORAGE.add_key_internal(&keypair);
assert!(add_result.is_ok());
let get_pubkeys_result = STORAGE.get_pubkeys();
assert_eq!(get_pubkeys_result.len() - num_keys_before_test, 1);
let remove_result = STORAGE.delete_key(&keypair.pubkey);
assert!(remove_result.is_ok());
let keys = STORAGE.get_pubkeys();
assert_eq!(keys.len() - num_keys_before_test, 0);
}
fn add_and_remove_full_n(n: usize) {
let num_keys_before_test = STORAGE.get_all_keypairs().len();
// there must be zero keys in storage for the test to work as intended
assert_eq!(num_keys_before_test, 0);
let expected_keypairs: Vec<Keypair> = (0..n)
.map(|_| FullKeypair::generate().to_keypair())
.collect();
expected_keypairs.iter().for_each(|keypair| {
let add_result = STORAGE.add_key_internal(keypair);
assert!(add_result.is_ok());
});
let asserted_keypairs = STORAGE.get_all_keypairs();
assert_eq!(expected_keypairs, asserted_keypairs);
expected_keypairs.iter().for_each(|keypair| {
let remove_result = STORAGE.delete_key(&keypair.pubkey);
assert!(remove_result.is_ok());
});
let num_keys_after_test = STORAGE.get_all_keypairs().len();
assert_eq!(num_keys_after_test, 0);
}
#[test]
#[ignore]
fn add_and_remove_full() {
add_and_remove_full_n(1);
}
#[test]
#[ignore]
fn add_and_remove_full_10() {
add_and_remove_full_n(10);
}
}