1
use std::{collections::HashMap, sync::Arc, time::Duration};
2

            
3
use ashpd::WindowIdentifier;
4
#[cfg(feature = "async-std")]
5
use async_lock::RwLock;
6
#[cfg(feature = "tokio")]
7
use tokio::sync::RwLock;
8
use zbus::zvariant::ObjectPath;
9

            
10
use super::{Algorithm, Error, api};
11
use crate::{AsAttributes, Key, Secret};
12

            
13
/// A secret with a label and attributes to identify it.
14
///
15
/// An item might be locked or unlocked, use [`Item::lock`] or [`Item::unlock`]
16
/// to lock or unlock it. Note that the Secret Service might not be able to
17
/// lock/unlock individual items and may lock/unlock the entire collection in
18
/// such case.
19
///
20
/// The item is attributes are used to identify and find the item later using
21
/// [`Collection::search_items`](crate::dbus::Collection::search_items).
22
/// They are not stored or transferred in a secure manner.
23
///
24
/// **Note**
25
///
26
/// If the item is deleted using [`Item::delete`] any future usage of it API
27
/// will fail with [`Error::Deleted`].
28
#[derive(Debug)]
29
pub struct Item<'a> {
30
    inner: Arc<api::Item<'a>>,
31
    session: Arc<api::Session<'a>>,
32
    service: Arc<api::Service<'a>>,
33
    algorithm: Algorithm,
34
    /// Defines whether the Item has been deleted or not
35
    available: RwLock<bool>,
36
    aes_key: Option<Arc<Key>>,
37
}
38

            
39
impl<'a> Item<'a> {
40
2
    pub(crate) fn new(
41
        service: Arc<api::Service<'a>>,
42
        session: Arc<api::Session<'a>>,
43
        algorithm: Algorithm,
44
        item: api::Item<'a>,
45
        aes_key: Option<Arc<Key>>,
46
    ) -> Item<'a> {
47
        Self {
48
4
            inner: Arc::new(item),
49
            service,
50
            session,
51
            algorithm,
52
2
            available: RwLock::new(true),
53
            aes_key,
54
        }
55
    }
56

            
57
8
    pub(crate) async fn is_available(&self) -> bool {
58
4
        *self.available.read().await
59
    }
60

            
61
    /// Get whether the item is locked.
62
8
    pub async fn is_locked(&self) -> Result<bool, Error> {
63
6
        if !self.is_available().await {
64
2
            Err(Error::Deleted)
65
        } else {
66
6
            self.inner.is_locked().await
67
        }
68
    }
69

            
70
    /// The item label.
71
8
    pub async fn label(&self) -> Result<String, Error> {
72
6
        if !self.is_available().await {
73
2
            Err(Error::Deleted)
74
        } else {
75
6
            self.inner.label().await
76
        }
77
    }
78

            
79
    /// Set the item label.
80
8
    pub async fn set_label(&self, label: &str) -> Result<(), Error> {
81
6
        if !self.is_available().await {
82
2
            Err(Error::Deleted)
83
        } else {
84
6
            self.inner.set_label(label).await
85
        }
86
    }
87

            
88
    /// The UNIX time when the item was created.
89
8
    pub async fn created(&self) -> Result<Duration, Error> {
90
6
        if !self.is_available().await {
91
2
            Err(Error::Deleted)
92
        } else {
93
6
            self.inner.created().await
94
        }
95
    }
96

            
97
    /// The UNIX time when the item was modified.
98
8
    pub async fn modified(&self) -> Result<Duration, Error> {
99
6
        if !self.is_available().await {
100
2
            Err(Error::Deleted)
101
        } else {
102
6
            self.inner.modified().await
103
        }
104
    }
105

            
106
    /// Retrieve the item attributes.
107
8
    pub async fn attributes(&self) -> Result<HashMap<String, String>, Error> {
108
6
        if !self.is_available().await {
109
2
            Err(Error::Deleted)
110
        } else {
111
6
            self.inner.attributes().await
112
        }
113
    }
