1use std::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5
6#[derive(Default, PartialEq, Eq, Copy, Clone, Debug, zvariant::Type)]
7#[zvariant(signature = "s")]
8pub enum ContentType {
9 Text,
10 #[default]
11 Blob,
12}
13
14impl Serialize for ContentType {
15 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
16 where
17 S: serde::Serializer,
18 {
19 self.as_str().serialize(serializer)
20 }
21}
22
23impl<'de> Deserialize<'de> for ContentType {
24 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
25 where
26 D: serde::Deserializer<'de>,
27 {
28 let s = String::deserialize(deserializer)?;
29 Self::from_str(&s).map_err(serde::de::Error::custom)
30 }
31}
32
33impl FromStr for ContentType {
34 type Err = String;
35
36 fn from_str(s: &str) -> Result<Self, Self::Err> {
37 let media_type = s
39 .split_once(';')
40 .map_or(s, |(media_type, _)| media_type)
41 .trim();
42
43 if media_type.eq_ignore_ascii_case("text/plain")
44 || media_type.eq_ignore_ascii_case("text/utf8")
45 {
46 Ok(Self::Text)
47 } else if media_type.eq_ignore_ascii_case("application/octet-stream") {
48 Ok(Self::Blob)
49 } else {
50 Err(format!("Invalid content type: {s}"))
51 }
52 }
53}
54
55impl ContentType {
56 pub const fn as_str(&self) -> &'static str {
57 match self {
58 Self::Text => "text/plain",
59 Self::Blob => "application/octet-stream",
60 }
61 }
62}
63
64#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
66pub enum Secret {
67 Text(String),
69 Blob(Vec<u8>),
71}
72
73impl std::fmt::Debug for Secret {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::Text(_) => write!(f, "Secret::Text([REDACTED])"),
77 Self::Blob(_) => write!(f, "Secret::Blob([REDACTED])"),
78 }
79 }
80}
81
82impl Secret {
83 pub fn random() -> Result<Self, getrandom::Error> {
85 let mut secret = [0; 64];
86 getrandom::fill(&mut secret)?;
88
89 Ok(Self::blob(secret))
90 }
91
92 pub async fn sandboxed() -> Result<Self, crate::file::Error> {
95 Ok(Self::blob(
96 ashpd::desktop::secret::retrieve()
97 .await
98 .map_err(crate::file::Error::from)?,
99 ))
100 }
101
102 pub fn text(value: impl AsRef<str>) -> Self {
104 Self::Text(value.as_ref().to_owned())
105 }
106
107 pub fn blob(value: impl AsRef<[u8]>) -> Self {
110 Self::Blob(value.as_ref().to_owned())
111 }
112
113 pub const fn content_type(&self) -> ContentType {
114 match self {
115 Self::Text(_) => ContentType::Text,
116 Self::Blob(_) => ContentType::Blob,
117 }
118 }
119
120 pub fn as_str(&self) -> Option<&str> {
123 match self {
124 Self::Text(text) => Some(text.as_str()),
125 Self::Blob(bytes) => std::str::from_utf8(bytes).ok(),
126 }
127 }
128
129 pub fn as_bytes(&self) -> &[u8] {
130 match self {
131 Self::Text(text) => text.as_bytes(),
132 Self::Blob(bytes) => bytes.as_ref(),
133 }
134 }
135
136 pub fn with_content_type(content_type: ContentType, secret: impl AsRef<[u8]>) -> Self {
137 match content_type {
138 ContentType::Text => match String::from_utf8(secret.as_ref().to_owned()) {
139 Ok(text) => Secret::text(text),
140 Err(_e) => {
141 #[cfg(feature = "tracing")]
142 tracing::warn!(
143 "Failed to decode secret as UTF-8: {}, falling back to blob",
144 _e
145 );
146
147 Secret::blob(secret)
148 }
149 },
150 _ => Secret::blob(secret),
151 }
152 }
153}
154
155impl From<&[u8]> for Secret {
156 fn from(value: &[u8]) -> Self {
157 Self::blob(value)
158 }
159}
160
161impl From<Zeroizing<Vec<u8>>> for Secret {
162 fn from(value: Zeroizing<Vec<u8>>) -> Self {
163 Self::blob(value)
164 }
165}
166
167impl From<Vec<u8>> for Secret {
168 fn from(value: Vec<u8>) -> Self {
169 Self::blob(value)
170 }
171}
172
173impl From<&Vec<u8>> for Secret {
174 fn from(value: &Vec<u8>) -> Self {
175 Self::blob(value)
176 }
177}
178
179impl<const N: usize> From<&[u8; N]> for Secret {
180 fn from(value: &[u8; N]) -> Self {
181 Self::blob(value)
182 }
183}
184
185impl From<String> for Secret {
186 fn from(value: String) -> Self {
187 Self::text(value)
188 }
189}
190
191impl From<&str> for Secret {
192 fn from(value: &str) -> Self {
193 Self::text(value)
194 }
195}
196
197impl std::ops::Deref for Secret {
198 type Target = [u8];
199
200 fn deref(&self) -> &Self::Target {
201 self.as_bytes()
202 }
203}
204
205impl AsRef<[u8]> for Secret {
206 fn as_ref(&self) -> &[u8] {
207 self.as_bytes()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use zvariant::{Endian, serialized::Context, to_bytes};
214
215 use super::*;
216
217 #[test]
218 fn secret_debug_is_redacted() {
219 let text_secret = Secret::text("password");
220 let blob_secret = Secret::blob([1, 2, 3]);
221
222 assert_eq!(format!("{:?}", text_secret), "Secret::Text([REDACTED])");
223 assert_eq!(format!("{:?}", blob_secret), "Secret::Blob([REDACTED])");
224 }
225
226 #[test]
227 fn content_type_serialization() {
228 let ctxt = Context::new_dbus(Endian::Little, 0);
229
230 let encoded = to_bytes(ctxt, &ContentType::Text).unwrap();
232 let value: String = encoded.deserialize().unwrap().0;
233 assert_eq!(value, "text/plain");
234
235 let encoded = to_bytes(ctxt, &ContentType::Blob).unwrap();
237 let value: String = encoded.deserialize().unwrap().0;
238 assert_eq!(value, "application/octet-stream");
239
240 let encoded = to_bytes(ctxt, &"text/plain").unwrap();
242 let content_type: ContentType = encoded.deserialize().unwrap().0;
243 assert_eq!(content_type, ContentType::Text);
244
245 let encoded = to_bytes(ctxt, &"text/plain; charset=utf8").unwrap();
247 let content_type: ContentType = encoded.deserialize().unwrap().0;
248 assert_eq!(content_type, ContentType::Text);
249
250 let encoded = to_bytes(ctxt, &"application/octet-stream").unwrap();
252 let content_type: ContentType = encoded.deserialize().unwrap().0;
253 assert_eq!(content_type, ContentType::Blob);
254
255 let encoded = to_bytes(ctxt, &"invalid/type").unwrap();
257 let result: Result<(ContentType, _), _> = encoded.deserialize();
258 assert!(result.is_err());
259 assert!(
260 result
261 .unwrap_err()
262 .to_string()
263 .contains("Invalid content type")
264 );
265 }
266
267 #[test]
268 fn content_type_from_str() {
269 for content_type in [
270 "text/plain",
271 "text/plain; charset=utf8",
272 "TEXT/PLAIN; CHARSET=UTF-8",
273 ] {
274 assert_eq!(
275 ContentType::from_str(content_type).unwrap(),
276 ContentType::Text
277 );
278 }
279
280 for content_type in [
281 "application/octet-stream",
282 "application/octet-stream; version=1",
283 ] {
284 assert_eq!(
285 ContentType::from_str(content_type).unwrap(),
286 ContentType::Blob
287 );
288 }
289
290 let result = ContentType::from_str("text/html; charset=utf8");
292 assert!(result.is_err());
293 let error = result.unwrap_err();
294 assert!(error.contains("Invalid content type"));
295 assert!(error.contains("text/html; charset=utf8"));
296 }
297
298 #[test]
299 fn invalid_utf8() {
300 let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
302
303 let secret = Secret::with_content_type(ContentType::Text, &invalid_utf8);
305 assert_eq!(secret.content_type(), ContentType::Blob);
306 assert_eq!(&*secret, &[0xFF, 0xFE, 0xFD]);
307
308 let valid_utf8 = "Hello, World!";
310 let secret = Secret::with_content_type(ContentType::Text, valid_utf8.as_bytes());
311 assert_eq!(secret.content_type(), ContentType::Text);
312 assert_eq!(&*secret, valid_utf8.as_bytes());
313
314 let data = vec![1, 2, 3, 4];
316 let secret = Secret::with_content_type(ContentType::Blob, &data);
317 assert_eq!(secret.content_type(), ContentType::Blob);
318 assert_eq!(&*secret, &[1, 2, 3, 4]);
319 }
320
321 #[test]
322 fn random() {
323 let secret1 = Secret::random().unwrap();
324 let secret2 = Secret::random().unwrap();
325
326 assert_eq!(secret1.content_type(), ContentType::Blob);
328 assert_eq!(secret2.content_type(), ContentType::Blob);
329
330 assert_eq!(secret1.as_bytes().len(), 64);
332 assert_eq!(secret2.as_bytes().len(), 64);
333
334 assert_ne!(secret1.as_bytes(), secret2.as_bytes());
336 }
337}