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
73
    pub(crate) fn new(
31
        label: impl ToString,
32
        attributes: &impl AsAttributes,
33
        secret: impl Into<Secret>,
34
    ) -> Self {
35
211
        let now = std::time::SystemTime::UNIX_EPOCH
36
            .elapsed()
37
            .unwrap()
38
            .as_secs();
39

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

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

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

            
60
    /// Retrieve the item attributes.
61
23
    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
8
        self.attributes == attributes.as_attributes()
68
    }
69

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

            
97
    /// Update the item attributes.
98
15
    pub fn set_attributes(&mut self, attributes: &impl AsAttributes) {
99
15
        let mut new_attributes = attributes.as_attributes();
100

            
101
        // Preserve MIME type if not explicitly set in new attributes
102
30
        if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
103
30
            if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) {
104
15
                new_attributes.insert(
105
30
                    CONTENT_TYPE_ATTRIBUTE.to_string(),
106
15
                    existing_mime_type.clone(),
107
                );
108
            } else {
109
                new_attributes.insert(
110
                    CONTENT_TYPE_ATTRIBUTE.to_owned(),
111
                    ContentType::default().as_str().to_string(),
112
                );
113
            }
114
        }
115

            
116
15
        self.attributes = new_attributes;
117
30
        self.modified = std::time::SystemTime::UNIX_EPOCH
118
15
            .elapsed()
119
15
            .unwrap()
120
15
            .as_secs();
121
    }
122

            
123
    /// The item label.
124
21
    pub fn label(&self) -> &str {
125
22
        &self.label
126
    }
127

            
128
    /// Set the item label.
129
12
    pub fn set_label(&mut self, label: impl ToString) {
130
24
        self.modified = std::time::SystemTime::UNIX_EPOCH
131
12
            .elapsed()
132
12
            .unwrap()
133
12
            .as_secs();
134
12
        self.label = label.to_string();
135
    }
136

            
137
    /// Retrieve the currently stored secret.
138
19
    pub fn secret(&self) -> Secret {
139
19
        let content_type = self
140
            .attributes
141
19
            .get(CONTENT_TYPE_ATTRIBUTE)
142
59
            .and_then(|c| ContentType::from_str(c).ok())
143
            .unwrap_or_default();
144

            
145
20
        Secret::with_content_type(content_type, &self.secret)
146
    }
147

            
148
    /// Store a new secret.
149
20
    pub fn set_secret(&mut self, secret: impl Into<Secret>) {
150
60
        self.modified = std::time::SystemTime::UNIX_EPOCH
151
20
            .elapsed()
152
20
            .unwrap()
153
20
            .as_secs();
154
20
        self.secret = secret.into().as_bytes().to_vec();
155
    }
156

            
157
    /// The UNIX time when the item was created.
158
15
    pub const fn created(&self) -> Duration {
159
15
        Duration::from_secs(self.created)
160
    }
161

            
162
    /// The UNIX time when the item was modified.
163
15
    pub const fn modified(&self) -> Duration {
164
15
        Duration::from_secs(self.modified)
165
    }
166

            
167
    /// Lock the item with the given key.
168
8
    pub fn lock(self, key: Option<&Key>) -> Result<LockedItem, Error> {
169
16
        let inner = self.encrypt(key)?;
170
8
        Ok(LockedItem { inner })
171
    }
172

            
173
23
    pub(crate) fn encrypt(&self, key: Option<&Key>) -> Result<EncryptedItem, Error> {
174
23
        match key {
175
23
            Some(key) => {
176
25
                key.check_strength()?;
177
23
                let iv = crypto::generate_iv()?;
178
46
                self.encrypt_encrypted(key, &iv)
179
            }
180
8
            None => self.encrypt_plaintext(),
181
        }
182
    }
183

            
184
8
    fn encrypt_plaintext(&self) -> Result<EncryptedItem, Error> {
185
8
        let blob = zgvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec();
186
9
        Ok(EncryptedItem {
187
8
            hashed_attributes: self
188
                .attributes
189
8
                .iter()
190
27
                .map(|(k, v)| (k.to_owned(), Mac::new(v.as_bytes().to_vec())))
191
9
                .collect(),
192
11
            blob,
193
        })
194
    }
195

            
196
21
    fn encrypt_encrypted(&self, key: &Key, iv: &[u8]) -> Result<EncryptedItem, Error> {
197
32
        let decrypted = Zeroizing::new(zgvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
198

            
199
23
        let mut blob = crypto::encrypt(&*decrypted, key, iv)?;
200

            
201
24
        blob.extend_from_slice(iv);
202
22
        let mac = crypto::compute_mac(&blob, key)?;
203
46
        blob.extend_from_slice(mac.as_slice());
204

            
205
22
        let hashed_attributes = self
206
            .attributes
207
            .iter()
208
72
            .filter_map(|(k, v)| Some((k.to_owned(), crypto::compute_mac(v.as_bytes(), key).ok()?)))
209
            .collect();
210

            
211
25
        Ok(EncryptedItem {
212
            hashed_attributes,
213
24
            blob,
214
        })
215
    }
216
}
217

            
218
impl TryFrom<&[u8]> for UnlockedItem {
219
    type Error = Error;
220

            
221
22
    fn try_from(value: &[u8]) -> Result<Self, Error> {
222
47
        let mut item: UnlockedItem = zgvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
223
25
            .deserialize()?
224
25
            .0;
225

            
226
        // Ensure MIME type attribute exists for backward compatibility
227
23
        if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
228
8
            item.attributes.insert(
229
8
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
230
8
                ContentType::default().as_str().to_string(),
231
            );
232
        }
233

            
234
23
        Ok(item)
235
    }
