1
use std::sync::Arc;
2

            
3
use formatx::formatx;
4
use gettextrs::gettext;
5
use oo7::{Key, ashpd::WindowIdentifierType, dbus::ServiceError};
6
use serde::{Deserialize, Serialize};
7
use tokio::sync::OnceCell;
8
use zbus::zvariant::{self, ObjectPath, Optional, OwnedObjectPath, Type, Value, as_value};
9

            
10
use super::secret_exchange;
11
use crate::{
12
    error::custom_service_error,
13
    prompt::{Prompt, PromptRole},
14
    service::Service,
15
};
16

            
17
/// Custom serde module to handle GCR's double-Value wrapping bug
18
///
19
/// See: https://gitlab.gnome.org/GNOME/gcr/-/merge_requests/169
20
mod double_value_optional {
21
    use super::*;
22

            
23
26
    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
24
    where
25
        D: serde::Deserializer<'de>,
26
        T: TryFrom<Value<'de>> + zvariant::Type,
27
        T::Error: std::fmt::Display,
28
    {
29
24
        let outer_value = Value::deserialize(deserializer)?;
30

            
31
        // Try to downcast to check if it's double-wrapped
32
49
        let value_to_deserialize = match outer_value.downcast_ref::<Value>() {
33
49
            Ok(_) => outer_value.downcast::<Value>().map_err(|e| {
34
                serde::de::Error::custom(format!("Failed to unwrap double-wrapped Value: {e}"))
35
            })?,
36
            Err(_) => outer_value,
37
        };
38

            
39
26
        match T::try_from(value_to_deserialize) {
40
24
            Ok(val) => Ok(Some(val)),
41
            Err(_) => Ok(None),
42
        }
43
    }
44
}
45

            
46
#[derive(Serialize, Deserialize, Type, Default)]
47
#[zvariant(signature = "dict")]
48
#[serde(rename_all = "kebab-case")]
49
// GcrPrompt properties <https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/gcr-prompt.c#L95>
50
pub struct Properties {
51
    #[serde(
52
        serialize_with = "as_value::optional::serialize",
53
        deserialize_with = "double_value_optional::deserialize",
54
        skip_serializing_if = "Option::is_none",
55
        default
56
    )]
57
    title: Option<String>,
58
    #[serde(
59
        serialize_with = "as_value::optional::serialize",
60
        deserialize_with = "double_value_optional::deserialize",
61
        skip_serializing_if = "Option::is_none",
62
        default
63
    )]
64
    message: Option<String>,
65
    #[serde(
66
        serialize_with = "as_value::optional::serialize",
67
        deserialize_with = "double_value_optional::deserialize",
68
        skip_serializing_if = "Option::is_none",
69
        default
70
    )]
71
    description: Option<String>,
72
    #[serde(
73
        serialize_with = "as_value::optional::serialize",
74
        deserialize_with = "double_value_optional::deserialize",
75
        skip_serializing_if = "Option::is_none",
76
        default
77
    )]
78
    warning: Option<String>,
79
    #[serde(
80
        serialize_with = "as_value::optional::serialize",
81
        deserialize_with = "double_value_optional::deserialize",
82
        skip_serializing_if = "Option::is_none",
83
        default
84
    )]
85
    password_new: Option<bool>,
86
    #[serde(
87
        serialize_with = "as_value::optional::serialize",
88
        deserialize_with = "double_value_optional::deserialize",
89
        skip_serializing_if = "Option::is_none",
90
        default
91
    )]
92
    password_strength: Option<i32>,
93
    #[serde(
94
        serialize_with = "as_value::optional::serialize",
95
        deserialize_with = "double_value_optional::deserialize",
96
        skip_serializing_if = "Option::is_none",
97
        default
98
    )]
99
    choice_label: Option<String>,
100
    #[serde(
101
        serialize_with = "as_value::optional::serialize",
102
        deserialize_with = "double_value_optional::deserialize",
103
        skip_serializing_if = "Option::is_none",
104
        default
105
    )]
106
    choice_chosen: Option<bool>,
107
    #[serde(
