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

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

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

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

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

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

            
92
    /// Update the item attributes.
93
15
    pub fn set_attributes(&mut self, attributes: &impl AsAttributes) {
94
15
        let mut new_attributes = attributes.as_attributes();
95

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

            
111
15
        self.attributes = new_attributes;
112
30
        self.modified = std::time::SystemTime::UNIX_EPOCH
113
15
            .elapsed()
114
15
            .unwrap()
115
15
            .as_secs();
116
    }
117

            
118
    /// The item label.
119
19
    pub fn label(&self) -> &str {
120
19
        &self.label
121
    }
122

            
123
    /// Set the item label.
124
12
    pub fn set_label(&mut self, label: impl ToString) {
125
24
        self.modified = std::time::SystemTime::UNIX_EPOCH
126
12
            .elapsed()
127
12
            .unwrap()
128
12
            .as_secs();
129
12
        self.label = label.to_string();
130
    }
131

            
132
    /// Retrieve the currently stored secret.
133
19
    pub fn secret(&self) -> Secret {
134
19
        let content_type = self
135
            .attributes
136
19
            .get(CONTENT_TYPE_ATTRIBUTE)
137
57
            .and_then(|c| ContentType::from_str(c).ok())
138
            .unwrap_or_default();
139

            
140
19
        Secret::with_content_type(content_type, &self.secret)
141
    }
142

            
143
    /// Store a new secret.
144
18
    pub fn set_secret(&mut self, secret: impl Into<Secret>) {
145
54
        self.modified = std::time::SystemTime::UNIX_EPOCH
146
18
            .elapsed()
147
18
            .unwrap()
148
18
            .as_secs();
149
18
        self.secret = secret.into().as_bytes().to_vec();
150
    }
151

            
152
    /// The UNIX time when the item was created.
153
14
    pub const fn created(&self) -> Duration {
154
14
        Duration::from_secs(self.created)
155
    }
156

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

            
162
    /// Lock the item with the given key.
163
8
    pub fn lock(self, key: Option<&Key>) -> Result<LockedItem, Error> {
164
16
        let inner = self.encrypt(key)?;
165
8
        Ok(LockedItem { inner })
166
    }
167

            
168
26
    pub(crate) fn encrypt(&self, key: Option<&Key>) -> Result<EncryptedItem, Error> {
169
28
        match key {
170
26
            Some(key) => {
171
30
                key.check_strength()?;
172
24
                let iv = crypto::generate_iv()?;
173
51
                self.encrypt_encrypted(key, &iv)
174
            }
175
7
            None => self.encrypt_plaintext(),
176
        }
177
    }
178

            
179
9
    fn encrypt_plaintext(&self) -> Result<EncryptedItem, Error> {
180
7
        let blob = zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec();
181
8
        Ok(EncryptedItem {
182
9
            hashed_attributes: self
183
                .attributes
184
9
                .iter()
185
23
                .map(|(k, v)| (k.to_owned(), Mac::new(v.as_bytes().to_vec())))
186
9
                .collect(),
187
8
            blob,
188
        })
189
    }
190

            
191
31
    fn encrypt_encrypted(&self, key: &Key, iv: &[u8]) -> Result<EncryptedItem, Error> {
192
27
        let decrypted = Zeroizing::new(zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
193

            
194
32
        let mut blob = crypto::encrypt(&*decrypted, key, iv)?;
195

            
196
28
        blob.extend_from_slice(iv);
197
28
        let mac = crypto::compute_mac(&blob, key)?;
198
65
        blob.extend_from_slice(mac.as_slice());
199

            
200
30
        let hashed_attributes = self
201
            .attributes
202
            .iter()
203
94
            .filter_map(|(k, v)| Some((k.to_owned(), crypto::compute_mac(v.as_bytes(), key).ok()?)))
204
            .collect();
205

            
206
28
        Ok(EncryptedItem {
207
            hashed_attributes,
208
30
            blob,
209
        })
210
    }
211
}
212

            
213
impl TryFrom<&[u8]> for UnlockedItem {
214
    type Error = Error;
215

            
216
19
    fn try_from(value: &[u8]) -> Result<Self, Error> {
217
44
        let mut item: UnlockedItem = zvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
218
22
            .deserialize()?
219
22
            .0;
220

            
221
        // Ensure MIME type attribute exists for backward compatibility
222
20
        if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
223
8
            item.attributes.insert(
224
8
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
225
8
                ContentType::default().as_str().to_string(),
226
            );
227
        }
228

            
229
19
        Ok(item)
230
    }