114

            
115
    /// Update the item attributes.
116
24
    pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<(), Error> {
117
14
        if !self.is_available().await {
118
2
            Err(Error::Deleted)
119
        } else {
120
12
            self.inner.set_attributes(attributes).await
121
        }
122
    }
123

            
124
    /// Delete the item.
125
8
    pub async fn delete(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
126
8
        if !self.is_available().await {
127
2
            Err(Error::Deleted)
128
        } else {
129
6
            self.inner.delete(window_id).await?;
130
2
            *self.available.write().await = false;
131
2
            Ok(())
132
        }
133
    }
134

            
135
    /// Retrieve the currently stored secret.
136
8
    pub async fn secret(&self) -> Result<Secret, Error> {
137
6
        if !self.is_available().await {
138
2
            Err(Error::Deleted)
139
        } else {
140
8
            self.inner
141
2
                .secret(&self.session)
142
8
                .await?
143
2
                .decrypt(self.aes_key.as_ref())
144
        }
145
    }
146

            
147
    /// Modify the stored secret on the item.
148
    ///
149
    /// # Arguments
150
    ///
151
    /// * `secret` - The secret to store.
152
    #[doc(alias = "SetSecret")]
153
16
    pub async fn set_secret(&self, secret: impl Into<Secret>) -> Result<(), Error> {
154
10
        if !self.is_available().await {
155
2
            Err(Error::Deleted)
156
        } else {
157
4
            let secret = match self.algorithm {
158
2
                Algorithm::Plain => api::DBusSecret::new(Arc::clone(&self.session), secret),
159
                Algorithm::Encrypted => {
160
8
                    let aes_key = self.aes_key.as_ref().unwrap();
161
4
                    api::DBusSecret::new_encrypted(Arc::clone(&self.session), secret, aes_key)?
162
                }
163
            };
164
12
            self.inner.set_secret(&secret).await?;
165
4
            Ok(())
166
        }
167
    }
168

            
169
    /// Unlock the item.
170
8
    pub async fn unlock(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
171
6
        if !self.is_available().await {
172
2
            Err(Error::Deleted)
173
        } else {
174
            self.service
175
                .unlock(&[self.inner.inner().path()], window_id)
176
                .await?;
177
            Ok(())
178
        }
179
    }
180

            
181
    /// Lock the item.
182
8
    pub async fn lock(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
183
6
        if !self.is_available().await {
184
2
            Err(Error::Deleted)
185
        } else {
186
            self.service
187
                .lock(&[self.inner.inner().path()], window_id)
188
                .await?;
189
            Ok(())
190
        }
191
    }
192

            
193
    /// Returns item path
194
2
    pub fn path(&self) -> &ObjectPath<'_> {
195
2
        self.inner.inner().path()
196
    }
