1use anyhow::Result;
4use secstr::SecVec;
5use thiserror::Error;
6use zeroize::Zeroize;
7
8const PROPERTY_DELIMITER: char = ':';
10
11#[cfg(not(windows))]
13pub const NEWLINE: &str = "\n";
14#[cfg(windows)]
15pub const NEWLINE: &str = "\r\n";
16
17pub struct Ciphertext(SecVec<u8>);
22
23impl Ciphertext {
24 pub fn empty() -> Self {
26 vec![].into()
27 }
28
29 pub(crate) fn unsecure_ref(&self) -> &[u8] {
39 self.0.unsecure()
40 }
41}
42
43impl From<Vec<u8>> for Ciphertext {
44 fn from(mut other: Vec<u8>) -> Ciphertext {
45 let into = Ciphertext(other.to_vec().into());
47 other.zeroize();
48 into
49 }
50}
51
52#[derive(Clone, Eq, PartialEq)]
57pub struct Plaintext(SecVec<u8>);
58
59impl Plaintext {
60 pub fn empty() -> Self {
62 vec![].into()
63 }
64
65 pub fn unsecure_ref(&self) -> &[u8] {
75 self.0.unsecure()
76 }
77
78 pub fn unsecure_to_str(&self) -> Result<&str, std::str::Utf8Error> {
88 std::str::from_utf8(self.unsecure_ref())
89 }
90
91 pub fn first_line(&self) -> Result<Plaintext> {
95 Ok(self
96 .unsecure_to_str()
97 .map_err(Err::Utf8)?
98 .lines()
99 .next()
100 .map(|l| l.as_bytes().into())
101 .unwrap_or_else(Vec::new)
102 .into())
103 }
104
105 pub fn except_first_line(&self) -> Result<Plaintext> {
109 Ok(self
110 .unsecure_to_str()
111 .map_err(Err::Utf8)?
112 .lines()
113 .skip(1)
114 .collect::<Vec<&str>>()
115 .join(NEWLINE)
116 .into_bytes()
117 .into())
118 }
119
120 pub fn property(&self, property: &str) -> Result<Plaintext> {
127 let property = property.trim().to_uppercase();
128 self.unsecure_to_str()
129 .map_err(Err::Utf8)?
130 .lines()
131 .skip(1)
132 .find_map(|line| {
133 let mut parts = line.splitn(2, PROPERTY_DELIMITER);
134 if parts.next().unwrap().trim().to_uppercase() == property {
135 Some(parts.next().map(|value| value.trim()).unwrap_or("").into())
136 } else {
137 None
138 }
139 })
140 .ok_or_else(|| Err::Property(property.to_lowercase()).into())
141 }
142
143 pub fn append(&mut self, other: Plaintext, newline: bool) {
147 let mut data = self.unsecure_ref().to_vec();
148 if newline {
149 data.extend_from_slice(NEWLINE.as_bytes());
150 }
151 data.extend_from_slice(other.unsecure_ref());
152 self.0 = data.into();
153 }
154
155 pub fn is_empty(&self) -> bool {
160 self.unsecure_ref().is_empty()
161 || std::str::from_utf8(self.unsecure_ref())
162 .map(|s| s.trim().is_empty())
163 .unwrap_or(false)
164 }
165}
166
167impl From<String> for Plaintext {
168 fn from(mut other: String) -> Plaintext {
169 let into = Plaintext(other.as_bytes().into());
171 other.zeroize();
172 into
173 }
174}
175
176impl From<Vec<u8>> for Plaintext {
177 fn from(mut other: Vec<u8>) -> Plaintext {
178 let into = Plaintext(other.to_vec().into());
180 other.zeroize();
181 into
182 }
183}
184
185impl From<&str> for Plaintext {
186 fn from(s: &str) -> Self {
187 Self(s.as_bytes().into())
188 }
189}
190
191#[derive(Debug, Error)]
193pub enum Err {
194 #[error("failed parse plaintext as UTF-8")]
195 Utf8(#[source] std::str::Utf8Error),
196
197 #[error("property '{}' does not exist in plaintext", _0)]
198 Property(String),
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn plaintext_empty() {
207 let empty = Plaintext::empty();
208 assert!(empty.is_empty(), "empty plaintext should be empty");
209 }
210
211 #[test]
212 fn plaintext_is_empty() {
213 let mut plaintext = Plaintext::from("");
215 assert!(plaintext.is_empty(), "empty plaintext should be empty");
216 assert!(
217 plaintext.unsecure_ref().is_empty(),
218 "empty plaintext should be empty"
219 );
220
221 plaintext.append(Plaintext::from("abc"), false);
223 assert!(!plaintext.is_empty(), "empty plaintext should not be empty");
224 assert!(
225 !plaintext.unsecure_ref().is_empty(),
226 "empty plaintext should not be empty"
227 );
228 }
229
230 #[test]
231 fn plaintext_first_line() {
232 let set = vec![
234 ("", ""),
235 ("\n", ""),
236 ("abc", "abc"),
237 ("abc\n", "abc"),
238 ("abc\ndef\r\nghi", "abc"),
239 ("abc\r\ndef\nghi", "abc"),
240 ];
241
242 for (input, output) in set {
243 assert_eq!(
244 Plaintext::from(input)
245 .first_line()
246 .unwrap()
247 .unsecure_to_str()
248 .unwrap(),
249 output,
250 "first line of plaintext is incorrect",
251 );
252 }
253 }
254
255 #[test]
256 fn plaintext_except_first_line() {
257 let set = vec![
259 ("", ""),
260 ("\n", ""),
261 ("abc", ""),
262 ("abc\n", ""),
263 ("abc\ndef\r\nghi", "def\nghi"),
264 ("abc\r\ndef\nghi", "def\nghi"),
265 ];
266
267 for (input, output) in set {
268 assert_eq!(
269 Plaintext::from(input)
270 .except_first_line()
271 .unwrap()
272 .unsecure_to_str()
273 .unwrap(),
274 output,
275 "first line of plaintext is incorrect",
276 );
277 }
278 }
279
280 #[test]
281 fn plaintext_append() {
282 let mut plaintext = Plaintext::empty();
284 plaintext.append(Plaintext::from("abc"), false);
285 assert_eq!(plaintext.unsecure_to_str().unwrap(), "abc");
286 plaintext.append(Plaintext::from("def"), false);
287 assert_eq!(plaintext.unsecure_to_str().unwrap(), "abcdef");
288
289 let mut plaintext = Plaintext::empty();
291 plaintext.append(Plaintext::from("abc"), true);
292 assert_eq!(
293 plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"),
294 "\nabc"
295 );
296 plaintext.append(Plaintext::from("def"), true);
297 assert_eq!(
298 plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"),
299 "\nabc\ndef"
300 );
301
302 let mut plaintext = Plaintext::empty();
304 plaintext.append(Plaintext::empty(), false);
305 assert!(plaintext.is_empty());
306 plaintext.append(Plaintext::empty(), true);
307 assert_eq!(
308 plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"),
309 "\n"
310 );
311
312 let mut plaintext = Plaintext::from("\n\n");
314 plaintext.append(Plaintext::from("\n\n"), false);
315 assert_eq!(plaintext.unsecure_to_str().unwrap(), "\n\n\n\n");
316 plaintext.append(Plaintext::from("\n\n"), true);
317 assert_eq!(
318 plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"),
319 "\n\n\n\n\n\n\n"
320 );
321 }
322
323 #[quickcheck]
324 fn plaintext_append_string(a: String, b: String, c: String) {
325 let mut plaintext = Plaintext::from(a);
327 plaintext.append(Plaintext::from(b), false);
328 plaintext.append(Plaintext::from(c), true);
329 plaintext.unsecure_to_str().unwrap();
330 }
331
332 #[test]
333 fn plaintext_property() {
334 assert!(
336 Plaintext::from("Name: abc").property("name").is_err(),
337 "should never select property from first line"
338 );
339 assert_eq!(
340 Plaintext::from("Name: abc\nName: def")
341 .property("name")
342 .unwrap()
343 .unsecure_to_str()
344 .unwrap(),
345 "def",
346 "should select property value from all but the first line"
347 );
348
349 #[rustfmt::skip]
351 let set = vec![
352 ("", "", None),
354
355 ("\nName: abc", "Name", Some("abc")),
357 ("\n Name : abc ", "Name", Some("abc")),
358 ("\nName: abc\nName: def", "Name", Some("abc")),
359 ("\nName: abc\nMail: abc@example.com", "Mail", Some("abc@example.com")),
360 ("\nName: abc\nMail: abc@example.com", "Name", Some("abc")),
361
362 ("\nEmpty:", "Empty", Some("")),
364 ("\nEmpty: ", "Empty", Some("")),
365
366 ("\nName: abc\nMail: abc@example.com", "missing", None),
368
369 ("\nName: abc", "name", Some("abc")),
371 ("\nName: abc", "NAME", Some("abc")),
372 ("\nName: abc", "nAME", Some("abc")),
373 ("\nNAME: abc", "name", Some("abc")),
374 ("\nnAmE: abc", "name", Some("abc")),
375 ("\nNAME: abc\nname: def", "name", Some("abc")),
376 ];
377
378 for (input, property, output) in set {
379 let val = Plaintext::from(input).property(property).ok();
380 if let Some(output) = output {
381 assert_eq!(
382 val.unwrap().unsecure_to_str().unwrap(),
383 output,
384 "incorrect property value",
385 );
386 } else {
387 assert!(val.is_none(), "no property should be selected",);
388 }
389 }
390 }
391
392 #[quickcheck]
393 fn plaintext_must_zero_on_drop(plaintext: String) -> bool {
394 if plaintext.len() < 16 || plaintext.bytes().all(|b| b == 0) {
396 return true;
397 }
398
399 let plaintext = Plaintext::from(plaintext);
401 let must_not_match = plaintext.0.unsecure().to_vec();
402 let range = plaintext.0.unsecure().as_ptr_range();
403 drop(plaintext);
404
405 let slice: &[u8] = unsafe {
407 std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
408 };
409
410 slice != must_not_match
412 }
413
414 #[test]
415 fn ciphertext_empty() {
416 let empty = Ciphertext::empty();
417 assert!(
418 empty.unsecure_ref().is_empty(),
419 "empty ciphertext should be empty"
420 );
421 }
422
423 #[quickcheck]
424 fn ciphertext_must_zero_on_drop(ciphertext: Vec<u8>) -> bool {
425 if ciphertext.len() < 16 || ciphertext.iter().all(|b| *b == 0) {
427 return true;
428 }
429
430 let ciphertext = Ciphertext::from(ciphertext);
432 let must_not_match = ciphertext.0.unsecure().to_vec();
433 let range = ciphertext.0.unsecure().as_ptr_range();
434 drop(ciphertext);
435
436 let slice: &[u8] = unsafe {
438 std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
439 };
440
441 slice != must_not_match
443 }
444}