Skip to content

Commit 903185b

Browse files
committed
server: kde plasma prompter support
going to be part of the upcoming Plasma 6.6 release
1 parent f18937d commit 903185b

11 files changed

Lines changed: 755 additions & 61 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ formatx = "0.2"
1919
gettext-rs = {version = "0.7", features = ["gettext-system"]}
2020
hkdf = { version = "0.12", optional = true }
2121
libc = "0.2"
22-
nix = { version = "0.30", default-features = false, features = ["user"]}
22+
nix = { version = "0.30", default-features = false, features = ["user", "socket"]}
2323
num = "0.4.0"
2424
num-bigint-dig.workspace = true
2525
openssl = { version = "0.10", optional = true }
@@ -36,7 +36,7 @@ zbus = { workspace = true, features = ["p2p"] }
3636
zeroize.workspace = true
3737

3838
[features]
39-
default = ["native_crypto"]
39+
default = ["native_crypto", "plasma"]
4040
gnome = ["dep:base64"]
4141
native_crypto = [
4242
"gnome",
@@ -49,6 +49,7 @@ openssl_crypto = [
4949
"dep:openssl",
5050
"oo7/openssl_crypto"
5151
]
52+
plasma = []
5253

5354
[dev-dependencies]
5455
serial_test = "3.3"

server/src/collection/tests.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
use oo7::dbus;
44
use tokio_stream::StreamExt;
55

6-
use crate::tests::TestServiceSetup;
6+
use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test};
77

88
#[tokio::test]
99
async fn create_item_plain() -> Result<(), Box<dyn std::error::Error>> {
@@ -545,7 +545,15 @@ async fn collection_deleted_signal() -> Result<(), Box<dyn std::error::Error>> {
545545
Ok(())
546546
}
547547

548-
#[tokio::test]
548+
gnome_prompter_test!(
549+
create_item_in_locked_collection_gnome,
550+
create_item_in_locked_collection
551+
);
552+
plasma_prompter_test!(
553+
create_item_in_locked_collection_plasma,
554+
create_item_in_locked_collection
555+
);
556+
549557
async fn create_item_in_locked_collection() -> Result<(), Box<dyn std::error::Error>> {
550558
let setup = TestServiceSetup::plain_session(true).await?;
551559

@@ -602,7 +610,15 @@ async fn create_item_in_locked_collection() -> Result<(), Box<dyn std::error::Er
602610
Ok(())
603611
}
604612

605-
#[tokio::test]
613+
gnome_prompter_test!(
614+
delete_locked_collection_with_prompt_gnome,
615+
delete_locked_collection_with_prompt
616+
);
617+
plasma_prompter_test!(
618+
delete_locked_collection_with_prompt_plasma,
619+
delete_locked_collection_with_prompt
620+
);
621+
606622
async fn delete_locked_collection_with_prompt() -> Result<(), Box<dyn std::error::Error>> {
607623
let setup = TestServiceSetup::plain_session(true).await?;
608624
let default_collection = setup.default_collection().await?;
@@ -654,7 +670,9 @@ async fn delete_locked_collection_with_prompt() -> Result<(), Box<dyn std::error
654670
Ok(())
655671
}
656672

657-
#[tokio::test]
673+
gnome_prompter_test!(unlock_retry_gnome, unlock_retry);
674+
plasma_prompter_test!(unlock_retry_plasma, unlock_retry);
675+
658676
async fn unlock_retry() -> Result<(), Box<dyn std::error::Error>> {
659677
let setup = TestServiceSetup::plain_session(true).await?;
660678
let default_collection = setup.default_collection().await?;
@@ -680,7 +698,6 @@ async fn unlock_retry() -> Result<(), Box<dyn std::error::Error>> {
680698
);
681699

682700
setup
683-
.mock_prompter
684701
.set_password_queue(vec![
685702
oo7::Secret::from("wrong-password"),
686703
oo7::Secret::from("wrong-password2"),

server/src/item/tests.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
use oo7::dbus;
44
use tokio_stream::StreamExt;
55

6-
use crate::tests::TestServiceSetup;
6+
use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test};
77

88
#[tokio::test]
99
async fn label_property() -> Result<(), Box<dyn std::error::Error>> {
@@ -440,7 +440,15 @@ async fn item_changed_signal() -> Result<(), Box<dyn std::error::Error>> {
440440
Ok(())
441441
}
442442

443-
#[tokio::test]
443+
gnome_prompter_test!(
444+
delete_locked_item_with_prompt_gnome,
445+
delete_locked_item_with_prompt
446+
);
447+
plasma_prompter_test!(
448+
delete_locked_item_with_prompt_plasma,
449+
delete_locked_item_with_prompt
450+
);
451+
444452
async fn delete_locked_item_with_prompt() -> Result<(), Box<dyn std::error::Error>> {
445453
let setup = TestServiceSetup::plain_session(true).await?;
446454
let default_collection = setup.default_collection().await?;

server/src/main.rs

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

0 commit comments

Comments
 (0)