Skip to content

Commit

Permalink
Add prompt implementation for service method lock
Browse files Browse the repository at this point in the history
Even though gnome-keyring-daemon doesn't implement/show a prompt during
the service method lock. The secret service spec says a prompt can be
used during lock. So, we're adding this to the new daemon.

Signed-off-by: Dhanuka Warusadura <[email protected]>
  • Loading branch information
warusadura committed Jan 13, 2025
1 parent 085a3da commit 90ca8c6
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 14 deletions.
3 changes: 2 additions & 1 deletion server/src/gnome/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod secret_exchange;
pub mod prompter;
pub mod secret_exchange;
226 changes: 226 additions & 0 deletions server/src/gnome/prompter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// org.gnome.keyring.Prompter
// https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/org.gnome.keyring.Prompter.xml

use std::collections::HashMap;

use clap::error::Result;
use oo7::dbus::ServiceError;
use zbus::{
interface, proxy,
zvariant::{DeserializeDict, OwnedObjectPath, OwnedValue, SerializeDict, Type, Value},
};

use crate::{
gnome::secret_exchange::SecretExchange,
prompt::{Prompt, PromptRole},
service::Service,
};

// System prompt properties
#[derive(DeserializeDict, SerializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct Properties {
title: String,
#[zvariant(rename = "choice-label")]
choice_label: String,
description: String,
message: String,
#[zvariant(rename = "caller-window")]
caller_window: String,
warning: String,
#[zvariant(rename = "password-new")]
password_new: bool,
#[zvariant(rename = "choice-chosen")]
choice_chosen: bool,
#[zvariant(rename = "continue-label")]
continue_label: String,
#[zvariant(rename = "cancel-label")]
cancel_label: String,
}

// org.gnome.keyring.internal.Prompter

#[proxy(
default_service = "org.gnome.keyring.SystemPrompter",
interface = "org.gnome.keyring.internal.Prompter",
default_path = "/org/gnome/keyring/Prompter"
)]
pub trait Prompter {
fn begin_prompting(&self, callback: &OwnedObjectPath) -> Result<(), ServiceError>;

fn perform_prompt(
&self,
callback: OwnedObjectPath,
type_: &str,
properties: Properties,
exchange: &str,
) -> Result<(), ServiceError>;

fn stop_prompting(&self, callback: OwnedObjectPath) -> Result<(), ServiceError>;
}

// org.gnome.keyring.internal.Prompter.Callback

pub struct PrompterCallback {
service: Service,
path: OwnedObjectPath,
}

#[interface(name = "org.gnome.keyring.internal.Prompter.Callback")]
impl PrompterCallback {
pub async fn prompt_ready(
&self,
reply: &str,
properties: HashMap<&str, OwnedValue>,
exchange: &str,
#[zbus(connection)] connection: &zbus::Connection,
) -> Result<(), ServiceError> {
let Some(prompt) = self.service.prompt().await else {
return Err(ServiceError::NoSuchObject(
"Prompt does not exist.".to_string(),
));
};

match prompt.role() {
PromptRole::Lock => {
if properties.is_empty() {
// First PromptReady call
let secret_exchange = SecretExchange::new().map_err(|err| {
ServiceError::ZBus(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(
format!("Failed to generate SecretExchange {err}."),
))))
})?;
let exchange = secret_exchange.begin();

let properties = Properties {
title: "".to_string(),
choice_label: "".to_string(),
description: "Confirm locking \"login\", Keyring.".to_string(),
message: "Lock Keyring".to_string(),
caller_window: "".to_string(),
warning: "".to_string(),
password_new: false,
choice_chosen: false,
continue_label: "Lock".to_string(),
cancel_label: "Cancel".to_string(),
};

let connection = connection.clone();
let path = self.path.clone();

tokio::spawn(PrompterCallback::perform_prompt(
connection, path, "confirm", properties, exchange,
));
} else if reply == "no" {
// Second PromptReady call and the prompt is dismissed
tracing::debug!("Prompt is being dismissed.");

tokio::spawn(PrompterCallback::stop_prompting(
connection.clone(),
self.path.clone(),
));

let signal_emitter = self.service.signal_emitter(prompt.path().clone())?;
let result = Value::new::<Vec<OwnedObjectPath>>(vec![])
.try_to_owned()
.unwrap();

tokio::spawn(PrompterCallback::prompt_completed(
signal_emitter,
true,
result,
));
} else {
// Second PromptReady call with the final exchange
let service = self.service.clone();
let objects = prompt.objects().clone();
let result = Value::new(&objects).try_to_owned().unwrap();

tokio::spawn(async move {
let _ = service.set_locked(true, &objects, true).await;
});

tokio::spawn(PrompterCallback::stop_prompting(
connection.clone(),
self.path.clone(),
));

let signal_emitter = self.service.signal_emitter(prompt.path().clone())?;

tokio::spawn(PrompterCallback::prompt_completed(
signal_emitter,
false,
result,
));
}
}
PromptRole::Unlock => todo!(),
PromptRole::CreateCollection => todo!(),
};

