1
// org.freedesktop.Secret.Collection
2

            
3
use std::{
4
    collections::HashMap,
5
    sync::Arc,
6
    time::{Duration, SystemTime},
7
};
8

            
9
use oo7::{
10
    Secret,
11
    dbus::{
12
        ServiceError,
13
        api::{DBusSecretInner, Properties},
14
    },
15
    file::Keyring,
16
};
17
use tokio::sync::{Mutex, RwLock};
18
use zbus::{interface, object_server::SignalEmitter, proxy::Defaults, zvariant};
19
use zvariant::{ObjectPath, OwnedObjectPath};
20

            
21
use crate::{
22
    Service,
23
    error::{Error, custom_service_error},
24
    item,
25
};
26

            
27
#[derive(Debug, Clone)]
28
pub struct Collection {
29
    // Properties
30
    items: Arc<Mutex<Vec<item::Item>>>,
31
    label: Arc<Mutex<String>>,
32
    created: Duration,
33
    modified: Arc<Mutex<Duration>>,
34
    // Other attributes
35
    alias: Arc<Mutex<String>>,
36
    pub(crate) keyring: Arc<RwLock<Option<Keyring>>>,
37
    service: Service,
38
    item_index: Arc<RwLock<u32>>,
39
    path: OwnedObjectPath,
40
}
41

            
42
#[interface(name = "org.freedesktop.Secret.Collection")]
43
impl Collection {
44
    #[zbus(out_args("prompt"))]
45
8
    pub async fn delete(&self) -> Result<OwnedObjectPath, ServiceError> {
46
        // Check if collection is locked
47
4
        if self.is_locked().await {
48
            // Create a prompt to unlock and delete the collection
49
            let prompt = crate::prompt::Prompt::new(
50
4
                self.service.clone(),
51
2
                crate::prompt::PromptRole::Unlock,
52
4
                self.label().await,
53
4
                Some(self.clone()),
54
            )
55
6
            .await;
56
4
            let prompt_path = OwnedObjectPath::from(prompt.path().clone());
57

            
58
2
            let collection = self.clone();
59
10
            let action =
60
                crate::prompt::PromptAction::new(move |unlock_secret: Secret| async move {
61
                    // Unlock the collection
62
4
                    collection.set_locked(false, Some(unlock_secret)).await?;
63

            
64
2
                    collection.delete_unlocked().await?;
65

            
66
4
                    Ok(zvariant::Value::new(OwnedObjectPath::default())
67
2
                        .try_into_owned()
68
2
                        .unwrap())
69
                });
70

            
71
2
            prompt.set_action(action).await;
72

            
73
6
            self.service
74
4
                .register_prompt(prompt_path.clone(), prompt.clone())
75
4
                .await;
76

            
77
8
            self.service
78
                .object_server()
79
2
                .at(&prompt_path, prompt)
80
6
                .await?;
81

            
82
4
            tracing::debug!(
83
                "Delete prompt created at `{}` for locked collection `{}`",
84
                prompt_path,
85
                self.path
86
            );
87

            
88
2
            return Ok(prompt_path);
89
        }
90

            
91
4
        self.delete_unlocked().await?;
92
2
        Ok(OwnedObjectPath::default())
93
    }
94

            
95
8
    async fn delete_unlocked(&self) -> Result<(), ServiceError> {
96
4
        let keyring = self.keyring.read().await;
97
4
        let keyring = keyring.as_ref().unwrap().as_unlocked();
98

            
99
2
        let object_server = self.service.object_server();
100

            
101
        // Remove all items from the object server
102
4
        let items = self.items.lock().await;
103
6
        for item in items.iter() {
104
6
            object_server.remove::<item::Item, _>(item.path()).await?;
105
        }
106
2
        drop(items);
107

            
108
        // Delete the keyring file if it's persistent
109
2
        if let Some(path) = keyring.path() {
110
            tokio::fs::remove_file(&path).await.map_err(|err| {
111
                custom_service_error(&format!("Failed to delete keyring file: {err}"))
112
            })?;
113
            tracing::debug!("Deleted keyring file: {}", path.display());
114
        }
115

            
116
        // Emit CollectionDeleted signal before removing from object server
117
4
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
118
2
        let signal_emitter = self.service.signal_emitter(service_path)?;
119
4
        Service::collection_deleted(&signal_emitter, &self.path).await?;
120

            
121
        // Remove collection from object server
122
2
        object_server.remove::<Collection, _>(&self.path).await?;
123

            
124
        // Notify service to remove from collections list
125
2
        self.service.remove_collection(&self.path).await;
126

            
127
4
        tracing::info!("Collection `{}` deleted.", self.path);
128

            
129
2
        Ok(())
130
    }
131

            
132
    #[zbus(out_args("results"))]
133
2
    pub async fn search_items(
134
        &self,
135
        attributes: HashMap<String, String>,
136
    ) -> Result<Vec<OwnedObjectPath>, ServiceError> {
137
8
        let results = self
138
2
            .search_inner_items(&attributes)
139
6
            .await?
140
            .iter()
141
6
            .map(|item| item.path().clone().into())
142
            .collect::<Vec<OwnedObjectPath>>();
143

            
144
2
        if results.is_empty() {
145
5
            tracing::debug!(
146
                "Items with attributes {:?} does not exist in collection: {}.",
147
                attributes,
148
                self.path
149
            );
150
        } else {
151
5
            tracing::debug!(
152
                "Items with attributes {:?} found in collection: {}.",
153
                attributes,
154
                self.path
155
            );
156
        }
157

            
158
2
        Ok(results)
159
    }
160

            
161
    #[zbus(out_args("item", "prompt"))]
162
4
    pub async fn create_item(
163
        &self,
164
        properties: Properties,
165
        secret: DBusSecretInner,
166
        replace: bool,
167
    ) -> Result<(OwnedObjectPath, OwnedObjectPath), ServiceError> {
168
6
        if self.is_locked().await {
169
            // Create a prompt to unlock the collection and create the item
170
            let prompt = crate::prompt::Prompt::new(
171
4
                self.service.clone(),
172
2
                crate::prompt::PromptRole::Unlock,
173
4
                self.label().await,
174
4
                Some(self.clone()),
175
            )
176
6
            .await;
177
4
            let prompt_path = OwnedObjectPath::from(prompt.path().clone());
178

            
179
2
            let collection = self.clone();
180
10
            let action =
181
                crate::prompt::PromptAction::new(move |unlock_secret: Secret| async move {
182
4
                    collection.set_locked(false, Some(unlock_secret)).await?;
183

            
184
8
                    let item_path = collection
185
2
                        .create_item_unlocked(properties, secret, replace)
186
6
                        .await?;
187

            
188
4
                    Ok(zvariant::Value::new(item_path).try_into_owned().unwrap())
189
                });
190

            
191
2
            prompt.set_action(action).await;
192

            
193
6
            self.service
194
4
                .register_prompt(prompt_path.clone(), prompt.clone())
195
4
                .await;
196

            
197
8
            self.service
198
                .object_server()
199
2
                .at(&prompt_path, prompt)
200
6
                .await?;
201

            
202
4
            tracing::debug!(
203
                "CreateItem prompt created at `{}` for locked collection `{}`",
204
                prompt_path,
205
                self.path
206
            );
207

            
208
4
            return Ok((OwnedObjectPath::default(), prompt_path));
209
        }
210

            
211
10
        let item_path = self
212
2
            .create_item_unlocked(properties, secret, replace)
213
10
            .await?;
214

            
215
2
        Ok((item_path, OwnedObjectPath::default()))
216
    }
217

            
218
2
    async fn create_item_unlocked(
219
        &self,
220
        properties: Properties,
221
        secret: DBusSecretInner,
222
        replace: bool,
223
    ) -> Result<OwnedObjectPath, ServiceError> {
224
4
        let keyring = self.keyring.read().await;
225
4
        let keyring = keyring.as_ref().unwrap().as_unlocked();
226

            
227
2
        let DBusSecretInner(session_path, iv, secret_bytes, content_type) = secret;
228
4
        let label = properties.label();
229
        // Safe to unwrap as an item always has attributes
230
2
        let mut attributes = properties.attributes().unwrap().to_owned();
231

            
232
4
        let Some(session) = self.service.session(&session_path).await else {
233
5
            tracing::error!("The session `{}` does not exist.", session_path);
234
4
            return Err(ServiceError::NoSession(format!(
235
                "The session `{session_path}` does not exist."
236
            )));
237
        };
238

            
239
4
        let secret = match session.aes_key() {
240
4
            Some(key) => oo7::crypto::decrypt(secret_bytes, &key, &iv)
241
4
                .map_err(|err| custom_service_error(&format!("Failed to decrypt secret {err}.")))?,
242
2
            None => zeroize::Zeroizing::new(secret_bytes),
243
        };
244

            
245
        // Ensure content-type attribute is stored
246
4
        if !attributes.contains_key(oo7::CONTENT_TYPE_ATTRIBUTE) {
247
4
            attributes.insert(
248
4
                oo7::CONTENT_TYPE_ATTRIBUTE.to_owned(),
249
4
                content_type.as_str().to_owned(),
250
            );
251
        }
252

            
253
8
        let item = keyring
254
2
            .create_item(label, &attributes, secret, replace)
255
8
            .await
256
2
            .map_err(|err| custom_service_error(&format!("Failed to create a new item {err}.")))?;
257

            
258
4
        let n_items = *self.item_index.read().await;
259
4
        let item_path = OwnedObjectPath::try_from(format!("{}/{n_items}", self.path)).unwrap();
260

            
261
        let item = item::Item::new(
262
2
            item,
263
4
            self.service.clone(),
264
4
            self.path.clone(),
265
2
            item_path.clone(),
266
        );
267
4
        *self.item_index.write().await = n_items + 1;
268

            
269
2
        let object_server = self.service.object_server();
270
2
        let signal_emitter = self.service.signal_emitter(&self.path)?;
271

            
272
        // Remove any existing items with the same attributes
273
2
        if replace {
274
4
            let existing_items = self.search_inner_items(&attributes).await?;
275
6
            if !existing_items.is_empty() {
276
4
                let mut items = self.items.lock().await;
277
8
                for existing in &existing_items {
278
4
                    let existing_path = existing.path();
279

            
280
6
                    items.retain(|i| i.path() != existing_path);
281
4
                    object_server.remove::<item::Item, _>(existing_path).await?;
282
4
                    Self::item_deleted(&signal_emitter, existing_path).await?;
283

            
284
4
                    tracing::debug!("Replaced item `{}`", existing_path);
285
                }
286
2
                drop(items);
287
            }
288
        }
289

            
290
4
        self.items.lock().await.push(item.clone());
291

            
292
2
        object_server.at(&item_path, item).await?;
293

            
294
2
        self.update_modified().await?;
295

            
296
2
        Self::item_created(&signal_emitter, &item_path).await?;
297
2
        self.items_changed(&signal_emitter).await?;
298

            
299
4
        tracing::info!("Item `{item_path}` created.");
300

            
301
2
        Ok(item_path)
302
    }
303

            
304
    #[zbus(property, name = "Items")]
305
8
    pub async fn items(&self) -> Vec<OwnedObjectPath> {
306
10
        self.items
307
            .lock()
308
6
            .await
309
            .iter()
310
6
            .map(|i| i.path().to_owned().into())
311
            .collect()
312
    }
313

            
314
    #[zbus(property, name = "Label")]
315
8
    pub async fn label(&self) -> String {
316
4
        self.label.lock().await.clone()
317
    }
318

            
319
    #[zbus(property, name = "Label")]
320
8
    pub async fn set_label(&self, label: &str) -> Result<(), zbus::Error> {
321
4
        if self.is_locked().await {
322
5
            tracing::error!("Cannot set label of a locked collection `{}`", self.path);
323
2
            return Err(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(
324
4
                format!("Cannot set label of a locked collection `{}`.", self.path),
325
            ))));
326
        }