197
}
198

            
199
#[cfg(test)]
200
#[cfg(feature = "tokio")]
201
mod tests {
202
    use crate::dbus::Service;
203

            
204
    #[tokio::test]
205
    async fn label_mutation() {
206
        let service = Service::plain().await.unwrap();
207
        let collection = service.default_collection().await.unwrap();
208

            
209
        let secret = crate::Secret::text("test secret");
210

            
211
        let item = collection
212
            .create_item(
213
                "Original Label",
214
                &[("test", "label-mutation")],
215
                secret,
216
                true,
217
                None,
218
            )
219
            .await
220
            .unwrap();
221

            
222
        let initial_label = item.label().await.unwrap();
223
        assert_eq!(initial_label, "Original Label");
224

            
225
        item.set_label("Updated Label").await.unwrap();
226

            
227
        let updated_label = item.label().await.unwrap();
228
        assert_eq!(updated_label, "Updated Label");
229

            
230
        item.delete(None).await.unwrap();
231
    }
232

            
233
    #[tokio::test]
234
    async fn secret_mutation() {
235
        let service = Service::plain().await.unwrap();
236
        let collection = service.default_collection().await.unwrap();
237

            
238
        let original_secret = crate::Secret::text("original secret");
239

            
240
        let item = collection
241
            .create_item(
242
                "Secret Test",
243
                &[("test", "secret-mutation")],
244
                original_secret.clone(),
245
                true,
246
                None,
247
            )
248
            .await
249
            .unwrap();
250

            
251
        assert_eq!(item.secret().await.unwrap(), original_secret);
252

            
253
        let new_secret = crate::Secret::text("updated secret");
254
        item.set_secret(new_secret.clone()).await.unwrap();
255

            
256
        assert_eq!(item.secret().await.unwrap(), new_secret);
257

            
258
        item.delete(None).await.unwrap();
259
    }
260

            
261
    #[tokio::test]
262
    async fn secret_mutation_encrypted() {
263
        let service = Service::encrypted().await.unwrap();
264
        let collection = service.default_collection().await.unwrap();
265

            
266
        let original_secret = crate::Secret::text("original encrypted secret");
267

            
268
        let item = collection
269
            .create_item(
270
                "Encrypted Secret Test",
271
                &[("test", "secret-mutation-encrypted")],
272
                original_secret.clone(),
273
                true,
274
                None,
275
            )
276
            .await
277
            .unwrap();
278

            
279
        assert_eq!(item.secret().await.unwrap(), original_secret);
280

            
281
        let new_secret = crate::Secret::text("updated encrypted secret");
282
        item.set_secret(new_secret.clone()).await.unwrap();
283

            
284
        assert_eq!(item.secret().await.unwrap(), new_secret);
285

            
286
        item.delete(None).await.unwrap();
287
    }
288

            
289
    #[tokio::test]
290
    async fn attributes_mutation() {
291
        let service = Service::plain().await.unwrap();
292
        let collection = service.default_collection().await.unwrap();
293

            
294
        let secret = crate::Secret::text("test secret");
295

            
296
        let item = collection
297
            .create_item(
298
                "Attributes Test",
299
                &[("service", "email"), ("username", "user1")],
300
                secret,
301
                true,
302
                None,
303
            )
304
            .await
305
            .unwrap();
306

            
307
        let retrieved_attrs = item.attributes().await.unwrap();
308
        assert_eq!(retrieved_attrs.get("service"), Some(&"email".to_string()));
309
        assert_eq!(retrieved_attrs.get("username"), Some(&"user1".to_string()));
310

            
311
        item.set_attributes(&[
312
            ("service", "web"),
313
            ("username", "user2"),
314
            ("domain", "example.com"),
315
        ])
316
        .await
317
        .unwrap();
318

            
319
        let updated_attrs = item.attributes().await.unwrap();
320
        assert_eq!(updated_attrs.get("service"), Some(&"web".to_string()));
321
        assert_eq!(updated_attrs.get("username"), Some(&"user2".to_string()));
322
        assert_eq!(
323
            updated_attrs.get("domain"),
324
            Some(&"example.com".to_string())
325
        );
326
        assert!(!updated_attrs.contains_key("email")); // old attribute should be gone
327

            
328
        item.delete(None).await.unwrap();
329
    }
330

            
331
    #[tokio::test]
332
    async fn text_secret_type() {
333
        let service = Service::plain().await.unwrap();
334
        let collection = service.default_collection().await.unwrap();
335

            
336
        let text_secret = crate::Secret::text("text password");
337
        let text_item = collection
338
            .create_item(
339
                "Text Secret",
340
                &[("type", "text-secret")],
341
                text_secret.clone(),
342
                true,
343
                None,
344
            )
345
            .await
346
            .unwrap();
347

            
348
        assert_eq!(text_item.secret().await.unwrap(), text_secret);
349
        text_item.delete(None).await.unwrap();
350
    }
351

            
352
    #[tokio::test]
353
    async fn blob_secret_type() {
354
        let service = Service::plain().await.unwrap();
355
        let collection = service.default_collection().await.unwrap();
356

            
357
        let blob_secret = crate::Secret::blob(b"binary data");
358
        let blob_item = collection
359
            .create_item(
360
                "Blob Secret",
361
                &[("type", "blob-secret")],
362
                blob_secret.clone(),
363
                true,
364
                None,
365
            )
366
            .await
367
            .unwrap();
368

            
369
        let retrieved_secret = blob_item.secret().await.unwrap();
370

            
371
        // TODO: gnome-keyring doesn't preserve content types - everything becomes
372
        // text/plain But the actual secret data should be preserved
373
        assert_eq!(retrieved_secret.as_bytes(), blob_secret.as_bytes());
374
        blob_item.delete(None).await.unwrap();
375
    }
376

            
377
    #[tokio::test]
378
    async fn timestamps() {
379
        let service = Service::plain().await.unwrap();
380
        let collection = service.default_collection().await.unwrap();
381

            
382
        let secret = crate::Secret::text("timestamp test");
383

            
384
        let item = collection
385
            .create_item(
386
                "Timestamp Test",
387
                &[("test", "timestamps")],
388
                secret,
389
                true,
390
                None,
391
            )
392
            .await
393
            .unwrap();
394

            
395
        let created = item.created().await.unwrap();
396
        let modified = item.modified().await.unwrap();
397

            
398
        eprintln!("Created: {:?}, Modified: {:?}", created, modified);
399
        assert_eq!(created, modified);
400

            
401
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
402
        item.set_label("Updated Label").await.unwrap();
403

            
404
        // Allow time for D-Bus changes to propagate
405
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
406

            
407
        let new_modified = item.modified().await.unwrap();
408
        assert!(new_modified > modified);
409
        assert_eq!(item.created().await.unwrap(), created);
410

            
411
        item.delete(None).await.unwrap();
412
    }
413

            
414
    #[tokio::test]
415
    async fn deleted_error() {
416
        let service = Service::plain().await.unwrap();
417
        let collection = service.default_collection().await.unwrap();
418

            
419
        let attributes = &[("test", "deleted-error")];
420
        let secret = crate::Secret::text("delete test");
421

            
422
        let item = collection
423
            .create_item("Delete Test", attributes, secret, true, None)
424
            .await
425
            .unwrap();
426

            
427
        // Verify item works before deletion
428
        assert!(item.label().await.is_ok());
429

            
430
        // Delete the item
431
        item.delete(None).await.unwrap();
432

            
433
        // All operations should now return Error::Deleted
434
        assert!(matches!(item.label().await, Err(super::Error::Deleted)));
435
        assert!(matches!(
436
            item.set_label("New").await,
437
            Err(super::Error::Deleted)
438
        ));
439
        assert!(matches!(item.secret().await, Err(super::Error::Deleted)));
440
        assert!(matches!(
441
            item.set_secret("new secret").await,
442
            Err(super::Error::Deleted)
443
        ));
444
        assert!(matches!(
445
            item.attributes().await,
446
            Err(super::Error::Deleted)
447
        ));
448
        assert!(matches!(
449
            item.set_attributes(attributes).await,
450
            Err(super::Error::Deleted)
451
        ));
452
        assert!(matches!(item.created().await, Err(super::Error::Deleted)));
453
        assert!(matches!(item.modified().await, Err(super::Error::Deleted)));
454
        assert!(matches!(item.is_locked().await, Err(super::Error::Deleted)));
455
        assert!(matches!(item.lock(None).await, Err(super::Error::Deleted)));
456
        assert!(matches!(
457
            item.unlock(None).await,
458
            Err(super::Error::Deleted)
459
        ));
460
        assert!(matches!(
461
            item.delete(None).await,
462
            Err(super::Error::Deleted)
463
        ));
464
    }
465
}