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(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
    name: String,
36
    alias: Arc<Mutex<String>>,
37
    pub(crate) keyring: Arc<RwLock<Option<Keyring>>>,
38
    service: Service,
39
    item_index: Arc<RwLock<u32>>,
40
    path: OwnedObjectPath,
41
}
42

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

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

            
65
4
                    collection.delete_unlocked().await?;
66

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

            
72
4
            prompt.set_action(action).await;
73

            
74
12
            self.service
75
8
                .register_prompt(prompt_path.clone(), prompt.clone())
76
8
                .await;
77

            
78
16
            self.service
79
                .object_server()
80
4
                .at(&prompt_path, prompt)
81
12
                .await?;
82

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

            
89
4
            return Ok(prompt_path);
90
        }
91

            
92
24
        self.delete_unlocked().await?;
93
8
        Ok(OwnedObjectPath::default())
94
    }
95

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

            
100
9
        let object_server = self.service.object_server();
101

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

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

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

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

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

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

            
130
9
        Ok(())
131
    }
132

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

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

            
159
11
        Ok(results)
160
    }
161

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

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

            
185
18
                    let item_path = collection
186
5
                        .create_item_unlocked(properties, secret, replace)
187
15
                        .await?;
188

            
189
10
                    Ok(zvariant::Value::new(item_path).try_into_owned().unwrap())
190
                });
191

            
192
4
            prompt.set_action(action).await;
193

            
194
12
            self.service
195
8
                .register_prompt(prompt_path.clone(), prompt.clone())
196
8
                .await;
197

            
198
16
            self.service
199
                .object_server()
200
4
                .at(&prompt_path, prompt)
201
12
                .await?;
202

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

            
209
8
            return Ok((OwnedObjectPath::default(), prompt_path));
210
        }
211

            
212
67
        let item_path = self
213
16
            .create_item_unlocked(properties, secret, replace)
214
62
            .await?;
215

            
216
17
        Ok((item_path, OwnedObjectPath::default()))
217
    }
218

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

            
228
17
        let DBusSecretInner(ref session_path, ref iv, ref secret_bytes, ref content_type) = secret;
229
14
        let label = properties.label();
230
        // Safe to unwrap as an item always has attributes
231
18
        let mut attributes = properties.attributes().unwrap().to_owned();
232

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

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

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

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

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

            
262
        let item = item::Item::new(
263
35
            item.into(),
264
35
            self.service.clone(),
265
34
            self.path.clone(),
266
17
            item_path.clone(),
267
        );
268
42
        *self.item_index.write().await = n_items + 1;
269

            
270
17
        let object_server = self.service.object_server();
271
18
        let signal_emitter = self.service.signal_emitter(&self.path)?;
272

            
273
        // Remove any existing items with the same attributes
274
17
        if replace {
275
37
            let existing_items = self.search_inner_items(&attributes).await?;
276
31
            if !existing_items.is_empty() {
277
13
                let mut items = self.items.lock().await;
278
24
                for existing in &existing_items {
279
12
                    let existing_path = existing.path();
280

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

            
285
10
                    tracing::debug!("Replaced item `{}`", existing_path);
286
                }
287
6
                drop(items);
288
            }
289
        }
290

            
291
34
        self.items.lock().await.push(item.clone());
292

            
293
20
        object_server.at(&item_path, item).await?;
294

            
295
20
        self.update_modified().await?;
296

            
297
23
        Self::item_created(&signal_emitter, &item_path).await?;
298
26
        self.items_changed(&signal_emitter).await?;
299

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

            
302
16
        Ok(item_path)
303
    }
304

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

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

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

            
329
10
        *self.label.lock().await = label.to_owned();
330

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

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

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

            
348
8
        Ok(())
349
    }
350

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

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

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

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

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

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

            
390
36
pub(crate) fn collection_path(label: &str) -> Result<OwnedObjectPath, zvariant::Error> {
391
84
    let sanitized_label = label.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "_");
392

            
393
60
    OwnedObjectPath::try_from(format!(
394
        "/org/freedesktop/secrets/collection/{sanitized_label}"
395
    ))
