Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/secrets/collection.rs

193 lines
6.2 KiB
Rust

//! `org.freedesktop.Secret.Collection` — a single fixed collection at
//! `/org/freedesktop/secrets/collection/souveraine`.
//!
//! Real secret-service daemons support multiple named collections
//! (keyrings). We don't: every secret libsecret clients store goes into one
//! collection backed by `SecretStore`, encrypted under the machine-derived
//! key. Adding real multi-collection support is meaningful added complexity
//! for a feature no caller in this codebase (Chatty, keyring-core, the
//! player, culver) needs — this can grow a real implementation later if
//! something actually requires it.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedObjectPath, OwnedValue};
use crate::error::SecretError;
use crate::item::Item;
use crate::session::Sessions;
use crate::store::SecretStore;
use crate::types::Secret;
pub const COLLECTION_PATH: &str = "/org/freedesktop/secrets/collection/souveraine";
/// The spec's well-known alias object path — libsecret talks to the default
/// collection through this path directly, so the same interface is served
/// there too.
pub const DEFAULT_ALIAS_PATH: &str = "/org/freedesktop/secrets/aliases/default";
const NULL_PATH: &str = "/";
pub struct Collection {
pub store: Arc<Mutex<SecretStore>>,
pub sessions: Arc<Sessions>,
}
#[interface(name = "org.freedesktop.Secret.Collection")]
impl Collection {
/// Refuse rather than pretend — there is exactly one collection and it
/// isn't going away while the daemon that backs libsecret is running.
async fn delete(&self) -> zbus::fdo::Result<OwnedObjectPath> {
Err(zbus::fdo::Error::NotSupported(
"the souveraine collection cannot be deleted".into(),
))
}
async fn search_items(
&self,
attributes: HashMap<String, String>,
) -> zbus::fdo::Result<Vec<OwnedObjectPath>> {
let store = self.store.lock().unwrap();
Ok(store
.search(&attributes)
.iter()
.map(|id| item_path(id).try_into().unwrap())
.collect())
}
async fn create_item(
&self,
properties: HashMap<String, OwnedValue>,
secret: Secret,
replace: bool,
#[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
) -> Result<(OwnedObjectPath, OwnedObjectPath), SecretError> {
let label = properties
.get("org.freedesktop.Secret.Item.Label")
.and_then(|v| String::try_from(v.clone()).ok())
.unwrap_or_default();
let attributes: HashMap<String, String> = properties
.get("org.freedesktop.Secret.Item.Attributes")
.and_then(|v| HashMap::<String, String>::try_from(v.clone()).ok())
.unwrap_or_default();
let plaintext = self
.sessions
.unseal(&secret.session, &secret.parameters, &secret.value)
.ok_or_else(|| SecretError::NoSession(format!("no such session: {}", secret.session)))?
.map_err(|e| SecretError::failed(format!("failed to unseal secret: {e}")))?;
let existing_id = if replace {
let store = self.store.lock().unwrap();
store.search(&attributes).into_iter().next()
} else {
None
};
let is_new = existing_id.is_none();
// Hyphen-less: the id is a D-Bus object path segment, where `-` is
// an invalid character.
let id = existing_id.unwrap_or_else(|| Uuid::new_v4().simple().to_string());
{
let mut store = self.store.lock().unwrap();
store
.set(
id.clone(),
label,
attributes,
secret.content_type.clone(),
&plaintext,
)
.map_err(|e| SecretError::failed(format!("failed to store item: {e}")))?;
}
let path = item_path(&id);
if is_new {
emitter
.connection()
.object_server()
.at(
path.clone(),
Item {
id: id.clone(),
store: self.store.clone(),
sessions: self.sessions.clone(),
},
)
.await
.map_err(|e| SecretError::failed(format!("failed to register item object: {e}")))?;
}
let item: OwnedObjectPath = path.try_into().unwrap();
if is_new {
let _ = Self::item_created(&emitter, item.clone()).await;
} else {
let _ = Self::item_changed(&emitter, item.clone()).await;
}
let prompt: OwnedObjectPath = NULL_PATH.try_into().unwrap();
Ok((item, prompt))
}
#[zbus(signal)]
pub async fn item_created(
emitter: &SignalEmitter<'_>,
item: OwnedObjectPath,
) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn item_deleted(
emitter: &SignalEmitter<'_>,
item: OwnedObjectPath,
) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn item_changed(
emitter: &SignalEmitter<'_>,
item: OwnedObjectPath,
) -> zbus::Result<()>;
#[zbus(property)]
async fn items(&self) -> Vec<OwnedObjectPath> {
let store = self.store.lock().unwrap();
store
.all_ids()
.iter()
.map(|id| item_path(id).try_into().unwrap())
.collect()
}
#[zbus(property)]
async fn label(&self) -> String {
"souveraine".to_string()
}
#[zbus(property)]
async fn set_label(&self, _value: String) -> zbus::Result<()> {
// Single fixed collection — relabeling is a no-op, not an error;
// clients that just set the label they already read shouldn't fail.
Ok(())
}
#[zbus(property)]
async fn locked(&self) -> bool {
false
}
#[zbus(property)]
async fn created(&self) -> u64 {
self.store.lock().unwrap().collection_created()
}
#[zbus(property)]
async fn modified(&self) -> u64 {
self.store.lock().unwrap().collection_modified()
}
}
pub fn item_path(id: &str) -> String {
format!("{COLLECTION_PATH}/{id}")
}