1
use std::str::FromStr;
2

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

            
7
#[derive(Default, PartialEq, Eq, Copy, Clone, Debug, zvariant::Type)]
8
#[zvariant(signature = "s")]
9
pub enum ContentType {
10
    Text,
11
    #[default]
12
    Blob,
13
}
14

            
15
impl Serialize for ContentType {
16
26
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
17
    where
18
        S: serde::Serializer,
19
    {
20
52
        self.as_str().serialize(serializer)
21
    }
22
}
23

            
24
impl<'de> Deserialize<'de> for ContentType {
25
23
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
26
    where
27
        D: serde::Deserializer<'de>,
28
    {
29
22
        let s = String::deserialize(deserializer)?;
30
46
        Self::from_str(&s).map_err(serde::de::Error::custom)
31
    }
32
}
33

            
34
impl FromStr for ContentType {
35
    type Err = String;
36

            
37
25
    fn from_str(s: &str) -> Result<Self, Self::Err> {
38
        // MIME types may include parameters, which are irrelevant to ContentType.
39
47
        let media_type = s
40
            .split_once(';')
41
24
            .map_or(s, |(media_type, _)| media_type)
42
            .trim();
43

            
44
47
        if media_type.eq_ignore_ascii_case("text/plain")
45
13
            || media_type.eq_ignore_ascii_case("text/utf8")
46
13
            || media_type.eq_ignore_ascii_case("application/json")
47
        {
48
21
            Ok(Self::Text)
49
26
        } else if media_type.eq_ignore_ascii_case("application/octet-stream")
50
2
            || media_type.eq_ignore_ascii_case("application/binary")
51
        {
52
13
            Ok(Self::Blob)
53
        } else {
54
2
            Err(format!("Invalid content type: {s}"))
55
        }
56
    }
57
}
58

            
59
impl ContentType {
60
25
    pub const fn as_str(&self) -> &'static str {
61
25
        match self {
62
22
            Self::Text => "text/plain",
63
13
            Self::Blob => "application/octet-stream",
64
        }
65
    }
66
}
67

            
68
/// A wrapper around a combination of (secret, content-type).
69
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
70
pub enum Secret {
71
    /// Corresponds to [`ContentType::Text`]
72
    Text(String),
73
    /// Corresponds to [`ContentType::Blob`]
74
    Blob(Vec<u8>),
75
}
76

            
77
impl std::fmt::Debug for Secret {
78
2
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79
2
        match self {
80
2
            Self::Text(_) => write!(f, "Secret::Text([REDACTED])"),
81
2
            Self::Blob(_) => write!(f, "Secret::Blob([REDACTED])"),
82
        }
83
    }
