prs_lib/
types.rs

1//! Secret plaintext and ciphertext types.
2
3use anyhow::Result;
4use secstr::SecVec;
5use thiserror::Error;
6use zeroize::Zeroize;
7
8/// Delimiter for properties.
9const PROPERTY_DELIMITER: char = ':';
10
11/// Newline character(s) on this platform.
12#[cfg(not(windows))]
13pub const NEWLINE: &str = "\n";
14#[cfg(windows)]
15pub const NEWLINE: &str = "\r\n";
16
17/// Ciphertext.
18///
19/// Wraps ciphertext bytes. This type is limited on purpose, to prevent accidentally leaking the
20/// ciphertext. Security properties are enforced by `secstr::SecVec`.
21pub struct Ciphertext(SecVec<u8>);
22
23impl Ciphertext {
24    /// New empty ciphertext.
25    pub fn empty() -> Self {
26        vec![].into()
27    }
28
29    /// Get unsecure reference to inner data.
30    ///
31    /// # Warning
32    ///
33    /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care!
34    ///
35    /// The reference itself is safe to use and share. Data may be cloned from this reference
36    /// though, when that happens we lose track of it and are unable to securely handle it in
37    /// memory. You should clone `Ciphertext` instead.
38    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        // Explicit zeroing of unsecure buffer required
46        let into = Ciphertext(other.to_vec().into());
47        other.zeroize();
48        into
49    }
50}
51
52/// Plaintext.
53///
54/// Wraps plaintext bytes. This type is limited on purpose, to prevent accidentally leaking the
55/// plaintext. Security properties are enforced by `secstr::SecVec`.
56#[derive(Clone, Eq, PartialEq)]
57pub struct Plaintext(SecVec<u8>);
58
59impl Plaintext {
60    /// New empty plaintext.
61    pub fn empty() -> Self {
62        vec![].into()
63    }
64
65    /// Get unsecure reference to inner data.
66    ///
67    /// # Warning
68    ///
69    /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care!
70    ///
71    /// The reference itself is safe to use and share. Data may be cloned from this reference
72    /// though, when that happens we lose track of it and are unable to securely handle it in
73    /// memory. You should clone `Plaintext` instead.
74    pub fn unsecure_ref(&self) -> &[u8] {
75        self.0.unsecure()
76    }
77
78    /// Get the plaintext as UTF8 string.
79    ///
80    /// # Warning
81    ///
82    /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care!
83    ///
84    /// The reference itself is safe to use and share. Data may be cloned from this reference
85    /// though, when that happens we lose track of it and are unable to securely handle it in
86    /// memory. You should clone `Plaintext` instead.
87    pub fn unsecure_to_str(&self) -> Result<&str, std::str::Utf8Error> {
88        std::str::from_utf8(self.unsecure_ref())
89    }
90
91    /// Get the first line of this secret as plaintext.
92    ///
93    /// Returns empty plaintext if there are no lines.
94    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    /// Get all lines execpt the first one.
106    ///
107    /// Returns empty plaintext if there are no lines.
108    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    /// Get line with the given property.
121    ///
122    /// Returns line with the given property. The property prefix is removed, and only the trimmed
123    /// value is returned. Returns an error if the property does not exist.
124    ///
125    /// This will never return the first line being the password.
126    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    /// Append other plaintext.
144    ///
145    /// Optionally adds platform newline.
146    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    /// Check whether this plaintext is empty.
156    ///
157    /// - Empty if 0 bytes
158    /// - Empty if bytes parsed as UTF-8 has trimmed length of 0 characters (ignored on encoding failure)
159    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        // Explicit zeroing of unsecure buffer required
170        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        // Explicit zeroing of unsecure buffer required
179        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/// A plaintext or ciphertext handling error.
192#[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        // Test empty
214        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        // Test not empty
222        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        // (input, output)
233        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        // (input, output)
258        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        // Append to empty without newline
283        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        // Append to empty with newline
290        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        // Append empty to empty
303        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        // Keep existing newlines
313        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        // Appending lots of random stuff and parsing as string should never fail
326        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        // Never select property from first line, but do from others
335        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        // (input, property to select, output)
350        #[rustfmt::skip]
351        let set = vec![
352            // Nothing/empty
353            ("", "", None),
354
355            // Properties
356            ("\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            // Empty property
363            ("\nEmpty:", "Empty", Some("")),
364            ("\nEmpty:   ", "Empty", Some("")),
365
366            // Missing
367            ("\nName: abc\nMail: abc@example.com", "missing", None),
368
369            // Capitalization
370            ("\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        // Skip all-zero/empty because we cannot reliably test
395        if plaintext.len() < 16 || plaintext.bytes().all(|b| b == 0) {
396            return true;
397        }
398
399        // Create plaintext, remember memory range and data, then drop plaintext
400        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        // Retake same slice of memory that we've dropped
406        let slice: &[u8] = unsafe {
407            std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
408        };
409
410        // Memory must have been explicitly zeroed, it must never be the same as before
411        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        // Skip all-zero/empty because we cannot reliably test
426        if ciphertext.len() < 16 || ciphertext.iter().all(|b| *b == 0) {
427            return true;
428        }
429
430        // Create ciphertext, remember memory range and data, then drop ciphertext
431        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        // Retake same slice of memory that we've dropped
437        let slice: &[u8] = unsafe {
438            std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
439        };
440
441        // Memory must have been explicitly zeroed, it must never be the same as before
442        slice != must_not_match
443    }
444}