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

75
Cargo.lock generated
View File

@@ -991,6 +991,27 @@ dependencies = [
"subtle",
]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -1358,6 +1379,12 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fdeflate"
version = "0.3.4"
@@ -2183,6 +2210,16 @@ dependencies = [
"redox_syscall 0.4.1",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@@ -2503,6 +2540,7 @@ dependencies = [
"base32",
"bitflags 2.6.0",
"console_error_panic_hook",
"dirs",
"eframe",
"egui",
"egui_extras",
@@ -2527,6 +2565,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
@@ -2816,13 +2855,19 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orbclient"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
dependencies = [
"libredox",
"libredox 0.0.2",
]
[[package]]
@@ -3333,6 +3378,17 @@ dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox 0.1.3",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.6"
@@ -3500,9 +3556,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.36"
version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags 2.6.0",
"errno",
@@ -4076,6 +4132,19 @@ version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"

View File

@@ -43,7 +43,10 @@ strum_macros = "0.26"
bitflags = "2.5.0"
uuid = { version = "1.10.0", features = ["v4"] }
indexmap = "2.6.0"
dirs = "5.0.1"
[dev-dependencies]
tempfile = "3.13.0"
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.11.0"

View File

@@ -6,15 +6,15 @@ use nostrdb::Ndb;
use crate::{
column::Columns,
imgcache::ImageCache,
key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType},
login_manager::LoginState,
route::{Route, Router},
storage::{KeyStorageResponse, KeyStorageType},
ui::{
account_login_view::{AccountLoginResponse, AccountLoginView},
account_management::{AccountsView, AccountsViewResponse},
},
};
use tracing::info;
use tracing::{error, info};
pub use crate::user_account::UserAccount;
@@ -96,13 +96,14 @@ pub fn process_accounts_view_response(
}
impl AccountManager {
pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorageType) -> Self {
pub fn new(key_store: KeyStorageType) -> Self {
let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() {
res.unwrap_or_default()
} else {
Vec::new()
};
let currently_selected_account = get_selected_index(&accounts, &key_store);
AccountManager {
currently_selected_account,
accounts,
@@ -188,16 +189,31 @@ impl AccountManager {
}
pub fn select_account(&mut self, index: usize) {
if self.accounts.get(index).is_some() {
self.currently_selected_account = Some(index)
if let Some(account) = self.accounts.get(index) {
self.currently_selected_account = Some(index);
self.key_store.select_key(Some(account.pubkey));
}
}
pub fn clear_selected_account(&mut self) {
self.currently_selected_account = None
self.currently_selected_account = None;
self.key_store.select_key(None);
}
}
fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> {
match keystore.get_selected_key() {
KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => {
return accounts.iter().position(|account| account.pubkey == pubkey);
}
KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e),
_ => (),
};
None
}
pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) {
match response {
AccountLoginResponse::CreateNew => {
@@ -207,4 +223,5 @@ pub fn process_login_view_response(manager: &mut AccountManager, response: Accou
manager.add_account(keypair);
}
}
manager.select_account(manager.num_accounts() - 1);
}

View File

@@ -9,19 +9,19 @@ use crate::{
filter::{self, FilterState},
frame_history::FrameHistory,
imgcache::ImageCache,
key_storage::KeyStorageType,
nav,
note::NoteRef,
notecache::{CachedNote, NoteCache},
notes_holder::NotesHolderStorage,
profile::Profile,
storage::{Directory, FileKeyStorage, KeyStorageType},
subscriptions::{SubKind, Subscriptions},
thread::Thread,
timeline::{Timeline, TimelineId, TimelineKind, ViewFilter},
ui::{self, DesktopSidePanel},
unknowns::UnknownIds,
view_state::ViewState,
Result,
DataPaths, Result,
};
use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool};
@@ -664,23 +664,32 @@ impl Damus {
let mut config = Config::new();
config.set_ingester_threads(4);
let mut accounts = AccountManager::new(
// TODO: should pull this from settings
None,
// TODO: use correct KeyStorage mechanism for current OS arch
KeyStorageType::None,
);
let keystore = if parsed_args.use_keystore {
if let Ok(keys_path) = DataPaths::Keys.get_path() {
if let Ok(selected_key_path) = DataPaths::SelectedKey.get_path() {
KeyStorageType::FileSystem(FileKeyStorage::new(
Directory::new(keys_path),
Directory::new(selected_key_path),
))
} else {
error!("Could not find path for selected key");
KeyStorageType::None
}
} else {
error!("Could not find data path for keys");
KeyStorageType::None
}
} else {
KeyStorageType::None
};
let mut accounts = AccountManager::new(keystore);
for key in parsed_args.keys {
info!("adding account: {}", key.pubkey);
accounts.add_account(key);
}
// TODO: pull currently selected account from settings
if accounts.num_accounts() > 0 {
accounts.select_account(0);
}
// setup relays if we have them
let pool = if parsed_args.relays.is_empty() {
let mut pool = RelayPool::new();
@@ -817,7 +826,7 @@ impl Damus {
columns,
textmode: false,
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
accounts: AccountManager::new(None, KeyStorageType::None),
accounts: AccountManager::new(KeyStorageType::None),
frame_history: FrameHistory::default(),
view_state: ViewState::default(),
}

View File

@@ -13,6 +13,7 @@ pub struct Args {
pub light: bool,
pub debug: bool,
pub textmode: bool,
pub use_keystore: bool,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
@@ -28,6 +29,7 @@ impl Args {
since_optimize: true,
debug: false,
textmode: false,
use_keystore: true,
dbpath: None,
datapath: None,
};
@@ -210,6 +212,9 @@ impl Args {
} else {
error!("failed to parse filter in '{}'", filter_file);
}
} else if arg == "--no-keystore" {
i += 1;
res.use_keystore = false;
}
i += 1;

View File

@@ -1,90 +0,0 @@
use enostr::Keypair;
#[cfg(target_os = "linux")]
use crate::linux_key_storage::LinuxKeyStorage;
#[cfg(target_os = "macos")]
use crate::macos_key_storage::MacOSKeyStorage;
#[cfg(target_os = "macos")]
pub const SERVICE_NAME: &str = "Notedeck";
#[derive(Debug, PartialEq)]
pub enum KeyStorageType {
None,
#[cfg(target_os = "macos")]
MacOS,
#[cfg(target_os = "linux")]
Linux,
// TODO:
// Windows,
// Android,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub enum KeyStorageResponse<R> {
Waiting,
ReceivedResult(Result<R, KeyStorageError>),
}
pub trait KeyStorage {
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>>;
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()>;
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()>;
}
impl KeyStorage for KeyStorageType {
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
#[cfg(target_os = "macos")]
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).get_keys(),
#[cfg(target_os = "linux")]
Self::Linux => LinuxKeyStorage::new().get_keys(),
}
}
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
let _ = key;
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
#[cfg(target_os = "macos")]
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).add_key(key),
#[cfg(target_os = "linux")]
Self::Linux => LinuxKeyStorage::new().add_key(key),
}
}
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
let _ = key;
match self {
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
#[cfg(target_os = "macos")]
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).remove_key(key),
#[cfg(target_os = "linux")]
Self::Linux => LinuxKeyStorage::new().remove_key(key),
}
}
}
#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub enum KeyStorageError {
Retrieval(String),
Addition(String),
Removal(String),
OSError(String),
}
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::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 {}