236
}
237

            
238
#[cfg(test)]
239
mod tests {
240
    use super::*;
241

            
242
    #[tokio::test]
243
    async fn set_label() {
244
        let mut item = UnlockedItem::new(
245
            "Original Label",
246
            &[("service", "test-service")],
247
            Secret::text("secret"),
248
        );
249

            
250
        let original_modified = item.modified();
251
        tokio::time::sleep(Duration::from_secs(1)).await;
252

            
253
        item.set_label("New Label");
254

            
255
        assert_eq!(item.label(), "New Label");
256
        assert!(item.modified() > original_modified);
257
        assert_eq!(item.secret().as_bytes(), b"secret");
258
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
259
    }
260

            
261
    #[tokio::test]
262
    async fn set_secret_text() {
263
        let mut item = UnlockedItem::new(
264
            "Test Item",
265
            &[("service", "test-service")],
266
            Secret::text("original"),
267
        );
268

            
269
        let original_modified = item.modified();
270
        tokio::time::sleep(Duration::from_secs(1)).await;
271

            
272
        item.set_secret(Secret::text("new secret"));
273

            
274
        assert_eq!(item.secret().as_bytes(), b"new secret");
275
        assert!(item.modified() > original_modified);
276
        assert_eq!(item.label(), "Test Item");
277
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
278
    }
279

            
280
    #[tokio::test]
281
    async fn set_secret_blob() {
282
        let mut item = UnlockedItem::new(
283
            "Binary Item",
284
            &[("type", "binary")],
285
            Secret::blob(b"binary data"),
286
        );
287

            
288
        let original_modified = item.modified();
289
        tokio::time::sleep(Duration::from_secs(1)).await;
290

            
291
        item.set_secret(Secret::blob(b"new binary data"));
292

            
293
        assert_eq!(item.secret().as_bytes(), b"new binary data");
294
        assert!(item.modified() > original_modified);
295
        assert_eq!(item.label(), "Binary Item");
296
    }
297

            
298
    #[tokio::test]
299
    async fn created_timestamp() {
300
        let item = UnlockedItem::new(
301
            "Timestamp Test",
302
            &[("test", "timestamp")],
303
            Secret::text("data"),
304
        );
305

            
306
        let created_time = item.created();
307
        assert!(created_time.as_secs() > 0);
308

            
309
        let modified_time = item.modified();
310
        assert_eq!(created_time, modified_time);
311
    }
312

            
313
    #[tokio::test]
314
    async fn modified_timestamp_updates() {
315
        let mut item = UnlockedItem::new(
316
            "Modification Test",
317
            &[("test", "modification")],
318
            Secret::text("data"),
319
        );
320

            
321
        let original_created = item.created();
322
        let original_modified = item.modified();
323

            
324
        tokio::time::sleep(Duration::from_secs(1)).await;
325

            
326
        item.set_label("Updated Label");
327

            
328
        assert_eq!(item.created(), original_created);
329
        assert!(item.modified() > original_modified);
330

            
331
        let mid_modified = item.modified();
332
        tokio::time::sleep(Duration::from_secs(1)).await;
333

            
334
        item.set_secret(Secret::text("updated secret"));
335

            
336
        assert_eq!(item.created(), original_created);
337
        assert!(item.modified() > mid_modified);
338
    }
339

            
340
    #[test]
341
    fn serialization() {
342
        let key = Key::new(vec![
343
            204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26,
344
        ]);
345
        let n_mac = crypto::mac_len();
346
        let n_iv = crypto::iv_len();
347

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

            
351
        let attribute_value = "5".to_string();
352
        let attribute_value_mac = crypto::compute_mac(attribute_value.as_bytes(), &key).unwrap();
353

            
354
        let mut item = UnlockedItem {
355
            attributes: HashMap::from([("fooness".to_string(), attribute_value)]),
356
            label: "foo".to_string(),
357
            created: 50,
358
            modified: 50,
359
            secret: b"bar".to_vec(),
360
        };
361

            
362
        let encrypted = item.encrypt_encrypted(&key, &iv).unwrap();
363
        assert!(encrypted.has_attribute("fooness", &attribute_value_mac));
364

            
365
        let blob = &encrypted.blob;
366
        let n = blob.len();
367

            
368
        // encrypted.blob should be the concatenation of the encrypted data, the
369
        // iv, and the mac.
370
        let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv];
371
        let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap();
372

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

            
384
        let decrypted = encrypted.decrypt(Some(&key)).unwrap();
385

            
386
        // The decrypted item matches the original one but with the content-type
387
        // attribute set.
388
        item.attributes.insert(
389
            crate::CONTENT_TYPE_ATTRIBUTE.to_string(),
390
            crate::secret::ContentType::Blob.as_str().to_string(),
391
        );
392
        assert_eq!(decrypted, item);
393
    }
394
}