108
        with = "as_value::optional",
109
        skip_serializing_if = "Option::is_none",
110
        default
111
    )]
112
    caller_window: Option<WindowIdentifierType>,
113
    #[serde(
114
        serialize_with = "as_value::optional::serialize",
115
        deserialize_with = "double_value_optional::deserialize",
116
        skip_serializing_if = "Option::is_none",
117
        default
118
    )]
119
    continue_label: Option<String>,
120
    #[serde(
121
        serialize_with = "as_value::optional::serialize",
122
        deserialize_with = "double_value_optional::deserialize",
123
        skip_serializing_if = "Option::is_none",
124
        default
125
    )]
126
    cancel_label: Option<String>,
127
}
128

            
129
impl Properties {
130
4
    fn for_change_password(keyring: &str, window_id: Option<&WindowIdentifierType>) -> Self {
131
        Self {
132
4
            title: Some(gettext("Change Keyring Password")),
133
16
            message: Some(formatx!(gettext("Choose a new password for the “{}” keyring"), keyring).expect("Wrong format in translatable string")),
134
4
            description: Some(
135
                formatx!(
136
                    gettext("An application wants to change the password for the “{}” keyring. Choose the new password you want to use for it."),
137
                    keyring,
138
                )
139
                .expect("Wrong format in translatable string"),
140
            ),
141
8
            warning: Some(gettext("This operation cannot be reverted")),
142
            password_new: Some(true),
143
            password_strength: None,
144
            choice_label: None,
145
            choice_chosen: None,
146
4
            caller_window: window_id.map(ToOwned::to_owned),
147
8
            continue_label: Some(gettext("Continue")),
148
8
            cancel_label: Some(gettext("Cancel")),
149
        }
150
    }
151

            
152
8
    fn for_unlock(
153
        keyring: &str,
154
        warning: Option<&str>,
155
        window_id: Option<&WindowIdentifierType>,
156
    ) -> Self {
157
        Self {
158
8
            title: Some(gettext("Unlock Keyring")),
159
16
            message: Some(gettext("Authentication required")),
160
8
            description: Some(
161
                formatx!(
162
                    gettext("An application wants access to the keyring '{}', but it is locked",),
163
                    keyring,
164
                )
165
                .expect("Wrong format in translatable string"),
166
            ),
167
8
            warning: warning.map(ToOwned::to_owned),
168
            password_new: None,
169
            password_strength: None,
170
            choice_label: None,
171
            choice_chosen: None,
172
8
            caller_window: window_id.map(ToOwned::to_owned),
173
16
            continue_label: Some(gettext("Unlock")),
174
16
            cancel_label: Some(gettext("Cancel")),
175
        }
176
    }
177

            
178
9
    fn for_create_collection(label: &str, window_id: Option<&WindowIdentifierType>) -> Self {
179
        Self {
180
10
            title: Some(gettext("New Keyring Password")),
181
19
            message: Some(gettext("Choose password for new keyring")),
182
9
            description: Some(
183
                formatx!(
184
                    gettext("An application wants to create a new keyring called “{}”. Choose the password you want to use for it."),
185
                    &label
186
                )
187
                .expect("Wrong format in translatable string")
188
            ),
189
            warning: None,
190
            password_new: Some(true),
191
            password_strength: None,
192
            choice_label: None,
193
            choice_chosen: None,
194
9
            caller_window: window_id.map(ToOwned::to_owned),
195
19
            continue_label: Some(gettext("Create")),
196
19
            cancel_label: Some(gettext("Cancel")),
197
        }
198
    }
199
}
200

            
201
#[derive(Deserialize, Serialize, Debug, Type)]
202
#[serde(rename_all = "lowercase")]
203
#[zvariant(signature = "s")]
204
pub enum Reply {
205
    No,
206
    Yes,
207
}
208

            
209
impl zvariant::NoneValue for Reply {
210
    type NoneType = String;
211

            
212
11
    fn null_value() -> Self::NoneType {
213
10
        String::new()
214
    }
215
}
216

            
217
impl TryFrom<String> for Reply {
218
    type Error = String;
219

            
220
14
    fn try_from(value: String) -> Result<Self, Self::Error> {
221
23
        match value.as_str() {
222
17
            "no" => Ok(Reply::No),
223
42
            "yes" => Ok(Reply::Yes),
224
            _ => Err("Invalid value".to_string()),
225
        }
226
    }
227
}
228

            
229
#[derive(Debug, Deserialize, Serialize, Type, PartialEq, Eq, PartialOrd, Ord)]
230
#[serde(rename_all = "lowercase")]
231
#[zvariant(signature = "s")]
232
pub enum PromptType {
233
    Confirm,
234
    Password,
235
}
236

            
237
#[zbus::proxy(
238
    default_service = "org.gnome.keyring.SystemPrompter",
