Skip to content

Commit

Permalink
Merge MacOS key storage
Browse files Browse the repository at this point in the history
kernelkind (3):
      Add MacOS key storage
      Conditionally compile MacOS key storage code
      macos_key_storage: runner ignore tests
  • Loading branch information
jb55 committed May 23, 2024
2 parents 83100d7 + adc1d25 commit c421a49
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 12 deletions.
11 changes: 6 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ bitflags = "2.5.0"
egui_virtual_list = "0.3.0"
#egui_virtual_list = { path = "/home/jb55/dev/github/lucasmerlin/hello_egui/crates/egui_virtual_list" }

[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.11.0"


[features]
default = []
Expand Down
33 changes: 26 additions & 7 deletions src/key_storage.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,39 +20,50 @@ impl KeyStorage {
pub fn get_keys(&self) -> Result<Vec<FullKeypair>, KeyStorageError> {
match self {
Self::None => Ok(Vec::new()),
#[cfg(target_os = "macos")]
Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_fullkeypairs()),
}
}

pub fn add_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> {
let _ = key;
match self {
Self::None => Ok(()),
#[cfg(target_os = "macos")]
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).add_key(key),
}
}

pub fn remove_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> {
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 {}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
171 changes: 171 additions & 0 deletions src/macos_key_storage.rs
Original file line number Diff line number Diff line change
@@ -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<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
}

pub 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
}
}

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

0 comments on commit c421a49

Please sign in to comment.