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
4
    fn unwrap_double_value<'de>(outer_value: Value<'de>) -> Result<Value<'de>, String> {
24
8
        match outer_value.downcast_ref::<Value>() {
25
8
            Ok(_) => outer_value
26
                .downcast::<Value>()
27
4
                .map_err(|e| format!("Failed to unwrap double-wrapped Value: {e}")),
28
            Err(_) => Ok(outer_value),
29
        }
30
    }
31

            
32
12
    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
33
    where
34
        D: serde::Deserializer<'de>,
35
        T: TryFrom<Value<'de>> + zvariant::Type,
36
        T::Error: std::fmt::Display,
37
    {
38
12
        let outer_value = Value::deserialize(deserializer)?;
39
24
        let value_to_deserialize =
40
            unwrap_double_value(outer_value).map_err(serde::de::Error::custom)?;
41

            
42
24
        match T::try_from(value_to_deserialize) {
43
12
            Ok(val) => Ok(Some(val)),
44
            Err(_) => Ok(None),
45
        }
46
    }
47

            
48
    pub fn deserialize_window_id<'de, D>(
49
        deserializer: D,
50
    ) -> Result<Option<WindowIdentifierType>, D::Error>
51
    where
52
        D: serde::Deserializer<'de>,
53
    {
54
        let outer_value = Value::deserialize(deserializer)?;
55
        let value = unwrap_double_value(outer_value).map_err(serde::de::Error::custom)?;
56

            
57
        match String::try_from(value) {
58
            Ok(s) if !s.is_empty() => Ok(std::str::FromStr::from_str(&s).ok()),
59
            _ => Ok(None),
60
        }
61
    }
62
}
63

            
64
#[derive(Serialize, Deserialize, Type, Default)]
65
#[zvariant(signature = "dict")]
66
#[serde(rename_all = "kebab-case")]
67
// GcrPrompt properties <https://gitlab.gnome.org/GNOME/gcr/-/blob/main/gcr/gcr-prompt.c#L95>
68
pub struct Properties {
69
    #[serde(
70
        serialize_with = "as_value::optional::serialize",
71
        deserialize_with = "double_value_optional::deserialize",
72
        skip_serializing_if = "Option::is_none",
73
        default
74
    )]
75
    title: Option<String>,
76
    #[serde(
77
        serialize_with = "as_value::optional::serialize",
78
        deserialize_with = "double_value_optional::deserialize",
79
        skip_serializing_if = "Option::is_none",
80
        default
81
    )]
82
    message: Option<String>,
83
    #[serde(
84
        serialize_with = "as_value::optional::serialize",
85
        deserialize_with = "double_value_optional::deserialize",
86
        skip_serializing_if = "Option::is_none",
87
        default
88
    )]
89
    description: Option<String>,
90
    #[serde(
91
        serialize_with = "as_value::optional::serialize",
92
        deserialize_with = "double_value_optional::deserialize",
93
        skip_serializing_if = "Option::is_none",
94
        default
95
    )]
96
    warning: Option<String>,
97
    #[serde(
98
        serialize_with = "as_value::optional::serialize",
99
        deserialize_with = "double_value_optional::deserialize",
100
        skip_serializing_if = "Option::is_none",
101
        default
102
    )]
103
    password_new: Option<bool>,
104
    #[serde(
105
        serialize_with = "as_value::optional::serialize",
106
        deserialize_with = "double_value_optional::deserialize",
107
        skip_serializing_if = "Option::is_none",
108
        default
109
    )]
110
    password_strength: Option<i32>,
111
    #[serde(
112
        serialize_with = "as_value::optional::serialize",
113
        deserialize_with = "double_value_optional::deserialize",
114
        skip_serializing_if = "Option::is_none",
115
        default
116
    )]
117
    choice_label: Option<String>,
118
    #[serde(
119
        serialize_with = "as_value::optional::serialize",
120
        deserialize_with = "double_value_optional::deserialize",
121
        skip_serializing_if = "Option::is_none",
122
        default
123
    )]
124
    choice_chosen: Option<bool>,
125
    #[serde(
126
        serialize_with = "as_value::optional::serialize",
