1
use std::sync::Arc;
2

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

            
12
use super::secret_exchange;
13
use crate::{
14
    error::custom_service_error,
15
    i18n::i18n_f,
16
    prompt::{Prompt, PromptRole},
17
    service::Service,
18
};
19

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

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

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

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

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

            
132
impl Properties {
133
2
    fn for_unlock(
134
        keyring: &str,
135
        warning: Option<&str>,
136
        window_id: Option<&WindowIdentifierType>,
137
    ) -> Self {
138
        Self {
139
2
            title: Some(gettext("Unlock Keyring")),
140
4
            message: Some(gettext("Authentication required")),
141
4
            description: Some(i18n_f(
142
                "An application wants access to the keyring '{}', but it is locked",
143
                &[keyring],
144
            )),
145
2
            warning: warning.map(ToOwned::to_owned),
146
            password_new: None,
147
            password_strength: None,
148
            choice_label: None,
149
            choice_chosen: None,
150
2
            caller_window: window_id.map(ToOwned::to_owned),
151
4
            continue_label: Some(gettext("Unlock")),
152
4
            cancel_label: Some(gettext("Cancel")),
153
        }
154
    }
155

            
156
2
    fn for_create_collection(label: &str, window_id: Option<&WindowIdentifierType>) -> Self {
157
        Self {
158
2
            title: Some(gettext("New Keyring Password")),
159
4
            message: Some(gettext("Choose password for new keyring")),
160
4
            description: Some(i18n_f(
161
                "An application wants to create a new keyring called '{}'. Choose the password you want to use for it.",
162
                &[label],
163
            )),
164
            warning: None,
165
            password_new: Some(true),
166
            password_strength: None,
167
            choice_label: None,
168
            choice_chosen: None,
169
2
            caller_window: window_id.map(ToOwned::to_owned),
170
4
            continue_label: Some(gettext("Create")),
171
4
            cancel_label: Some(gettext("Cancel")),
172
        }
173
    }
174
}
175

            
176
#[derive(Deserialize, Serialize, Debug, Type)]
177
#[serde(rename_all = "lowercase")]
178
#[zvariant(signature = "s")]
179
pub enum Reply {
180
    No,
181
    Yes,
182
}
183

            
184
impl zvariant::NoneValue for Reply {
185
    type NoneType = String;
186

            
187
2
    fn null_value() -> Self::NoneType {
188
2
        String::new()
189
    }
190
}
191

            
192
impl TryFrom<String> for Reply {
193
    type Error = String;
194

            
195
2
    fn try_from(value: String) -> Result<Self, Self::Error> {
196
4
        match value.as_str() {
197
4
            "no" => Ok(Reply::No),
198
6
            "yes" => Ok(Reply::Yes),
199
            _ => Err("Invalid value".to_string()),
200
        }
201
    }
202
}
203

            
204
#[derive(Deserialize, Serialize, Debug, Type, PartialEq, Eq, PartialOrd, Ord)]
205
#[serde(rename_all = "lowercase")]
206
#[zvariant(signature = "s")]
207
pub enum PromptType {
208
    Confirm,
209
    Password,
210
}
211

            
212
#[zbus::proxy(
213
    default_service = "org.gnome.keyring.SystemPrompter",
214
    interface = "org.gnome.keyring.internal.Prompter",
215
    default_path = "/org/gnome/keyring/Prompter",
216
    gen_blocking = false