239
    interface = "org.gnome.keyring.internal.Prompter",
240
    default_path = "/org/gnome/keyring/Prompter",
241
    gen_blocking = false
242
)]
243
pub trait GNOMEPrompter {
244
    fn begin_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
245

            
246
    fn perform_prompt(
247
        &self,
248
        callback: &ObjectPath<'_>,
249
        type_: PromptType,
250
        properties: Properties,
251
        exchange: &str,
252
    ) -> Result<(), ServiceError>;
253

            
254
    fn stop_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
255
}
256

            
257
#[derive(Clone)]
258
pub struct GNOMEPrompterCallback {
259
    window_id: Option<WindowIdentifierType>,
260
    private_key: Arc<Key>,
261
    public_key: Arc<Key>,
262
    exchange: OnceCell<String>,
263
    service: Service,
264
    prompt_path: OwnedObjectPath,
265
    path: OwnedObjectPath,
266
}
267

            
268
#[zbus::interface(name = "org.gnome.keyring.internal.Prompter.Callback")]
269
impl GNOMEPrompterCallback {
270
12
    pub async fn prompt_ready(
271
        &self,
272
        reply: Optional<Reply>,
273
        _properties: Properties,
274
        exchange: &str,
275
    ) -> Result<(), ServiceError> {
276
10
        let prompt_path = &self.prompt_path;
277
26
        let Some(prompt) = self.service.prompt(prompt_path).await else {
278
8
            return Err(ServiceError::NoSuchObject(format!(
279
                "Prompt '{prompt_path}' does not exist."
280
            )));
281
        };
282

            
283
22
        match *reply {
284
            // First PromptReady call
285
12
            None => {
286
16
                self.prompter_init(&prompt).await?;
287
            }
288
            // Second PromptReady call with final exchange
289
16
            Some(Reply::Yes) => {
290
43
                self.prompter_done(&prompt, exchange).await?;
291
            }
292
            // Dismissed prompt
293
4
            Some(Reply::No) => {
294
16
                self.prompter_dismissed(prompt.path().clone().into())
295
16
                    .await?;
296
            }
297
        };
298
11
        Ok(())
299
    }
300

            
301
41
    async fn prompt_done(&self) -> Result<(), ServiceError> {
302
        // This is only does check if the prompt is tracked on Service
303
10
        let path = &self.prompt_path;
304
24
        if self.service.prompt(path).await.is_some() {
305
43
            self.service
306
                .object_server()
307
11
                .remove::<Prompt, _>(path)
308
34
                .await?;
309
15
            self.service.remove_prompt(path).await;
310
        }
311
44
        self.service
312
            .object_server()
313
11
            .remove::<Self, _>(&self.path)
314
37
            .await?;
315

            
316
11
        Ok(())
317
    }
318
}
319

            
320
impl GNOMEPrompterCallback {
321
14
    pub async fn new(
322
        window_id: Option<WindowIdentifierType>,
323
        service: Service,
324
        prompt_path: OwnedObjectPath,
325
    ) -> Result<Self, oo7::crypto::Error> {
326
32
        let index = service.prompt_index().await;
327
27
        let private_key = Arc::new(Key::generate_private_key()?);
328
26
        let public_key = Arc::new(crate::gnome::crypto::generate_public_key(&private_key)?);
329
12
        Ok(Self {
330
11
            window_id,
331
11
            public_key,
332
13
            private_key,
333
12
            exchange: Default::default(),
334
25
            path: OwnedObjectPath::try_from(format!("/org/gnome/keyring/Prompt/p{index}")).unwrap(),
335
13
            service,
336
12
            prompt_path,
337
        })
338
    }
339

            
340
12
    pub fn path(&self) -> &ObjectPath<'_> {
341
12
        &self.path
342
    }
