use anyhow::Result;
use secstr::SecVec;
use thiserror::Error;
use zeroize::Zeroize;
const PROPERTY_DELIMITER: char = ':';
#[cfg(not(windows))]
pub const NEWLINE: &str = "\n";
#[cfg(windows)]
pub const NEWLINE: &str = "\r\n";
pub struct Ciphertext(SecVec<u8>);
impl Ciphertext {
pub fn empty() -> Self {
vec![].into()
}
pub(crate) fn unsecure_ref(&self) -> &[u8] {
self.0.unsecure()
}
}
impl From<Vec<u8>> for Ciphertext {
fn from(mut other: Vec<u8>) -> Ciphertext {
let into = Ciphertext(other.to_vec().into());
other.zeroize();
into
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Plaintext(SecVec<u8>);
impl Plaintext {
pub fn empty() -> Self {
vec![].into()
}
pub fn unsecure_ref(&self) -> &[u8] {
self.0.unsecure()
}
pub fn unsecure_to_str(&self) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(self.unsecure_ref())
}
pub fn first_line(self) -> Result<Plaintext> {
Ok(self
.unsecure_to_str()
.map_err(Err::Utf8)?
.lines()
.next()
.map(|l| l.as_bytes().into())
.unwrap_or_else(|| vec![])
.into())
}
pub fn except_first_line(self) -> Result<Plaintext> {
Ok(self
.unsecure_to_str()
.map_err(Err::Utf8)?
.lines()
.skip(1)
.collect::<Vec<&str>>()
.join(NEWLINE)
.into_bytes()
.into())
}
pub fn property(self, property: &str) -> Result<Plaintext> {
let property = property.trim().to_uppercase();
self.unsecure_to_str()
.map_err(Err::Utf8)?
.lines()
.skip(1)
.find_map(|line| {
let mut parts = line.splitn(2, PROPERTY_DELIMITER);
if parts.next().unwrap().trim().to_uppercase() == property {
Some(parts.next().map(|value| value.trim()).unwrap_or("").into())
} else {
None
}
})
.ok_or_else(|| Err::Property(property.to_lowercase()).into())
}
pub fn append(&mut self, other: Plaintext, newline: bool) {
let mut data = self.unsecure_ref().to_vec();
if newline {
data.extend_from_slice(NEWLINE.as_bytes());
}
data.extend_from_slice(other.unsecure_ref());
self.0 = data.into();
}
pub fn is_empty(&self) -> bool {
self.unsecure_ref().is_empty()
|| std::str::from_utf8(self.unsecure_ref())
.map(|s| s.trim().is_empty())
.unwrap_or(false)
}
}
impl From<String> for Plaintext {
fn from(mut other: String) -> Plaintext {
let into = Plaintext(other.as_bytes().into());
other.zeroize();
into
}
}
impl From<Vec<u8>> for Plaintext {
fn from(mut other: Vec<u8>) -> Plaintext {
let into = Plaintext(other.to_vec().into());
other.zeroize();
into
}
}
impl From<&str> for Plaintext {
fn from(s: &str) -> Self {
Self(s.as_bytes().into())
}
}
#[derive(Debug, Error)]
pub enum Err {
#[error("failed parse plaintext as UTF-8")]
Utf8(#[source] std::str::Utf8Error),
#[error("property '{}' does not exist in plaintext", _0)]
Property(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plaintext_empty() {
let empty = Plaintext::empty();
assert!(empty.is_empty(), "empty plaintext should be empty");
}
#[test]
fn plaintext_is_empty() {
let mut plaintext = Plaintext::from("");
assert!(plaintext.is_empty(), "empty plaintext should be empty");
assert!(
plaintext.unsecure_ref().is_empty(),
"empty plaintext should be empty"
);
plaintext.append(Plaintext::from("abc"), false);
assert!(!plaintext.is_empty(), "empty plaintext should not be empty");
assert!(
!plaintext.unsecure_ref().is_empty(),
"empty plaintext should not be empty"
);
}
#[test]
fn plaintext_first_line() {
let set = vec![
("", ""),
("\n", ""),
("abc", "abc"),
("abc\n", "abc"),
("abc\ndef\r\nghi", "abc"),
("abc\r\ndef\nghi", "abc"),
];
for (input, output) in set {
assert_eq!(
Plaintext::from(input)
.first_line()
.unwrap()
.unsecure_to_str()
.unwrap(),
output,
"first line of plaintext is incorrect",
);
}
}
#[test]
fn plaintext_except_first_line() {
let set = vec![
("", ""),
("\n", ""),
("abc", ""),
("abc\n", ""),
("abc\ndef\r\nghi", "def\nghi"),
("abc\r\ndef\nghi", "def\nghi"),
];
for (input, output) in set {
assert_eq!(
Plaintext::from(input)
.except_first_line()
.unwrap()
.unsecure_to_str()
.unwrap(),
output,
"first line of plaintext is incorrect",
);
}
}
#[test]
fn plaintext_append() {
let mut plaintext = Plaintext::empty();
plaintext.append(Plaintext::from("abc"), false);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "abc");
plaintext.append(Plaintext::from("def"), false);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "abcdef");
let mut plaintext = Plaintext::empty();
plaintext.append(Plaintext::from("abc"), true);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "\nabc");
plaintext.append(Plaintext::from("def"), true);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "\nabc\ndef");
let mut plaintext = Plaintext::empty();
plaintext.append(Plaintext::empty(), false);
assert!(plaintext.is_empty());
plaintext.append(Plaintext::empty(), true);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "\n");
let mut plaintext = Plaintext::from("\n\n");
plaintext.append(Plaintext::from("\n\n"), false);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "\n\n\n\n");
plaintext.append(Plaintext::from("\n\n"), true);
assert_eq!(plaintext.unsecure_to_str().unwrap(), "\n\n\n\n\n\n\n");
}
#[quickcheck]
fn plaintext_append_string(a: String, b: String, c: String) {
let mut plaintext = Plaintext::from(a);
plaintext.append(Plaintext::from(b), false);
plaintext.append(Plaintext::from(c), true);
plaintext.unsecure_to_str().unwrap();
}
#[test]
fn plaintext_property() {
assert!(
Plaintext::from("Name: abc").property("name").is_err(),
"should never select property from first line"
);
assert_eq!(
Plaintext::from("Name: abc\nName: def")
.property("name")
.unwrap()
.unsecure_to_str()
.unwrap(),
"def",
"should select property value from all but the first line"
);
#[rustfmt::skip]
let set = vec![
("", "", None),
("\nName: abc", "Name", Some("abc")),
("\n Name : abc ", "Name", Some("abc")),
("\nName: abc\nName: def", "Name", Some("abc")),
("\nName: abc\nMail: abc@example.com", "Mail", Some("abc@example.com")),
("\nName: abc\nMail: abc@example.com", "Name", Some("abc")),
("\nEmpty:", "Empty", Some("")),
("\nEmpty: ", "Empty", Some("")),
("\nName: abc\nMail: abc@example.com", "missing", None),
("\nName: abc", "name", Some("abc")),
("\nName: abc", "NAME", Some("abc")),
("\nName: abc", "nAME", Some("abc")),
("\nNAME: abc", "name", Some("abc")),
("\nnAmE: abc", "name", Some("abc")),
("\nNAME: abc\nname: def", "name", Some("abc")),
];
for (input, property, output) in set {
let val = Plaintext::from(input).property(property).ok();
if let Some(output) = output {
assert_eq!(
val.unwrap().unsecure_to_str().unwrap(),
output,
"incorrect property value",
);
} else {
assert!(val.is_none(), "no property should be selected",);
}
}
}
#[quickcheck]
fn plaintext_must_zero_on_drop(plaintext: String) -> bool {
if plaintext.len() < 16 || plaintext.bytes().all(|b| b == 0) {
return true;
}
let plaintext = Plaintext::from(plaintext);
let must_not_match = plaintext.0.unsecure().to_vec();
let range = plaintext.0.unsecure().as_ptr_range();
drop(plaintext);
let slice: &[u8] = unsafe {
std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
};
slice != &must_not_match
}
#[test]
fn ciphertext_empty() {
let empty = Ciphertext::empty();
assert!(
empty.unsecure_ref().is_empty(),
"empty ciphertext should be empty"
);
}
#[quickcheck]
fn ciphertext_must_zero_on_drop(ciphertext: Vec<u8>) -> bool {
if ciphertext.len() < 16 || ciphertext.iter().all(|b| *b == 0) {
return true;
}
let ciphertext = Ciphertext::from(ciphertext);
let must_not_match = ciphertext.0.unsecure().to_vec();
let range = ciphertext.0.unsecure().as_ptr_range();
drop(ciphertext);
let slice: &[u8] = unsafe {
std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize)
};
slice != &must_not_match
}
}