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::{PrompterCallback, PrompterProxy};
15
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
16
use crate::plasma::prompter::{PlasmaPrompterCallback, in_plasma_environment};
17
use crate::{error::custom_service_error, service::Service};
18

            
19
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20
pub enum PromptRole {
21
    Unlock,
22
    CreateCollection,
23
}
24

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

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

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

            
49
    /// Execute the action with the provided secret
50
8
    pub async fn execute(self, secret: Secret) -> Result<OwnedValue, ServiceError> {
51
6
        (self.action)(secret).await
52
    }
53
}
54

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

            
74
// Manual impl because OnceCell doesn't impl Debug
75
impl std::fmt::Debug for Prompt {
76
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77
        f.debug_struct("Prompt")
78
            .field("service", &self.service)
79
            .field("role", &self.role)
80
            .field("path", &self.path)
81
            .field("label", &self.label)
82
            .field("collection", &self.collection)
83
            .finish()
84
    }
85
}
86

            
87
#[cfg(any(
88
    feature = "gnome_openssl_crypto",
89
    feature = "gnome_native_crypto",
90
    feature = "plasma_native_crypto",
91
    feature = "plasma_openssl_crypto"
92
))] // User has to enable at least one prompt backend
93
#[interface(name = "org.freedesktop.Secret.Prompt")]
94
impl Prompt {
95
8
    pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> {
96
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
97
4
        if in_plasma_environment(self.service.connection()).await {
98
            use ashpd::WindowIdentifierType;
99

            
100
4
            if self.callback_plasma.get().is_some() {
101
4
                return Err(custom_service_error(
102
                    "A prompt callback is ongoing already.",
103
                ));
104
            }
105

            
106
2
            let callback =
107
                PlasmaPrompterCallback::new(self.service.clone(), self.path.clone()).await;
108
4
            let path = OwnedObjectPath::from(callback.path().clone());
109

            
110
2
            self.callback_plasma
111
2
                .set(callback.clone())
112
                .expect("A prompt callback is only set once");
113
8
            self.service
114
                .object_server()
115
2
                .at(&path, callback.clone())
116
6
                .await?;
117
2
            tracing::debug!("Prompt `{}` created.", self.path);
118

            
119
8
            return callback
120
2
                .start(
121
2
                    &self.role,
122
6
                    WindowIdentifierType::from_str(window_id.unwrap_or("")).ok(),
123
2
                    &self.label,
124
                )
125
6
                .await;
126
        }
127

            
128
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
129
        {
130
            if self.callback.get().is_some() {
131
                return Err(custom_service_error(
132
                    "A prompt callback is ongoing already.",
133
                ));
134
            };
135

            
136
            let callback = PrompterCallback::new(
137
                (*window_id).and_then(|w| ashpd::WindowIdentifierType::from_str(w).ok()),
138
                self.service.clone(),
139
                self.path.clone(),
140
            )
141
            .await
142
            .map_err(|err| {
143
                custom_service_error(&format!("Failed to create PrompterCallback {err}."))
144
            })?;
145

            
146
            let path = OwnedObjectPath::from(callback.path().clone());
147

            
148
            self.callback
149
                .set(callback.clone())
150
                .expect("A prompt callback is only set once");
151

            
152
            self.service.object_server().at(&path, callback).await?;
153
            tracing::debug!("Prompt `{}` created.", self.path);
154

            
155
            // Starts GNOME System Prompting.
156
            // Spawned separately to avoid blocking the early return of the current
157
            // execution.
158
            let prompter = PrompterProxy::new(self.service.connection()).await?;
159
            tokio::spawn(async move { prompter.begin_prompting(&path).await });
160

            
161
            return Ok(());
162
        }
163

            
164
        #[allow(unreachable_code)]
165
        Err(custom_service_error(
166
            "No prompt backend available in the current environment.",
167
        ))
168
    }
169

            
170
8
    pub async fn dismiss(&self) -> Result<(), ServiceError> {
171
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
172
4
        if let Some(callback_plasma) = self.callback_plasma.get() {
173
            let emitter = SignalEmitter::from_parts(
174
                self.service.connection().clone(),
175
                callback_plasma.path().clone(),
176
            );
177
            PlasmaPrompterCallback::dismiss(&emitter).await?;
178
        }
179

            
180
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
181
4
        if let Some(_callback) = self.callback.get() {
182
            // TODO: figure out if we should destroy the un-export the callback
183
            // here?
184
        }
185

            
186
8
        self.service
187
            .object_server()
188
2
            .remove::<Self, _>(&self.path)
189
6
            .await?;
190
2
        self.service.remove_prompt(&self.path).await;
191

            
192
2
        Ok(())
193
    }
194

            
195
    #[zbus(signal, name = "Completed")]
196
    pub async fn completed(
197
2
        signal_emitter: &SignalEmitter<'_>,
198
2
        dismissed: bool,
199
4
        result: OwnedValue,
200
    ) -> zbus::Result<()>;
201
}
202

            
203
impl Prompt {
204
2
    pub async fn new(
205
        service: Service,
206
        role: PromptRole,
207
        label: String,
208
        collection: Option<crate::collection::Collection>,
209
    ) -> Self {
210
4
        let index = service.prompt_index().await;
211
        Self {
212
2
            path: OwnedObjectPath::try_from(format!("/org/freedesktop/secrets/prompt/p{index}"))
213
                .unwrap(),
214
            service,
215
            role,
216
            label,
217
            collection,
218
            #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
219
4
            callback: Default::default(),
220
            #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
221
4
            callback_plasma: Default::default(),
222
4
            action: Arc::new(Mutex::new(None)),
223
        }
224
    }
225

            
226
2
    pub fn path(&self) -> &ObjectPath<'_> {
227
2
        &self.path
228
    }
