1
// org.freedesktop.Secret.Prompt
2

            
3
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
4

            
5
use oo7::{Secret, dbus::ServiceError};
6
use tokio::sync::{Mutex, OnceCell};
7
use zbus::{
8
    interface,
9
    object_server::SignalEmitter,
10
    zvariant::{ObjectPath, Optional, OwnedObjectPath, OwnedValue},
11
};
12

            
13
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
14
use crate::gnome::prompter::{GNOMEPrompterCallback, GNOMEPrompterProxy};
15
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
16
use crate::plasma::prompter::PlasmaPrompterCallback;
17
use crate::{
18
    error::custom_service_error,
19
    service::{PrompterType, Service},
20
};
21

            
22
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23
pub enum PromptRole {
24
    Unlock,
25
    CreateCollection,
26
    ChangePassword,
27
}
28

            
29
/// A boxed future that represents the action to be taken when a prompt
30
/// completes
31
pub type PromptActionFuture =
32
    Pin<Box<dyn Future<Output = Result<OwnedValue, ServiceError>> + Send + 'static>>;
33

            
34
/// Represents the action to be taken when a prompt completes
35
pub struct PromptAction {
36
    /// The async function to execute when the prompt is accepted
37
    action: Box<dyn FnOnce(Secret) -> PromptActionFuture + Send>,
38
}
39

            
40
impl PromptAction {
41
    /// Create a new prompt action from a closure that takes an optional secret
42
    /// and returns a future
43
35
    pub fn new<F, Fut>(f: F) -> Self
44
    where
45
        F: FnOnce(Secret) -> Fut + Send + 'static,
46
        Fut: Future<Output = Result<OwnedValue, ServiceError>> + Send + 'static,
47
    {
48
        Self {
49
103
            action: Box::new(move |secret| Box::pin(f(secret))),
50
        }
51
    }
52

            
53
    /// Execute the action with the provided secret
54
48
    pub async fn execute(self, secret: Secret) -> Result<OwnedValue, ServiceError> {
55
33
        (self.action)(secret).await
56
    }
57
}
58

            
59
#[derive(Clone)]
60
pub struct Prompt {
61
    service: Service,
62
    role: PromptRole,
63
    path: OwnedObjectPath,
64
    /// The label of the collection/keyring being prompted for
65
    label: String,
66
    /// The collection for Unlock prompts (needed for secret validation)
67
    collection: Option<crate::collection::Collection>,
68
    /// GNOME Specific
69
    #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
70
    gnome_callback: Arc<OnceCell<GNOMEPrompterCallback>>,
71
    /// KDE Plasma Specific
72
    #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
73
    plasma_callback: Arc<OnceCell<PlasmaPrompterCallback>>,
74
    /// The action to execute when the prompt completes
75
    action: Arc<Mutex<Option<PromptAction>>>,
76
}
77

            
78
#[cfg(any(
79
    feature = "gnome_openssl_crypto",
80
    feature = "gnome_native_crypto",
81
    feature = "plasma_native_crypto",
82
    feature = "plasma_openssl_crypto"