127
        deserialize_with = "double_value_optional::deserialize_window_id",
128
        skip_serializing_if = "Option::is_none",
129
        default
130
    )]
131
    caller_window: Option<WindowIdentifierType>,
132
    #[serde(
133
        serialize_with = "as_value::optional::serialize",
134
        deserialize_with = "double_value_optional::deserialize",
135
        skip_serializing_if = "Option::is_none",
136
        default
137
    )]
138
    continue_label: Option<String>,
139
    #[serde(
140
        serialize_with = "as_value::optional::serialize",
141
        deserialize_with = "double_value_optional::deserialize",
142
        skip_serializing_if = "Option::is_none",
143
        default
144
    )]
145
    cancel_label: Option<String>,
146
}
147

            
148
impl Properties {
149
4
    fn for_change_password(keyring: &str, window_id: Option<&WindowIdentifierType>) -> Self {
150
        Self {
151
4
            title: Some(gettext("Change Keyring Password")),
152
12
            message: Some(formatx!(gettext("Choose a new password for the “{}” keyring"), keyring).expect("Wrong format in translatable string")),
153
4
            description: Some(
154
                formatx!(
155
                    gettext("An application wants to change the password for the “{}” keyring. Choose the new password you want to use for it."),
156
                    keyring,
157
                )
158
                .expect("Wrong format in translatable string"),
159
            ),
160
8
            warning: Some(gettext("This operation cannot be reverted")),
161
            password_new: Some(true),
162
            password_strength: None,
163
            choice_label: None,
164
            choice_chosen: None,
165
4
            caller_window: window_id.map(ToOwned::to_owned),
166
8
            continue_label: Some(gettext("Continue")),
167
8
            cancel_label: Some(gettext("Cancel")),
168
        }
169
    }
170

            
171
4
    fn for_unlock(
172
        keyring: &str,
173
        warning: Option<&str>,
174
        window_id: Option<&WindowIdentifierType>,
175
    ) -> Self {
176
        Self {
177
4
            title: Some(gettext("Unlock Keyring")),
178
8
            message: Some(gettext("Authentication required")),
179
4
            description: Some(
180
                formatx!(
181
                    gettext("An application wants access to the keyring '{}', but it is locked",),
182
                    keyring,
183
                )
184
                .expect("Wrong format in translatable string"),
185
            ),
186
4
            warning: warning.map(ToOwned::to_owned),
187
            password_new: None,
188
            password_strength: None,
189
            choice_label: None,
190
            choice_chosen: None,
191
4
            caller_window: window_id.map(ToOwned::to_owned),
192
8
            continue_label: Some(gettext("Unlock")),
193
8
            cancel_label: Some(gettext("Cancel")),
194
        }
195
    }
196

            
197
4
    fn for_create_collection(label: &str, window_id: Option<&WindowIdentifierType>) -> Self {
198
        Self {
199
4
            title: Some(gettext("New Keyring Password")),
200
8
            message: Some(gettext("Choose password for new keyring")),
201
4
            description: Some(
202
                formatx!(
203
                    gettext("An application wants to create a new keyring called “{}”. Choose the password you want to use for it."),
204
                    label
205
                )
206
                .expect("Wrong format in translatable string")
207
            ),
208
            warning: None,
209
            password_new: Some(true),
210
            password_strength: None,
211
            choice_label: None,
212
            choice_chosen: None,
213
4
            caller_window: window_id.map(ToOwned::to_owned),
214
8
            continue_label: Some(gettext("Create")),
215
8
            cancel_label: Some(gettext("Cancel")),
216
        }
217
    }
218
}
219

            
220
#[derive(Deserialize, Serialize, Debug, Type)]
221
#[serde(rename_all = "lowercase")]
222
#[zvariant(signature = "s")]
223
pub enum Reply {
224
    No,
225
    Yes,
226
}
227

            
228
impl zvariant::NoneValue for Reply {
229
    type NoneType = String;
230

            
231
4
    fn null_value() -> Self::NoneType {
232
4
        String::new()
233
    }
234
}
235

            
236
impl TryFrom<String> for Reply {
237
    type Error = String;
238

            
239
4
    fn try_from(value: String) -> Result<Self, Self::Error> {
240
8
        match value.as_str() {
241
8
            "no" => Ok(Reply::No),
242
12
            "yes" => Ok(Reply::Yes),
243
            _ => Err("Invalid value".to_string()),
244
        }
245
    }
246
}
247

            
248
#[derive(Debug, Deserialize, Serialize, Type, PartialEq, Eq, PartialOrd, Ord)]
249
#[serde(rename_all = "lowercase")]
250
#[zvariant(signature = "s")]
251
pub enum PromptType {
252
    Confirm,
253
    Password,
254
}
255

            
256
#[zbus::proxy(
257
    default_service = "org.gnome.keyring.SystemPrompter",