327

            
328
2
        *self.label.lock().await = label.to_owned();
329

            
330
8
        self.update_modified()
331
6
            .await
332
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
333

            
334
2
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
335
4
        let signal_emitter = self
336
            .service
337
2
            .signal_emitter(service_path)
338
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
339
4
        Service::collection_changed(&signal_emitter, &self.path).await?;
340

            
341
4
        let signal_emitter = self
342
            .service
343
2
            .signal_emitter(&self.path)
344
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
345
4
        self.label_changed(&signal_emitter).await?;
346

            
347
2
        Ok(())
348
    }
349

            
350
    #[zbus(property, name = "Locked")]
351
11
    pub async fn is_locked(&self) -> bool {
352
11
        self.keyring
353
            .read()
354
8
            .await
355
            .as_ref()
356
7
            .map(|k| k.is_locked())
357
            .unwrap_or(true)
358
    }
359

            
360
    #[zbus(property, name = "Created")]
361
2
    pub fn created_at(&self) -> u64 {
362
2
        self.created.as_secs()
363
    }
364

            
365
    #[zbus(property, name = "Modified")]
366
8
    pub async fn modified_at(&self) -> u64 {
367
4
        self.modified.lock().await.as_secs()
368
    }
369

            
370
    #[zbus(signal, name = "ItemCreated")]
