Skip to content

Commit 5f73e80

Browse files
hsitterbilelmoussaoui
authored andcommitted
server: kde plasma prompter support
going to be part of the upcoming Plasma 6.6 release
1 parent f18937d commit 5f73e80

11 files changed

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

0 commit comments

Comments
 (0)