396
}
397

            
398
impl Collection {
399
29
    pub async fn new(
400
        name: &str,
401
        label: &str,
402
        alias: &str,
403
        service: Service,
404
        keyring: Keyring,
405
    ) -> Self {
406
65
        let modified = keyring.modified_time().await;
407
51
        let created = keyring.created_time().await.unwrap_or(modified);
408

            
409
        Self {
410
26
            items: Default::default(),
411
60
            label: Arc::new(Mutex::new(label.to_owned())),
412
60
            modified: Arc::new(Mutex::new(modified)),
413
33
            name: name.to_owned(),
414
56
            alias: Arc::new(Mutex::new(alias.to_owned())),
415
60
            item_index: Arc::new(RwLock::new(0)),
416
25
            path: collection_path(label)
417
                .expect("Label should already be sanitized and produce valid object path"),
418
            created,
419
            service,
420
60
            keyring: Arc::new(RwLock::new(Some(keyring))),
421
        }
422
    }
423

            
424
33
    pub fn path(&self) -> &ObjectPath<'_> {
425
31
        &self.path
426
    }
427

            
428
8
    pub fn name(&self) -> &str {
429
8
        &self.name
430
    }
431

            
432
16
    pub async fn set_alias(&self, alias: &str) {
433
4
        *self.alias.lock().await = alias.to_owned();
434
    }
435

            
436
85
    pub async fn alias(&self) -> String {
437
50
        self.alias.lock().await.clone()
438
    }
439

            
440
15
    pub async fn search_inner_items(
441
        &self,
442
        attributes: &HashMap<String, String>,
443
    ) -> Result<Vec<item::Item>, ServiceError> {
444
        // If collection is locked, we can't search
445
39
        if self.is_locked().await {
446
            return Ok(Vec::new());
447
        }
448

            
449
32
        let keyring_guard = self.keyring.read().await;
450
32
        let keyring = keyring_guard.as_ref().unwrap().as_unlocked();
451

            
452
52
        let key = keyring
453
            .key()
454
49
            .await
455
16
            .map_err(|err| custom_service_error(&format!("Failed to derive key: {err}")))?;
456

            
457
16
        let mut matching_items = Vec::new();
458
32
        let items = self.items.lock().await;
459

            
460
56
        for item_wrapper in items.iter() {
461
33
            let inner = item_wrapper.inner.lock().await;
462
20
            let file_item = inner.as_ref().unwrap();
463

            
464
            // Use the oo7::file::Item's matches_attributes method
465
12
            if file_item.matches_attributes(attributes, &key) {
466
12
                matching_items.push(item_wrapper.clone());
467
            }
468
        }
469

            
470
14
        Ok(matching_items)
471
    }
472

            
473
55
    pub async fn item_from_path(&self, path: &ObjectPath<'_>) -> Option<item::Item> {
474
35
        let items = self.items.lock().await;
475

            
476
58
        items.iter().find(|i| i.path() == path).cloned()
477
    }
478

            
479
8
    pub async fn set_locked(
480
        &self,
481
        locked: bool,
482
        secret: Option<Secret>,
483
    ) -> Result<(), ServiceError> {
484
18
        let mut keyring_guard = self.keyring.write().await;
485

            
486
24
        if let Some(old_keyring) = keyring_guard.take() {
487
16
            let new_keyring = match (old_keyring, locked) {
488
8
                (Keyring::Unlocked(unlocked), true) => {
489
18
                    let items = self.items.lock().await;
490
22
                    for item in items.iter() {
491
19
                        item.set_locked(locked, &unlocked).await?;
492
                    }
493
8
                    drop(items);
494

            
495
8
                    Keyring::Locked(unlocked.lock())
496
                }
497
8
                (Keyring::Locked(locked_kr), false) => {
498
16
                    let secret = secret.ok_or_else(|| {
499
                        custom_service_error("Cannot unlock collection without a secret")
500
                    })?;
501

            
502
28
                    let keyring_path = locked_kr.path().map(|p| p.to_path_buf());
503

            
504
16
                    let unlocked = match locked_kr.unlock(secret).await {
505
9
                        Ok(unlocked) => unlocked,
506
4
                        Err(err) => {
507
                            // Reload the locked keyring from disk before returning error
508
8
                            if let Some(path) = keyring_path
509
15
                                && let Ok(reloaded) = oo7::file::LockedKeyring::load(&path).await
510
                            {
511
4
                                *keyring_guard = Some(Keyring::Locked(reloaded));
512
                            }
513
12
                            return Err(custom_service_error(&format!(
514
                                "Failed to unlock keyring: {err}"
515
                            )));
516
                        }
517
                    };
518

            
519
20
                    let items = self.items.lock().await;
520
24
                    for item in items.iter() {
521
19
                        item.set_locked(locked, &unlocked).await?;
522
                    }
523
9
                    drop(items);
524

            
525
9
                    Keyring::Unlocked(unlocked)
526
                }
527
4
                (other, _) => other,
528
            };
529
8
            *keyring_guard = Some(new_keyring);
530
        }
531

            
532
8
        drop(keyring_guard);
533

            
534
        // Emit signals
535
8
        let signal_emitter = self.service.signal_emitter(&self.path)?;
536
18
        self.locked_changed(&signal_emitter).await?;
537

            
538
8
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
539
8
        let signal_emitter = self.service.signal_emitter(service_path)?;
540
18
        Service::collection_changed(&signal_emitter, &self.path).await?;
541

            
542
6
        tracing::debug!(
543
            "Collection: {} is {}.",
544
            self.path,
545
            if locked { "locked" } else { "unlocked" }
546
        );
547

            
548
8
        Ok(())
549
    }
