1use std::{2 fmt::{self, Display},3 str::FromStr,4};56use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};7use serde::{Deserialize, Deserializer, Serialize, de::Error};8use unicode_categories::UnicodeCategories;910#[derive(Debug, PartialEq, Clone)]11pub struct SecretData {12 pub data: Vec<u8>,13 pub encrypted: bool,14}1516const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";1718const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";19const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2021const SECRET_PREFIX: &str = "<ENCRYPTED>";2223impl<'de> Deserialize<'de> for SecretData {24 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>25 where26 D: Deserializer<'de>,27 {28 let string = String::deserialize(deserializer)?;29 string.parse().map_err(D::Error::custom)30 }31}3233impl Serialize for SecretData {34 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>35 where36 S: serde::Serializer,37 {38 self.to_string().serialize(serializer)39 }40}4142impl FromStr for SecretData {43 type Err = String;4445 fn from_str(string: &str) -> Result<Self, Self::Err> {46 let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {47 (true, unprefixed)48 } else {49 (false, string)50 };51 let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {52 STANDARD_NO_PAD53 .decode(unprefixed.replace(['\n', '\t', ' '], ""))54 .map_err(|e| format!("base64-encoded failed: {e}"))?55 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {56 unprefixed.as_bytes().to_owned()57 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {58 unprefixed.as_bytes().to_owned()59 } else {60 return Err(format!("unknown secret encoding"));61 };62 Ok(Self { data, encrypted })63 }64}6566impl Display for SecretData {67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {68 let mut readable = std::str::from_utf8(&self.data).ok();69 if self.encrypted {70 write!(f, "{SECRET_PREFIX}")?;71 72 readable = None;73 }74 if Some(false) == readable.map(is_printable) {75 readable = None76 };77 78 if let Some(plaintext) = readable {79 if plaintext.ends_with('\n') {80 write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;81 } else {82 write!(f, "{PLAINTEXT_PREFIX}")?;83 }84 write!(f, "{plaintext}")?;85 } else {86 write!(f, "{BASE64_ENCODED_PREFIX}")?;87 let encoded = STANDARD_NO_PAD.encode(&self.data);88 for ele in encoded.as_bytes().chunks(64) {89 let chunk = std::str::from_utf8(ele).expect(90 "any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",91 );92 writeln!(f, "{chunk}")?;93 }94 };95 Ok(())96 }97}9899fn is_printable(text: &str) -> bool {100 text.chars().all(|c| {101 c.is_letter()102 || c.is_mark()103 || c.is_number()104 || c.is_punctuation()105 || c.is_separator()106 || c == '\n' || c == '\t'107 108 || c == '/' || c == '+'109 || c == '='110 })111}112113#[test]114fn test() {115 fn check_roundtrip(data: SecretData, expected: &str) {116 let string = data.to_string();117 assert_eq!(string, expected, "unexpected encoding");118 let roundtrip: SecretData = string.parse().expect("roundtrip parse");119 assert_eq!(data, roundtrip, "roundtrip didn't match");120 }121 check_roundtrip(122 SecretData {123 data: vec![1, 2, 3, 4, 5, 6],124 encrypted: false,125 },126 "<BASE64-ENCODED>\nAQIDBAUG\n",127 );128 check_roundtrip(129 SecretData {130 data: vec![1, 2, 3, 4, 5, 6],131 encrypted: true,132 },133 "<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",134 );135 check_roundtrip(136 SecretData {137 data: "Привет, мир!\n".to_owned().into(),138 encrypted: false,139 },140 "<PLAINTEXT-NL>\nПривет, мир!\n",141 );142 check_roundtrip(143 SecretData {144 data: "Привет, мир!".to_owned().into(),145 encrypted: false,146 },147 "<PLAINTEXT>Привет, мир!",148 );149}