Skip to content

Commit 6feabe1

Browse files
committed
server: kde plasma prompter support
going to be part of the upcoming Plasma 6.6 release
1 parent aa78bde commit 6feabe1

5 files changed

Lines changed: 337 additions & 34 deletions

File tree

server/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ zbus = { workspace = true, features = ["p2p"] }
3636
zeroize.workspace = true
3737

3838
[features]
39-
default = ["native_crypto"]
39+
default = ["gnome_native_crypto", "plasma"]
40+
gnome = []
41+
gnome_native_crypto = ["gnome", "native_crypto"]
42+
gnome_openssl_crypto = ["gnome", "openssl_crypto"]
4043
native_crypto = [
4144
"dep:hkdf",
4245
"dep:sha2",
@@ -46,7 +49,8 @@ openssl_crypto = [
4649
"dep:openssl",
4750
"oo7/openssl_crypto"
4851
]
52+
plasma = []
4953

5054
[dev-dependencies]
5155
serial_test = "3.3"
52-
tempfile.workspace = true
56+
tempfile.workspace = true

server/src/main.rs

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

0 commit comments

Comments
 (0)