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::{AttributeValue, 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, AttributeValue>,
19
    #[zeroize(skip)]
20
    label: String,
21
    #[zeroize(skip)]
22
    created: u64,
23
    #[zeroize(skip)]
24
    modified: u64,
25
    secret: Vec<u8>,
26
}
27

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

            
39
        let mut item_attributes: HashMap<String, AttributeValue> = attributes
40
            .as_attributes()
41
            .into_iter()
42
92
            .map(|(k, v)| (k.to_string(), v.into()))
43
            .collect();
44

            
45
32
        let secret = secret.into();
46
        // Set default MIME type if not provided
47
64
        if !item_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
48
28
            item_attributes.insert(
49
56
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
50
56
                secret.content_type().as_str().into(),
51
            );
52
        }
53

            
54
        Self {
55
            attributes: item_attributes,
56
32
            label: label.to_string(),
57
            created: now,
58
            modified: now,
59
64
            secret: secret.as_bytes().to_vec(),
60
        }
61
    }
62

            
63
    /// Retrieve the item attributes.
64
4
    pub fn attributes(&self) -> &HashMap<String, AttributeValue> {
65
4
        &self.attributes
66
    }
67

            
68
    /// Update the item attributes.
69
8
    pub fn set_attributes(&mut self, attributes: &impl AsAttributes) {
70
8
        let mut new_attributes: HashMap<String, AttributeValue> = attributes
71
            .as_attributes()
72
            .into_iter()
73
24
            .map(|(k, v)| (k.to_string(), v.into()))
74
            .collect();
75

            
76
        // Preserve MIME type if not explicitly set in new attributes
77
16
        if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
78
12
            if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) {
79
6
                new_attributes.insert(
80
12
                    CONTENT_TYPE_ATTRIBUTE.to_string(),
81
6
                    existing_mime_type.clone(),
82
                );
83
            } else {
84
                new_attributes.insert(
85
                    CONTENT_TYPE_ATTRIBUTE.to_owned(),
86
                    ContentType::default().as_str().into(),
87
                );
88
            }
89
        }
90

            
91
8
        self.attributes = new_attributes;
92
16
        self.modified = std::time::SystemTime::UNIX_EPOCH
93
8
            .elapsed()
94
8
            .unwrap()
95
8
            .as_secs();
96
    }
97

            
98
    /// The item label.
99
4
    pub fn label(&self) -> &str {
100
4
        &self.label
101
    }
102

            
103
    /// Set the item label.
104
4
    pub fn set_label(&mut self, label: impl ToString) {
105
8
        self.modified = std::time::SystemTime::UNIX_EPOCH
106
4
            .elapsed()
107
4
            .unwrap()
108
4
            .as_secs();
109
4
        self.label = label.to_string();
110
    }
111

            
112
    /// Retrieve the currently stored secret.
113
4
    pub fn secret(&self) -> Secret {
114
4
        let content_type = self
115
            .attributes
116
4
            .get(CONTENT_TYPE_ATTRIBUTE)
117
12
            .and_then(|c| ContentType::from_str(c).ok())
118
            .unwrap_or_default();
119

            
120
4
        Secret::with_content_type(content_type, &self.secret)
121
    }
122

            
123
    /// Store a new secret.
124
8
    pub fn set_secret(&mut self, secret: impl Into<Secret>) {
125
24
        self.modified = std::time::SystemTime::UNIX_EPOCH
126
8
            .elapsed()
127
8
            .unwrap()
128
8
            .as_secs();
129
8
        self.secret = secret.into().as_bytes().to_vec();
130
    }
131

            
132
    /// The UNIX time when the item was created.
133
4
    pub const fn created(&self) -> Duration {
134
4
        Duration::from_secs(self.created)
135
    }
136

            
137
    /// The UNIX time when the item was modified.
138
4
    pub const fn modified(&self) -> Duration {
139
4
        Duration::from_secs(self.modified)
140
    }
141

            
142
    /// Lock the item with the given key.
143
4
    pub fn lock(self, key: &Key) -> Result<LockedItem, Error> {
144
8
        let inner = self.encrypt(key)?;
145
4
        Ok(LockedItem { inner })
146
    }
147

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

            
151
4
        let iv = crypto::generate_iv()?;
152

            
153
8
        self.encrypt_inner(key, &iv)
154
    }
155

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

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

            
161
4
        blob.extend_from_slice(iv);
162
4
        let mac = crypto::compute_mac(&blob, key)?;
163
8
        blob.extend_from_slice(mac.as_slice());
164

            
165
4
        let hashed_attributes = self
166
            .attributes
167
            .iter()
168
12
            .filter_map(|(k, v)| Some((k.to_owned(), v.mac(key).ok()?)))
169
            .collect();
170

            
171
4
        Ok(EncryptedItem {
172
            hashed_attributes,
173
4
            blob,
174
        })
175
    }
176
}
177

            
178
impl TryFrom<&[u8]> for UnlockedItem {
179
    type Error = Error;
180

            
181
4
    fn try_from(value: &[u8]) -> Result<Self, Error> {
182
8
        let mut item: UnlockedItem = zvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
183
4
            .deserialize()?
184
4
            .0;
185

            
186
        // Ensure MIME type attribute exists for backward compatibility
187
4
        if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
188
4
            item.attributes.insert(
189
4
                CONTENT_TYPE_ATTRIBUTE.to_owned(),
190
4
                ContentType::default().as_str().into(),
191
            );
192
        }
193

            
194
4
        Ok(item)
195
    }