View File

@@ -18,9 +18,7 @@ mod frame_history;
mod images;
mod imgcache;
mod key_parsing;
mod key_storage;
pub mod login_manager;
mod macos_key_storage;
mod multi_subscriber;
mod nav;
mod note;
@@ -45,11 +43,13 @@ mod view_state;
#[cfg(test)]
#[macro_use]
mod test_utils;
mod linux_key_storage;
mod storage;
pub use app::Damus;
pub use error::Error;
pub use profile::DisplayName;
pub use storage::DataPaths;
#[cfg(target_os = "android")]
use winit::platform::android::EventLoopBuilderExtAndroid;

View File

@@ -1,210 +0,0 @@
#![cfg(target_os = "linux")]
use enostr::{Keypair, SerializableKeypair};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::{env, fs::File};
use crate::key_storage::{KeyStorage, KeyStorageError, KeyStorageResponse};
use tracing::debug;
enum LinuxKeyStorageType {
BasicFileStorage,
// TODO(kernelkind): could use the secret service api, and maybe even allow password manager integration via a settings menu
}
pub struct LinuxKeyStorage {}
// TODO(kernelkind): read from settings instead of hard-coding
static USE_MECHANISM: LinuxKeyStorageType = LinuxKeyStorageType::BasicFileStorage;
impl LinuxKeyStorage {
pub fn new() -> Self {
Self {}
}
}
impl KeyStorage for LinuxKeyStorage {
fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
match USE_MECHANISM {
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().get_keys(),
}
}
fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
match USE_MECHANISM {
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().add_key(key),
}
}
fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
match USE_MECHANISM {
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().remove_key(key),
}
}
}
struct BasicFileStorage {
credential_dir_name: String,
}
impl BasicFileStorage {
pub fn new() -> Self {
Self {
credential_dir_name: ".credentials".to_string(),
}
}
fn mock() -> Self {
Self {
credential_dir_name: ".credentials_test".to_string(),
}
}
fn get_cred_dirpath(&self) -> Result<PathBuf, KeyStorageError> {
let home_dir = env::var("HOME")
.map_err(|_| KeyStorageError::OSError("HOME env variable not set".to_string()))?;
let home_path = std::path::PathBuf::from(home_dir);
let project_path_str = "notedeck";
let config_path = {
if let Some(xdg_config_str) = env::var_os("XDG_CONFIG_HOME") {
let xdg_path = PathBuf::from(xdg_config_str);
let xdg_path_config = if xdg_path.is_absolute() {
xdg_path
} else {
home_path.join(".config")
};
xdg_path_config.join(project_path_str)
} else {
home_path.join(format!(".{}", project_path_str))
}
}
.join(self.credential_dir_name.clone());
std::fs::create_dir_all(&config_path).map_err(|_| {
KeyStorageError::OSError(format!(
"could not create config path: {}",
config_path.display()
))
})?;
Ok(config_path)
}
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
let mut file_path = self.get_cred_dirpath()?;
file_path.push(format!("{}", &key.pubkey));
let mut file = File::create(file_path)
.map_err(|_| KeyStorageError::Addition("could not create or open file".to_string()))?;
let json_str = serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
.map_err(|e| KeyStorageError::Addition(e.to_string()))?;
file.write_all(json_str.as_bytes()).map_err(|_| {
KeyStorageError::Addition("could not write keypair to file".to_string())
})?;
Ok(())
}
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
let file_path = self.get_cred_dirpath()?;
let mut keys: Vec<Keypair> = Vec::new();
if !file_path.is_dir() {
return Err(KeyStorageError::Retrieval(
"path is not a directory".to_string(),
));
}
let dir = fs::read_dir(file_path).map_err(|_| {
KeyStorageError::Retrieval("problem accessing credentials directory".to_string())
})?;
for entry in dir {
let entry = entry.map_err(|_| {
KeyStorageError::Retrieval("problem accessing crediential file".to_string())
})?;
let path = entry.path();
if path.is_file() {
if let Some(path_str) = path.to_str() {
debug!("key path {}", path_str);
let json_string = fs::read_to_string(path_str).map_err(|e| {
KeyStorageError::OSError(format!("File reading problem: {}", e))
})?;
let key: SerializableKeypair =
serde_json::from_str(&json_string).map_err(|e| {
KeyStorageError::OSError(format!(
"Deserialization problem: {}",
(e.to_string().as_str())
))
})?;
keys.push(key.to_keypair(""))
}
}
}
Ok(keys)
}
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
let path = self.get_cred_dirpath()?;
let filepath = path.join(key.pubkey.to_string());
if filepath.exists() && filepath.is_file() {
fs::remove_file(&filepath)
.map_err(|e| KeyStorageError::OSError(format!("failed to remove file: {}", e)))?;
}
Ok(())
}
}
impl KeyStorage for BasicFileStorage {
fn get_keys(&self) -> crate::key_storage::KeyStorageResponse<Vec<enostr::Keypair>> {
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
}
fn add_key(&self, key: &enostr::Keypair) -> crate::key_storage::KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
}
fn remove_key(&self, key: &enostr::Keypair) -> crate::key_storage::KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
}
}
mod tests {
use crate::key_storage::{KeyStorage, KeyStorageResponse};
use super::BasicFileStorage;
#[test]
fn test_basic() {
let kp = enostr::FullKeypair::generate().to_keypair();
let resp = BasicFileStorage::mock().add_key(&kp);
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
assert_num_storage(1);
let resp = BasicFileStorage::mock().remove_key(&kp);
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
assert_num_storage(0);
}
#[allow(dead_code)]
fn assert_num_storage(n: usize) {
let resp = BasicFileStorage::mock().get_keys();
if let KeyStorageResponse::ReceivedResult(Ok(vec)) = resp {
assert_eq!(vec.len(), n);
return;
}
panic!();
}
}

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

