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
#[must_use]
29
8
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
    #[cfg(not(test))]
37
    {
38
        static IS_PLASMA: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
39
        if let Some(cached_value) = IS_PLASMA.get() {
40
            return *cached_value;
41
        }
42

            
43
        let is_plasma = async {
44
            match env::var("XDG_CURRENT_DESKTOP").map(|v| v.to_lowercase() == "kde") {
45
                Ok(_) => (),
46
                Err(_) => return false,
47
            };
48

            
49
            let proxy = match zbus::fdo::DBusProxy::new(_connection).await {
50
                Ok(proxy) => proxy,
51
                Err(_) => return false,
52
            };
53
            let activatable_names = match proxy.list_activatable_names().await {
54
                Ok(names) => names,
55
                Err(_) => return false,
56
            };
57
            activatable_names
58
                .iter()
59
                .any(|name| name.as_str() == "org.kde.secretprompter")
60
        }
61
        .await;
62

            
63
        *IS_PLASMA.get_or_init(|| is_plasma)
64
    }
65
}
66

            
67
#[zbus::proxy(
68
    default_service = "org.kde.secretprompter",
69
    interface = "org.kde.secretprompter",
70
    default_path = "/SecretPrompter",
71
    gen_blocking = false
72
)]
73
pub trait PlasmaPrompter {
74
    fn unlock_collection_prompt(
75
        &self,
76
        request: &ObjectPath<'_>,
77
        window_id: &str,
78
        activation_token: &str,
79
        collection_name: &str,
80
    ) -> Result<(), ServiceError>;
81
    fn create_collection_prompt(
82
        &self,
83
        request: &ObjectPath<'_>,
84
        window_id: &str,
85
        activation_token: &str,
86
        collection_name: &str,
87
    ) -> Result<(), ServiceError>;
88
}
89

            
90
#[derive(Debug, Clone)]
91
pub struct PlasmaPrompterCallback {
92
    service: Service,
93
    prompt_path: OwnedObjectPath,
94
    path: OwnedObjectPath,
95
}
96

            
97
#[zbus::interface(name = "org.kde.secretprompter.request")]
98
impl PlasmaPrompterCallback {
99
14
    pub async fn accepted(&self, result_fd: OwnedFd) -> Result<CallbackAction, ServiceError> {
100
2
        let prompt_path = &self.prompt_path;
101
4
        let Some(prompt) = self.service.prompt(prompt_path).await else {
102
            return Err(ServiceError::NoSuchObject(format!(
103
                "Prompt '{prompt_path}' does not exist."
104
            )));
105
        };
106

            
107
4
        tracing::debug!("User accepted the prompt.");
108

            
109
        let secret = {
110
4
            let borrowed_fd = result_fd.as_fd();
111
            let std_stream = std::os::unix::net::UnixStream::from(
112
2
                borrowed_fd
113
2
                    .try_clone_to_owned()
114
2
                    .expect("Failed to clone fd"),
115
            );
116
2
            let mut stream = tokio::net::UnixStream::from_std(std_stream)
117
                .expect("Failed to create Tokio UnixStream");
118
2
            let mut buffer = String::new();
119
6
            stream
120
2
                .read_to_string(&mut buffer)
121
8
                .await
122
                .expect("error reading secret");
123
2
            tracing::debug!("Read secret from fd, length {}", buffer.len());
124
2
            oo7::Secret::from(buffer)
125
        };
126

            
127
4
        self.on_reply(&prompt, secret).await
128
    }
129

            
130
8
    pub async fn rejected(&self) -> Result<CallbackAction, ServiceError> {
131
4
        tracing::debug!("User rejected the prompt.");
132
4
        self.prompter_dismissed(self.prompt_path.clone()).await?;
133
2
        Ok(CallbackAction::Dismiss) // simply dismiss without further action
134
    }
135

            
136
    pub async fn dismissed(&self) -> Result<(), ServiceError> {
137
        // This is only does check if the prompt is tracked on Service
138
        let path = &self.prompt_path;
139
        if let Some(_prompt) = self.service.prompt(path).await {
140
            self.service
141
                .object_server()
142
                .remove::<Prompt, _>(path)
143
                .await?;
144
            self.service.remove_prompt(path).await;
145
        }
146
        self.service
147
            .object_server()
148
            .remove::<Self, _>(&self.path)
149
            .await?;
150

            
151
        Ok(())
152
    }
153

            
154
    #[zbus(signal)]
155
2
    pub async fn retry(signal_emitter: &SignalEmitter<'_>, reason: &str) -> zbus::Result<()>;
156

            
157
    #[zbus(signal)]
158
    pub async fn dismiss(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
159
}
160

            
161
impl PlasmaPrompterCallback {
162
8
    pub async fn new(service: Service, prompt_path: OwnedObjectPath) -> Self {
163
4
        let index = service.prompt_index().await;
164
        Self {
165
2
            path: OwnedObjectPath::try_from(format!("/org/plasma/keyring/Prompt/p{index}"))
166
                .unwrap(),
167
            service,
168
            prompt_path,
169
        }
170
    }
171

            
172
2
    pub fn path(&self) -> &ObjectPath<'_> {
173
2
        &self.path
174
    }
175

            
176
2
    pub async fn start(
177
        &self,
178
        role: &PromptRole,
179
        window_id: Option<WindowIdentifierType>,
180
        collection_name: &str,
181
    ) -> Result<(), ServiceError> {
182
4
        let path = self.path.clone();
183
4
        let prompter = PlasmaPrompterProxy::new(self.service.connection()).await?;
184
2
        let window_id = match window_id {
185
            Some(id) => id.to_string(),
186
4
            None => String::new(),
187
        };
188
2
        let collection_name = collection_name.to_string();
189

            
190
2
        match role {
191
            PromptRole::Unlock => {
192
6
                tokio::spawn(async move {
193
6
                    prompter
194
4
                        .unlock_collection_prompt(&path, &window_id, "", collection_name.as_str())
195
8
                        .await
196
                });
197
            }
198
            PromptRole::CreateCollection => {
199
8
                tokio::spawn(async move {
200
6
                    prompter
201
4
                        .create_collection_prompt(&path, &window_id, "", collection_name.as_str())
202
8
                        .await
203
                });
204
            }
205
        }
206

            
207
2
        Ok(())
208
    }
209

            
210
2
    async fn on_reply(
211
        &self,
212
        prompt: &Prompt,
213
        secret: Secret,
214
    ) -> Result<CallbackAction, ServiceError> {
215
        // Handle each role differently based on what validation/preparation is needed
216
4
        match prompt.role() {
217
            PromptRole::Unlock => {
218
4
                if prompt.on_unlock_collection(secret).await? {
219
2
                    Ok(CallbackAction::Dismiss)
220
                } else {
221
4
                    tracing::debug!("Unlock failed, sending retry signal.");
222
                    let emitter = SignalEmitter::from_parts(
223
4
                        self.service.connection().clone(),
224
4
                        self.path().clone(),
225
                    );
226
                    PlasmaPrompterCallback::retry(
227
2
                        &emitter,
228
4
                        &gettext("The unlock password was incorrect"),
229
                    )
230
8
                    .await?;
231

            
232
2
                    Ok(CallbackAction::Keep) // we retry
233
                }
234
            }
235
            PromptRole::CreateCollection => {
236
6
                prompt.on_create_collection(secret).await?;
237
2
                Ok(CallbackAction::Dismiss)
238
            }
239
        }
240
    }
241

            
242
8
    async fn prompter_dismissed(&self, prompt_path: OwnedObjectPath) -> Result<(), ServiceError> {
243
4
        let signal_emitter = self.service.signal_emitter(prompt_path)?;
244
4
        let result = zvariant::Value::new::<Vec<OwnedObjectPath>>(vec![])
245
            .try_into_owned()
246
            .unwrap();
247

            
248
6
        tokio::spawn(async move { Prompt::completed(&signal_emitter, true, result).await });
249
2
        Ok(())
250
    }
251
}