196
}
197

            
198
#[cfg(test)]
199
mod tests {
200
    use super::*;
201

            
202
    #[tokio::test]
203
    async fn set_label() {
204
        let mut item = UnlockedItem::new(
205
            "Original Label",
206
            &[("service", "test-service")],
207
            Secret::text("secret"),
208
        );
209

            
210
        let original_modified = item.modified();
211
        tokio::time::sleep(Duration::from_secs(1)).await;
212

            
213
        item.set_label("New Label");
214

            
215
        assert_eq!(item.label(), "New Label");
216
        assert!(item.modified() > original_modified);
217
        assert_eq!(item.secret().as_bytes(), b"secret");
218
        assert_eq!(item.attributes().get("service").unwrap(), "test-service");
219
    }
220

            
221
    #[tokio::test]
222
    async fn set_secret_text() {
223
        let mut item = UnlockedItem::new(
224
            "Test Item",
225
            &[("service", "test-service")],
226
            Secret::text("original"),
227
        );
228

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

            
232
        item.set_secret(Secret::text("new secret"));
233

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

            
240
    #[tokio::test]
241
    async fn set_secret_blob() {
242
        let mut item = UnlockedItem::new(
243
            "Binary Item",
244
            &[("type", "binary")],
245
            Secret::blob(b"binary data"),
246
        );
247

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

            
251
        item.set_secret(Secret::blob(b"new binary data"));
252

            
253
        assert_eq!(item.secret().as_bytes(), b"new binary data");
254
        assert!(item.modified() > original_modified);
255
        assert_eq!(item.label(), "Binary Item");
256
    }
257

            
258
    #[tokio::test]
259
    async fn created_timestamp() {
260
        let item = UnlockedItem::new(
261
            "Timestamp Test",
262
            &[("test", "timestamp")],
263
            Secret::text("data"),
264
        );
265

            
266
        let created_time = item.created();
267
        assert!(created_time.as_secs() > 0);
268

            
269
        let modified_time = item.modified();
270
        assert_eq!(created_time, modified_time);
271
    }
272

            
273
    #[tokio::test]
274
    async fn modified_timestamp_updates() {
275
        let mut item = UnlockedItem::new(
276
            "Modification Test",
277
            &[("test", "modification")],
278
            Secret::text("data"),
279
        );
280

            
281
        let original_created = item.created();
282
        let original_modified = item.modified();
283

            
284
        tokio::time::sleep(Duration::from_secs(1)).await;
285

            
286
        item.set_label("Updated Label");
287

            
288
        assert_eq!(item.created(), original_created);
289
        assert!(item.modified() > original_modified);
290

            
291
        let mid_modified = item.modified();
292
        tokio::time::sleep(Duration::from_secs(1)).await;
293

            
294
        item.set_secret(Secret::text("updated secret"));
295

            
296
        assert_eq!(item.created(), original_created);
297
        assert!(item.modified() > mid_modified);
298
    }
299

            
300
    #[test]
301
    fn serialization() {
302
        let key = Key::new(vec![
303
            204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26,
304
        ]);
305
        let n_mac = crypto::mac_len();
306
        let n_iv = crypto::iv_len();
307

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

            
311
        let attribute_value = AttributeValue::from("5");
312
        let attribute_value_mac = attribute_value.mac(&key).unwrap();
313

            
314
        let mut item = UnlockedItem {
315
            attributes: HashMap::from([("fooness".to_string(), attribute_value)]),
316
            label: "foo".to_string(),
317
            created: 50,
318
            modified: 50,
319
            secret: b"bar".to_vec(),
320
        };
321

            
322
        let encrypted = item.encrypt_inner(&key, &iv).unwrap();
323
        assert!(encrypted.has_attribute("fooness", &attribute_value_mac));
324

            
325
        let blob = &encrypted.blob;
326
        let n = blob.len();
327

            
328
        // encrypted.blob should be the concatenation of the encrypted data, the
329
        // iv, and the mac.
330
        let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv];
331
        let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap();
332

            
333
        assert_eq!(&blob[n - n_mac..], item_mac.as_slice());
334
        assert_eq!(&blob[n - n_mac - n_iv..n - n_mac], &iv);
335
        assert_eq!(
336
            encrypted_item_blob,
337
            vec![
338
                196, 246, 127, 53, 194, 30, 176, 37, 128, 145, 195, 96, 211, 161, 60, 150, 160,
339
                126, 85, 125, 85, 238, 5, 93, 153, 128, 176, 205, 31, 87, 48, 82, 121, 230, 143,
340
                152, 153, 193, 182, 114, 59, 157, 85, 41, 50, 1, 142, 112
341
            ]
342
        );
343

            
344
        let decrypted = encrypted.decrypt(&key).unwrap();
345

            
346
        // The decrypted item matches the original one but with the content-type
347
        // attribute set.
348
        item.attributes.insert(
349
            crate::CONTENT_TYPE_ATTRIBUTE.to_string(),
350
            AttributeValue::from(crate::secret::ContentType::Blob.as_str()),
351
        );
352
        assert_eq!(decrypted, item);
353
    }
354
}