1
use std::{collections::HashMap, str::FromStr, time::Duration};
2

            
3
use serde::{Deserialize, Serialize};
4
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5

            
6
use super::{
7
    Error, LockedItem,
8
    api::{EncryptedItem, GVARIANT_ENCODING},
9
};
10
use crate::{AsAttributes, CONTENT_TYPE_ATTRIBUTE, Key, Mac, Secret, crypto, secret::ContentType};
11

            
12
/// An item stored in the file backend.
13
#[derive(
14
    Deserialize, Serialize, zgvariant::Type, Clone, Debug, Zeroize, ZeroizeOnDrop, PartialEq,
15
)]
16
pub struct UnlockedItem {
17
    #[zeroize(skip)]
18
    attributes: HashMap<String, String>,
19
    #[zeroize(skip)]
20
    label: String,
21
    #[zeroize(skip)]
22
    created: u64,
23
    #[zeroize(skip)]
24
    modified: u64,
25
    #[serde(with = "serde_bytes")]
26
    secret: Vec<u8>,
27
}
28

            
29
impl UnlockedItem {
30
80
    pub(crate) fn new(
31
        label: impl ToString,
32
        attributes: &impl AsAttributes,
33
        secret: impl Into<Secret>,
34
    ) -> Self {
35
180
        let now = std::time::SystemTime::UNIX_EPOCH
36
            .elapsed()
37
            .unwrap()
38
            .as_secs();
39

            
40
80
        let mut item_attributes = attributes.as_attributes();
41

            
42
80
        let secret = secret.into();
43
        // Set default MIME type if not provided
44
156
        if !item_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
45
57
            item_attributes.insert(
46
116
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
47
114
                secret.content_type().as_str().to_string(),
48
            );
49
        }
50

            
51
        Self {
52
            attributes: item_attributes,
53
78
            label: label.to_string(),
54
            created: now,
55
            modified: now,
56
153
            secret: secret.as_bytes().to_vec(),
57
        }
58
    }
59

            
60
    /// Retrieve the item attributes.
61
25
    pub fn attributes(&self) -> &HashMap<String, String> {
62
22
        &self.attributes
63
    }
64

            
65
    /// Check whether the attribute maps match.
66
8
    pub fn matches_exact(&self, attributes: &impl AsAttributes) -> bool {
67
        // The secret's content type does not identify the item.
68
8
        let requested = attributes.as_search_attributes();
69
16
        let count = self.attributes.len()
70
8
            - usize::from(self.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE));
71
16
        count == requested.len()
72
8
            && requested
73
8
                .iter()
74
24
                .all(|(key, value)| self.attributes.get(key) == Some(value))
75
    }
76

            
77
    /// Retrieve the item attributes as a typed schema.
78
    ///
79
    /// # Example
80
    ///
81
    /// ```no_run
82
    /// # use oo7::{SecretSchema, file::UnlockedItem};
83
    /// # #[derive(SecretSchema, Debug)]
84
    /// # #[schema(name = "org.example.Password")]
85
    /// # struct PasswordSchema {
86
    /// #     username: String,
87
    /// #     server: String,
88
    /// # }
89
    /// # fn example(item: &UnlockedItem) -> Result<(), oo7::file::Error> {
90
    /// let schema = item.attributes_as::<PasswordSchema>()?;
91
    /// println!("Username: {}", schema.username);
92
    /// # Ok(())
93
    /// # }
94
    /// ```
95
    #[cfg(feature = "schema")]
96
    #[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
97
1
    pub fn attributes_as<T>(&self) -> Result<T, Error>
98
    where
99
        T: for<'a> std::convert::TryFrom<&'a HashMap<String, String>, Error = crate::SchemaError>,
100
    {
101
1
        T::try_from(&self.attributes).map_err(Into::into)
102
    }
103

            
104
    /// Update the item attributes.
105
15
    pub fn set_attributes(&mut self, attributes: &impl AsAttributes) {
106
15
        let mut new_attributes = attributes.as_attributes();
107

            
108
        // Preserve MIME type if not explicitly set in new attributes
109
30
        if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
110
30
            if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) {
111
15
                new_attributes.insert(
112
30
                    CONTENT_TYPE_ATTRIBUTE.to_string(),
113
15
                    existing_mime_type.clone(),
114
                );
115
            } else {
116
                new_attributes.insert(
117
                    CONTENT_TYPE_ATTRIBUTE.to_owned(),
118
                    ContentType::default().as_str().to_string(),
119
                );
120
            }
121
        }
122

            
123
15
        self.attributes = new_attributes;
124
30
        self.modified = std::time::SystemTime::UNIX_EPOCH
125
15
            .elapsed()
126
15
            .unwrap()
127
15
            .as_secs();
128
    }
129

            
130
    /// The item label.
131
18
    pub fn label(&self) -> &str {
132
18
        &self.label
133
    }