371
    async fn item_created(
372
2
        signal_emitter: &SignalEmitter<'_>,
373
2
        item: &ObjectPath<'_>,
374
    ) -> zbus::Result<()>;
375

            
376
    #[zbus(signal, name = "ItemDeleted")]
377
    pub async fn item_deleted(
378
2
        signal_emitter: &SignalEmitter<'_>,
379
2
        item: &ObjectPath<'_>,
380
    ) -> zbus::Result<()>;
381

            
382
    #[zbus(signal, name = "ItemChanged")]
383
    pub async fn item_changed(
384
2
        signal_emitter: &SignalEmitter<'_>,
385
2
        item: &ObjectPath<'_>,
386
    ) -> zbus::Result<()>;
387
}
388

            
389
impl Collection {
390
18
    pub async fn new(label: &str, alias: &str, service: Service, keyring: Keyring) -> Self {
391
8
        let modified = keyring.modified_time().await;
392
6
        let created = keyring.created_time().await.unwrap_or(modified);
393

            
394
4
        let sanitized_label = label
395
            .chars()
396
8
            .map(|c| {
397
8
                if c.is_alphanumeric() || c == '_' {
398
4
                    c
399
                } else {
400
                    '_'
401
                }
402
            })
403
            .collect::<String>();
404

            
405
        Self {
406
8
            items: Default::default(),
407
8
            label: Arc::new(Mutex::new(label.to_owned())),
408
8
            modified: Arc::new(Mutex::new(modified)),
409
8
            alias: Arc::new(Mutex::new(alias.to_owned())),
410
8
            item_index: Arc::new(RwLock::new(0)),
411
8
            path: OwnedObjectPath::try_from(format!(
412
                "/org/freedesktop/secrets/collection/{sanitized_label}"
413
            ))
414
            .expect("Sanitized label should always produce valid object path"),
415
            created,
416
            service,
417
8
            keyring: Arc::new(RwLock::new(Some(keyring))),
418
        }
419
    }
420

            
421
4
    pub fn path(&self) -> &ObjectPath<'_> {
422
4
        &self.path
423
    }
