218 lines
7.3 KiB
Rust
218 lines
7.3 KiB
Rust
//! `org.freedesktop.Secret.Service` — the root object at
|
|
//! `/org/freedesktop/secrets`.
|
|
//!
|
|
//! Everything is permanently unlocked: the seed identity is resident in this
|
|
//! process the whole time it runs, so there is no separate "unlock the
|
|
//! keyring" step the way there is for a passphrase-based store. `Unlock`
|
|
//! always succeeds immediately with no prompt.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use num_bigint::BigUint;
|
|
use tracing::warn;
|
|
use zbus::interface;
|
|
use zbus::object_server::SignalEmitter;
|
|
use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value};
|
|
|
|
use crate::dh::DhSession;
|
|
use crate::error::SecretError;
|
|
use crate::session::{SessionCrypto, Sessions};
|
|
use crate::session_object::Session;
|
|
use crate::store::SecretStore;
|
|
use crate::types::Secret;
|
|
|
|
const COLLECTION_PATH: &str = "/org/freedesktop/secrets/collection/souveraine";
|
|
const NULL_PATH: &str = "/";
|
|
|
|
pub struct SecretService {
|
|
pub store: Arc<Mutex<SecretStore>>,
|
|
pub sessions: Arc<Sessions>,
|
|
}
|
|
|
|
#[interface(name = "org.freedesktop.Secret.Service")]
|
|
impl SecretService {
|
|
async fn open_session(
|
|
&self,
|
|
algorithm: String,
|
|
input: OwnedValue,
|
|
#[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
|
|
) -> Result<(OwnedValue, OwnedObjectPath), SecretError> {
|
|
let path = self.sessions.next_path();
|
|
|
|
let out = match algorithm.as_str() {
|
|
"plain" => {
|
|
self.sessions.insert(path.clone(), SessionCrypto::Plain);
|
|
OwnedValue::try_from(Value::from("")).unwrap()
|
|
}
|
|
"dh-ietf1024-sha256-aes128-cbc-pkcs7" => {
|
|
let peer_public_bytes: Vec<u8> = Vec::<u8>::try_from(input).map_err(|_| {
|
|
SecretError::failed(
|
|
"OpenSession input must be the client's DH public value as bytes",
|
|
)
|
|
})?;
|
|
let peer_public = BigUint::from_bytes_be(&peer_public_bytes);
|
|
|
|
let dh = DhSession::new()
|
|
.map_err(|e| SecretError::failed(format!("DH session setup failed: {e}")))?;
|
|
let key = dh
|
|
.derive_session_key(&peer_public)
|
|
.map_err(|e| SecretError::failed(format!("DH key derivation failed: {e}")))?;
|
|
|
|
self.sessions
|
|
.insert(path.clone(), SessionCrypto::Aes { key });
|
|
|
|
let our_public_bytes = dh.public.to_bytes_be();
|
|
OwnedValue::try_from(Value::from(our_public_bytes)).unwrap()
|
|
}
|
|
other => {
|
|
return Err(SecretError::failed(format!(
|
|
"unsupported OpenSession algorithm: {other}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
emitter
|
|
.connection()
|
|
.object_server()
|
|
.at(
|
|
path.clone(),
|
|
Session {
|
|
path: path.clone(),
|
|
sessions: self.sessions.clone(),
|
|
},
|
|
)
|
|
.await
|
|
.map_err(|e| SecretError::failed(format!("failed to register session object: {e}")))?;
|
|
|
|
Ok((out, path))
|
|
}
|
|
|
|
async fn create_collection(
|
|
&self,
|
|
_properties: HashMap<String, OwnedValue>,
|
|
_alias: String,
|
|
) -> zbus::fdo::Result<(OwnedObjectPath, OwnedObjectPath)> {
|
|
// Single fixed collection — see Collection docs for why.
|
|
let path: OwnedObjectPath = COLLECTION_PATH.try_into().unwrap();
|
|
let prompt: OwnedObjectPath = NULL_PATH.try_into().unwrap();
|
|
Ok((path, prompt))
|
|
}
|
|
|
|
async fn search_items(
|
|
&self,
|
|
attributes: HashMap<String, String>,
|
|
) -> zbus::fdo::Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>)> {
|
|
let store = self.store.lock().unwrap();
|
|
let ids = store.search(&attributes);
|
|
let unlocked: Vec<OwnedObjectPath> = ids
|
|
.iter()
|
|
.map(|id| item_path(id).try_into().unwrap())
|
|
.collect();
|
|
Ok((unlocked, Vec::new()))
|
|
}
|
|
|
|
/// Everything is already unlocked (the seed never locks). Report success
|
|
/// immediately with no prompt.
|
|
async fn unlock(
|
|
&self,
|
|
objects: Vec<OwnedObjectPath>,
|
|
) -> zbus::fdo::Result<(Vec<OwnedObjectPath>, OwnedObjectPath)> {
|
|
let prompt: OwnedObjectPath = NULL_PATH.try_into().unwrap();
|
|
Ok((objects, prompt))
|
|
}
|
|
|
|
/// There is nothing to lock — refuse the concept rather than pretend.
|
|
async fn lock(
|
|
&self,
|
|
_objects: Vec<OwnedObjectPath>,
|
|
) -> zbus::fdo::Result<(Vec<OwnedObjectPath>, OwnedObjectPath)> {
|
|
let prompt: OwnedObjectPath = NULL_PATH.try_into().unwrap();
|
|
Ok((Vec::new(), prompt))
|
|
}
|
|
|
|
async fn get_secrets(
|
|
&self,
|
|
items: Vec<OwnedObjectPath>,
|
|
session: OwnedObjectPath,
|
|
) -> Result<HashMap<OwnedObjectPath, Secret>, SecretError> {
|
|
let store = self.store.lock().unwrap();
|
|
let mut out = HashMap::new();
|
|
|
|
for item_obj_path in items {
|
|
let Some(id) = id_from_item_path(&item_obj_path) else {
|
|
continue;
|
|
};
|
|
let Ok(Some(plaintext)) = store.get(id) else {
|
|
continue;
|
|
};
|
|
let content_type = store
|
|
.content_type(id)
|
|
.unwrap_or_else(|| "text/plain".to_string());
|
|
let Some((parameters, value)) = self.sessions.seal(&session, &plaintext) else {
|
|
return Err(SecretError::NoSession(format!(
|
|
"no such session: {session}"
|
|
)));
|
|
};
|
|
out.insert(
|
|
item_obj_path,
|
|
Secret {
|
|
session: session.clone(),
|
|
parameters,
|
|
value,
|
|
content_type,
|
|
},
|
|
);
|
|
}
|
|
|
|
Ok(out)
|
|
}
|
|
|
|
// The spec's collection lifecycle signals. With one fixed collection they
|
|
// never fire, but clients that introspect for them must find them.
|
|
#[zbus(signal)]
|
|
pub async fn collection_created(
|
|
emitter: &SignalEmitter<'_>,
|
|
collection: OwnedObjectPath,
|
|
) -> zbus::Result<()>;
|
|
|
|
#[zbus(signal)]
|
|
pub async fn collection_deleted(
|
|
emitter: &SignalEmitter<'_>,
|
|
collection: OwnedObjectPath,
|
|
) -> zbus::Result<()>;
|
|
|
|
#[zbus(signal)]
|
|
pub async fn collection_changed(
|
|
emitter: &SignalEmitter<'_>,
|
|
collection: OwnedObjectPath,
|
|
) -> zbus::Result<()>;
|
|
|
|
async fn read_alias(&self, name: String) -> zbus::fdo::Result<OwnedObjectPath> {
|
|
if name == "default" {
|
|
Ok(COLLECTION_PATH.try_into().unwrap())
|
|
} else {
|
|
Ok(NULL_PATH.try_into().unwrap())
|
|
}
|
|
}
|
|
|
|
async fn set_alias(&self, name: String, collection: OwnedObjectPath) -> zbus::fdo::Result<()> {
|
|
if name != "default" || collection.as_str() != COLLECTION_PATH {
|
|
warn!(%name, %collection, "set_alias: only the 'default' alias on the souveraine collection is supported, ignoring");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[zbus(property)]
|
|
async fn collections(&self) -> Vec<OwnedObjectPath> {
|
|
vec![COLLECTION_PATH.try_into().unwrap()]
|
|
}
|
|
}
|
|
|
|
fn item_path(id: &str) -> String {
|
|
format!("{COLLECTION_PATH}/{id}")
|
|
}
|
|
|
|
fn id_from_item_path<'a>(path: &'a ObjectPath<'_>) -> Option<&'a str> {
|
|
path.as_str().strip_prefix(&format!("{COLLECTION_PATH}/"))
|
|
}
|