343

            
344
44
    async fn prompter_init(&self, prompt: &Prompt) -> Result<(), ServiceError> {
345
23
        let connection = self.service.connection();
346
11
        let exchange = secret_exchange::begin(&self.public_key);
347
12
        self.exchange.set(exchange).unwrap();
348

            
349
11
        let label = prompt.label();
350
23
        let (properties, prompt_type) = match prompt.role() {
351
8
            PromptRole::Unlock => (
352
16
                Properties::for_unlock(label, None, self.window_id.as_ref()),
353
                PromptType::Password,
354
            ),
355
10
            PromptRole::CreateCollection => (
356
19
                Properties::for_create_collection(label, self.window_id.as_ref()),
357
                PromptType::Password,
358
            ),
359
4
            PromptRole::ChangePassword => (
360
8
                Properties::for_change_password(label, self.window_id.as_ref()),
361
                PromptType::Password,
362
            ),
363
        };
364

            
365
27
        let prompter = GNOMEPrompterProxy::new(connection).await?;
366
23
        let path = self.path.clone();
367
23
        let exchange = self.exchange.get().unwrap().clone();
368
35
        tokio::spawn(async move {
369
47
            prompter
370
35
                .perform_prompt(&path, prompt_type, properties, &exchange)
371
60
                .await
372
        });
373
11
        Ok(())
374
    }
375

            
376
71
    async fn prompter_done(&self, prompt: &Prompt, exchange: &str) -> Result<(), ServiceError> {
377
32
        let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?;
378
28
        let aes_key = secret_exchange::handshake(&self.private_key, exchange).map_err(|err| {
379
            custom_service_error(&format!(
380
                "Failed to generate AES key for SecretExchange {err}."
381
            ))
382
        })?;
383

            
384
26
        let Some(secret) = secret_exchange::retrieve(exchange, &aes_key) else {
385
            return Err(custom_service_error(
386
                "Failed to retrieve keyring secret from SecretExchange.",
387
            ));
388
        };
389

            
390
        // Handle each role differently based on what validation/preparation is needed
391
24
        match prompt.role() {
392
            PromptRole::Unlock => {
393
24
                if prompt.on_unlock_collection(secret).await? {
394
8
                    let path = self.path.clone();
395
28
                    tokio::spawn(async move { prompter.stop_prompting(&path).await });
396
                } else {
397
                    let properties = Properties::for_unlock(
398
4
                        prompt.label(),
399
                        Some("The unlock password was incorrect"),
400
4
                        self.window_id.as_ref(),
401
                    );
402
4
                    let server_exchange = self
403
                        .exchange
404
                        .get()
405
                        .expect("Exchange cannot be empty at this stage")
406
                        .clone();
407
4
                    let path = self.path.clone();
408

            
409
12
                    tokio::spawn(async move {
410
16
                        prompter
411
4
                            .perform_prompt(
412
4
                                &path,
413
                                PromptType::Password,
414
4
                                properties,
415
4
                                &server_exchange,
416
                            )
417
24
                            .await
418
                    });
419
                }
420
            }
421
            PromptRole::CreateCollection => {
422
33
                prompt.on_create_collection(secret).await?;
423

            
424
14
                let path = self.path.clone();
425
43
                tokio::spawn(async move { prompter.stop_prompting(&path).await });
426
            }
427
            PromptRole::ChangePassword => {
428
16
                prompt.on_change_password(secret).await?;
429

            
430
2
                let path = self.path.clone();
431
8
                tokio::spawn(async move { prompter.stop_prompting(&path).await });
432
            }
433
        }
434
16
        Ok(())
435
    }