Ok(())
}

pub async fn prompt_done(
&self,
#[zbus(object_server)] object_server: &zbus::ObjectServer,
) -> Result<(), ServiceError> {
if let Some(prompt) = self.service.prompt().await {
object_server.remove::<Prompt, _>(prompt.path()).await?;
self.service.remove_prompt().await;
}
object_server.remove::<Self, _>(&self.path).await?;

Ok(())
}
}

impl PrompterCallback {
pub async fn new(service: Service) -> Self {
let index = service.prompt_index().await;
Self {
path: OwnedObjectPath::try_from(format!("/org/gnome/keyring/Prompt/p{index}")).unwrap(),
service,
}
}

pub fn path(&self) -> &OwnedObjectPath {
&self.path
}

pub async fn perform_prompt(
connection: zbus::Connection,
path: OwnedObjectPath,
prompt_type: &str,
properties: Properties,
exchange: String,
) -> Result<(), ServiceError> {
let prompter = PrompterProxy::new(&connection).await?;
prompter
.perform_prompt(path, prompt_type, properties, &exchange)
.await?;

Ok(())
}

pub async fn stop_prompting(
connection: zbus::Connection,
path: OwnedObjectPath,
) -> Result<(), ServiceError> {
let prompter = PrompterProxy::new(&connection).await?;
prompter.stop_prompting(path).await?;

Ok(())
}

pub async fn prompt_completed(
signal_emitter: zbus::object_server::SignalEmitter<'_>,
dismissed: bool,
result: OwnedValue,
) -> Result<(), ServiceError> {
Prompt::completed(&signal_emitter, dismissed, result).await?;
tracing::debug!("Prompt completed.");

Ok(())
}
}
86 changes: 78 additions & 8 deletions server/src/prompt.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,95 @@
// org.freedesktop.Secret.Prompt

use oo7::dbus::ServiceError;
use zbus::{interface, object_server::SignalEmitter, zvariant::OwnedValue};
use zbus::{
interface,
object_server::SignalEmitter,
zvariant::{OwnedObjectPath, OwnedValue},
};

#[derive(Debug)]
pub struct Prompt {}
use crate::{
gnome::prompter::{PrompterCallback, PrompterProxy},
service::Service,
};

#[derive(Debug, Clone)]
#[allow(unused)]
pub enum PromptRole {
Lock,
Unlock,
CreateCollection,
}

#[derive(Debug, Clone)]
pub struct Prompt {
service: Service,
objects: Vec<OwnedObjectPath>,
role: PromptRole,
path: OwnedObjectPath,
}

#[interface(name = "org.freedesktop.Secret.Prompt")]
impl Prompt {
pub async fn prompt(&self, _window_id: &str) -> Result<(), ServiceError> {
todo!()
pub async fn prompt(
&self,
_window_id: &str,
#[zbus(connection)] connection: &zbus::Connection,
#[zbus(object_server)] object_server: &zbus::ObjectServer,
) -> Result<(), ServiceError> {
let callback = PrompterCallback::new(self.service.clone()).await;
let path = callback.path().clone();
let connection = connection.clone();

object_server.at(&path, callback).await?;
tracing::debug!("Prompt `{}` created.", self.path);

tokio::spawn(async move {
let prompter = PrompterProxy::new(&connection).await.unwrap();
prompter.begin_prompting(&path).await.unwrap();
});

Ok(())
}

pub async fn dismiss(&self) -> Result<(), ServiceError> {
todo!()
pub async fn dismiss(
&self,
#[zbus(object_server)] object_server: &zbus::ObjectServer,
) -> Result<(), ServiceError> {
object_server.remove::<Self, _>(&self.path).await?;
self.service.remove_prompt().await;

Ok(())
}

#[zbus(signal, name = "Completed")]
async fn completed(
pub async fn completed(
signal_emitter: &SignalEmitter<'_>,
dismissed: bool,
result: OwnedValue,
) -> zbus::Result<()>;
}

impl Prompt {
pub async fn new(service: Service, objects: Vec<OwnedObjectPath>, role: PromptRole) -> Self {
let index = service.prompt_index().await;
Self {
path: OwnedObjectPath::try_from(format!("/org/freedesktop/secrets/prompt/p{index}"))
.unwrap(),
service,
objects,
role,
}
}

pub fn path(&self) -> &OwnedObjectPath {
&self.path
}

pub fn role(&self) -> &PromptRole {
&self.role
}

pub fn objects(&self) -> &Vec<OwnedObjectPath> {
&self.objects
}
}
Loading

0 comments on commit 90ca8c6

Please sign in to comment.