424

            
425
8
    pub async fn set_alias(&self, alias: &str) {
426
2
        *self.alias.lock().await = alias.to_owned();
427
    }
428

            
429
8
    pub async fn alias(&self) -> String {
430
4
        self.alias.lock().await.clone()
431
    }
432

            
433
2
    pub async fn search_inner_items(
434
        &self,
435
        attributes: &HashMap<String, String>,
436
    ) -> Result<Vec<item::Item>, ServiceError> {
437
        // If collection is locked, we can't search
438
4
        if self.is_locked().await {
439
            return Ok(Vec::new());
440
        }
441

            
442
4
        let keyring_guard = self.keyring.read().await;
443
4
        let keyring = keyring_guard.as_ref().unwrap().as_unlocked();
444

            
445
6
        let key = keyring
446
            .key()
447
8
            .await
448
2
            .map_err(|err| custom_service_error(&format!("Failed to derive key: {err}")))?;
449

            
450
2
        let mut matching_items = Vec::new();
451
4
        let items = self.items.lock().await;
452

            
453
8
        for item_wrapper in items.iter() {
454
6
            let inner = item_wrapper.inner.lock().await;
455
4
            let file_item = inner.as_ref().unwrap();
456

            
457
            // Use the oo7::file::Item's matches_attributes method
458
2
            if file_item.matches_attributes(attributes, &key) {
459
2
                matching_items.push(item_wrapper.clone());
460
            }
461
        }
462

            
463
2
        Ok(matching_items)
464
    }