231
}
232

            
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236

            
237
    #[tokio::test]
238
    async fn set_label() {
239
        let mut item = UnlockedItem::new(
240
            "Original Label",
241
            &[("service", "test-service")],
242
            Secret::text("secret"),
243
        );
244

            
245
        let original_modified = item.modified();
246
        tokio::time::sleep(Duration::from_secs(1)).await;
247

            
248
        item.set_label("New Label");
249

            
250
        assert_eq!(item.label(), "New Label");
251
        assert!(item.modified() > original_modified);
252
        assert_eq!(item.secret().as_bytes(), b"secret");
253
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
254
    }
255

            
256
    #[tokio::test]
257
    async fn set_secret_text() {
258
        let mut item = UnlockedItem::new(
259
            "Test Item",
260
            &[("service", "test-service")],
261
            Secret::text("original"),
262
        );
263

            
264
        let original_modified = item.modified();
265
        tokio::time::sleep(Duration::from_secs(1)).await;
266

            
267
        item.set_secret(Secret::text("new secret"));
268

            
269
        assert_eq!(item.secret().as_bytes(), b"new secret");
270
        assert!(item.modified() > original_modified);
271
        assert_eq!(item.label(), "Test Item");
272
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
273
    }
274

            
275
    #[tokio::test]
276
    async fn set_secret_blob() {
277
        let mut item = UnlockedItem::new(
278
            "Binary Item",
279
            &[("type", "binary")],
280
            Secret::blob(b"binary data"),
281
        );
282

            
283
        let original_modified = item.modified();
284
        tokio::time::sleep(Duration::from_secs(1)).await;
285

            
286
        item.set_secret(Secret::blob(b"new binary data"));
287

            
288
        assert_eq!(item.secret().as_bytes(), b"new binary data");
289
        assert!(item.modified() > original_modified);
290
        assert_eq!(item.label(), "Binary Item");
291
    }
292

            
293
    #[tokio::test]
294
    async fn created_timestamp() {
295
        let item = UnlockedItem::new(
296
            "Timestamp Test",
297
            &[("test", "timestamp")],
298
            Secret::text("data"),
299
        );
300

            
301
        let created_time = item.created();
302
        assert!(created_time.as_secs() > 0);
303

            
304
        let modified_time = item.modified();
305
        assert_eq!(created_time, modified_time);
306
    }
307

            
308
    #[tokio::test]
309
    async fn modified_timestamp_updates() {
310
        let mut item = UnlockedItem::new(
311
            "Modification Test",
312
            &[("test", "modification")],
313
            Secret::text("data"),
314
        );
315

            
316
        let original_created = item.created();
317
        let original_modified = item.modified();
318

            
319
        tokio::time::sleep(Duration::from_secs(1)).await;
320

            
321
        item.set_label("Updated Label");
322

            
323
        assert_eq!(item.created(), original_created);
324
        assert!(item.modified() > original_modified);
325

            
326
        let mid_modified = item.modified();
327
        tokio::time::sleep(Duration::from_secs(1)).await;
328

            
329
        item.set_secret(Secret::text("updated secret"));
330

            
331
        assert_eq!(item.created(), original_created);
332
        assert!(item.modified() > mid_modified);
333
    }
334

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

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

            
346
        let attribute_value = "5".to_string();
347
        let attribute_value_mac = crypto::compute_mac(attribute_value.as_bytes(), &key).unwrap();
348

            
349
        let mut item = UnlockedItem {
350
            attributes: HashMap::from([("fooness".to_string(), attribute_value)]),
351
            label: "foo".to_string(),
352
            created: 50,
353
            modified: 50,
354
            secret: b"bar".to_vec(),
355
        };
356

            
357
        let encrypted = item.encrypt_encrypted(&key, &iv).unwrap();
358
        assert!(encrypted.has_attribute("fooness", &attribute_value_mac));
359

            
360
        let blob = &encrypted.blob;
361
        let n = blob.len();
362

            
363
        // encrypted.blob should be the concatenation of the encrypted data, the
364
        // iv, and the mac.
365
        let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv];
366
        let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap();
367

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

            
379
        let decrypted = encrypted.decrypt(Some(&key)).unwrap();
380

            
381
        // The decrypted item matches the original one but with the content-type
382
        // attribute set.
383
        item.attributes.insert(
384
            crate::CONTENT_TYPE_ATTRIBUTE.to_string(),
385
            crate::secret::ContentType::Blob.as_str().to_string(),
386
        );
387
        assert_eq!(decrypted, item);
388
    }
389
}