diff --git a/Cargo.lock b/Cargo.lock index ad29c0c8..4981eb13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,27 +436,12 @@ dependencies = [ "serde", ] -[[package]] -name = "bitcoin-private" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" - [[package]] name = "bitcoin_hashes" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" -[[package]] -name = "bitcoin_hashes" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" -dependencies = [ - "bitcoin-private", -] - [[package]] name = "bitcoin_hashes" version = "0.13.0" @@ -2468,6 +2453,7 @@ dependencies = [ "puffin", "puffin_egui", "reqwest", + "security-framework", "serde", "serde_derive", "serde_json", @@ -3404,7 +3390,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ - "bitcoin_hashes 0.12.0", + "bitcoin_hashes 0.13.0", "rand", "secp256k1-sys", "serde", @@ -3421,11 +3407,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -3434,9 +3420,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", diff --git a/Cargo.toml b/Cargo.toml index 97f29002..09799703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,9 @@ strum_macros = "0.26" bitflags = "2.5.0" egui_virtual_list = "0.3.0" +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "2.11.0" + [features] default = [] diff --git a/src/key_storage.rs b/src/key_storage.rs index 7b6fb823..06d18141 100644 --- a/src/key_storage.rs +++ b/src/key_storage.rs @@ -1,7 +1,15 @@ use enostr::FullKeypair; +#[cfg(target_os = "macos")] +use crate::macos_key_storage::MacOSKeyStorage; + +#[cfg(target_os = "macos")] +pub const SERVICE_NAME: &str = "Notedeck"; + pub enum KeyStorage { None, + #[cfg(target_os = "macos")] + MacOS, // TODO: // Linux, // Windows, @@ -12,6 +20,8 @@ impl KeyStorage { pub fn get_keys(&self) -> Result, KeyStorageError> { match self { Self::None => Ok(Vec::new()), + #[cfg(target_os = "macos")] + Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_fullkeypairs()), } } @@ -19,6 +29,8 @@ impl KeyStorage { let _ = key; match self { Self::None => Ok(()), + #[cfg(target_os = "macos")] + Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).add_key(key), } } @@ -26,25 +38,32 @@ impl KeyStorage { let _ = key; match self { Self::None => Ok(()), + #[cfg(target_os = "macos")] + Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).delete_key(&key.pubkey), } } } #[derive(Debug, PartialEq)] -pub enum KeyStorageError<'a> { +pub enum KeyStorageError { Retrieval, - Addition(&'a FullKeypair), - Removal(&'a FullKeypair), + Addition(String), + Removal(String), + UnsupportedPlatform, } -impl std::fmt::Display for KeyStorageError<'_> { +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::Addition(key) => write!(f, "Failed to add key: {:?}", key.pubkey), - Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key.pubkey), + 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." + ), } } } -impl std::error::Error for KeyStorageError<'_> {} +impl std::error::Error for KeyStorageError {} diff --git a/src/lib.rs b/src/lib.rs index 2a36d0fa..bb33cb64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ mod imgcache; mod key_parsing; mod key_storage; pub mod login_manager; +mod macos_key_storage; mod notecache; mod profile; mod relay_generation; diff --git a/src/macos_key_storage.rs b/src/macos_key_storage.rs new file mode 100644 index 00000000..e47a1c25 --- /dev/null +++ b/src/macos_key_storage.rs @@ -0,0 +1,171 @@ +#![cfg(target_os = "macos")] + +use enostr::{FullKeypair, 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; + +pub struct MacOSKeyStorage<'a> { + pub service_name: &'a str, +} + +impl<'a> MacOSKeyStorage<'a> { + pub fn new(service_name: &'a str) -> Self { + MacOSKeyStorage { service_name } + } + + pub fn add_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> { + match set_generic_password( + self.service_name, + key.pubkey.hex().as_str(), + key.secret_key.as_secret_bytes(), + ) { + Ok(_) => Ok(()), + Err(_) => Err(KeyStorageError::Addition(key.pubkey.hex())), + } + } + + fn get_pubkey_strings(&self) -> Vec { + 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 + } + + pub fn get_pubkeys(&self) -> Vec { + 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> { + 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 { + if let Some(bytes) = self.get_privkey_bytes_for(pubkey.hex().as_str()) { + SecretKey::from_slice(bytes.as_slice()).ok() + } else { + None + } + } + + pub fn get_all_fullkeypairs(&self) -> Vec { + self.get_pubkeys() + .iter() + .filter_map(|pubkey| { + let maybe_secret = self.get_secret_key_for_pubkey(pubkey); + maybe_secret.map(|secret| FullKeypair::new(pubkey.clone(), secret)) + }) + .collect() + } + + pub 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); + Err(KeyStorageError::Removal(pubkey.hex())) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + static TEST_SERVICE_NAME: &str = "NOTEDECKTEST"; + static STORAGE: MacOSKeyStorage = MacOSKeyStorage { + service_name: 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(); + let add_result = STORAGE.add_key(&keypair); + assert_eq!(add_result, 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(())); + + 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_fullkeypairs().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 = (0..n).map(|_| FullKeypair::generate()).collect(); + + expected_keypairs.iter().for_each(|keypair| { + let add_result = STORAGE.add_key(keypair); + assert_eq!(add_result, Ok(())); + }); + + let asserted_keypairs = STORAGE.get_all_fullkeypairs(); + assert_eq!(expected_keypairs, asserted_keypairs); + + expected_keypairs.iter().for_each(|keypair| { + let remove_result = STORAGE.delete_key(&keypair.pubkey); + assert_eq!(remove_result, Ok(())); + }); + + let num_keys_after_test = STORAGE.get_all_fullkeypairs().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); + } +}