mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-31 23:14:23 +01:00
Merge MacOS key storage
kernelkind (3):
Add MacOS key storage
Conditionally compile MacOS key storage code
macos_key_storage: runner ignore tests
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -2452,6 +2452,7 @@ dependencies = [
|
||||
"puffin",
|
||||
"puffin_egui",
|
||||
"reqwest",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -3405,11 +3406,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",
|
||||
@@ -3418,9 +3419,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",
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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<Vec<FullKeypair>, 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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
171
src/macos_key_storage.rs
Normal file
171
src/macos_key_storage.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user