feat: implement multiple keystores (#187)
* keystore * . * fix * . * allow user disable keyring * update texts
This commit is contained in:
@@ -19,6 +19,9 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
flume.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
fuzzy-matcher = "0.3.7"
|
||||
rustls = "0.23.23"
|
||||
|
||||
191
crates/registry/src/keystore.rs
Normal file
191
crates/registry/src/keystore.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::AsyncApp;
|
||||
use states::paths::config_dir;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KeyItem {
|
||||
User,
|
||||
Bunker,
|
||||
Client,
|
||||
Encryption,
|
||||
}
|
||||
|
||||
impl Display for KeyItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::User => write!(f, "coop-user"),
|
||||
Self::Bunker => write!(f, "coop-bunker"),
|
||||
Self::Client => write!(f, "coop-client"),
|
||||
Self::Encryption => write!(f, "coop-encryption"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyItem> for String {
|
||||
fn from(item: KeyItem) -> Self {
|
||||
item.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait KeyStore: Any + Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Reads the credentials from the provider.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
|
||||
|
||||
/// Writes the credentials to the provider.
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
|
||||
/// Deletes the credentials from the provider.
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
}
|
||||
|
||||
/// A credentials provider that stores credentials in the system keychain.
|
||||
pub struct KeyringProvider;
|
||||
|
||||
impl KeyStore for KeyringProvider {
|
||||
fn name(&self) -> &str {
|
||||
"keyring"
|
||||
}
|
||||
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||
async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
cx.update(move |cx| cx.write_credentials(url, username, password))?
|
||||
.await
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
/// A credentials provider that stores credentials in a local file.
|
||||
pub struct FileProvider {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileProvider {
|
||||
pub fn new() -> Self {
|
||||
let path = config_dir().join(".keys");
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
|
||||
let json = std::fs::read(&self.path)?;
|
||||
let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
pub fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
|
||||
let json = serde_json::to_string(credentials)?;
|
||||
std::fs::write(&self.path, json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStore for FileProvider {
|
||||
fn name(&self) -> &str {
|
||||
"file"
|
||||
}
|
||||
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||
async move {
|
||||
Ok(self
|
||||
.load_credentials()
|
||||
.unwrap_or_default()
|
||||
.get(url)
|
||||
.cloned())
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
let mut credentials = self.load_credentials().unwrap_or_default();
|
||||
credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
|
||||
|
||||
self.save_credentials(&credentials)
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
let mut credentials = self.load_credentials()?;
|
||||
credentials.remove(url);
|
||||
|
||||
self.save_credentials(&credentials)
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::Error;
|
||||
use common::event::EventUtils;
|
||||
@@ -14,13 +15,19 @@ use room::RoomKind;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::KEYRING_URL;
|
||||
use states::state::UnwrappingStatus;
|
||||
|
||||
use crate::keystore::{FileProvider, KeyStore, KeyringProvider};
|
||||
use crate::room::Room;
|
||||
|
||||
pub mod keystore;
|
||||
pub mod message;
|
||||
pub mod room;
|
||||
|
||||
pub static DISABLE_KEYRING: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
Registry::set_global(cx.new(Registry::new), cx);
|
||||
}
|
||||
@@ -36,7 +43,6 @@ pub enum RegistryEvent {
|
||||
NewRequest(RoomKind),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
/// Collection of all chat rooms
|
||||
pub rooms: Vec<Entity<Room>>,
|
||||
@@ -47,11 +53,17 @@ pub struct Registry {
|
||||
/// Status of the unwrapping process
|
||||
pub unwrapping_status: Entity<UnwrappingStatus>,
|
||||
|
||||
/// Key Store for storing credentials
|
||||
pub keystore: Arc<dyn KeyStore>,
|
||||
|
||||
/// Whether the keystore has been initialized
|
||||
pub initialized_keystore: bool,
|
||||
|
||||
/// Public Key of the currently activated signer
|
||||
signer_pubkey: Option<PublicKey>,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
_tasks: SmallVec<[Task<()>; 2]>,
|
||||
}
|
||||
|
||||
impl EventEmitter<RegistryEvent> for Registry {}
|
||||
@@ -75,8 +87,38 @@ impl Registry {
|
||||
/// Create a new registry instance
|
||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
|
||||
let read_credential = cx.read_credentials(KEYRING_URL);
|
||||
let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
|
||||
let keystore: Arc<dyn KeyStore> = if cfg!(debug_assertions) || *DISABLE_KEYRING {
|
||||
Arc::new(FileProvider::default())
|
||||
} else {
|
||||
Arc::new(KeyringProvider)
|
||||
};
|
||||
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
if !(cfg!(debug_assertions) || *DISABLE_KEYRING) {
|
||||
tasks.push(
|
||||
// Verify the keyring access
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = read_credential.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Err(e) = result {
|
||||
log::error!("Keyring error: {e}");
|
||||
// For Linux:
|
||||
// The user has not installed secret service on their system
|
||||
// Fall back to the file provider
|
||||
this.keystore = Arc::new(FileProvider::default());
|
||||
}
|
||||
this.initialized_keystore = true;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
tasks.push(
|
||||
// Load all user profiles from the database
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -96,6 +138,8 @@ impl Registry {
|
||||
|
||||
Self {
|
||||
unwrapping_status,
|
||||
keystore,
|
||||
initialized_keystore,
|
||||
rooms: vec![],
|
||||
persons: HashMap::new(),
|
||||
signer_pubkey: None,
|
||||
@@ -122,6 +166,16 @@ impl Registry {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the keystore.
|
||||
pub fn keystore(&self) -> Arc<dyn KeyStore> {
|
||||
Arc::clone(&self.keystore)
|
||||
}
|
||||
|
||||
/// Returns true if the keystore is a file keystore.
|
||||
pub fn is_using_file_keystore(&self) -> bool {
|
||||
self.keystore.name() == "file"
|
||||
}
|
||||
|
||||
/// Returns the public key of the currently activated signer.
|
||||
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
||||
self.signer_pubkey
|
||||
|
||||
Reference in New Issue
Block a user