258
    interface = "org.gnome.keyring.internal.Prompter",
259
    default_path = "/org/gnome/keyring/Prompter",
260
    gen_blocking = false
261
)]
262
pub trait GNOMEPrompter {
263
    fn begin_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
264

            
265
    fn perform_prompt(
266
        &self,
267
        callback: &ObjectPath<'_>,
268
        type_: PromptType,
269
        properties: Properties,
270
        exchange: &str,
271
    ) -> Result<(), ServiceError>;
272

            
273
    fn stop_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
274
}
275

            
276
#[derive(Clone)]
277
pub struct GNOMEPrompterCallback {
278
    window_id: Option<WindowIdentifierType>,
279
    private_key: Arc<Key>,
280
    public_key: Arc<Key>,
281
    exchange: OnceCell<String>,
282
    service: Service,
283
    prompt_path: OwnedObjectPath,
284
    path: OwnedObjectPath,
285
}
286

            
287
#[zbus::interface(name = "org.gnome.keyring.internal.Prompter.Callback")]
288
impl GNOMEPrompterCallback {
289
4
    pub async fn prompt_ready(
290
        &self,
291
        reply: Optional<Reply>,
292
        _properties: Properties,
293
        exchange: &str,
294
    ) -> Result<(), ServiceError> {
295
4
        let prompt_path = &self.prompt_path;
296
8
        let Some(prompt) = self.service.prompt(prompt_path).await else {
297
8
            return Err(ServiceError::NoSuchObject(format!(
298
                "Prompt '{prompt_path}' does not exist."
299
            )));
300
        };
301

            
302
8
        match *reply {
303
            // First PromptReady call
304
4
            None => {
305
4
                self.prompter_init(&prompt).await?;
306
            }
307
            // Second PromptReady call with final exchange
308
4
            Some(Reply::Yes) => {
309
14
                self.prompter_done(&prompt, exchange).await?;
310
            }
311
            // Dismissed prompt
312
4
            Some(Reply::No) => {
313
16
                self.prompter_dismissed(prompt.path().clone().into())
314
12
                    .await?;
315
            }
316
        };
317
4
        Ok(())
318
    }
319

            
320
16
    async fn prompt_done(&self) -> Result<(), ServiceError> {
321
        // This is only does check if the prompt is tracked on Service
322
4
        let path = &self.prompt_path;
323
8
        if self.service.prompt(path).await.is_some() {
324
16
            self.service
325
                .object_server()
326
4
                .remove::<Prompt, _>(path)
327
12
                .await?;
328
4
            self.service.remove_prompt(path).await;
329
        }
330
16
        self.service
331
            .object_server()
332
4
            .remove::<Self, _>(&self.path)
333
12
            .await?;
334

            
335
4
        Ok(())
336
    }
337
}
338

            
339
impl GNOMEPrompterCallback {
340
4
    pub async fn new(
341
        window_id: Option<WindowIdentifierType>,
342
        service: Service,
343
        prompt_path: OwnedObjectPath,
344
    ) -> Result<Self, oo7::crypto::Error> {
345
8
        let index = service.prompt_index().await;
346
8
        let private_key = Arc::new(Key::generate_private_key()?);
347
8
        let public_key = Arc::new(crate::gnome::crypto::generate_public_key(&private_key)?);
348
4
        Ok(Self {
349
4
            window_id,
350
4
            public_key,
351
4
            private_key,
352
4
            exchange: Default::default(),
353
8
            path: OwnedObjectPath::try_from(format!("/org/gnome/keyring/Prompt/p{index}")).unwrap(),
354
4
            service,
355
4
            prompt_path,
356
        })
357
    }
358

            
359
4
    pub fn path(&self) -> &ObjectPath<'_> {
360
4
        &self.path
361
    }
