use std::collections::HashMap;
use std::sync::mpsc;
use std::time::Duration;
use zbus::blocking::{Connection, MessageIterator, connection};
use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value};
use zbus::{MatchRule, message};
use zeroize::Zeroizing;
use super::SecretError;
const DESTINATION: &str = "org.freedesktop.secrets";
const SERVICE_PATH: &str = "/org/freedesktop/secrets";
const SERVICE: &str = "org.freedesktop.Secret.Service";
const COLLECTION: &str = "org.freedesktop.Secret.Collection";
const ITEM: &str = "org.freedesktop.Secret.Item";
const PROMPT: &str = "org.freedesktop.Secret.Prompt";
const CALL_WAIT: Duration = Duration::from_secs(25);
const PROMPT_WAIT: Duration = Duration::from_secs(120);
const NONE: &str = "/";
#[derive(Clone)]
pub(super) struct Keyring {
connection: Connection,
session: OwnedObjectPath,
}
type Secret = (OwnedObjectPath, Vec<u8>, Vec<u8>, String);
impl Keyring {
pub(super) fn open(address: &str) -> Result<Self, SecretError> {
if address.is_empty() {
return Err(SecretError::Unavailable);
}
let connection = connection::Builder::address(address)
.and_then(|builder| builder.method_timeout(CALL_WAIT).build())
.map_err(|_| SecretError::Unavailable)?;
Self::over(connection)
}
pub(super) fn over(connection: Connection) -> Result<Self, SecretError> {
let reply = connection
.call_method(Some(DESTINATION), SERVICE_PATH, Some(SERVICE), "OpenSession", &("plain", Value::from("")))
.map_err(answer)?;
let (_, session): (OwnedValue, OwnedObjectPath) = reply.body().deserialize().map_err(answer)?;
Ok(Self { connection, session })
}
pub(super) fn store(
&self,
application: &str,
account: &str,
label: &str,
secret: &[u8],
) -> Result<(), SecretError> {
let collection = self.default_collection()?;
self.unlock(std::slice::from_ref(&collection))?;
let mut properties: HashMap<&str, Value<'_>> = HashMap::new();
properties.insert("org.freedesktop.Secret.Item.Label", Value::from(label));
properties.insert("org.freedesktop.Secret.Item.Attributes", Value::from(attributes(application, account)));
let secret = (self.session.as_ref(), Vec::<u8>::new(), secret, "application/octet-stream");
let (_, prompt): (OwnedObjectPath, OwnedObjectPath) =
self.call(collection.as_ref(), COLLECTION, "CreateItem", &(properties, secret, true))?;
self.prompt(&prompt).map(drop)
}
pub(super) fn lookup(&self, application: &str, account: &str) -> Result<Option<Zeroizing<Vec<u8>>>, SecretError> {
let Some(item) = self.items(application, account)?.into_iter().next() else {
return Ok(None);
};
let reply = self
.connection
.call_method(Some(DESTINATION), item.as_ref(), Some(ITEM), "GetSecret", &(self.session.as_ref(),))
.map_err(answer)?;
let (_, _, value, _): Secret = reply.body().deserialize().map_err(answer)?;
Ok(Some(Zeroizing::new(value)))
}
pub(super) fn delete(&self, application: &str, account: &str) -> Result<(), SecretError> {
for item in self.items(application, account)? {
let prompt: OwnedObjectPath = self.call(item.as_ref(), ITEM, "Delete", &())?;
self.prompt(&prompt)?;
}
Ok(())
}
fn items(&self, application: &str, account: &str) -> Result<Vec<OwnedObjectPath>, SecretError> {
let (mut unlocked, locked): (Vec<OwnedObjectPath>, Vec<OwnedObjectPath>) = self.call(
SERVICE_PATH.try_into().map_err(answer)?,
SERVICE,
"SearchItems",
&(attributes(application, account),),
)?;
if !locked.is_empty() {
unlocked.extend(self.unlock(&locked)?);
}
Ok(unlocked)
}
fn default_collection(&self) -> Result<OwnedObjectPath, SecretError> {
let service: ObjectPath<'_> = SERVICE_PATH.try_into().map_err(answer)?;
let collection: OwnedObjectPath = self.call(service.clone(), SERVICE, "ReadAlias", &("default",))?;
if collection.as_str() != NONE {
return Ok(collection);
}
let mut properties: HashMap<&str, Value<'_>> = HashMap::new();
properties.insert("org.freedesktop.Secret.Collection.Label", Value::from("Default keyring"));
let (made, prompt): (OwnedObjectPath, OwnedObjectPath) =
self.call(service, SERVICE, "CreateCollection", &(properties, "default"))?;
if made.as_str() != NONE {
return Ok(made);
}
let result = self.prompt(&prompt)?.ok_or(SecretError::Denied)?;
OwnedObjectPath::try_from(result).map_err(answer)
}
fn unlock(&self, objects: &[OwnedObjectPath]) -> Result<Vec<OwnedObjectPath>, SecretError> {
let (mut unlocked, prompt): (Vec<OwnedObjectPath>, OwnedObjectPath) =
self.call(SERVICE_PATH.try_into().map_err(answer)?, SERVICE, "Unlock", &(objects,))?;
if let Some(result) = self.prompt(&prompt)? {
unlocked.extend(Vec::<OwnedObjectPath>::try_from(result).map_err(answer)?);
}
Ok(unlocked)
}
fn prompt(&self, prompt: &OwnedObjectPath) -> Result<Option<OwnedValue>, SecretError> {
if prompt.as_str() == NONE {
return Ok(None);
}
let rule = MatchRule::builder()
.msg_type(message::Type::Signal)
.interface(PROMPT)
.and_then(|rule| rule.member("Completed"))
.and_then(|rule| rule.path(prompt.as_ref()))
.map_err(answer)?
.build();
let completions = MessageIterator::for_match_rule(rule, &self.connection, None).map_err(answer)?;
let (sender, received) = mpsc::channel();
std::thread::spawn(move || {
let completed = completions
.into_iter()
.flatten()
.find_map(|signal| signal.body().deserialize::<(bool, OwnedValue)>().ok());
let _ = sender.send(completed);
});
let () = self.call(prompt.as_ref(), PROMPT, "Prompt", &("",))?;
match received.recv_timeout(PROMPT_WAIT) {
Ok(Some((false, result))) => Ok(Some(result)),
Ok(Some((true, _))) => Err(SecretError::Denied),
Ok(None) => Err(SecretError::Other("the keyring went away during its prompt".to_owned())),
Err(_) => {
let _: Result<(), SecretError> = self.call(prompt.as_ref(), PROMPT, "Dismiss", &());
Err(SecretError::Locked)
}
}
}
fn call<B, R>(&self, path: ObjectPath<'_>, interface: &str, method: &str, body: &B) -> Result<R, SecretError>
where
B: zbus::export::serde::Serialize + zbus::zvariant::DynamicType,
R: for<'d> zbus::zvariant::DynamicDeserialize<'d>,
{
let reply =
self.connection.call_method(Some(DESTINATION), path, Some(interface), method, body).map_err(answer)?;
reply.body().deserialize().map_err(answer)
}
}
fn attributes<'a>(application: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> {
HashMap::from([("application", application), ("account", account)])
}
fn answer(error: impl Into<zbus::Error>) -> SecretError {
match error.into() {
zbus::Error::MethodError(name, detail, _) => match name.as_str() {
"org.freedesktop.DBus.Error.ServiceUnknown" | "org.freedesktop.DBus.Error.NameHasNoOwner" => {
SecretError::Unavailable
}
"org.freedesktop.Secret.Error.IsLocked" => SecretError::Locked,
"org.freedesktop.DBus.Error.AccessDenied" => SecretError::Denied,
_ => SecretError::Other(detail.map_or_else(|| name.to_string(), |detail| format!("{name}: {detail}"))),
},
other => SecretError::Other(other.to_string()),
}
}