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
use crate::{
14
    error::custom_service_error,
15
    gnome::prompter::{PrompterCallback, PrompterProxy},
16
    service::Service,
17
};
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
    callback: Arc<OnceCell<PrompterCallback>>,
66
    /// The action to execute when the prompt completes
67
    action: Arc<Mutex<Option<PromptAction>>>,
68
}
69

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

            
83
#[interface(name = "org.freedesktop.Secret.Prompt")]
84
impl Prompt {
85
8
    pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> {
86
4
        if self.callback.get().is_some() {
87
4
            return Err(custom_service_error(
88
                "A prompt callback is ongoing already.",
89
            ));
90
        };
91

            
92
        let callback = PrompterCallback::new(
93
4
            (*window_id).and_then(|w| ashpd::WindowIdentifierType::from_str(w).ok()),
94
4
            self.service.clone(),
95
2
            self.path.clone(),
96
        )
97
6
        .await
98
2
        .map_err(|err| {
99
            custom_service_error(&format!("Failed to create PrompterCallback {err}."))
100
        })?;
101

            
102
4
        let path = OwnedObjectPath::from(callback.path().clone());
103

            
104
2
        self.callback
105
2
            .set(callback.clone())
106
            .expect("A prompt callback is only set once");
107

            
108
2
        self.service.object_server().at(&path, callback).await?;
109
4
        tracing::debug!("Prompt `{}` created.", self.path);
110

            
111
        // Starts GNOME System Prompting.
112
        // Spawned separately to avoid blocking the early return of the current
113
        // execution.
114
4
        let prompter = PrompterProxy::new(self.service.connection()).await?;
115
8
        tokio::spawn(async move { prompter.begin_prompting(&path).await });
116

            
117
2
        Ok(())
118
    }
119

            
120
8
    pub async fn dismiss(&self) -> Result<(), ServiceError> {
121
4
        if let Some(_callback) = self.callback.get() {
122
            // TODO: figure out if we should destroy the un-export the callback
123
            // here?
124
        }
125

            
126
8
        self.service
127
            .object_server()
128
2
            .remove::<Self, _>(&self.path)
129
6
            .await?;
130
2
        self.service.remove_prompt(&self.path).await;
131

            
132
2
        Ok(())
133
    }
134

            
135
    #[zbus(signal, name = "Completed")]
136
    pub async fn completed(
137
2
        signal_emitter: &SignalEmitter<'_>,
138
2
        dismissed: bool,
139
4
        result: OwnedValue,
140
    ) -> zbus::Result<()>;
141
}
142

            
143
impl Prompt {
144
2
    pub async fn new(
145
        service: Service,
146
        role: PromptRole,
147
        label: String,
148
        collection: Option<crate::collection::Collection>,
149
    ) -> Self {
150
4
        let index = service.prompt_index().await;
151
        Self {
152
2
            path: OwnedObjectPath::try_from(format!("/org/freedesktop/secrets/prompt/p{index}"))
153
                .unwrap(),
154
            service,
155
            role,
156
            label,
157
            collection,
158
4
            callback: Default::default(),
159
4
            action: Arc::new(Mutex::new(None)),
160
        }
161
    }
162

            
163
2
    pub fn path(&self) -> &ObjectPath<'_> {
164
2
        &self.path
165
    }
166

            
167
2
    pub fn role(&self) -> PromptRole {
168
2
        self.role
169
    }
170

            
171
2
    pub fn label(&self) -> &str {
172
2
        &self.label
173
    }
174

            
175
2
    fn collection(&self) -> Option<&crate::collection::Collection> {
176
2
        self.collection.as_ref()
177
    }
178

            
179
    /// Set the action to execute when the prompt completes
180
8
    pub async fn set_action(&self, action: PromptAction) {
181
2
        *self.action.lock().await = Some(action);
182
    }
183

            
184
    /// Take the action, consuming it so it can only be executed once
185
8
    async fn take_action(&self) -> Option<PromptAction> {
186
4
        self.action.lock().await.take()
187
    }
188

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

            
192
        // Get the collection to validate the secret
193
2
        let collection = self.collection().expect("Unlock requires a collection");
194
2
        let label = self.label();
195

            
196
        // Validate the secret using the already-open keyring
197
4
        let keyring_guard = collection.keyring.read().await;
198
8
        let is_valid = keyring_guard
199
            .as_ref()
200
            .unwrap()
201
2
            .validate_secret(&secret)
202
6
            .await
203
2
            .map_err(|err| {
204
                custom_service_error(&format!(
205
                    "Failed to validate secret for {label} keyring: {err}."
206
                ))
207
            })?;
208
2
        drop(keyring_guard);
209

            
210
6
        if is_valid {
211
5
            tracing::debug!("Keyring secret matches for {label}.");
212

            
213
4
            let Some(action) = self.take_action().await else {
214
                return Err(custom_service_error(
215
                    "Prompt action was already executed or not set",
216
                ));
217
            };
218

            
219
            // Execute the unlock action after successful validation
220
4
            let result_value = action.execute(secret).await?;
221

            
222
4
            let prompt_path = self.path().to_owned();
223
4
            let signal_emitter = self.service.signal_emitter(&prompt_path)?;
224
8
            tokio::spawn(async move {
225
6
                tracing::debug!("Unlock prompt completed.");
226
4
                let _ = Prompt::completed(&signal_emitter, false, result_value).await;
227
            });
228
2
            Ok(true)
229
        } else {
230
5
            tracing::error!("Keyring {label} failed to unlock, incorrect secret.");
231

            
232
2
            Ok(false)
233
        }
234
    }
235

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

            
239
2
        let Some(action) = self.take_action().await else {
240
            return Err(custom_service_error(
241
                "Prompt action was already executed or not set",
242
            ));
243
        };
244

            
245
        // Execute the collection creation action with the secret
246
6
        match action.execute(secret).await {
247
2
            Ok(collection_path_value) => {
248
5
                tracing::info!("CreateCollection action completed successfully");
249

            
250
4
                let signal_emitter = self.service.signal_emitter(self.path().to_owned())?;
251

            
252
8
                tokio::spawn(async move {
253
6
                    tracing::debug!("CreateCollection prompt completed.");
254
4
                    let _ = Prompt::completed(&signal_emitter, false, collection_path_value).await;
255
                });
256
2
                Ok(())
257
            }
258
            Err(err) => Err(custom_service_error(&format!(
259
                "Failed to create collection: {err}."
260
            ))),
261
        }
262
    }
263
}
264

            
265
#[cfg(test)]
266
mod tests;