84
}
85

            
86
impl Secret {
87
    /// Generate a random secret, used when creating a session collection.
88
37
    pub fn random() -> Result<Self, getrandom::Error> {
89
34
        let mut secret = [0; 64];
90
        // Equivalent of `ring::rand::SecureRandom`
91
34
        getrandom::fill(&mut secret)?;
92

            
93
32
        Ok(Self::blob(secret))
94
    }
95

            
96
    /// Get the sandboxed app secret if the app is sandboxed using
97
    /// org.freedesktop.portal.Secret portal.
98
    pub async fn sandboxed() -> Result<Self, crate::file::Error> {
99
        Ok(Self::blob(
100
            ashpd::desktop::secret::retrieve()
101
                .await
102
                .map_err(crate::file::Error::from)?,
103
        ))
104
    }
105

            
106
    /// Create a text secret, stored with `text/plain` content type.
107
55
    pub fn text(value: impl AsRef<str>) -> Self {
108
102
        Self::Text(value.as_ref().to_owned())
109
    }
110

            
111
    /// Create a blob secret, stored with `application/octet-stream` content
112
    /// type.
113
120
    pub fn blob(value: impl AsRef<[u8]>) -> Self {
114
225
        Self::Blob(value.as_ref().to_owned())
115
    }
116

            
117
23
    pub const fn content_type(&self) -> ContentType {
118
26
        match self {
119
24
            Self::Text(_) => ContentType::Text,
120
13
            Self::Blob(_) => ContentType::Blob,
121
        }
122
    }
123

            
124
    /// Returns the secret as a string slice, or `None` if it is not valid
125
    /// UTF-8.
126
    pub fn as_str(&self) -> Option<&str> {
127
        match self {
128
            Self::Text(text) => Some(text.as_str()),
129
            Self::Blob(bytes) => std::str::from_utf8(bytes).ok(),
130
        }
131
    }
132

            
133
35
    pub fn as_bytes(&self) -> &[u8] {
134
80
        match self {
135
33
            Self::Text(text) => text.as_bytes(),
136
28
            Self::Blob(bytes) => bytes.as_ref(),
137
        }
138
    }
139

            
140
22
    pub fn with_content_type(content_type: ContentType, secret: impl AsRef<[u8]>) -> Self {
141
22
        match content_type {
142
43
            ContentType::Text => match String::from_utf8(secret.as_ref().to_owned()) {
143
21
                Ok(text) => Secret::text(text),
144
2
                Err(_e) => {
145
                    #[cfg(feature = "tracing")]
146
                    tracing::warn!(
147
                        "Failed to decode secret as UTF-8: {}, falling back to blob",
148
                        _e
149
                    );
150

            
151
2
                    Secret::blob(secret)
152
                }
153
            },
154
26
            _ => Secret::blob(secret),
155
        }
156
    }
157
}
158

            
159
impl From<&[u8]> for Secret {
160
8
    fn from(value: &[u8]) -> Self {
161
8
        Self::blob(value)
162
    }
163
}
164

            
165
impl From<Zeroizing<Vec<u8>>> for Secret {
166
20
    fn from(value: Zeroizing<Vec<u8>>) -> Self {
167
17
        Self::blob(value)
168
    }
169
}
170

            
171
impl From<Vec<u8>> for Secret {
172
18
    fn from(value: Vec<u8>) -> Self {
173
19
        Self::blob(value)
174
    }
175
}
176

            
177
impl From<&Vec<u8>> for Secret {
178
6
    fn from(value: &Vec<u8>) -> Self {
179
6
        Self::blob(value)
180
    }
181
}
182

            
183
impl<const N: usize> From<&[u8; N]> for Secret {
184
    fn from(value: &[u8; N]) -> Self {
185
        Self::blob(value)
186
    }
187
}
188

            
189
impl From<String> for Secret {
190
6
    fn from(value: String) -> Self {
191
6
        Self::text(value)
192
    }
193
}
194

            
195
impl From<&str> for Secret {
196
28
    fn from(value: &str) -> Self {
197
31
        Self::text(value)
198
    }
199
}
200

            
201
impl std::ops::Deref for Secret {
202
    type Target = [u8];
203

            
204
33
    fn deref(&self) -> &Self::Target {
205
39
        self.as_bytes()
206
    }
207
}
208

            
209
impl AsRef<[u8]> for Secret {
210
12
    fn as_ref(&self) -> &[u8] {
211
12
        self.as_bytes()
212
    }
213
}
214

            
215
#[cfg(test)]
216
mod tests {
217
    use zbus::zvariant::{Endian, serialized::Context, to_bytes};
218

            
219
    use super::*;
220

            
221
    #[test]
222
    fn secret_debug_is_redacted() {
223
        let text_secret = Secret::text("password");
224
        let blob_secret = Secret::blob([1, 2, 3]);
225

            
226
        assert_eq!(format!("{:?}", text_secret), "Secret::Text([REDACTED])");
227
        assert_eq!(format!("{:?}", blob_secret), "Secret::Blob([REDACTED])");
228
    }
229

            
230
    #[test]
231
    fn content_type_serialization() {
232
        let ctxt = Context::new_dbus(Endian::Little, 0);
233

            
234
        // Test Text serialization
235
        let encoded = to_bytes(ctxt, &ContentType::Text).unwrap();
236
        let value: String = encoded.deserialize().unwrap().0;
237
        assert_eq!(value, "text/plain");
238

            
239
        // Test Blob serialization
240
        let encoded = to_bytes(ctxt, &ContentType::Blob).unwrap();
241
        let value: String = encoded.deserialize().unwrap().0;
242
        assert_eq!(value, "application/octet-stream");
243

            
244
        // Test Text deserialization
245
        let encoded = to_bytes(ctxt, &"text/plain").unwrap();
246
        let content_type: ContentType = encoded.deserialize().unwrap().0;
247
        assert_eq!(content_type, ContentType::Text);
248

            
249
        // Test Text deserialization with MIME parameters
250
        let encoded = to_bytes(ctxt, &"text/plain; charset=utf8").unwrap();
251
        let content_type: ContentType = encoded.deserialize().unwrap().0;
252
        assert_eq!(content_type, ContentType::Text);
253

            
254
        // Test Blob deserialization
255
        let encoded = to_bytes(ctxt, &"application/octet-stream").unwrap();
256
        let content_type: ContentType = encoded.deserialize().unwrap().0;
257
        assert_eq!(content_type, ContentType::Blob);
258

            
259
        // application/json deserializes as Text
260
        let encoded = to_bytes(ctxt, &"application/json").unwrap();
261
        let content_type: ContentType = encoded.deserialize().unwrap().0;
262
        assert_eq!(content_type, ContentType::Text);
263

            
264
        // Test invalid content type deserialization
265
        let encoded = to_bytes(ctxt, &"invalid/type").unwrap();
266
        let result: Result<(ContentType, _), _> = encoded.deserialize();
267
        assert!(result.is_err());
268
        assert!(
269
            result
270
                .unwrap_err()
271
                .to_string()
272
                .contains("Invalid content type")
273
        );
274
    }
275

            
276
    #[test]
277
    fn content_type_from_str() {
278
        for content_type in [
279
            "text/plain",
280
            "text/plain; charset=utf8",
281
            "TEXT/PLAIN; CHARSET=UTF-8",
282
            "application/json",
283
            "application/json; charset=utf8",
284
        ] {
285
            assert_eq!(
286
                ContentType::from_str(content_type).unwrap(),
287
                ContentType::Text
288
            );
289
        }
290

            
291
        for content_type in [
292
            "application/octet-stream",
293
            "application/octet-stream; version=1",
294
        ] {
295
            assert_eq!(
296
                ContentType::from_str(content_type).unwrap(),
297
                ContentType::Blob
298
            );
299
        }
300

            
301
        // Test error case
302
        let result = ContentType::from_str("text/html; charset=utf8");
303
        assert!(result.is_err());
304
        let error = result.unwrap_err();
305
        assert!(error.contains("Invalid content type"));
306
        assert!(error.contains("text/html; charset=utf8"));
307
    }
308

            
309
    #[test]
310
    fn invalid_utf8() {
311
        // Test with invalid UTF-8 bytes
312
        let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
313

            
314
        // Should fall back to blob when UTF-8 decoding fails
315
        let secret = Secret::with_content_type(ContentType::Text, &invalid_utf8);
316
        assert_eq!(secret.content_type(), ContentType::Blob);
317
        assert_eq!(&*secret, &[0xFF, 0xFE, 0xFD]);
318

            
319
        // Test with valid UTF-8
320
        let valid_utf8 = "Hello, World!";
321
        let secret = Secret::with_content_type(ContentType::Text, valid_utf8.as_bytes());
322
        assert_eq!(secret.content_type(), ContentType::Text);
323
        assert_eq!(&*secret, valid_utf8.as_bytes());
324

            
325
        // Test with blob content type
326
        let data = vec![1, 2, 3, 4];
327
        let secret = Secret::with_content_type(ContentType::Blob, &data);
328
        assert_eq!(secret.content_type(), ContentType::Blob);
329
        assert_eq!(&*secret, &[1, 2, 3, 4]);
330
    }
331

            
332
    #[test]
333
    fn random() {
334
        let secret1 = Secret::random().unwrap();
335
        let secret2 = Secret::random().unwrap();
336

            
337
        // Random secrets should be blobs
338
        assert_eq!(secret1.content_type(), ContentType::Blob);
339
        assert_eq!(secret2.content_type(), ContentType::Blob);
340

            
341
        // Should be 64 bytes
342
        assert_eq!(secret1.as_bytes().len(), 64);
343
        assert_eq!(secret2.as_bytes().len(), 64);
344

            
345
        // Should be different
346
        assert_ne!(secret1.as_bytes(), secret2.as_bytes());
347
    }
348
}