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

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

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

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

            
60
    /// Retrieve the item attributes.
61
4
    pub fn attributes(&self) -> &HashMap<String, String> {
62
4
        &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
    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
        T::try_from(&self.attributes).map_err(Into::into)
90
    }
91

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

            
96
        // Preserve MIME type if not explicitly set in new attributes
97
4
        if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
98
4
            if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) {
99
2
                new_attributes.insert(
100
4
                    CONTENT_TYPE_ATTRIBUTE.to_string(),
101
2
                    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
2
        self.attributes = new_attributes;
112
4
        self.modified = std::time::SystemTime::UNIX_EPOCH
113
2
            .elapsed()
114
2
            .unwrap()
115
2
            .as_secs();
116
    }
117

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

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

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

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

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

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

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

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

            
168
4
    pub(crate) fn encrypt(&self, key: &Key) -> Result<EncryptedItem, Error> {
169
4
        key.check_strength()?;
170

            
171
4
        let iv = crypto::generate_iv()?;
172

            
173
8
        self.encrypt_inner(key, &iv)
174
    }
175

            
176
4
    fn encrypt_inner(&self, key: &Key, iv: &[u8]) -> Result<EncryptedItem, Error> {
177
4
        let decrypted = Zeroizing::new(zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
178

            
179
4
        let mut blob = crypto::encrypt(&*decrypted, key, iv)?;
180

            
181
4
        blob.extend_from_slice(iv);
182
4
        let mac = crypto::compute_mac(&blob, key)?;
183
8
        blob.extend_from_slice(mac.as_slice());
184

            
185
4
        let hashed_attributes = self
186
            .attributes
187
            .iter()
188
12
            .filter_map(|(k, v)| Some((k.to_owned(), crypto::compute_mac(v.as_bytes(), key).ok()?)))
189
            .collect();
190

            
191
4
        Ok(EncryptedItem {
192
            hashed_attributes,
193
4
            blob,
194
        })
195
    }
196
}
197

            
198
impl TryFrom<&[u8]> for UnlockedItem {
199
    type Error = Error;
200

            
201
4
    fn try_from(value: &[u8]) -> Result<Self, Error> {
202
8
        let mut item: UnlockedItem = zvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
203
4
            .deserialize()?
204
4
            .0;
205

            
206
        // Ensure MIME type attribute exists for backward compatibility
207
4
        if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
208
4
            item.attributes.insert(
209
4
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
210
4
                ContentType::default().as_str().to_string(),
211
            );
212
        }
213

            
214
4
        Ok(item)
215
    }
216
}
217

            
218
#[cfg(test)]
219
mod tests {
220
    use super::*;
221

            
222
    #[tokio::test]
223
    async fn set_label() {
224
        let mut item = UnlockedItem::new(
225
            "Original Label",
226
            &[("service", "test-service")],
227
            Secret::text("secret"),
228
        );
229

            
230
        let original_modified = item.modified();
231
        tokio::time::sleep(Duration::from_secs(1)).await;
232

            
233
        item.set_label("New Label");
234

            
235
        assert_eq!(item.label(), "New Label");
236
        assert!(item.modified() > original_modified);
237
        assert_eq!(item.secret().as_bytes(), b"secret");
238
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
239
    }
240

            
241
    #[tokio::test]
242
    async fn set_secret_text() {
243
        let mut item = UnlockedItem::new(
244
            "Test Item",
245
            &[("service", "test-service")],
246
            Secret::text("original"),
247
        );
248

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

            
252
        item.set_secret(Secret::text("new secret"));
253

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

            
260
    #[tokio::test]
261
    async fn set_secret_blob() {
262
        let mut item = UnlockedItem::new(
263
            "Binary Item",
264
            &[("type", "binary")],
265
            Secret::blob(b"binary data"),
266
        );
267

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

            
271
        item.set_secret(Secret::blob(b"new binary data"));
272

            
273
        assert_eq!(item.secret().as_bytes(), b"new binary data");
274
        assert!(item.modified() > original_modified);
275
        assert_eq!(item.label(), "Binary Item");
276
    }
277

            
278
    #[tokio::test]
279
    async fn created_timestamp() {
280
        let item = UnlockedItem::new(
281
            "Timestamp Test",
282
            &[("test", "timestamp")],
283
            Secret::text("data"),
284
        );
285

            
286
        let created_time = item.created();
287
        assert!(created_time.as_secs() > 0);
288

            
289
        let modified_time = item.modified();
290
        assert_eq!(created_time, modified_time);
291
    }
292

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

            
301
        let original_created = item.created();
302
        let original_modified = item.modified();
303

            
304
        tokio::time::sleep(Duration::from_secs(1)).await;
305

            
306
        item.set_label("Updated Label");
307

            
308
        assert_eq!(item.created(), original_created);
309
        assert!(item.modified() > original_modified);
310

            
311
        let mid_modified = item.modified();
312
        tokio::time::sleep(Duration::from_secs(1)).await;
313

            
314
        item.set_secret(Secret::text("updated secret"));
315

            
316
        assert_eq!(item.created(), original_created);
317
        assert!(item.modified() > mid_modified);
318
    }
319

            
320
    #[test]
321
    fn serialization() {
322
        let key = Key::new(vec![
323
            204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26,
324
        ]);
325
        let n_mac = crypto::mac_len();
326
        let n_iv = crypto::iv_len();
327

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

            
331
        let attribute_value = "5".to_string();
332
        let attribute_value_mac = crypto::compute_mac(attribute_value.as_bytes(), &key).unwrap();
333

            
334
        let mut item = UnlockedItem {
335
            attributes: HashMap::from([("fooness".to_string(), attribute_value)]),
336
            label: "foo".to_string(),
337
            created: 50,
338
            modified: 50,
339
            secret: b"bar".to_vec(),
340
        };
341

            
342
        let encrypted = item.encrypt_inner(&key, &iv).unwrap();
343
        assert!(encrypted.has_attribute("fooness", &attribute_value_mac));
344

            
345
        let blob = &encrypted.blob;
346
        let n = blob.len();
347

            
348
        // encrypted.blob should be the concatenation of the encrypted data, the
349
        // iv, and the mac.
350
        let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv];
351
        let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap();
352

            
353
        assert_eq!(&blob[n - n_mac..], item_mac.as_slice());
354
        assert_eq!(&blob[n - n_mac - n_iv..n - n_mac], &iv);
355
        assert_eq!(
356
            encrypted_item_blob,
357
            vec![
358
                196, 246, 127, 53, 194, 30, 176, 37, 128, 145, 195, 96, 211, 161, 60, 150, 160,
359
                126, 85, 125, 85, 238, 5, 93, 153, 128, 176, 205, 31, 87, 48, 82, 121, 230, 143,
360
                152, 153, 193, 182, 114, 59, 157, 85, 41, 50, 1, 142, 112
361
            ]
362
        );
363

            
364
        let decrypted = encrypted.decrypt(&key).unwrap();
365

            
366
        // The decrypted item matches the original one but with the content-type
367
        // attribute set.
368
        item.attributes.insert(
369
            crate::CONTENT_TYPE_ATTRIBUTE.to_string(),
370
            crate::secret::ContentType::Blob.as_str().to_string(),
371
        );
372
        assert_eq!(decrypted, item);
373
    }
374
}