217
)]
218
pub trait Prompter {
219
    fn begin_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
220

            
221
    fn perform_prompt(
222
        &self,
223
        callback: &ObjectPath<'_>,
224
        type_: PromptType,
225
        properties: Properties,
226
        exchange: &str,
227
    ) -> Result<(), ServiceError>;
228

            
229
    fn stop_prompting(&self, callback: &ObjectPath<'_>) -> Result<(), ServiceError>;
230
}
231

            
232
#[derive(Debug, Clone)]
233
pub struct PrompterCallback {
234
    window_id: Option<WindowIdentifierType>,
235
    private_key: Arc<Key>,
236
    public_key: Arc<Key>,
237
    exchange: OnceCell<String>,
238
    service: Service,
239
    prompt_path: OwnedObjectPath,
240
    path: OwnedObjectPath,
241
}
242

            
243
#[zbus::interface(name = "org.gnome.keyring.internal.Prompter.Callback")]
244
impl PrompterCallback {
245
2
    pub async fn prompt_ready(
246
        &self,
247
        reply: Optional<Reply>,
248
        _properties: Properties,
249
        exchange: &str,
250
        #[zbus(connection)] connection: &zbus::Connection,
251
    ) -> Result<(), ServiceError> {
252
2
        let prompt_path = &self.prompt_path;
253
4
        let Some(prompt) = self.service.prompt(prompt_path).await else {
254
4
            return Err(ServiceError::NoSuchObject(format!(
255
                "Prompt '{prompt_path}' does not exist."
256
            )));
257
        };
258

            
259
4
        match *reply {
260
            // First PromptReady call
261
2
            None => {
262
2
                self.prompter_init(&prompt).await?;
263
            }
264
            // Second PromptReady call with final exchange
265
2
            Some(Reply::Yes) => {
266
6
                self.prompter_done(&prompt, exchange).await?;
267
            }
268
            // Dismissed prompt
269
2
            Some(Reply::No) => {
270
8
                self.prompter_dismissed(prompt.path().clone().into())
271
6
                    .await?;
272
            }
273
        };
274
2
        Ok(())
275
    }
276

            
277
8
    async fn prompt_done(&self) -> Result<(), ServiceError> {
278
        // This is only does check if the prompt is tracked on Service
279
2
        let path = &self.prompt_path;
280
4
        if let Some(prompt) = self.service.prompt(path).await {
281
8
            self.service
282
                .object_server()
283
2
                .remove::<Prompt, _>(path)
284
6
                .await?;
285
2
            self.service.remove_prompt(path).await;
286
        }
287
8
        self.service
288
            .object_server()
289
2
            .remove::<Self, _>(&self.path)
290
6
            .await?;
291

            
292
2
        Ok(())
293
    }
294
}
295

            
296
impl PrompterCallback {
297
2
    pub async fn new(
298
        window_id: Option<WindowIdentifierType>,
299
        service: Service,
300
        prompt_path: OwnedObjectPath,
301
    ) -> Result<Self, oo7::crypto::Error> {
302
4
        let index = service.prompt_index().await;
303
4
        let private_key = Arc::new(Key::generate_private_key()?);
304
4
        let public_key = Arc::new(crate::gnome::crypto::generate_public_key(&private_key)?);
305
2
        Ok(Self {
306
2
            window_id,
307
2
            public_key,
308
2
            private_key,
309
2
            exchange: Default::default(),
310
4
            path: OwnedObjectPath::try_from(format!("/org/gnome/keyring/Prompt/p{index}")).unwrap(),
311
2
            service,
312
2
            prompt_path,
313
        })
314
    }
315

            
316
2
    pub fn path(&self) -> &ObjectPath<'_> {
317
2
        &self.path
318
    }
319

            
320
8
    async fn prompter_init(&self, prompt: &Prompt) -> Result<(), ServiceError> {
321
4
        let connection = self.service.connection();
322
2
        let exchange = secret_exchange::begin(&self.public_key);
323
2
        self.exchange.set(exchange).unwrap();
324

            
325
2
        let label = prompt.label();
326
4
        let (properties, prompt_type) = match prompt.role() {
327
2
            PromptRole::Unlock => (
328
2
                Properties::for_unlock(label, None, self.window_id.as_ref()),
329
                PromptType::Password,
330
            ),
331
2
            PromptRole::CreateCollection => (
332
4
                Properties::for_create_collection(label, self.window_id.as_ref()),
333
                PromptType::Password,
334
            ),
335
        };
336

            
337
4
        let prompter = PrompterProxy::new(connection).await?;
338
4
        let path = self.path.clone();
339
4
        let exchange = self.exchange.get().unwrap().clone();
340
6
        tokio::spawn(async move {
341
8
            prompter
342
6
                .perform_prompt(&path, prompt_type, properties, &exchange)
343
10
                .await
344
        });
345
2
        Ok(())
346
    }
347

            
348
8
    async fn prompter_done(&self, prompt: &Prompt, exchange: &str) -> Result<(), ServiceError> {
349
4
        let prompter = PrompterProxy::new(self.service.connection()).await?;
350

            
351
        // Handle each role differently based on what validation/preparation is needed
352
4
        match prompt.role() {
353
            PromptRole::Unlock => {
354
2
                let aes_key =
355
                    secret_exchange::handshake(&self.private_key, exchange).map_err(|err| {
356
                        custom_service_error(&format!(
357
                            "Failed to generate AES key for SecretExchange {err}."
358
                        ))
359
                    })?;
360

            
361
4
                let Some(secret) = secret_exchange::retrieve(exchange, &aes_key) else {
362
                    return Err(custom_service_error(
363
                        "Failed to retrieve keyring secret from SecretExchange.",
364
                    ));
365
                };
366

            
367
                // Get the collection to validate the secret
368
4
                let collection = prompt.collection().expect("Unlock requires a collection");
369
2
                let label = prompt.label();
370

            
371
                // Validate the secret using the already-open keyring
372
4
                let keyring_guard = collection.keyring.read().await;
373
8
                let is_valid = keyring_guard
374
                    .as_ref()
375
                    .unwrap()
376
2
                    .validate_secret(&secret)
377
6
                    .await
378
2
                    .map_err(|err| {
379
                        custom_service_error(&format!(
380
                            "Failed to validate secret for {label} keyring: {err}."
381
                        ))
382
                    })?;
383
2
                drop(keyring_guard);
384

            
385
4
                if is_valid {
386
5
                    tracing::debug!("Keyring secret matches for {label}.");
387

            
388
4
                    let Some(action) = prompt.take_action().await else {
389
                        return Err(custom_service_error(
390
                            "Prompt action was already executed or not set",
391
                        ));
392
                    };
393

            
394
                    // Execute the unlock action after successful validation
395
4
                    let result_value = action.execute(secret).await?;
396

            
397
4
                    let path = self.path.clone();
398
4
                    let prompt_path = OwnedObjectPath::from(prompt.path().clone());
399
8
                    tokio::spawn(async move { prompter.stop_prompting(&path).await });
400

            
401
4
                    let signal_emitter = self.service.signal_emitter(prompt_path)?;
402
8
                    tokio::spawn(async move {
403
6
                        tracing::debug!("Unlock prompt completed.");
404
4
                        let _ = Prompt::completed(&signal_emitter, false, result_value).await;
405
                    });
406
2
                    Ok(())
407
                } else {
408
5
                    tracing::error!("Keyring {label} failed to unlock, incorrect secret.");
409
                    let properties = Properties::for_unlock(
410
2
                        label,
411
                        Some("The unlock password was incorrect"),
412
2
                        self.window_id.as_ref(),
413
                    );
414
2
                    let server_exchange = self
415
                        .exchange
416
                        .get()
417
                        .expect("Exchange cannot be empty at this stage")
418
                        .clone();
419
2
                    let path = self.path.clone();
420

            
421
6
                    tokio::spawn(async move {
422
8
                        prompter
423
2
                            .perform_prompt(
424
2
                                &path,
425
                                PromptType::Password,
426
2
                                properties,
427
2
                                &server_exchange,
428
                            )
429
10
                            .await
430
                    });
431

            
432
2
                    Ok(())
433
                }
434
            }
435
            PromptRole::CreateCollection => {
436
                // Compute AES key from client's public key in the final exchange
437
4
                let aes_key =
438
                    secret_exchange::handshake(&self.private_key, exchange).map_err(|err| {
439
                        custom_service_error(&format!(
440
                            "Failed to generate AES key for SecretExchange {err}."
441
                        ))
442
                    })?;
443

            
444
4
                let Some(secret) = secret_exchange::retrieve(exchange, &aes_key) else {
445
                    return Err(custom_service_error(
446
                        "Failed to retrieve keyring secret from SecretExchange.",
447
                    ));
448
                };
449

            
450
4
                let Some(action) = prompt.take_action().await else {
451
                    return Err(custom_service_error(
452
                        "Prompt action was already executed or not set",
453
                    ));
454
                };
455

            
456
                // Execute the collection creation action with the secret
457
6
                match action.execute(secret).await {
458
2
                    Ok(collection_path_value) => {
459
5
                        tracing::info!("CreateCollection action completed successfully");
460

            
461
2
                        let path = self.path.clone();
462
6
                        tokio::spawn(async move { prompter.stop_prompting(&path).await });
463

            
464
4
                        let signal_emitter =
465
                            self.service.signal_emitter(prompt.path().to_owned())?;
466

            
467
8
                        tokio::spawn(async move {
468
6
                            tracing::debug!("CreateCollection prompt completed.");
469
                            let _ =
470
6
                                Prompt::completed(&signal_emitter, false, collection_path_value)
471
6
                                    .await;
472
                        });
473
2
                        Ok(())
474
                    }
475
                    Err(err) => Err(custom_service_error(&format!(
476
                        "Failed to create collection: {err}."
477
                    ))),
478
                }
479
            }
480
        }
481
    }
482

            
483
8
    async fn prompter_dismissed(&self, prompt_path: OwnedObjectPath) -> Result<(), ServiceError> {
484
4
        let path = self.path.clone();
485
4
        let prompter = PrompterProxy::new(self.service.connection()).await?;
486

            
487
8
        tokio::spawn(async move { prompter.stop_prompting(&path).await });
488
4
        let signal_emitter = self.service.signal_emitter(prompt_path)?;
489
4
        let result = zvariant::Value::new::<Vec<OwnedObjectPath>>(vec![])
490
            .try_into_owned()
491
            .unwrap();
492

            
493
6
        tokio::spawn(async move { Prompt::completed(&signal_emitter, true, result).await });
494
2
        Ok(())
495
    }
496
}
497

            
498
#[cfg(test)]
499
mod tests {
500
    use std::collections::HashMap;
501

            
502
    use zvariant::{serialized::Context, to_bytes};
503

            
504
    use super::*;
505

            
506
    #[test]
507
    fn properties_serialization_roundtrip() {
508
        let props = Properties {
509
            title: Some("Test Title".to_string()),
510
            message: Some("Test Message".to_string()),
511
            ..Default::default()
512
        };
513

            
514
        // Serialize to bytes
515
        let ctxt = Context::new_dbus(zvariant::LE, 0);
516
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
517

            
518
        // Deserialize back to verify roundtrip works
519
        let decoded: Properties = encoded.deserialize().unwrap().0;
520

            
521
        assert_eq!(decoded.title, Some("Test Title".to_string()));
522
        assert_eq!(decoded.message, Some("Test Message".to_string()));
523
    }
524

            
525
    #[test]
526
    fn deserialize_properties() {
527
        let mut map: HashMap<String, Value> = HashMap::new();
528

            
529
        // Double-wrap: Value<Value<String>>
530
        map.insert(
531
            "title".to_string(),
532
            Value::new(Value::new("Unlock Keyring")),
533
        );
534

            
535
        map.insert(
536
            "message".to_string(),
537
            Value::new(Value::new("Authentication required")),
538
        );
539

            
540
        // Serialize the HashMap
541
        let ctxt = Context::new_dbus(zvariant::LE, 0);
542
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
543

            
544
        // Deserialize as Properties
545
        let props: Properties = encoded.deserialize().unwrap().0;
546

            
547
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
548
        assert_eq!(props.message, Some("Authentication required".to_string()));
549

            
550
        let mut map: HashMap<String, Value> = HashMap::new();
551

            
552
        // Single-wrap: Value<String> (the correct format)
553
        map.insert("title".to_string(), Value::new("Unlock Keyring"));
554
        map.insert("message".to_string(), Value::new("Authentication required"));
555

            
556
        // Serialize the HashMap
557
        let ctxt = Context::new_dbus(zvariant::LE, 0);
558
        let encoded = to_bytes(ctxt, &map).expect("Failed to serialize test data");
559

            
560
        // Deserialize as Properties - should also work
561
        let props: Properties = encoded.deserialize().unwrap().0;
562

            
563
        assert_eq!(props.title, Some("Unlock Keyring".to_string()));
564
        assert_eq!(props.message, Some("Authentication required".to_string()));
565

            
566
        let props = Properties {
567
            title: None,
568
            message: Some("Test".to_string()),
569
            ..Default::default()
570
        };
571

            
572
        let ctxt = Context::new_dbus(zvariant::LE, 0);
573
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
574
        let decoded: Properties = encoded.deserialize().unwrap().0;
575

            
576
        assert_eq!(decoded.title, None);
577
        assert_eq!(decoded.message, Some("Test".to_string()));
578

            
579
        let props = Properties {
580
            password_new: Some(true),
581
            password_strength: Some(42),
582
            choice_chosen: Some(false),
583
            ..Default::default()
584
        };
585

            
586
        let ctxt = Context::new_dbus(zvariant::LE, 0);
587
        let encoded = to_bytes(ctxt, &props).expect("Failed to serialize");
588
        let decoded: Properties = encoded.deserialize().unwrap().0;
589

            
590
        assert_eq!(decoded.password_new, Some(true));
591
        assert_eq!(decoded.password_strength, Some(42));
592
        assert_eq!(decoded.choice_chosen, Some(false));
593
    }
594
}