362

            
363
16
    async fn prompter_init(&self, prompt: &Prompt) -> Result<(), ServiceError> {
364
8
        let connection = self.service.connection();
365
4
        let exchange = secret_exchange::begin(&self.public_key);
366
4
        self.exchange.set(exchange).unwrap();
367

            
368
4
        let label = prompt.label();
369
8
        let (properties, prompt_type) = match prompt.role() {
370
4
            PromptRole::Unlock => (
371
8
                Properties::for_unlock(label, None, self.window_id.as_ref()),
372
                PromptType::Password,
373
            ),
374
4
            PromptRole::CreateCollection => (
375
8
                Properties::for_create_collection(label, self.window_id.as_ref()),
376
                PromptType::Password,
377
            ),
378
4
            PromptRole::ChangePassword => (
379
8
                Properties::for_change_password(label, self.window_id.as_ref()),
380
                PromptType::Password,
381
            ),
382
        };
383

            
384
8
        let prompter = GNOMEPrompterProxy::new(connection).await?;
385
8
        let path = self.path.clone();
386
8
        let exchange = self.exchange.get().unwrap().clone();
387
12
        tokio::spawn(async move {
388
16
            prompter
389
12
                .perform_prompt(&path, prompt_type, properties, &exchange)
390
20
                .await
391
        });
392
4
        Ok(())
393
    }
394

            
395
26
    async fn prompter_done(&self, prompt: &Prompt, exchange: &str) -> Result<(), ServiceError> {
396
8
        let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?;
397
8
        let aes_key = secret_exchange::handshake(&self.private_key, exchange).map_err(|err| {
398
            custom_service_error(&format!(
399
                "Failed to generate AES key for SecretExchange {err}."
400
            ))
401
        })?;
402

            
403
8
        let Some(raw_secret) = secret_exchange::retrieve(exchange, &aes_key) else {
404
            return Err(custom_service_error(
405
                "Failed to retrieve keyring secret from SecretExchange.",
406
            ));
407
        };
408

            
409
12
        let secret = if raw_secret.as_bytes().is_empty() {
410
            None
411
        } else {
412
4
            Some(raw_secret)
413
        };
414

            
415
        // Handle each role differently based on what validation/preparation is needed
416
8
        match prompt.role() {
417
            PromptRole::Unlock => {
418
12
                if prompt.on_unlock_collection(secret).await? {
419
4
                    let path = self.path.clone();
420
12
                    tokio::spawn(async move { prompter.stop_prompting(&path).await });
421
                } else {
422
                    let properties = Properties::for_unlock(
423
4
                        prompt.label(),
424
                        Some("The unlock password was incorrect"),
425
4
                        self.window_id.as_ref(),
426
                    );
427
4
                    let server_exchange = self
428
                        .exchange
429
                        .get()
430
                        .expect("Exchange cannot be empty at this stage")
431
                        .clone();
432
4
                    let path = self.path.clone();
433

            
434
12
                    tokio::spawn(async move {
435
16
                        prompter
436
4
                            .perform_prompt(
437
4
                                &path,
438
                                PromptType::Password,
439
4
                                properties,
440
4
                                &server_exchange,
441
                            )
442
20
                            .await
443
                    });
444
                }
445
            }
446
            PromptRole::CreateCollection => {
447
12
                prompt.on_create_collection(secret).await?;
448

            
449
4
                let path = self.path.clone();
450
12
                tokio::spawn(async move { prompter.stop_prompting(&path).await });
451
            }
452
            PromptRole::ChangePassword => {
453
12
                prompt.on_change_password(secret).await?;
454

            
455
2
                let path = self.path.clone();
456
6
                tokio::spawn(async move { prompter.stop_prompting(&path).await });
457
            }
458
        }
459
4
        Ok(())
460
    }
