1
use std::str::FromStr;
2

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

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

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

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

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

            
36
21
    fn from_str(s: &str) -> Result<Self, Self::Err> {
37
        match s {
38
44
            "text/plain" => Ok(Self::Text),
39
26
            "application/octet-stream" => Ok(Self::Blob),
40
2
            e => Err(format!("Invalid content type: {e}")),
41
        }
42
    }
43
}
44

            
45
impl ContentType {
46
26
    pub const fn as_str(&self) -> &'static str {
47
23
        match self {
48
26
            Self::Text => "text/plain",
49
13
            Self::Blob => "application/octet-stream",
50
        }
51
    }
52
}
53

            
54
/// A wrapper around a combination of (secret, content-type).
55
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
56
pub enum Secret {
57
    /// Corresponds to [`ContentType::Text`]
58
    Text(String),
59
    /// Corresponds to [`ContentType::Blob`]
60
    Blob(Vec<u8>),
61
}
62

            
63
impl std::fmt::Debug for Secret {
64
2
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65
2
        match self {
66
2
            Self::Text(_) => write!(f, "Secret::Text([REDACTED])"),
67
2
            Self::Blob(_) => write!(f, "Secret::Blob([REDACTED])"),
68
        }
69
    }
70
}
71

            
72
impl Secret {
73
    /// Generate a random secret, used when creating a session collection.
74
32
    pub fn random() -> Result<Self, getrandom::Error> {
75
30
        let mut secret = [0; 64];
76
        // Equivalent of `ring::rand::SecureRandom`
77
34
        getrandom::fill(&mut secret)?;
78

            
79
30
        Ok(Self::blob(secret))
80
    }
81

            
82
    /// Get the sandboxed app secret if the app is sandboxed using
83
    /// org.freedesktop.portal.Secret portal.
84
    pub async fn sandboxed() -> Result<Self, crate::file::Error> {
85
        Ok(Self::blob(
86
            ashpd::desktop::secret::retrieve()
87
                .await
88
                .map_err(crate::file::Error::from)?,
89
        ))
90
    }
91

            
92
    /// Create a text secret, stored with `text/plain` content type.
93
58
    pub fn text(value: impl AsRef<str>) -> Self {
94
117
        Self::Text(value.as_ref().to_owned())
95
    }
96

            
97
    /// Create a blob secret, stored with `application/octet-stream` content
98
    /// type.
99
119
    pub fn blob(value: impl AsRef<[u8]>) -> Self {
100
233
        Self::Blob(value.as_ref().to_owned())
101
    }
102

            
103
25
    pub const fn content_type(&self) -> ContentType {
104
26
        match self {
105
24
            Self::Text(_) => ContentType::Text,
106
13
            Self::Blob(_) => ContentType::Blob,
107
        }
108
    }
109

            
110
    /// Returns the secret as a string slice, or `None` if it is not valid
111
    /// UTF-8.
112
    pub fn as_str(&self) -> Option<&str> {
113
        match self {
114
            Self::Text(text) => Some(text.as_str()),
115
            Self::Blob(bytes) => std::str::from_utf8(bytes).ok(),
116
        }
117
    }
118

            
119
37
    pub fn as_bytes(&self) -> &[u8] {
120
90
        match self {
121
35
            Self::Text(text) => text.as_bytes(),
122
26
            Self::Blob(bytes) => bytes.as_ref(),
123
        }
124
    }
125

            
126
22
    pub fn with_content_type(content_type: ContentType, secret: impl AsRef<[u8]>) -> Self {
127
22
        match content_type {
128
44
            ContentType::Text => match String::from_utf8(secret.as_ref().to_owned()) {
129
23
                Ok(text) => Secret::text(text),
130
2
                Err(_e) => {
131
                    #[cfg(feature = "tracing")]
132
                    tracing::warn!(
133
                        "Failed to decode secret as UTF-8: {}, falling back to blob",
134
                        _e
135
                    );
136

            
137
2
                    Secret::blob(secret)
138
                }
139
            },
140
26
            _ => Secret::blob(secret),
141
        }
142
    }
143
}
144

            
145
impl From<&[u8]> for Secret {
146
8
    fn from(value: &[u8]) -> Self {
147
8
        Self::blob(value)
148
    }
149
}
150

            
151
impl From<Zeroizing<Vec<u8>>> for Secret {
152
21
    fn from(value: Zeroizing<Vec<u8>>) -> Self {
153
22
        Self::blob(value)
154
    }
155
}
156

            
157
impl From<Vec<u8>> for Secret {
158
21
    fn from(value: Vec<u8>) -> Self {
159
18
        Self::blob(value)
160
    }
161
}
162

            
163
impl From<&Vec<u8>> for Secret {
164
6
    fn from(value: &Vec<u8>) -> Self {
165
6
        Self::blob(value)
166
    }
167
}
168

            
169
impl<const N: usize> From<&[u8; N]> for Secret {
170
    fn from(value: &[u8; N]) -> Self {
171
        Self::blob(value)
172
    }
173
}
174

            
175
impl From<String> for Secret {
176
6
    fn from(value: String) -> Self {
177
6
        Self::text(value)
178
    }
179
}
180

            
181
impl From<&str> for Secret {
182
41
    fn from(value: &str) -> Self {
183
34
        Self::text(value)
184
    }
185
}
186

            
187
impl std::ops::Deref for Secret {
188
    type Target = [u8];
189

            
190
34
    fn deref(&self) -> &Self::Target {
191
44
        self.as_bytes()
192
    }
193
}
194

            
195
impl AsRef<[u8]> for Secret {
196
12
    fn as_ref(&self) -> &[u8] {
197
12
        self.as_bytes()
198
    }
199
}
200

            
201
#[cfg(test)]
202
mod tests {
203
    use zvariant::{Endian, serialized::Context, to_bytes};
204

            
205
    use super::*;
206

            
207
    #[test]
208
    fn secret_debug_is_redacted() {
209
        let text_secret = Secret::text("password");
210
        let blob_secret = Secret::blob([1, 2, 3]);
211

            
212
        assert_eq!(format!("{:?}", text_secret), "Secret::Text([REDACTED])");
213
        assert_eq!(format!("{:?}", blob_secret), "Secret::Blob([REDACTED])");
214
    }
215

            
216
    #[test]
217
    fn content_type_serialization() {
218
        let ctxt = Context::new_dbus(Endian::Little, 0);
219

            
220
        // Test Text serialization
221
        let encoded = to_bytes(ctxt, &ContentType::Text).unwrap();
222
        let value: String = encoded.deserialize().unwrap().0;
223
        assert_eq!(value, "text/plain");
224

            
225
        // Test Blob serialization
226
        let encoded = to_bytes(ctxt, &ContentType::Blob).unwrap();
227
        let value: String = encoded.deserialize().unwrap().0;
228
        assert_eq!(value, "application/octet-stream");
229

            
230
        // Test Text deserialization
231
        let encoded = to_bytes(ctxt, &"text/plain").unwrap();
232
        let content_type: ContentType = encoded.deserialize().unwrap().0;
233
        assert_eq!(content_type, ContentType::Text);
234

            
235
        // Test Blob deserialization
236
        let encoded = to_bytes(ctxt, &"application/octet-stream").unwrap();
237
        let content_type: ContentType = encoded.deserialize().unwrap().0;
238
        assert_eq!(content_type, ContentType::Blob);
239

            
240
        // Test invalid content type deserialization
241
        let encoded = to_bytes(ctxt, &"invalid/type").unwrap();
242
        let result: Result<(ContentType, _), _> = encoded.deserialize();
243
        assert!(result.is_err());
244
        assert!(
245
            result
246
                .unwrap_err()
247
                .to_string()
248
                .contains("Invalid content type")
249
        );
250
    }
251

            
252
    #[test]
253
    fn content_type_from_str() {
254
        assert_eq!(
255
            ContentType::from_str("text/plain").unwrap(),
256
            ContentType::Text
257
        );
258
        assert_eq!(
259
            ContentType::from_str("application/octet-stream").unwrap(),
260
            ContentType::Blob
261
        );
262

            
263
        // Test error case
264
        let result = ContentType::from_str("invalid");
265
        assert!(result.is_err());
266
        assert!(result.unwrap_err().contains("Invalid content type"));
267
    }
268

            
269
    #[test]
270
    fn invalid_utf8() {
271
        // Test with invalid UTF-8 bytes
272
        let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
273

            
274
        // Should fall back to blob when UTF-8 decoding fails
275
        let secret = Secret::with_content_type(ContentType::Text, &invalid_utf8);
276
        assert_eq!(secret.content_type(), ContentType::Blob);
277
        assert_eq!(&*secret, &[0xFF, 0xFE, 0xFD]);
278

            
279
        // Test with valid UTF-8
280
        let valid_utf8 = "Hello, World!";
281
        let secret = Secret::with_content_type(ContentType::Text, valid_utf8.as_bytes());
282
        assert_eq!(secret.content_type(), ContentType::Text);
283
        assert_eq!(&*secret, valid_utf8.as_bytes());
284

            
285
        // Test with blob content type
286
        let data = vec![1, 2, 3, 4];
287
        let secret = Secret::with_content_type(ContentType::Blob, &data);
288
        assert_eq!(secret.content_type(), ContentType::Blob);
289
        assert_eq!(&*secret, &[1, 2, 3, 4]);
290
    }
291

            
292
    #[test]
293
    fn random() {
294
        let secret1 = Secret::random().unwrap();
295
        let secret2 = Secret::random().unwrap();
296

            
297
        // Random secrets should be blobs
298
        assert_eq!(secret1.content_type(), ContentType::Blob);
299
        assert_eq!(secret2.content_type(), ContentType::Blob);
300

            
301
        // Should be 64 bytes
302
        assert_eq!(secret1.as_bytes().len(), 64);
303
        assert_eq!(secret2.as_bytes().len(), 64);
304

            
305
        // Should be different
306
        assert_ne!(secret1.as_bytes(), secret2.as_bytes());
307
    }
308
}