83
))] // User has to enable at least one prompt backend
84
#[interface(name = "org.freedesktop.Secret.Prompt")]
85
impl Prompt {
86
44
    pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> {
87
22
        let window_id = (*window_id).and_then(|w| ashpd::WindowIdentifierType::from_str(w).ok());
88

            
89
22
        let prompter_type = self.service.prompter_type().await;
90

            
91
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
92
        {
93
14
            if prompter_type == PrompterType::Plasma {
94
8
                if self.plasma_callback.get().is_some() {
95
8
                    return Err(custom_service_error(
96
                        "A prompt callback is ongoing already.",
97
                    ));
98
                }
99

            
100
4
                let callback =
101
                    PlasmaPrompterCallback::new(self.service.clone(), self.path.clone()).await;
102
8
                let path = OwnedObjectPath::from(callback.path().clone());
103

            
104
                // We are sure the callback is not set at this point, so it is fine to ignore
105
                // the result of set
106
8
                let _ = self.plasma_callback.set(callback.clone());
107
16
                self.service
108
                    .object_server()
109
4
                    .at(&path, callback.clone())
110
12
                    .await?;
111
4
                tracing::debug!("Prompt `{}` created.", self.path);
112

            
113
4
                return callback.start(&self.role, window_id, &self.label).await;
114
            }
115
        }
116

            
117
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
118
        {
119
28
            if prompter_type == PrompterType::GNOME {
120
28
                if self.gnome_callback.get().is_some() {
121
8
                    return Err(custom_service_error(
122
                        "A GNOME prompt callback is ongoing already.",
123
                    ));
124
                };
125

            
126
64
                let callback =
127
                    GNOMEPrompterCallback::new(window_id, self.service.clone(), self.path.clone())
128
40
                        .await
129
12
                        .map_err(|err| {
130
                            custom_service_error(&format!(
131
                                "Failed to create GNOMEPrompterCallback {err}."
132
                            ))
133
                        })?;
134

            
135
25
                let path = OwnedObjectPath::from(callback.path().clone());
136

            
137
                // We are sure the callback is not set at this point, so it is fine to ignore
138
                // the result of set
139
24
                let _ = self.gnome_callback.set(callback.clone());
140
10
                self.service.object_server().at(&path, callback).await?;
141
12
                tracing::debug!("Prompt `{}` created.", self.path);
142

            
143
                // Starts GNOME System Prompting.
144
                // Spawned separately to avoid blocking the early return of the current
145
                // execution.
146
21
                let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?;
147
44
                tokio::spawn(async move { prompter.begin_prompting(&path).await });
148

            
149
11
                return Ok(());
150
            }
151
        }
152

            
153
        #[allow(unreachable_code)]
154
        Err(custom_service_error(
155
            "No prompt backend available in the current environment.",
156
        ))
157
    }
158

            
159
16
    pub async fn dismiss(&self) -> Result<(), ServiceError> {
160
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
161
8
        if let Some(callback) = self.plasma_callback.get() {
162
            let emitter = SignalEmitter::from_parts(
163
                self.service.connection().clone(),
164
                callback.path().clone(),
165
            );
166
            PlasmaPrompterCallback::dismiss(&emitter).await?;
167
        }
168

            
169
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
170
8
        if let Some(_callback) = self.gnome_callback.get() {
171
            // TODO: figure out if we should destroy the un-export the callback
172
            // here?
173
        }
174

            
175
16
        self.service
176
            .object_server()
177
4
            .remove::<Self, _>(&self.path)
178
12
            .await?;
179
4
        self.service.remove_prompt(&self.path).await;
180

            
181
4
        Ok(())
182
    }
183

            
184
    #[zbus(signal, name = "Completed")]
185
    pub async fn completed(
186
16
        signal_emitter: &SignalEmitter<'_>,
187
16
        dismissed: bool,
188
33
        result: OwnedValue,
189
    ) -> zbus::Result<()>;
190
}
191

            
192
impl Prompt {
193
10
    pub async fn new(
194
        service: Service,
195
        role: PromptRole,
196
        label: String,
197
        collection: Option<crate::collection::Collection>,
198
    ) -> Self {
199
22
        let index = service.prompt_index().await;
200
        Self {
201
12
            path: OwnedObjectPath::try_from(format!("/org/freedesktop/secrets/prompt/p{index}"))
202
                .unwrap(),
203
            service,
204
            role,
205
            label,
206
            collection,
207
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
208
22
            gnome_callback: Default::default(),
209
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
210
22
            plasma_callback: Default::default(),
211
22
            action: Arc::new(Mutex::new(None)),
212
        }
213
    }
214

            
215
10
    pub fn path(&self) -> &ObjectPath<'_> {
216
12
        &self.path
217
    }
218

            
219
11
    pub fn role(&self) -> PromptRole {
220
12
        self.role
221
    }
222

            
223
12
    pub fn label(&self) -> &str {
224
11
        &self.label
225
    }
226

            
227
8
    fn collection(&self) -> Option<&crate::collection::Collection> {
228
8
        self.collection.as_ref()
229
    }
230

            
231
    /// Set the action to execute when the prompt completes
232
44
    pub async fn set_action(&self, action: PromptAction) {
233
10
        *self.action.lock().await = Some(action);
234
    }
235

            
236
    /// Take the action, consuming it so it can only be executed once