465

            
466
8
    pub async fn item_from_path(&self, path: &ObjectPath<'_>) -> Option<item::Item> {
467
4
        let items = self.items.lock().await;
468

            
469
8
        items.iter().find(|i| i.path() == path).cloned()
470
    }
471

            
472
2
    pub async fn set_locked(
473
        &self,
474
        locked: bool,
475
        secret: Option<Secret>,
476
    ) -> Result<(), ServiceError> {
477
4
        let mut keyring_guard = self.keyring.write().await;
478

            
479
6
        if let Some(old_keyring) = keyring_guard.take() {
480
4
            let new_keyring = match (old_keyring, locked) {
481
2
                (Keyring::Unlocked(unlocked), true) => {
482
4
                    let items = self.items.lock().await;
483
6
                    for item in items.iter() {
484
6
                        item.set_locked(locked, &unlocked).await?;
485
                    }
486
2
                    drop(items);
487

            
488
2
                    Keyring::Locked(unlocked.lock())
489
                }
490
2
                (Keyring::Locked(locked_kr), false) => {
491
4
                    let secret = secret.ok_or_else(|| {
492
                        custom_service_error("Cannot unlock collection without a secret")
493
                    })?;
494

            
495
8
                    let keyring_path = locked_kr.path().map(|p| p.to_path_buf());
496

            
497
4
                    let unlocked = match locked_kr.unlock(secret).await {
498
2
                        Ok(unlocked) => unlocked,
499
2
                        Err(err) => {
500
                            // Reload the locked keyring from disk before returning error
501
4
                            if let Some(path) = keyring_path {
502
8
                                if let Ok(reloaded) = oo7::file::LockedKeyring::load(&path).await {
503
2
                                    *keyring_guard = Some(Keyring::Locked(reloaded));
504
                                }
505
                            }
506
6
                            return Err(custom_service_error(&format!(
507
                                "Failed to unlock keyring: {err}"
508
                            )));
509
                        }
510
                    };
511

            
512
4
                    let items = self.items.lock().await;
513
6
                    for item in items.iter() {
514
6
                        item.set_locked(locked, &unlocked).await?;
515
                    }
516
2
                    drop(items);
517

            
518
2
                    Keyring::Unlocked(unlocked)
519
                }
520
                (other, _) => other,
521
            };
522
2
            *keyring_guard = Some(new_keyring);
523
        }
524

            
525
2
        drop(keyring_guard);
526

            
527
        // Emit signals
528
2
        let signal_emitter = self.service.signal_emitter(&self.path)?;
529
4
        self.locked_changed(&signal_emitter).await?;
530

            
531
2
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
532
2
        let signal_emitter = self.service.signal_emitter(service_path)?;
533
4
        Service::collection_changed(&signal_emitter, &self.path).await?;
534

            
535
3
        tracing::debug!(
536
            "Collection: {} is {}.",
537
            self.path,
538
            if locked { "locked" } else { "unlocked" }
539
        );
540

            
541
2
        Ok(())
542
    }
