Skip to content

Commit 19fe595

Browse files
committed
server: kde plasma prompter support
slated to be introduced as part of Plasma 6.6
1 parent aa78bde commit 19fe595

4 files changed

Lines changed: 254 additions & 3 deletions

File tree

server/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod error;
44
mod gnome;
55
mod item;
66
mod pam_listener;
7+
mod plasma;
78
mod prompt;
89
mod service;
910
mod session;

server/src/plasma/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: MIT
2+
// SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
3+
4+
pub mod prompter;

server/src/plasma/prompter.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// SPDX-License-Identifier: MIT
2+
// SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
3+
4+
use ashpd::{ActivationToken, WindowIdentifierType};
5+
use oo7::dbus::ServiceError;
6+
7+
use serde::Serialize;
8+
use zbus::{
9+
object_server::SignalEmitter,
10+
zvariant::{self, ObjectPath, Optional, OwnedFd, OwnedObjectPath, Type},
11+
};
12+
13+
use crate::{
14+
prompt::{Prompt, PromptRole},
15+
service::Service,
16+
};
17+
use tokio::io::AsyncReadExt;
18+
19+
use std::os::fd::AsFd;
20+
21+
#[repr(i32)]
22+
#[derive(Debug, Type, Serialize)]
23+
pub enum CallbackAction {
24+
Dismiss = 0,
25+
Keep = 1,
26+
}
27+
28+
#[zbus::proxy(
29+
default_service = "org.kde.secretprompter",
30+
interface = "org.kde.secretprompter",
31+
default_path = "/SecretPrompter",
32+
gen_blocking = false
33+
)]
34+
pub trait PlasmaPrompter {
35+
fn unlock_collection_prompt(
36+
&self,
37+
request: &ObjectPath<'_>,
38+
window_id: Optional<WindowIdentifierType>,
39+
activation_token: ActivationToken,
40+
collection_name: &str,
41+
) -> Result<(), ServiceError>;
42+
fn create_collection_prompt(
43+
&self,
44+
request: &ObjectPath<'_>,
45+
window_id: Optional<WindowIdentifierType>,
46+
activation_token: ActivationToken,
47+
collection_name: &str,
48+
) -> Result<(), ServiceError>;
49+
}
50+
51+
#[derive(Debug, Clone)]
52+
pub struct PlasmaPrompterCallback {
53+
service: Service,
54+
prompt_path: OwnedObjectPath,
55+
path: OwnedObjectPath,
56+
}
57+
58+
#[zbus::interface(name = "org.kde.secretprompter.request")]
59+
impl PlasmaPrompterCallback {
60+
pub async fn accepted(&self, result_fd: OwnedFd) -> Result<CallbackAction, ServiceError> {
61+
let prompt_path = &self.prompt_path;
62+
let Some(prompt) = self.service.prompt(prompt_path).await else {
63+
return Err(ServiceError::NoSuchObject(format!(
64+
"Prompt '{prompt_path}' does not exist."
65+
)));
66+
};
67+
68+
tracing::debug!("User accepted the prompt.");
69+
70+
let secret = {
71+
let borrowed_fd = result_fd.as_fd();
72+
let std_stream = std::os::unix::net::UnixStream::from(
73+
borrowed_fd
74+
.try_clone_to_owned()
75+
.expect("Failed to clone fd"),
76+
);
77+
let mut stream = tokio::net::UnixStream::from_std(std_stream).unwrap();
78+
let mut buffer = String::new();
79+
stream
80+
.read_to_string(&mut buffer)
81+
.await
82+
.expect("error reading secret");
83+
buffer
84+
};
85+
86+
self.on_reply(&prompt, &secret).await
87+
}
88+
89+
pub async fn rejected(&self) -> Result<CallbackAction, ServiceError> {
90+
tracing::debug!("User rejected the prompt.");
91+
self.prompter_dismissed(self.prompt_path.clone()).await?;
92+
Ok(CallbackAction::Dismiss) // simply dismiss without further action
93+
}
94+
95+
pub async fn dismissed(&self) -> Result<(), ServiceError> {
96+
// This is only does check if the prompt is tracked on Service
97+
let path = &self.prompt_path;
98+
if let Some(_prompt) = self.service.prompt(path).await {
99+
self.service
100+
.object_server()
101+
.remove::<Prompt, _>(path)
102+
.await?;
103+
self.service.remove_prompt(path).await;
104+
}
105+
self.service
106+
.object_server()
107+
.remove::<Self, _>(&self.path)
108+
.await?;
109+
110+
Ok(())
111+
}
112+
113+
#[zbus(signal)]
114+
pub async fn retry(signal_emitter: &SignalEmitter<'_>, reason: &str) -> zbus::Result<()>;
115+
116+
#[zbus(signal)]
117+
pub async fn dismiss(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
118+
}
119+
120+
impl PlasmaPrompterCallback {
121+
pub async fn new(
122+
service: Service,
123+
prompt_path: OwnedObjectPath,
124+
) -> Result<Self, oo7::crypto::Error> {
125+
let index = service.prompt_index().await;
126+
Ok(Self {
127+
path: OwnedObjectPath::try_from(format!("/org/plasma/keyring/Prompt/p{index}"))
128+
.unwrap(),
129+
service,
130+
prompt_path,
131+
})
132+
}
133+
134+
pub fn path(&self) -> &ObjectPath<'_> {
135+
&self.path
136+
}
137+
138+
async fn on_reply(&self, prompt: &Prompt, reply: &str) -> Result<CallbackAction, ServiceError> {
139+
let secret = oo7::Secret::from(reply);
140+
141+
// Handle each role differently based on what validation/preparation is needed
142+
match prompt.role() {
143+
PromptRole::Unlock => {
144+
if prompt.on_unlock_collection(secret).await? {
145+
Ok(CallbackAction::Dismiss)
146+
} else {
147+
let emitter = SignalEmitter::from_parts(
148+
self.service.connection().clone(),
149+
self.path().clone(),
150+
);
151+
PlasmaPrompterCallback::retry(&emitter, "The unlock password was incorrect")
152+
.await?;
153+
154+
Ok(CallbackAction::Keep) // we retry
155+
}
156+
}
157+
PromptRole::CreateCollection => {
158+
prompt.on_create_collection(secret).await?;
159+
Ok(CallbackAction::Dismiss)
160+
}
161+
}
162+
}
163+
164+
async fn prompter_dismissed(&self, prompt_path: OwnedObjectPath) -> Result<(), ServiceError> {
165+
let signal_emitter = self.service.signal_emitter(prompt_path)?;
166+
let result = zvariant::Value::new::<Vec<OwnedObjectPath>>(vec![])
167+
.try_into_owned()
168+
.unwrap();
169+
170+
tokio::spawn(async move { Prompt::completed(&signal_emitter, true, result).await });
171+
Ok(())
172+
}
173+
}

server/src/prompt/mod.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// org.freedesktop.Secret.Prompt
2+
use std::{env, future::Future, pin::Pin, str::FromStr, sync::Arc};
23

3-
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
4-
4+
use ashpd::{ActivationToken, WindowIdentifier};
55
use oo7::{Secret, dbus::ServiceError};
66
use tokio::sync::{Mutex, OnceCell};
77
use zbus::{
@@ -13,6 +13,7 @@ use zbus::{
1313
use crate::{
1414
error::custom_service_error,
1515
gnome::prompter::{PrompterCallback, PrompterProxy},
16+
plasma::prompter::{PlasmaPrompterCallback, PlasmaPrompterProxy},
1617
service::Service,
1718
};
1819

@@ -63,6 +64,8 @@ pub struct Prompt {
6364
collection: Option<crate::collection::Collection>,
6465
/// GNOME Specific
6566
callback: Arc<OnceCell<PrompterCallback>>,
67+
/// KDE Plasma Specific
68+
callback_plasma: Arc<OnceCell<PlasmaPrompterCallback>>,
6669
/// The action to execute when the prompt completes
6770
action: Arc<Mutex<Option<PromptAction>>>,
6871
}
@@ -83,6 +86,67 @@ impl std::fmt::Debug for Prompt {
8386
#[interface(name = "org.freedesktop.Secret.Prompt")]
8487
impl Prompt {
8588
pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> {
89+
let is_plasma = env::var("XDG_CURRENT_DESKTOP")
90+
.map(|v| v.to_lowercase() == "kde")
91+
.unwrap_or(false);
92+
93+
if is_plasma {
94+
if self.callback_plasma.get().is_some() {
95+
return Err(custom_service_error(
96+
"A prompt callback is ongoing already.",
97+
));
98+
};
99+
100+
let callback = PlasmaPrompterCallback::new(self.service.clone(), self.path.clone())
101+
.await
102+
.map_err(|err| {
103+
custom_service_error(&format!("Failed to create PrompterCallback {err}."))
104+
})?;
105+
106+
let path = OwnedObjectPath::from(callback.path().clone());
107+
108+
self.callback_plasma
109+
.set(callback.clone())
110+
.expect("A prompt callback is only set once");
111+
112+
self.service.object_server().at(&path, callback).await?;
113+
tracing::debug!("Prompt `{}` created.", self.path);
114+
115+
let prompter = PlasmaPrompterProxy::new(self.service.connection()).await?;
116+
let window_id =
117+
(*window_id).and_then(|w| ashpd::WindowIdentifierType::from_str(w).ok());
118+
119+
let collection_name = self.label.clone();
120+
match self.role() {
121+
PromptRole::Unlock => {
122+
tokio::spawn(async move {
123+
prompter
124+
.unlock_collection_prompt(
125+
&path,
126+
window_id,
127+
ActivationToken::from(""),
128+
collection_name.as_str(),
129+
)
130+
.await
131+
});
132+
}
133+
PromptRole::CreateCollection => {
134+
tokio::spawn(async move {
135+
prompter
136+
.create_collection_prompt(
137+
&path,
138+
window_id,
139+
ActivationToken::from(""),
140+
collection_name.as_str(),
141+
)
142+
.await
143+
});
144+
}
145+
}
146+
147+
return Ok(());
148+
}
149+
86150
if self.callback.get().is_some() {
87151
return Err(custom_service_error(
88152
"A prompt callback is ongoing already.",
@@ -108,7 +172,7 @@ impl Prompt {
108172
self.service.object_server().at(&path, callback).await?;
109173
tracing::debug!("Prompt `{}` created.", self.path);
110174

111-
// Starts GNOME System Prompting.
175+
// Starts System Prompting.
112176
// Spawned separately to avoid blocking the early return of the current
113177
// execution.
114178
let prompter = PrompterProxy::new(self.service.connection()).await?;
@@ -118,6 +182,14 @@ impl Prompt {
118182
}
119183

120184
pub async fn dismiss(&self) -> Result<(), ServiceError> {
185+
if let Some(callback_plasma) = self.callback_plasma.get() {
186+
let emitter = SignalEmitter::from_parts(
187+
self.service.connection().clone(),
188+
callback_plasma.path().clone(),
189+
);
190+
PlasmaPrompterCallback::dismiss(&emitter).await?;
191+
}
192+
121193
if let Some(_callback) = self.callback.get() {
122194
// TODO: figure out if we should destroy the un-export the callback
123195
// here?
@@ -156,6 +228,7 @@ impl Prompt {
156228
label,
157229
collection,
158230
callback: Default::default(),
231+
callback_plasma: Default::default(),
159232
action: Arc::new(Mutex::new(None)),
160233
}
161234
}

0 commit comments

Comments
 (0)