Introducing Damus Notedeck: a nostr browser
This splits notedeck into: - notedeck - notedeck_chrome - notedeck_columns The `notedeck` crate is the library that `notedeck_chrome` and `notedeck_columns`, use. It contains common functionality related to notedeck apps such as the NoteCache, ImageCache, etc. The `notedeck_chrome` crate is the binary and ui chrome. It is responsible for managing themes, user accounts, signing, data paths, nostrdb, image caches etc. It will eventually have its own ui which has yet to be determined. For now it just manages the browser data, which is passed to apps via a new struct called `AppContext`. `notedeck_columns` is our columns app, with less responsibility now that more things are handled by `notedeck_chrome` There is still much work left to do before this is a proper browser: - process isolation - sandboxing - etc This is the beginning of a new era! We're just getting started. Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -15,7 +15,7 @@ use crate::{
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::{write_file, DataPath, DataPathType, Directory};
|
||||
use notedeck::{storage, DataPath, DataPathType, Directory};
|
||||
|
||||
pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn save_decks_cache(path: &DataPath, decks_cache: &DecksCache) {
|
||||
|
||||
let data_path = path.path(DataPathType::Setting);
|
||||
|
||||
if let Err(e) = write_file(
|
||||
if let Err(e) = storage::write_file(
|
||||
&data_path,
|
||||
DECKS_CACHE_FILE.to_string(),
|
||||
&serialized_decks_cache,
|
||||
@@ -761,12 +761,13 @@ impl fmt::Display for Selection {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enostr::Pubkey;
|
||||
//use enostr::Pubkey;
|
||||
|
||||
use crate::{route::Route, test_data::test_app, timeline::TimelineRoute};
|
||||
//use crate::{route::Route, timeline::TimelineRoute};
|
||||
|
||||
use super::deserialize_columns;
|
||||
//use super::deserialize_columns;
|
||||
|
||||
/* TODO: re-enable once we have test_app working again
|
||||
#[test]
|
||||
fn test_deserialize_columns() {
|
||||
let serialized = vec![
|
||||
@@ -800,4 +801,5 @@ mod tests {
|
||||
panic!("The second router route is not a TimelineRoute::Timeline variant");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
use eframe::Result;
|
||||
use enostr::{Keypair, Pubkey, SerializableKeypair};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
use super::{
|
||||
file_storage::{delete_file, write_file, Directory},
|
||||
key_storage_impl::{KeyStorageError, KeyStorageResponse},
|
||||
};
|
||||
|
||||
static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey";
|
||||
|
||||
/// An OS agnostic file key storage implementation
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct FileKeyStorage {
|
||||
keys_directory: Directory,
|
||||
selected_key_directory: Directory,
|
||||
}
|
||||
|
||||
impl FileKeyStorage {
|
||||
pub fn new(keys_directory: Directory, selected_key_directory: Directory) -> Self {
|
||||
Self {
|
||||
keys_directory,
|
||||
selected_key_directory,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||
write_file(
|
||||
&self.keys_directory.file_path,
|
||||
key.pubkey.hex(),
|
||||
&serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
|
||||
.map_err(|e| KeyStorageError::Addition(Error::Generic(e.to_string())))?,
|
||||
)
|
||||
.map_err(KeyStorageError::Addition)
|
||||
}
|
||||
|
||||
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
|
||||
let keys = self
|
||||
.keys_directory
|
||||
.get_files()
|
||||
.map_err(KeyStorageError::Retrieval)?
|
||||
.values()
|
||||
.filter_map(|str_key| serde_json::from_str::<SerializableKeypair>(str_key).ok())
|
||||
.map(|serializable_keypair| serializable_keypair.to_keypair(""))
|
||||
.collect();
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||
delete_file(&self.keys_directory.file_path, key.pubkey.hex())
|
||||
.map_err(KeyStorageError::Removal)
|
||||
}
|
||||
|
||||
fn get_selected_pubkey(&self) -> Result<Option<Pubkey>, KeyStorageError> {
|
||||
let pubkey_str = self
|
||||
.selected_key_directory
|
||||
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||
.map_err(KeyStorageError::Selection)?;
|
||||
|
||||
serde_json::from_str(&pubkey_str)
|
||||
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))
|
||||
}
|
||||
|
||||
fn select_pubkey(&self, pubkey: Option<Pubkey>) -> Result<(), KeyStorageError> {
|
||||
if let Some(pubkey) = pubkey {
|
||||
write_file(
|
||||
&self.selected_key_directory.file_path,
|
||||
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||
&serde_json::to_string(&pubkey.hex())
|
||||
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))?,
|
||||
)
|
||||
.map_err(KeyStorageError::Selection)
|
||||
} else if self
|
||||
.selected_key_directory
|
||||
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||
.is_ok()
|
||||
{
|
||||
// Case where user chose to have no selected pubkey, but one already exists
|
||||
delete_file(
|
||||
&self.selected_key_directory.file_path,
|
||||
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||
)
|
||||
.map_err(KeyStorageError::Selection)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileKeyStorage {
|
||||
pub fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
|
||||
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
|
||||
}
|
||||
|
||||
pub fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
|
||||
}
|
||||
|
||||
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||
KeyStorageResponse::ReceivedResult(self.get_selected_pubkey())
|
||||
}
|
||||
|
||||
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.select_pubkey(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use enostr::Keypair;
|
||||
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||
|
||||
impl FileKeyStorage {
|
||||
fn mock() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
keys_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||
selected_key_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||
let storage = FileKeyStorage::mock().unwrap();
|
||||
let resp = storage.add_key(&kp);
|
||||
|
||||
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
|
||||
assert_num_storage(&storage.get_keys(), 1);
|
||||
|
||||
assert_eq!(
|
||||
storage.remove_key(&kp),
|
||||
KeyStorageResponse::ReceivedResult(Ok(()))
|
||||
);
|
||||
assert_num_storage(&storage.get_keys(), 0);
|
||||
}
|
||||
|
||||
fn assert_num_storage(keys_response: &KeyStorageResponse<Vec<Keypair>>, n: usize) {
|
||||
match keys_response {
|
||||
KeyStorageResponse::ReceivedResult(Ok(keys)) => {
|
||||
assert_eq!(keys.len(), n);
|
||||
}
|
||||
KeyStorageResponse::ReceivedResult(Err(_e)) => {
|
||||
panic!("could not get keys");
|
||||
}
|
||||
KeyStorageResponse::Waiting => {
|
||||
panic!("did not receive result");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_key() {
|
||||
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||
|
||||
let storage = FileKeyStorage::mock().unwrap();
|
||||
let _ = storage.add_key(&kp);
|
||||
assert_num_storage(&storage.get_keys(), 1);
|
||||
|
||||
let resp = storage.select_pubkey(Some(kp.pubkey));
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let resp = storage.get_selected_pubkey();
|
||||
|
||||
assert!(resp.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::{self, File},
|
||||
io::{self, BufRead},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataPath {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl DataPath {
|
||||
pub fn new(base: impl AsRef<Path>) -> Self {
|
||||
let base = base.as_ref().to_path_buf();
|
||||
Self { base }
|
||||
}
|
||||
|
||||
pub fn default_base() -> Option<PathBuf> {
|
||||
dirs::data_local_dir().map(|pb| pb.join("notedeck"))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DataPathType {
|
||||
Log,
|
||||
Setting,
|
||||
Keys,
|
||||
SelectedKey,
|
||||
Db,
|
||||
Cache,
|
||||
}
|
||||
|
||||
impl DataPath {
|
||||
pub fn rel_path(&self, typ: DataPathType) -> PathBuf {
|
||||
match typ {
|
||||
DataPathType::Log => PathBuf::from("logs"),
|
||||
DataPathType::Setting => PathBuf::from("settings"),
|
||||
DataPathType::Keys => PathBuf::from("storage").join("accounts"),
|
||||
DataPathType::SelectedKey => PathBuf::from("storage").join("selected_account"),
|
||||
DataPathType::Db => PathBuf::from("db"),
|
||||
DataPathType::Cache => PathBuf::from("cache"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self, typ: DataPathType) -> PathBuf {
|
||||
self.base.join(self.rel_path(typ))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Directory {
|
||||
pub file_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
pub fn new(file_path: PathBuf) -> Self {
|
||||
Self { file_path }
|
||||
}
|
||||
|
||||
/// Get the files in the current directory where the key is the file name and the value is the file contents
|
||||
pub fn get_files(&self) -> Result<HashMap<String, String>, Error> {
|
||||
let dir = fs::read_dir(self.file_path.clone())?;
|
||||
let map = dir
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter_map(|f| {
|
||||
let file_name = f.file_name().into_string().ok()?;
|
||||
let contents = fs::read_to_string(f.path()).ok()?;
|
||||
Some((file_name, contents))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn get_file_names(&self) -> Result<Vec<String>, Error> {
|
||||
let dir = fs::read_dir(self.file_path.clone())?;
|
||||
let names = dir
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter_map(|f| f.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn get_file(&self, file_name: String) -> Result<String, Error> {
|
||||
let filepath = self.file_path.clone().join(file_name.clone());
|
||||
|
||||
if filepath.exists() && filepath.is_file() {
|
||||
let filepath_str = filepath
|
||||
.to_str()
|
||||
.ok_or_else(|| Error::Generic("Could not turn path to string".to_owned()))?;
|
||||
Ok(fs::read_to_string(filepath_str)?)
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result<FileResult, Error> {
|
||||
let filepath = self.file_path.clone().join(file_name.clone());
|
||||
|
||||
if filepath.exists() && filepath.is_file() {
|
||||
let file = File::open(&filepath)?;
|
||||
let reader = io::BufReader::new(file);
|
||||
|
||||
let mut queue: VecDeque<String> = VecDeque::with_capacity(n);
|
||||
|
||||
let mut total_lines_in_file = 0;
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
|
||||
queue.push_back(line);
|
||||
|
||||
if queue.len() > n {
|
||||
queue.pop_front();
|
||||
}
|
||||
total_lines_in_file += 1;
|
||||
}
|
||||
|
||||
let output_num_lines = queue.len();
|
||||
let output = queue.into_iter().collect::<Vec<String>>().join("\n");
|
||||
Ok(FileResult {
|
||||
output,
|
||||
output_num_lines,
|
||||
total_lines_in_file,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the file name which is most recently modified in the directory
|
||||
pub fn get_most_recent(&self) -> Result<Option<String>, Error> {
|
||||
let mut most_recent: Option<(SystemTime, String)> = None;
|
||||
|
||||
for entry in fs::read_dir(&self.file_path)? {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
if metadata.is_file() {
|
||||
let modified = metadata.modified()?;
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
match most_recent {
|
||||
Some((last_modified, _)) if modified > last_modified => {
|
||||
most_recent = Some((modified, file_name));
|
||||
}
|
||||
None => {
|
||||
most_recent = Some((modified, file_name));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(most_recent.map(|(_, file_name)| file_name))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileResult {
|
||||
pub output: String,
|
||||
pub output_num_lines: usize,
|
||||
pub total_lines_in_file: usize,
|
||||
}
|
||||
|
||||
/// Write the file to the directory
|
||||
pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> {
|
||||
if !directory.exists() {
|
||||
fs::create_dir_all(directory)?
|
||||
}
|
||||
|
||||
std::fs::write(directory.join(file_name), data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> {
|
||||
let file_to_delete = directory.join(file_name.clone());
|
||||
if file_to_delete.exists() && file_to_delete.is_file() {
|
||||
fs::remove_file(file_to_delete).map_err(Error::Io)
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file to delete was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
storage::file_storage::{delete_file, write_file},
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::Directory;
|
||||
|
||||
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||
|
||||
#[test]
|
||||
fn test_add_get_delete() {
|
||||
if let Ok(path) = CREATE_TMP_DIR() {
|
||||
let directory = Directory::new(path);
|
||||
let file_name = "file_test_name.txt".to_string();
|
||||
let file_contents = "test";
|
||||
let write_res = write_file(&directory.file_path, file_name.clone(), file_contents);
|
||||
assert!(write_res.is_ok());
|
||||
|
||||
if let Ok(asserted_file_contents) = directory.get_file(file_name.clone()) {
|
||||
assert_eq!(asserted_file_contents, file_contents);
|
||||
} else {
|
||||
panic!("File not found");
|
||||
}
|
||||
|
||||
let delete_res = delete_file(&directory.file_path, file_name);
|
||||
assert!(delete_res.is_ok());
|
||||
} else {
|
||||
panic!("could not get interactor")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_multiple() {
|
||||
if let Ok(path) = CREATE_TMP_DIR() {
|
||||
let directory = Directory::new(path);
|
||||
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
let write_res = write_file(&directory.file_path, file_name, "test");
|
||||
assert!(write_res.is_ok());
|
||||
}
|
||||
|
||||
if let Ok(files) = directory.get_files() {
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(files.contains_key(&file_name));
|
||||
assert_eq!(files.get(&file_name).unwrap(), "test");
|
||||
}
|
||||
} else {
|
||||
panic!("Files not found");
|
||||
}
|
||||
|
||||
if let Ok(file_names) = directory.get_file_names() {
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(file_names.contains(&file_name));
|
||||
}
|
||||
} else {
|
||||
panic!("File names not found");
|
||||
}
|
||||
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(delete_file(&directory.file_path, file_name).is_ok());
|
||||
}
|
||||
} else {
|
||||
panic!("could not get interactor")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use enostr::{Keypair, Pubkey};
|
||||
|
||||
use super::file_key_storage::FileKeyStorage;
|
||||
use crate::Error;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::security_framework_key_storage::SecurityFrameworkKeyStorage;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum KeyStorageType {
|
||||
None,
|
||||
FileSystem(FileKeyStorage),
|
||||
#[cfg(target_os = "macos")]
|
||||
SecurityFramework(SecurityFrameworkKeyStorage),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum KeyStorageResponse<R> {
|
||||
Waiting,
|
||||
ReceivedResult(Result<R, KeyStorageError>),
|
||||
}
|
||||
|
||||
impl<R: PartialEq> PartialEq for KeyStorageResponse<R> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(KeyStorageResponse::Waiting, KeyStorageResponse::Waiting) => true,
|
||||
(
|
||||
KeyStorageResponse::ReceivedResult(Ok(r1)),
|
||||
KeyStorageResponse::ReceivedResult(Ok(r2)),
|
||||
) => r1 == r2,
|
||||
(
|
||||
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||
) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorageType {
|
||||
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
|
||||
Self::FileSystem(f) => f.get_keys(),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.get_keys(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
let _ = key;
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.add_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.add_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
let _ = key;
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.remove_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.remove_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(None)),
|
||||
Self::FileSystem(f) => f.get_selected_key(),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.select_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum KeyStorageError {
|
||||
Retrieval(Error),
|
||||
Addition(Error),
|
||||
Selection(Error),
|
||||
Removal(Error),
|
||||
OSError(Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e),
|
||||
Self::Addition(key) => write!(f, "Failed to add key: {:?}", key),
|
||||
Self::Selection(pubkey) => write!(f, "Failed to select key: {:?}", pubkey),
|
||||
Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key),
|
||||
Self::OSError(e) => write!(f, "OS had an error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for KeyStorageError {}
|
||||
@@ -9,10 +9,10 @@ use crate::{
|
||||
route::Route,
|
||||
timeline::{kind::ListKind, PubkeySource, Timeline, TimelineId, TimelineKind, TimelineRoute},
|
||||
ui::add_column::AddColumnRoute,
|
||||
Error,
|
||||
Result,
|
||||
};
|
||||
|
||||
use super::{DataPath, DataPathType, Directory};
|
||||
use notedeck::{DataPath, DataPathType, Directory};
|
||||
|
||||
pub static COLUMNS_FILE: &str = "columns.json";
|
||||
|
||||
@@ -123,7 +123,7 @@ struct MigrationColumn {
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MigrationColumn {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
@@ -266,9 +266,11 @@ pub fn deserialize_columns(path: &DataPath, ndb: &Ndb, user: Option<&[u8; 32]>)
|
||||
string_to_columns(columns_json(path)?, ndb, user)
|
||||
}
|
||||
|
||||
fn deserialize_columns_string(serialized_columns: String) -> Result<MigrationColumns, Error> {
|
||||
serde_json::from_str::<MigrationColumns>(&serialized_columns)
|
||||
.map_err(|e| Error::Generic(e.to_string()))
|
||||
fn deserialize_columns_string(serialized_columns: String) -> Result<MigrationColumns> {
|
||||
Ok(
|
||||
serde_json::from_str::<MigrationColumns>(&serialized_columns)
|
||||
.map_err(notedeck::Error::Json)?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
mod decks;
|
||||
mod file_key_storage;
|
||||
mod file_storage;
|
||||
mod migration;
|
||||
|
||||
pub use decks::{load_decks_cache, save_decks_cache, DECKS_CACHE_FILE};
|
||||
pub use file_key_storage::FileKeyStorage;
|
||||
pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory};
|
||||
pub use migration::{deserialize_columns, COLUMNS_FILE};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod security_framework_key_storage;
|
||||
|
||||
pub mod key_storage_impl;
|
||||
pub use key_storage_impl::{KeyStorageResponse, KeyStorageType};
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
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<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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn get_all_keypairs(&self) -> Vec<Keypair> {
|
||||
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<Vec<Keypair>> {
|
||||
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<Keypair> = (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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user