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 { 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 { 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 } } fn get_all_keypairs(&self) -> Vec { 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> { 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 = (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); } }