[PATCH notedeck] Add MacOS key storage

2 views
Skip to first unread message

kernelkind

unread,
May 21, 2024, 7:53:58 PMMay 21
to pat...@damus.io, kernelkind
Signed-off-by: kernelkind <kerne...@gmail.com>
---
Cargo.lock | 28 ++-----
Cargo.toml | 1 +
src/key_storage.rs | 22 ++++--
src/lib.rs | 1 +
src/macos_key_storage.rs | 162 +++++++++++++++++++++++++++++++++++++++
5 files changed, 186 insertions(+), 28 deletions(-)
create mode 100644 src/macos_key_storage.rs

diff --git a/Cargo.lock b/Cargo.lock
index ad29c0c86daa..4981eb136dd6 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 97f290023657..f3a513dba23e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,6 +41,7 @@ strum = "0.26"
strum_macros = "0.26"
bitflags = "2.5.0"
egui_virtual_list = "0.3.0"
+security-framework = "2.11.0"


[features]
diff --git a/src/key_storage.rs b/src/key_storage.rs
index 7b6fb8234340..795aa62f2ae4 100644
--- a/src/key_storage.rs
+++ b/src/key_storage.rs
@@ -1,7 +1,12 @@
use enostr::FullKeypair;

+use crate::macos_key_storage::MacOSKeyStorage;
+
+pub const SERVICE_NAME: &str = "Notedeck";
+
pub enum KeyStorage {
None,
+ MacOS,
// TODO:
// Linux,
// Windows,
@@ -12,6 +17,7 @@ impl KeyStorage {
pub fn get_keys(&self) -> Result<Vec<FullKeypair>, KeyStorageError> {
match self {
Self::None => Ok(Vec::new()),
+ Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_fullkeypairs()),
}
}

@@ -19,6 +25,7 @@ impl KeyStorage {
let _ = key;
match self {
Self::None => Ok(()),
+ Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).add_key(key),
}
}

@@ -26,25 +33,26 @@ impl KeyStorage {
let _ = key;
match self {
Self::None => Ok(()),
+ 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),
}

-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),
}
}
}

-impl std::error::Error for KeyStorageError<'_> {}
+impl std::error::Error for KeyStorageError {}
diff --git a/src/lib.rs b/src/lib.rs
index 2a36d0faefba..bb33cb6402c3 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 000000000000..387ff87da4ef
--- /dev/null
+++ b/src/macos_key_storage.rs
@@ -0,0 +1,162 @@
+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,
+ };
+
+ #[test]
+ 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]
+ fn add_and_remove_full() {
+ add_and_remove_full_n(1);
+ }
+
+ #[test]
+ fn add_and_remove_full_10() {
+ add_and_remove_full_n(10);
+ }
+}
--
2.39.3 (Apple Git-146)

William Casarin

unread,
May 23, 2024, 12:55:24 PMMay 23
to kernelkind, pat...@damus.io
On Tue, May 21, 2024 at 07:53:48PM GMT, kernelkind wrote:
>Signed-off-by: kernelkind <kerne...@gmail.com>
>---
> Cargo.lock | 28 ++-----
> Cargo.toml | 1 +
> src/key_storage.rs | 22 ++++--
> src/lib.rs | 1 +
> src/macos_key_storage.rs | 162 +++++++++++++++++++++++++++++++++++++++
> 5 files changed, 186 insertions(+), 28 deletions(-)
> create mode 100644 src/macos_key_storage.rs
>
>@@ -436,27 +436,12 @@ dependencies = [
>diff --git a/Cargo.toml b/Cargo.toml
>index 97f290023657..f3a513dba23e 100644
>--- a/Cargo.toml
>+++ b/Cargo.toml
>@@ -41,6 +41,7 @@ strum = "0.26"
> strum_macros = "0.26"
> bitflags = "2.5.0"
> egui_virtual_list = "0.3.0"
>+security-framework = "2.11.0"

I think we may need to make this a darwin-only dependency?

Getting some compile errors on linux:

error[E0432]: unresolved import `security_framework::item`
--> src/macos_key_storage.rs:2:25
|
2 | use security_framework::item::{ItemClass, ItemSearchOptions, Limit, SearchResult};
| ^^^^ could not find `item` in `security_framework`

kernelkind

unread,
May 23, 2024, 1:50:45 PMMay 23
to William Casarin, pat...@damus.io
I think we may need to make this a darwin-only dependency?


Hah, yes of course. I'll add it in v2 

kernelkind

unread,
May 23, 2024, 4:13:37 PMMay 23
to patches, kernelkind, pat...@damus.io, William Casarin
Reply all
Reply to author
Forward
0 new messages