550

            
551
152
    pub async fn dispatch_items(&self) -> Result<(), Error> {
552
60
        let keyring_guard = self.keyring.read().await;
553
62
        let keyring = keyring_guard.as_ref().unwrap();
554

            
555
107
        let keyring_items = keyring.items().await?;
556
78
        let mut items = self.items.lock().await;
557
64
        let object_server = self.service.object_server();
558
33
        let mut n_items = 1;
559

            
560
68
        for keyring_item in keyring_items {
561
8
            let item_path = OwnedObjectPath::try_from(format!("{}/{n_items}", self.path)).unwrap();
562
            let item = item::Item::new(
563
4
                keyring_item,
564
8
                self.service.clone(),
565
8
                self.path.clone(),
566
4
                item_path.clone(),
567
            );
568
4
            n_items += 1;
569

            
570
8
            items.push(item.clone());
571
12
            object_server.at(item_path, item).await?;
572
        }
573

            
574
43
        *self.item_index.write().await = n_items;
575

            
576
31
        Ok(())
577
    }
578

            
579
52
    pub async fn delete_item(&self, path: &ObjectPath<'_>) -> Result<(), ServiceError> {
580
32
        let Some(item) = self.item_from_path(path).await else {
581
            return Err(ServiceError::NoSuchObject(format!(
582
                "Item `{path}` does not exist."
583
            )));
584
        };
585

            
586
35
        if item.is_locked().await {
587
            return Err(ServiceError::IsLocked(format!(
588
                "Cannot delete a locked item `{path}`"
589
            )));
590
        }
591

            
592
35
        if self.is_locked().await {
593
            return Err(ServiceError::IsLocked(format!(
594
                "Cannot delete an item `{path}`  in a locked collection "
595
            )));
596
        }
597

            
598
35
        let attributes = item.attributes().await.map_err(|err| {
599
            custom_service_error(&format!("Failed to read item attributes {err}"))
600
        })?;
601

            
602
29
        let keyring = self.keyring.read().await;
603
29
        let keyring = keyring.as_ref().unwrap().as_unlocked();
604

            
605
59
        keyring
606
14
            .delete(&attributes)
607
51
            .await
608
15
            .map_err(|err| custom_service_error(&format!("Failed to deleted item {err}.")))?;
609

            
610
34
        let mut items = self.items.lock().await;
611
60
        items.retain(|item| item.path() != path);
612
17
        drop(items);
613

            
614
22
        self.update_modified().await?;
615

            
616
16
        let signal_emitter = self.service.signal_emitter(&self.path)?;
617
38
        self.items_changed(&signal_emitter).await?;
618

            
619
17
        Ok(())
620
    }
621

            
622
    /// Update the modified timestamp and emit the PropertiesChanged signal
623
68
    async fn update_modified(&self) -> Result<(), ServiceError> {
624
54
        let now = SystemTime::now()
625
18
            .duration_since(SystemTime::UNIX_EPOCH)
626
            .unwrap();
627
25
        *self.modified.lock().await = now;
628

            
629
19
        let signal_emitter = self.service.signal_emitter(&self.path)?;
630
41
        self.modified_changed(&signal_emitter).await?;
631

            
632
16
        Ok(())
633
    }
634
}
635

            
636
#[cfg(test)]
637
mod tests;