diff --git a/enostr/src/keypair.rs b/enostr/src/keypair.rs index 44929e8..aeb3a11 100644 --- a/enostr/src/keypair.rs +++ b/enostr/src/keypair.rs @@ -1,3 +1,7 @@ +use nostr::nips::nip49::EncryptedSecretKey; +use serde::Deserialize; +use serde::Serialize; + use crate::Pubkey; use crate::SecretKey; @@ -93,3 +97,34 @@ impl std::fmt::Display for FullKeypair { ) } } + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct SerializableKeypair { + pub pubkey: Pubkey, + pub encrypted_secret_key: Option, +} + +impl SerializableKeypair { + pub fn from_keypair(kp: &Keypair, pass: &str, log_n: u8) -> Self { + Self { + pubkey: kp.pubkey.clone(), + encrypted_secret_key: kp + .secret_key + .clone() + .map(|s| { + EncryptedSecretKey::new(&s, pass, log_n, nostr::nips::nip49::KeySecurity::Weak) + .ok() + }) + .flatten(), + } + } + + pub fn to_keypair(&self, pass: &str) -> Keypair { + Keypair::new( + self.pubkey.clone(), + self.encrypted_secret_key + .map(|e| e.to_secret_key(pass).ok()) + .flatten(), + ) + } +} diff --git a/enostr/src/lib.rs b/enostr/src/lib.rs index 9a7b30c..d35885b 100644 --- a/enostr/src/lib.rs +++ b/enostr/src/lib.rs @@ -11,7 +11,7 @@ pub use client::ClientMessage; pub use error::Error; pub use ewebsock; pub use filter::Filter; -pub use keypair::{FullKeypair, Keypair}; +pub use keypair::{FullKeypair, Keypair, SerializableKeypair}; pub use nostr::SecretKey; pub use note::{Note, NoteId}; pub use profile::Profile; diff --git a/src/account_manager.rs b/src/account_manager.rs index 67a1171..a083912 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use enostr::Keypair; -use crate::key_storage::KeyStorage; +use crate::key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType}; pub use crate::user_account::UserAccount; use tracing::info; @@ -11,12 +11,16 @@ use tracing::info; pub struct AccountManager { currently_selected_account: Option, accounts: Vec, - key_store: KeyStorage, + key_store: KeyStorageType, } impl AccountManager { - pub fn new(currently_selected_account: Option, key_store: KeyStorage) -> Self { - let accounts = key_store.get_keys().unwrap_or_default(); + pub fn new(currently_selected_account: Option, key_store: KeyStorageType) -> Self { + let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { + res.unwrap_or_default() + } else { + Vec::new() + }; AccountManager { currently_selected_account, diff --git a/src/app.rs b/src/app.rs index 1b8f3ce..86c4664 100644 --- a/src/app.rs +++ b/src/app.rs @@ -756,7 +756,7 @@ impl Damus { // TODO: should pull this from settings None, // TODO: use correct KeyStorage mechanism for current OS arch - crate::key_storage::KeyStorage::None, + crate::key_storage::KeyStorageType::None, ); for key in parsed_args.keys { @@ -805,7 +805,7 @@ impl Damus { timelines, textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), - account_manager: AccountManager::new(None, crate::key_storage::KeyStorage::None), + account_manager: AccountManager::new(None, crate::key_storage::KeyStorageType::None), frame_history: FrameHistory::default(), show_account_switcher: false, show_global_popup: true, diff --git a/src/key_storage.rs b/src/key_storage.rs index 0503dba..425020f 100644 --- a/src/key_storage.rs +++ b/src/key_storage.rs @@ -1,67 +1,88 @@ 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"; -pub enum KeyStorage { +#[derive(Debug, PartialEq)] +pub enum KeyStorageType { None, #[cfg(target_os = "macos")] MacOS, + #[cfg(target_os = "linux")] + Linux, // TODO: - // Linux, // Windows, // Android, } -impl KeyStorage { - pub fn get_keys(&self) -> Result, KeyStorageError> { +#[allow(dead_code)] +#[derive(Debug, PartialEq)] +pub enum KeyStorageResponse { + Waiting, + ReceivedResult(Result), +} + +pub trait KeyStorage { + fn get_keys(&self) -> KeyStorageResponse>; + fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()>; + fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()>; +} + +impl KeyStorage for KeyStorageType { + fn get_keys(&self) -> KeyStorageResponse> { match self { - Self::None => Ok(Vec::new()), + Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())), #[cfg(target_os = "macos")] - Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_keypairs()), + Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).get_keys(), + #[cfg(target_os = "linux")] + Self::Linux => LinuxKeyStorage::new().get_keys(), } } - pub fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> { let _ = key; match self { - Self::None => Ok(()), + 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), } } - pub fn remove_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> { let _ = key; match self { - Self::None => Ok(()), + Self::None => KeyStorageResponse::ReceivedResult(Ok(())), #[cfg(target_os = "macos")] - Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).delete_key(&key.pubkey), + 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, + Retrieval(String), Addition(String), Removal(String), - UnsupportedPlatform, + OSError(String), } impl std::fmt::Display for KeyStorageError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Self::Retrieval => write!(f, "Failed to retrieve keys."), + 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::UnsupportedPlatform => write!( - f, - "Attempted to use a key storage impl from an unsupported platform." - ), + Self::OSError(e) => write!(f, "OS had an error: {:?}", e), } } } diff --git a/src/lib.rs b/src/lib.rs index e43946e..3c87fea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ mod user_account; #[cfg(test)] #[macro_use] mod test_utils; +mod linux_key_storage; pub use app::Damus; pub use error::Error; diff --git a/src/linux_key_storage.rs b/src/linux_key_storage.rs new file mode 100644 index 0000000..4faa042 --- /dev/null +++ b/src/linux_key_storage.rs @@ -0,0 +1,210 @@ +#![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> { + 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 { + 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, KeyStorageError> { + let file_path = self.get_cred_dirpath()?; + let mut keys: Vec = 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> { + 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!(); + } +} diff --git a/src/macos_key_storage.rs b/src/macos_key_storage.rs index 5c96017..fdde618 100644 --- a/src/macos_key_storage.rs +++ b/src/macos_key_storage.rs @@ -5,7 +5,9 @@ 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::KeyStorageError; +use crate::key_storage::{KeyStorage, KeyStorageError, KeyStorageResponse}; + +use tracing::error; pub struct MacOSKeyStorage<'a> { pub service_name: &'a str, @@ -16,7 +18,7 @@ impl<'a> MacOSKeyStorage<'a> { MacOSKeyStorage { service_name } } - pub fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { match set_generic_password( self.service_name, key.pubkey.hex().as_str(), @@ -52,7 +54,7 @@ impl<'a> MacOSKeyStorage<'a> { accounts } - pub fn get_pubkeys(&self) -> Vec { + fn get_pubkeys(&self) -> Vec { self.get_pubkey_strings() .iter_mut() .filter_map(|pubkey_str| Pubkey::from_hex(pubkey_str.as_str()).ok()) @@ -84,7 +86,7 @@ impl<'a> MacOSKeyStorage<'a> { } } - pub fn get_all_keypairs(&self) -> Vec { + fn get_all_keypairs(&self) -> Vec { self.get_pubkeys() .iter() .map(|pubkey| { @@ -94,17 +96,31 @@ impl<'a> MacOSKeyStorage<'a> { .collect() } - pub fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> { + fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> { match delete_generic_password(self.service_name, pubkey.hex().as_str()) { Ok(_) => Ok(()), Err(e) => { - println!("got error: {}", e); + error!("delete key error {}", e); Err(KeyStorageError::Removal(pubkey.hex())) } } } } +impl<'a> KeyStorage for MacOSKeyStorage<'a> { + fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> { + KeyStorageResponse::ReceivedResult(self.add_key(key)) + } + + fn get_keys(&self) -> KeyStorageResponse> { + KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs())) + } + + fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> { + KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ui/account_login_view.rs b/src/ui/account_login_view.rs index 5e3d920..0b3531e 100644 --- a/src/ui/account_login_view.rs +++ b/src/ui/account_login_view.rs @@ -16,9 +16,8 @@ pub struct AccountLoginView<'a> { impl<'a> View for AccountLoginView<'a> { fn ui(&mut self, ui: &mut egui::Ui) { - if let Some(key) = self.manager.check_for_successful_login() { + if let Some(_key) = self.manager.check_for_successful_login() { // TODO: route to "home" - println!("successful login with key: {:?}", key); /* return if self.mobile { // route to "home" on mobile