134

            
135
    /// Set the item label.
136
12
    pub fn set_label(&mut self, label: impl ToString) {
137
12
        self.modified = std::time::SystemTime::UNIX_EPOCH
138
12
            .elapsed()
139
12
            .unwrap()
140
12
            .as_secs();
141
12
        self.label = label.to_string();
142
    }
143

            
144
    /// Retrieve the currently stored secret.
145
21
    pub fn secret(&self) -> Secret {
146
21
        let content_type = self
147
            .attributes
148
21
            .get(CONTENT_TYPE_ATTRIBUTE)
149
63
            .and_then(|c| ContentType::from_str(c).ok())
150
            .unwrap_or_default();
151

            
152
21
        Secret::with_content_type(content_type, &self.secret)
153
    }
154

            
155
    /// Store a new secret.
156
20
    pub fn set_secret(&mut self, secret: impl Into<Secret>) {
157
50
        self.modified = std::time::SystemTime::UNIX_EPOCH
158
20
            .elapsed()
159
20
            .unwrap()
160
20
            .as_secs();
161
20
        self.secret = secret.into().as_bytes().to_vec();
162
    }
163

            
164
    /// The UNIX time when the item was created.
165
16
    pub const fn created(&self) -> Duration {
166
17
        Duration::from_secs(self.created)
167
    }
168

            
169
    /// The UNIX time when the item was modified.
170
16
    pub const fn modified(&self) -> Duration {
171
16
        Duration::from_secs(self.modified)
172
    }
173

            
174
    /// Lock the item with the given key.
175
8
    pub fn lock(self, key: Option<&Key>) -> Result<LockedItem, Error> {
176
16
        let inner = self.encrypt(key)?;
177
8
        Ok(LockedItem { inner })
178
    }
179

            
180
24
    pub(crate) fn encrypt(&self, key: Option<&Key>) -> Result<EncryptedItem, Error> {
181
28
        match key {
182
25
            Some(key) => {
183
27
                key.check_strength()?;
184
23
                let iv = crypto::generate_iv()?;
185
49
                self.encrypt_encrypted(key, &iv)
186
            }
187
9
            None => self.encrypt_plaintext(),
188
        }
189
    }
190

            
191
9
    fn encrypt_plaintext(&self) -> Result<EncryptedItem, Error> {
192
9
        let blob = zgvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec();
193
9
        Ok(EncryptedItem {
194
9
            hashed_attributes: self
195
                .attributes
196
9
                .iter()
197
29
                .map(|(k, v)| (k.to_owned(), Mac::new(v.as_bytes().to_vec())))
198
9
                .collect(),
199
11
            blob,
200
        })
201
    }
202

            
203
26
    fn encrypt_encrypted(&self, key: &Key, iv: &[u8]) -> Result<EncryptedItem, Error> {
204
24
        let decrypted = Zeroizing::new(zgvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
205

            
206
25
        let mut blob = crypto::encrypt(&*decrypted, key, iv)?;
207

            
208
21
        blob.extend_from_slice(iv);
209
25
        let mac = crypto::compute_mac(&blob, key)?;
210
46
        blob.extend_from_slice(mac.as_slice());
211

            
212
22
        let hashed_attributes = self
213
            .attributes
214
            .iter()
215
72
            .filter_map(|(k, v)| Some((k.to_owned(), crypto::compute_mac(v.as_bytes(), key).ok()?)))
216
            .collect();
217

            
218
24
        Ok(EncryptedItem {
219
            hashed_attributes,
220
22
            blob,
221
        })
222
    }
223
}
224

            
225
impl TryFrom<&[u8]> for UnlockedItem {
226
    type Error = Error;
227

            
228
22
    fn try_from(value: &[u8]) -> Result<Self, Error> {
229
45
        let mut item: UnlockedItem = zgvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
230
23
            .deserialize()?
231
24
            .0;
232

            
233
        // Ensure MIME type attribute exists for backward compatibility
234
23
        if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
235
8
            item.attributes.insert(
236
8
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
237
8
                ContentType::default().as_str().to_string(),
238
            );
239
        }
240

            
241
23
        Ok(item)
242
    }