237
48
    async fn take_action(&self) -> Option<PromptAction> {
238
24
        self.action.lock().await.take()
239
    }
240

            
241
36
    pub async fn on_unlock_collection(&self, secret: Secret) -> Result<bool, ServiceError> {
242
        debug_assert_eq!(self.role, PromptRole::Unlock);
243

            
244
        // Get the collection to validate the secret
245
8
        let collection = self.collection().expect("Unlock requires a collection");
246
8
        let label = self.label();
247

            
248
        // Validate the secret using the already-open keyring
249
16
        let keyring_guard = collection.keyring.read().await;
250
40
        let is_valid = keyring_guard
251
            .as_ref()
252
            .unwrap()
253
8
            .validate_secret(&secret)
254
24
            .await
255
16
            .map_err(|err| {
256
                custom_service_error(&format!(
257
                    "Failed to validate secret for {label} keyring: {err}."
258
                ))
259
            })?;
260
8
        drop(keyring_guard);
261

            
262
20
        if is_valid {
263
16
            tracing::debug!("Keyring secret matches for {label}.");
264

            
265
16
            let Some(action) = self.take_action().await else {
266
                return Err(custom_service_error(
267
                    "Prompt action was already executed or not set",
268
                ));
269
            };
270

            
271
            // Execute the unlock action after successful validation
272
20
            let result_value = action.execute(secret).await?;
273

            
274
18
            let prompt_path = self.path().to_owned();
275
18
            let signal_emitter = self.service.signal_emitter(&prompt_path)?;
276
33
            tokio::spawn(async move {
277
16
                tracing::debug!("Unlock prompt completed.");
278
16
                let _ = Prompt::completed(&signal_emitter, false, result_value).await;
279
            });
280
8
            Ok(true)
281
        } else {
282
8
            tracing::error!("Keyring {label} failed to unlock, incorrect secret.");
283

            
284
4
            Ok(false)
285
        }
286
    }
287

            
288
49
    pub async fn on_create_collection(&self, secret: Secret) -> Result<(), ServiceError> {
289
        debug_assert_eq!(self.role, PromptRole::CreateCollection);
290

            
291
9
        let Some(action) = self.take_action().await else {
292
            return Err(custom_service_error(
293
                "Prompt action was already executed or not set",
294
            ));
295
        };
296

            
297
        // Execute the collection creation action with the secret
298
29
        match action.execute(secret).await {
299
11
            Ok(collection_path_value) => {
300
24
                tracing::info!("CreateCollection action completed successfully");
301

            
302
24
                let signal_emitter = self.service.signal_emitter(self.path().to_owned())?;
303

            
304
53
                tokio::spawn(async move {
305
29
                    tracing::debug!("CreateCollection prompt completed.");
306
28
                    let _ = Prompt::completed(&signal_emitter, false, collection_path_value).await;
307
                });
308
13
                Ok(())
309
            }
310
            Err(err) => Err(custom_service_error(&format!(
311
                "Failed to create collection: {err}."
312
            ))),
313
        }
314
    }
315

            
316
18
    pub async fn on_change_password(&self, secret: Secret) -> Result<(), ServiceError> {
317
        debug_assert_eq!(self.role, PromptRole::ChangePassword);
318

            
319
4
        let Some(action) = self.take_action().await else {
320
            return Err(custom_service_error(
321
                "Prompt action was already executed or not set",
322
            ));
323
        };
324

            
325
        // Execute the change password action with the new secret
326
10
        match action.execute(secret).await {
327
2
            Ok(result) => {
328
4
                tracing::info!("ChangePassword action completed successfully");
329

            
330
4
                let signal_emitter = self.service.signal_emitter(self.path().to_owned())?;
331

            
332
8
                tokio::spawn(async move {
333
4
                    tracing::debug!("ChangePassword prompt completed.");
334
4
                    let _ = Prompt::completed(&signal_emitter, false, result).await;
335
                });
336
2
                Ok(())
337
            }
338
6
            Err(err) => Err(custom_service_error(&format!(
339
                "Failed to change password: {err}."
340
            ))),
341
        }
342
    }
343
}
344

            
345
#[cfg(test)]
346
mod tests;