1use std::{2 collections::BTreeMap,3 fmt::{self, Display},4 str::FromStr,5};67use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};8use serde::{Deserialize, Deserializer, Serialize, de::Error};9use unicode_categories::UnicodeCategories;1011#[derive(Debug, PartialEq, Clone)]12pub struct SecretData {13 pub data: Vec<u8>,14 pub encrypted: bool,15}1617const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";18const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";1920const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";21const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2223const SECRET_PREFIX: &str = "<ENCRYPTED>";2425impl<'de> Deserialize<'de> for SecretData {26 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>27 where28 D: Deserializer<'de>,29 {30 let string = String::deserialize(deserializer)?;31 string.parse().map_err(D::Error::custom)32 }33}3435impl Serialize for SecretData {36 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>37 where38 S: serde::Serializer,39 {40 self.to_string().serialize(serializer)41 }42}4344impl FromStr for SecretData {45 type Err = String;4647 fn from_str(string: &str) -> Result<Self, Self::Err> {48 let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {49 (true, unprefixed)50 } else {51 (false, string)52 };53 let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {54 STANDARD_NO_PAD55 .decode(unprefixed.replace(['\n', '\t', ' '], ""))56 .map_err(|e| format!("base64-encoded failed: {e}"))?57 } else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {58 z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))59 .map_err(|e| format!("z85-encoded failed: {e}"))?60 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {61 unprefixed.as_bytes().to_owned()62 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {63 unprefixed.as_bytes().to_owned()64 } else {65 let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");66 return Err(format!(67 "unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"68 ));69 };70 Ok(Self { data, encrypted })71 }72}7374impl Display for SecretData {75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {76 let mut readable = std::str::from_utf8(&self.data).ok();77 if self.encrypted {78 write!(f, "{SECRET_PREFIX}")?;79 80 readable = None;81 }82 if Some(false) == readable.map(is_printable) {83 readable = None84 };85 86 if let Some(plaintext) = readable {87 if plaintext.ends_with('\n') {88 write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;89 } else {90 write!(f, "{PLAINTEXT_PREFIX}")?;91 }92 write!(f, "{plaintext}")?;93 } else {94 write!(f, "{BASE64_ENCODED_PREFIX}")?;95 let encoded = STANDARD_NO_PAD.encode(&self.data);96 for ele in encoded.as_bytes().chunks(64) {97 let chunk = std::str::from_utf8(ele).expect(98 "any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",99 );100 writeln!(f, "{chunk}")?;101 }102 };103 Ok(())104 }105}106107fn is_printable(text: &str) -> bool {108 text.chars().all(|c| {109 c.is_letter()110 || c.is_mark()111 || c.is_number()112 || c.is_punctuation()113 || c.is_separator()114 || c == '\n' || c == '\t'115 116 || c == '/' || c == '+'117 || c == '='118 })119}120121#[test]122fn test() {123 fn check_roundtrip(data: SecretData, expected: &str) {124 let string = data.to_string();125 assert_eq!(string, expected, "unexpected encoding");126 let roundtrip: SecretData = string.parse().expect("roundtrip parse");127 assert_eq!(data, roundtrip, "roundtrip didn't match");128 }129 check_roundtrip(130 SecretData {131 data: vec![1, 2, 3, 4, 5, 6],132 encrypted: false,133 },134 "<BASE64-ENCODED>\nAQIDBAUG\n",135 );136 check_roundtrip(137 SecretData {138 data: vec![1, 2, 3, 4, 5, 6],139 encrypted: true,140 },141 "<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",142 );143 check_roundtrip(144 SecretData {145 data: "Привет, мир!\n".to_owned().into(),146 encrypted: false,147 },148 "<PLAINTEXT-NL>\nПривет, мир!\n",149 );150 check_roundtrip(151 SecretData {152 data: "Привет, мир!".to_owned().into(),153 encrypted: false,154 },155 "<PLAINTEXT>Привет, мир!",156 );157}