436

            
437
16
    async fn prompter_dismissed(&self, prompt_path: OwnedObjectPath) -> Result<(), ServiceError> {
438
8
        let path = self.path.clone();
439
12
        let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?;
440

            
441
20
        tokio::spawn(async move { prompter.stop_prompting(&path).await });
442
8
        let signal_emitter = self.service.signal_emitter(prompt_path)?;
443
8
        let result = zvariant::Value::new::<Vec<OwnedObjectPath>>(vec![])
444
            .try_into_owned()
445
            .unwrap();
446

            
447
16
        tokio::spawn(async move { Prompt::completed(&signal_emitter, true, result).await });
448
4
        Ok(())
449
    }
450
}
451

            
452
#[cfg(test)]
453
mod tests {
454
    use std::collections::HashMap;
455

            
456
    use zvariant::{serialized::Context, to_bytes};
457

            
458
    use super::*;
459

            
460
    #[test]
461
    fn properties_serialization_roundtrip() {
462
        let props = Properties {
463
            title: Some("Test Title".to_string()),
464
            message: Some("Test Message".to_string()),
465
            ..Default::default()
466
        };
467

            
468
        // Serialize to bytes
469
        let ctxt = Context::new_dbus(zvariant::LE, 0);
470
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
471

            
472
        // Deserialize back to verify roundtrip works
473
        let decoded: Properties = encoded.deserialize().unwrap().0;
474

            
475
        assert_eq!(decoded.title, Some("Test Title".to_string()));
476
        assert_eq!(decoded.message, Some("Test Message".to_string()));
477
    }
478

            
479
    #[test]
480
    fn deserialize_properties() {
481
        let mut map: HashMap<String, Value> = HashMap::new();
482

            
483
        // Double-wrap: Value<Value<String>>
484
        map.insert(
485
            "title".to_string(),
486
            Value::new(Value::new("Unlock Keyring")),
487
        );
488

            
489
        map.insert(
490
            "message".to_string(),
491
            Value::new(Value::new("Authentication required")),
492
        );
493

            
494
        // Serialize the HashMap
495
        let ctxt = Context::new_dbus(zvariant::LE, 0);
496
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
497

            
498
        // Deserialize as Properties
499
        let props: Properties = encoded.deserialize().unwrap().0;
500

            
501
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
502
        assert_eq!(props.message, Some("Authentication required".to_string()));
503

            
504
        let mut map: HashMap<String, Value> = HashMap::new();
505

            
506
        // Single-wrap: Value<String> (the correct format)
507
        map.insert("title".to_string(), Value::new("Unlock Keyring"));
508
        map.insert("message".to_string(), Value::new("Authentication required"));
509

            
510
        // Serialize the HashMap
511
        let ctxt = Context::new_dbus(zvariant::LE, 0);
512
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
513

            
514
        // Deserialize as Properties - should also work
515
        let props: Properties = encoded.deserialize().unwrap().0;
516

            
517
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
518
        assert_eq!(props.message, Some("Authentication required".to_string()));
519

            
520
        let props = Properties {
521
            title: None,
522
            message: Some("Test".to_string()),
523
            ..Default::default()
524
        };
525

            
526
        let ctxt = Context::new_dbus(zvariant::LE, 0);
527
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
528
        let decoded: Properties = encoded.deserialize().unwrap().0;
529

            
530
        assert_eq!(decoded.title, None);
531
        assert_eq!(decoded.message, Some("Test".to_string()));
532

            
533
        let props = Properties {
534
            password_new: Some(true),
535
            password_strength: Some(42),
536
            choice_chosen: Some(false),
537
            ..Default::default()
538
        };
539

            
540
        let ctxt = Context::new_dbus(zvariant::LE, 0);
541
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
542
        let decoded: Properties = encoded.deserialize().unwrap().0;
543

            
544
        assert_eq!(decoded.password_new, Some(true));
545
        assert_eq!(decoded.password_strength, Some(42));
546
        assert_eq!(decoded.choice_chosen, Some(false));
547
    }
548
}