Skip to content

Commit e8d5078

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

4 files changed

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

server/src/prompt/mod.rs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// org.freedesktop.Secret.Prompt
2-
32
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
43

54
use oo7::{Secret, dbus::ServiceError};
@@ -13,6 +12,7 @@ use zbus::{
1312
use crate::{
1413
error::custom_service_error,
1514
gnome::prompter::{PrompterCallback, PrompterProxy},
15+
plasma::prompter::{PlasmaPrompterCallback, PlasmaPrompterProxy, in_plasma_environment},
1616
service::Service,
1717
};
1818

@@ -63,6 +63,8 @@ pub struct Prompt {
6363
collection: Option<crate::collection::Collection>,
6464
/// GNOME Specific
6565
callback: Arc<OnceCell<PrompterCallback>>,
66+
/// KDE Plasma Specific
67+
callback_plasma: Arc<OnceCell<PlasmaPrompterCallback>>,
6668
/// The action to execute when the prompt completes
6769
action: Arc<Mutex<Option<PromptAction>>>,
6870
}
@@ -83,6 +85,62 @@ impl std::fmt::Debug for Prompt {
8385
#[interface(name = "org.freedesktop.Secret.Prompt")]
8486
impl Prompt {
8587
pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> {
88+
if in_plasma_environment(&self.service.connection()).await {
89+
if self.callback_plasma.get().is_some() {
90+
return Err(custom_service_error(
91+
"A prompt callback is ongoing already.",
92+
));
93+
};
94+
95+
let callback = PlasmaPrompterCallback::new(self.service.clone(), self.path.clone())
96+
.await
97+
.map_err(|err| {
98+
custom_service_error(&format!("Failed to create PrompterCallback {err}."))
99+
})?;
100+
101+
let path = OwnedObjectPath::from(callback.path().clone());
102+
103+
self.callback_plasma
104+
.set(callback.clone())
105+
.expect("A prompt callback is only set once");
106+
107+
self.service.object_server().at(&path, callback).await?;
108+
tracing::debug!("Prompt `{}` created.", self.path);
109+
110+
let prompter = PlasmaPrompterProxy::new(self.service.connection()).await?;
111+
let window_id = window_id.unwrap_or("").to_string();
112+
113+
let collection_name = self.label.clone();
114+
match self.role() {
115+
PromptRole::Unlock => {
116+
tokio::spawn(async move {
117+
prompter
118+
.unlock_collection_prompt(
119+
&path,
120+
&window_id,
121+
"",
122+
collection_name.as_str(),
123+
)
124+
.await
125+
});
126+
}
127+
PromptRole::CreateCollection => {
128+
tokio::spawn(async move {
129+
prompter
130+
.create_collection_prompt(
131+
&path,
132+
&window_id,
133+
"",
134+
collection_name.as_str(),
135+
)
136+
.await
137+
});
138+
}
139+
}
140+
141+
return Ok(());
142+
}
143+
86144
if self.callback.get().is_some() {
87145
return Err(custom_service_error(
88146
"A prompt callback is ongoing already.",
@@ -108,7 +166,7 @@ impl Prompt {
108166
self.service.object_server().at(&path, callback).await?;
109167
tracing::debug!("Prompt `{}` created.", self.path);
110168

111-
// Starts GNOME System Prompting.
169+
// Starts System Prompting.
112170
// Spawned separately to avoid blocking the early return of the current
113171
// execution.
114172
let prompter = PrompterProxy::new(self.service.connection()).await?;
@@ -118,6 +176,14 @@ impl Prompt {
118176
}
119177

120178
pub async fn dismiss(&self) -> Result<(), ServiceError> {
179+
if let Some(callback_plasma) = self.callback_plasma.get() {
180+
let emitter = SignalEmitter::from_parts(
181+
self.service.connection().clone(),
182+
callback_plasma.path().clone(),
183+
);
184+
PlasmaPrompterCallback::dismiss(&emitter).await?;
185+
}
186+
121187
if let Some(_callback) = self.callback.get() {
122188
// TODO: figure out if we should destroy the un-export the callback
123189
// here?
@@ -156,6 +222,7 @@ impl Prompt {
156222
label,
157223
collection,
158224
callback: Default::default(),
225+
callback_plasma: Default::default(),
159226
action: Arc::new(Mutex::new(None)),
160227
}
161228
}

0 commit comments

Comments
 (0)