461

            
462
16
    async fn prompter_dismissed(&self, prompt_path: OwnedObjectPath) -> Result<(), ServiceError> {
463
8
        let path = self.path.clone();
464
8
        let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?;
465

            
466
16
        tokio::spawn(async move { prompter.stop_prompting(&path).await });
467
8
        let signal_emitter = self.service.signal_emitter(prompt_path)?;
468
8
        let result = zvariant::Value::new::<Vec<OwnedObjectPath>>(vec![])
469
            .try_into_owned()
470
            .unwrap();
471

            
472
12
        tokio::spawn(async move { Prompt::completed(&signal_emitter, true, result).await });
473
4
        Ok(())
474
    }
475
}
476

            
477
#[cfg(test)]
478
mod tests {
479
    use std::collections::HashMap;
480

            
481
    use zvariant::{serialized::Context, to_bytes};
482

            
483
    use super::*;
484

            
485
    #[test]
486
    fn properties_serialization_roundtrip() {
487
        let props = Properties {
488
            title: Some("Test Title".to_string()),
489
            message: Some("Test Message".to_string()),
490
            ..Default::default()
491
        };
492

            
493
        // Serialize to bytes
494
        let ctxt = Context::new_dbus(zvariant::LE, 0);
495
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
496

            
497
        // Deserialize back to verify roundtrip works
498
        let decoded: Properties = encoded.deserialize().unwrap().0;
499

            
500
        assert_eq!(decoded.title, Some("Test Title".to_string()));
501
        assert_eq!(decoded.message, Some("Test Message".to_string()));
502
    }
503

            
504
    #[test]
505
    fn deserialize_properties() {
506
        let mut map: HashMap<String, Value> = HashMap::new();
507

            
508
        // Double-wrap: Value<Value<String>>
509
        map.insert(
510
            "title".to_string(),
511
            Value::new(Value::new("Unlock Keyring")),
512
        );
513

            
514
        map.insert(
515
            "message".to_string(),
516
            Value::new(Value::new("Authentication required")),
517
        );
518

            
519
        // Serialize the HashMap
520
        let ctxt = Context::new_dbus(zvariant::LE, 0);
521
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
522

            
523
        // Deserialize as Properties
524
        let props: Properties = encoded.deserialize().unwrap().0;
525

            
526
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
527
        assert_eq!(props.message, Some("Authentication required".to_string()));
528

            
529
        let mut map: HashMap<String, Value> = HashMap::new();
530

            
531
        // Single-wrap: Value<String> (the correct format)
532
        map.insert("title".to_string(), Value::new("Unlock Keyring"));
533
        map.insert("message".to_string(), Value::new("Authentication required"));
534

            
535
        // Serialize the HashMap
536
        let ctxt = Context::new_dbus(zvariant::LE, 0);
537
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
538

            
539
        // Deserialize as Properties - should also work
540
        let props: Properties = encoded.deserialize().unwrap().0;
541

            
542
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
543
        assert_eq!(props.message, Some("Authentication required".to_string()));
544

            
545
        let props = Properties {
546
            title: None,
547
            message: Some("Test".to_string()),
548
            ..Default::default()
549
        };
550

            
551
        let ctxt = Context::new_dbus(zvariant::LE, 0);
552
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
553
        let decoded: Properties = encoded.deserialize().unwrap().0;
554

            
555
        assert_eq!(decoded.title, None);
556
        assert_eq!(decoded.message, Some("Test".to_string()));
557

            
558
        let props = Properties {
559
            password_new: Some(true),
560
            password_strength: Some(42),
561
            choice_chosen: Some(false),
562
            ..Default::default()
563
        };
564

            
565
        let ctxt = Context::new_dbus(zvariant::LE, 0);
566
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
567
        let decoded: Properties = encoded.deserialize().unwrap().0;
568

            
569
        assert_eq!(decoded.password_new, Some(true));
570
        assert_eq!(decoded.password_strength, Some(42));
571
        assert_eq!(decoded.choice_chosen, Some(false));
572
    }
573
}