@@ -1,40 +1,45 @@
#![cfg(target_os = "macos")]
use std::borrow::Cow;
use enostr::{Keypair, Pubkey, SecretKey};
use security_framework::item::{ItemClass, ItemSearchOptions, Limit, SearchResult};
use security_framework::passwords::{delete_generic_password, set_generic_password};
use crate::key_storage::{KeyStorage, KeyStorageError, KeyStorageResponse};
use security_framework::{
item::{ItemClass, ItemSearchOptions, Limit, SearchResult},
passwords::{delete_generic_password, set_generic_password},
};
use tracing::error;
pub struct MacOSKeyStorage<'a> {
pub service_name: &'a str,
use crate::Error;
use super::{key_storage_impl::KeyStorageError, KeyStorageResponse};
#[derive(Debug, PartialEq)]
pub struct SecurityFrameworkKeyStorage {
pub service_name: Cow<'static, str>,
}
impl<'a> MacOSKeyStorage<'a> {
pub fn new(service_name: &'a str) -> Self {
MacOSKeyStorage { service_name }
impl SecurityFrameworkKeyStorage {
pub fn new(service_name: String) -> Self {
SecurityFrameworkKeyStorage {
service_name: Cow::Owned(service_name),
}
}
fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> {
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
match set_generic_password(
self.service_name,
&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(_) => Err(KeyStorageError::Addition(key.pubkey.hex())),
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)
.service(&self.service_name)
.load_attributes(true)
.limit(Limit::All)
.search();
@@ -64,7 +69,7 @@ impl<'a> MacOSKeyStorage<'a> {
fn get_privkey_bytes_for(&self, account: &str) -> Option<Vec<u8>> {
let search_result = ItemSearchOptions::new()
.class(ItemClass::generic_password())
.service(self.service_name)
.service(&self.service_name)
.load_data(true)
.account(account)
.search();
@@ -97,26 +102,26 @@ impl<'a> MacOSKeyStorage<'a> {
}
fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> {
match delete_generic_password(self.service_name, pubkey.hex().as_str()) {
match delete_generic_password(&self.service_name, pubkey.hex().as_str()) {
Ok(_) => Ok(()),
Err(e) => {
error!("delete key error {}", e);
Err(KeyStorageError::Removal(pubkey.hex()))
Err(KeyStorageError::Removal(Error::Generic(e.to_string())))
}
}
}
}
impl<'a> KeyStorage for MacOSKeyStorage<'a> {
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.add_key(key))
impl SecurityFrameworkKeyStorage {
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
}
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs()))
}
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey))
}
}
@@ -127,8 +132,8 @@ mod tests {
use enostr::FullKeypair;
static TEST_SERVICE_NAME: &str = "NOTEDECKTEST";
static STORAGE: MacOSKeyStorage = MacOSKeyStorage {
service_name: TEST_SERVICE_NAME,
static STORAGE: SecurityFrameworkKeyStorage = SecurityFrameworkKeyStorage {
service_name: Cow::Borrowed(TEST_SERVICE_NAME),
};
// individual tests are ignored so test runner doesn't run them all concurrently
@@ -140,14 +145,14 @@ mod tests {
let num_keys_before_test = STORAGE.get_pubkeys().len();
let keypair = FullKeypair::generate().to_keypair();
let add_result = STORAGE.add_key(&keypair);
assert_eq!(add_result, Ok(()));
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_eq!(remove_result, Ok(()));
assert!(remove_result.is_ok());
let keys = STORAGE.get_pubkeys();
assert_eq!(keys.len() - num_keys_before_test, 0);
@@ -163,8 +168,8 @@ mod tests {
.collect();
expected_keypairs.iter().for_each(|keypair| {
let add_result = STORAGE.add_key(keypair);
assert_eq!(add_result, Ok(()));
let add_result = STORAGE.add_key_internal(keypair);
assert!(add_result.is_ok());
});
let asserted_keypairs = STORAGE.get_all_keypairs();
@@ -172,7 +177,7 @@ mod tests {
expected_keypairs.iter().for_each(|keypair| {
let remove_result = STORAGE.delete_key(&keypair.pubkey);
assert_eq!(remove_result, Ok(()));
assert!(remove_result.is_ok());
});
let num_keys_after_test = STORAGE.get_all_keypairs().len();