229

            
230
2
    pub fn role(&self) -> PromptRole {
231
2
        self.role
232
    }
233

            
234
2
    pub fn label(&self) -> &str {
235
2
        &self.label
236
    }
237

            
238
2
    fn collection(&self) -> Option<&crate::collection::Collection> {
239
2
        self.collection.as_ref()
240
    }
241

            
242
    /// Set the action to execute when the prompt completes
243
8
    pub async fn set_action(&self, action: PromptAction) {
244
2
        *self.action.lock().await = Some(action);
245
    }
246

            
247
    /// Take the action, consuming it so it can only be executed once
248
8
    async fn take_action(&self) -> Option<PromptAction> {
249
4
        self.action.lock().await.take()
250
    }
251

            
252
8
    pub async fn on_unlock_collection(&self, secret: Secret) -> Result<bool, ServiceError> {
253
        debug_assert_eq!(self.role, PromptRole::Unlock);
254

            
255
        // Get the collection to validate the secret
256
2
        let collection = self.collection().expect("Unlock requires a collection");
257
2
        let label = self.label();
258

            
259
        // Validate the secret using the already-open keyring
260
4
        let keyring_guard = collection.keyring.read().await;
261
10
        let is_valid = keyring_guard
262
            .as_ref()
263
            .unwrap()
264
2
            .validate_secret(&secret)
265
6
            .await
266
4
            .map_err(|err| {
267
                custom_service_error(&format!(
268
                    "Failed to validate secret for {label} keyring: {err}."
269
                ))
270
            })?;
271
2
        drop(keyring_guard);
272

            
273
6
        if is_valid {
274
4
            tracing::debug!("Keyring secret matches for {label}.");
275

            
276
4
            let Some(action) = self.take_action().await else {
277
                return Err(custom_service_error(
278
                    "Prompt action was already executed or not set",
279
                ));
280
            };
281

            
282
            // Execute the unlock action after successful validation
283
4
            let result_value = action.execute(secret).await?;
284

            
285
4
            let prompt_path = self.path().to_owned();
286
4
            let signal_emitter = self.service.signal_emitter(&prompt_path)?;
287
8
            tokio::spawn(async move {
288
4
                tracing::debug!("Unlock prompt completed.");
289
4
                let _ = Prompt::completed(&signal_emitter, false, result_value).await;
290
            });
291
2
            Ok(true)
292
        } else {
293
4
            tracing::error!("Keyring {label} failed to unlock, incorrect secret.");
294

            
295
2
            Ok(false)
296
        }
297
    }
298

            
299
10
    pub async fn on_create_collection(&self, secret: Secret) -> Result<(), ServiceError> {
300
        debug_assert_eq!(self.role, PromptRole::CreateCollection);
301

            
302
2
        let Some(action) = self.take_action().await else {
303
            return Err(custom_service_error(
304
                "Prompt action was already executed or not set",
305
            ));
306
        };
307

            
308
        // Execute the collection creation action with the secret
309
6
        match action.execute(secret).await {
310
2
            Ok(collection_path_value) => {
311
4
                tracing::info!("CreateCollection action completed successfully");
312

            
313
4
                let signal_emitter = self.service.signal_emitter(self.path().to_owned())?;
314

            
315
8
                tokio::spawn(async move {
316
4
                    tracing::debug!("CreateCollection prompt completed.");
317
4
                    let _ = Prompt::completed(&signal_emitter, false, collection_path_value).await;
318
                });
319
2
                Ok(())
320
            }
321
            Err(err) => Err(custom_service_error(&format!(
322
                "Failed to create collection: {err}."
323
            ))),
324
        }
325
    }
326
}
327

            
328
#[cfg(test)]
329
mod tests;