pub mod file;
use crate::{config::secured_config::service_name, errors::OpenVTCError};
pub const BACKEND_ATTR: &str = "openvtc-backend";
pub const BACKEND_FILE: &str = "file";
use keyring_core::{Entry, api::CredentialPersistence};
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Durability {
Durable,
UntilLogout,
UntilReboot,
UntilExit,
Unknown,
}
impl Durability {
#[must_use]
pub fn is_volatile(&self) -> bool {
matches!(
self,
Durability::UntilLogout | Durability::UntilReboot | Durability::UntilExit
)
}
#[must_use]
pub fn lifetime_phrase(&self) -> &'static str {
match self {
Durability::Durable => "kept until deleted",
Durability::UntilLogout => "lost when you log out",
Durability::UntilReboot => "lost when this machine reboots",
Durability::UntilExit => "lost when OpenVTC exits",
Durability::Unknown => "lifetime not reported by the store",
}
}
}
impl From<CredentialPersistence> for Durability {
fn from(p: CredentialPersistence) -> Self {
match p {
CredentialPersistence::UntilDelete => Durability::Durable,
CredentialPersistence::UntilLogout => Durability::UntilLogout,
CredentialPersistence::UntilReboot => Durability::UntilReboot,
CredentialPersistence::ProcessOnly | CredentialPersistence::EntryOnly => {
Durability::UntilExit
}
_ => Durability::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct StoreDescription {
pub label: String,
pub location: String,
pub durability: Durability,
pub inspect_hint: Option<String>,
}
static ACTIVE: OnceLock<StoreDescription> = OnceLock::new();
pub fn record_active(description: StoreDescription) {
let _ = ACTIVE.set(description);
}
#[must_use]
pub fn describe_active() -> Option<&'static StoreDescription> {
ACTIVE.get()
}
#[derive(Debug, Clone)]
pub struct EntryProbe {
pub status: EntryStatus,
pub durability: Durability,
pub path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntryStatus {
Present,
Missing,
Unavailable(String),
}
#[must_use]
pub fn probe(profile: &str) -> EntryProbe {
let store_durability = keyring_core::get_default_store()
.map_or(Durability::Unknown, |s| Durability::from(s.persistence()));
let entry = match Entry::new(service_name(), profile) {
Ok(entry) => entry,
Err(e) => {
return EntryProbe {
status: EntryStatus::Unavailable(e.to_string()),
durability: store_durability,
path: None,
};
}
};
match entry.get_attributes() {
Ok(attrs) => {
let durability = match attrs.get(BACKEND_ATTR).map(String::as_str) {
Some(BACKEND_FILE) => Durability::Durable,
_ => store_durability,
};
EntryProbe {
status: EntryStatus::Present,
durability,
path: attrs.get("path").cloned(),
}
}
Err(keyring_core::Error::NoEntry) => EntryProbe {
status: EntryStatus::Missing,
durability: store_durability,
path: None,
},
Err(e) => EntryProbe {
status: EntryStatus::Unavailable(e.to_string()),
durability: store_durability,
path: None,
},
}
}
pub fn secrets_dir(profile: &str) -> Result<std::path::PathBuf, OpenVTCError> {
Ok(crate::config::public_config::profile_dir(profile)?.join("secrets"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_default_store_is_durable() {
assert!(
!Durability::from(CredentialPersistence::UntilDelete).is_volatile(),
"a default store must be durable"
);
assert!(Durability::from(CredentialPersistence::UntilReboot).is_volatile());
}
#[test]
fn volatile_classification_matches_lifetime() {
assert!(!Durability::Durable.is_volatile());
assert!(Durability::UntilReboot.is_volatile());
assert!(Durability::UntilLogout.is_volatile());
assert!(Durability::UntilExit.is_volatile());
assert!(!Durability::Unknown.is_volatile());
}
#[test]
fn persistence_maps_to_durability() {
assert_eq!(
Durability::from(CredentialPersistence::UntilDelete),
Durability::Durable
);
assert_eq!(
Durability::from(CredentialPersistence::UntilReboot),
Durability::UntilReboot
);
assert_eq!(
Durability::from(CredentialPersistence::Unspecified),
Durability::Unknown
);
}
}