243
}
244

            
245
#[cfg(test)]
246
mod tests {
247
    use super::*;
248

            
249
    #[tokio::test]
250
    async fn set_label() {
251
        let mut item = UnlockedItem::new(
252
            "Original Label",
253
            &[("service", "test-service")],
254
            Secret::text("secret"),
255
        );
256

            
257
        let original_modified = item.modified();
258
        tokio::time::sleep(Duration::from_secs(1)).await;
259

            
260
        item.set_label("New Label");
261

            
262
        assert_eq!(item.label(), "New Label");
263
        assert!(item.modified() > original_modified);
264
        assert_eq!(item.secret().as_bytes(), b"secret");
265
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
266
    }
267

            
268
    #[tokio::test]
269
    async fn set_secret_text() {
270
        let mut item = UnlockedItem::new(
271
            "Test Item",
272
            &[("service", "test-service")],
273
            Secret::text("original"),
274
        );
275

            
276
        let original_modified = item.modified();
277
        tokio::time::sleep(Duration::from_secs(1)).await;
278

            
279
        item.set_secret(Secret::text("new secret"));
280

            
281
        assert_eq!(item.secret().as_bytes(), b"new secret");
282
        assert!(item.modified() > original_modified);
283
        assert_eq!(item.label(), "Test Item");
284
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
285
    }
286

            
287
    #[tokio::test]
288
    async fn set_secret_blob() {
289
        let mut item = UnlockedItem::new(
290
            "Binary Item",
291
            &[("type", "binary")],
292
            Secret::blob(b"binary data"),
293
        );
294

            
295
        let original_modified = item.modified();
296
        tokio::time::sleep(Duration::from_secs(1)).await;
297

            
298
        item.set_secret(Secret::blob(b"new binary data"));
299

            
300
        assert_eq!(item.secret().as_bytes(), b"new binary data");
301
        assert!(item.modified() > original_modified);
302
        assert_eq!(item.label(), "Binary Item");
303
    }
304

            
305
    #[tokio::test]
306
    async fn created_timestamp() {
307
        let item = UnlockedItem::new(
308
            "Timestamp Test",
309
            &[("test", "timestamp")],
310
            Secret::text("data"),
311
        );
312

            
313
        let created_time = item.created();
314
        assert!(created_time.as_secs() > 0);
315

            
316
        let modified_time = item.modified();
317
        assert_eq!(created_time, modified_time);
318
    }
319

            
320
    #[tokio::test]
321
    async fn modified_timestamp_updates() {
322
        let mut item = UnlockedItem::new(
323
            "Modification Test",
324
            &[("test", "modification")],
325
            Secret::text("data"),
326
        );
327

            
328
        let original_created = item.created();
329
        let original_modified = item.modified();
330

            
331
        tokio::time::sleep(Duration::from_secs(1)).await;
332

            
333
        item.set_label("Updated Label");
334

            
335
        assert_eq!(item.created(), original_created);
336
        assert!(item.modified() > original_modified);
337

            
338
        let mid_modified = item.modified();
339
        tokio::time::sleep(Duration::from_secs(1)).await;
340

            
341
        item.set_secret(Secret::text("updated secret"));
342

            
343
        assert_eq!(item.created(), original_created);
344
        assert!(item.modified() > mid_modified);
345
    }
346

            
347
    #[test]
348
    fn serialization() {
349
        let key = Key::new(vec![
350
            204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26,
351
        ]);
352
        let n_mac = crypto::mac_len();
353
        let n_iv = crypto::iv_len();
354

            
355
        let iv = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0];
356
        assert_eq!(iv.len(), n_iv);
357

            
358
        let attribute_value = "5".to_string();
359
        let attribute_value_mac = crypto::compute_mac(attribute_value.as_bytes(), &key).unwrap();
360

            
361
        let mut item = UnlockedItem {
362
            attributes: HashMap::from([("fooness".to_string(), attribute_value)]),
363
            label: "foo".to_string(),
364
            created: 50,
365
            modified: 50,
366
            secret: b"bar".to_vec(),
367
        };
368

            
369
        let encrypted = item.encrypt_encrypted(&key, &iv).unwrap();
370
        assert!(encrypted.has_attribute("fooness", &attribute_value_mac));
371

            
372
        let blob = &encrypted.blob;
373
        let n = blob.len();
374

            
375
        // encrypted.blob should be the concatenation of the encrypted data, the
376
        // iv, and the mac.
377
        let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv];
378
        let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap();
379

            
380
        assert_eq!(&blob[n - n_mac..], item_mac.as_slice());
381
        assert_eq!(&blob[n - n_mac - n_iv..n - n_mac], &iv);
382
        assert_eq!(
383
            encrypted_item_blob,
384
            vec![
385
                196, 246, 127, 53, 194, 30, 176, 37, 128, 145, 195, 96, 211, 161, 60, 150, 160,
386
                126, 85, 125, 85, 238, 5, 93, 153, 128, 176, 205, 31, 87, 48, 82, 121, 230, 143,
387
                152, 153, 193, 182, 114, 59, 157, 85, 41, 50, 1, 142, 112
388
            ]
389
        );
390

            
391
        let decrypted = encrypted.decrypt(Some(&key)).unwrap();
392

            
393
        // The decrypted item matches the original one but with the content-type
394
        // attribute set.
395
        item.attributes.insert(
396
            crate::CONTENT_TYPE_ATTRIBUTE.to_string(),
397
            crate::secret::ContentType::Blob.as_str().to_string(),
398
        );
399
        assert_eq!(decrypted, item);
400
    }
401
}