543

            
544
19
    pub async fn dispatch_items(&self) -> Result<(), Error> {
545
8
        let keyring_guard = self.keyring.read().await;
546
8
        let keyring = keyring_guard.as_ref().unwrap();
547

            
548
12
        let keyring_items = keyring.items().await?;
549
8
        let mut items = self.items.lock().await;
550
8
        let object_server = self.service.object_server();
551
4
        let mut n_items = 1;
552

            
553
10
        for keyring_item in keyring_items {
554
4
            let item_path = OwnedObjectPath::try_from(format!("{}/{n_items}", self.path)).unwrap();
555
            let item = item::Item::new(
556
4
                keyring_item.map_err(Error::InvalidItem)?,
557
4
                self.service.clone(),
558
4
                self.path.clone(),
559
2
                item_path.clone(),
560
            );
561
2
            n_items += 1;
562

            
563
4
            items.push(item.clone());
564
6
            object_server.at(item_path, item).await?;
565
        }
566

            
567
4
        *self.item_index.write().await = n_items;
568

            
569
4
        Ok(())
570
    }
571

            
572
8
    pub async fn delete_item(&self, path: &ObjectPath<'_>) -> Result<(), ServiceError> {
573
4
        let Some(item) = self.item_from_path(path).await else {
574
            return Err(ServiceError::NoSuchObject(format!(
575
                "Item `{path}` does not exist."
576
            )));
577
        };
578

            
579
4
        if item.is_locked().await {
580
            return Err(ServiceError::IsLocked(format!(
581
                "Cannot delete a locked item `{path}`"
582
            )));
583
        }
584

            
585
4
        if self.is_locked().await {
586
            return Err(ServiceError::IsLocked(format!(
587
                "Cannot delete an item `{path}`  in a locked collection "
588
            )));
589
        }
590

            
591
4
        let attributes = item.attributes().await.map_err(|err| {
592
            custom_service_error(&format!("Failed to read item attributes {err}"))
593
        })?;
594

            
595
4
        let keyring = self.keyring.read().await;
596
4
        let keyring = keyring.as_ref().unwrap().as_unlocked();
597

            
598
6
        keyring
599
2
            .delete(&attributes)
600
6
            .await
601
2
            .map_err(|err| custom_service_error(&format!("Failed to deleted item {err}.")))?;
602

            
603
4
        let mut items = self.items.lock().await;
604
8
        items.retain(|item| item.path() != path);
605
2
        drop(items);
606

            
607
2
        self.update_modified().await?;
608

            
609
2
        let signal_emitter = self.service.signal_emitter(&self.path)?;
610
4
        self.items_changed(&signal_emitter).await?;
611

            
612
2
        Ok(())
613
    }
614

            
615
    /// Update the modified timestamp and emit the PropertiesChanged signal
616
8
    async fn update_modified(&self) -> Result<(), ServiceError> {
617
6
        let now = SystemTime::now()
618
2
            .duration_since(SystemTime::UNIX_EPOCH)
619
            .unwrap();
620
2
        *self.modified.lock().await = now;
621

            
622
2
        let signal_emitter = self.service.signal_emitter(&self.path)?;
623
4
        self.modified_changed(&signal_emitter).await?;
624

            
625
2
        Ok(())
626
    }
627
}
628

            
629
#[cfg(test)]
630
mod tests;