git.delta.rocks / jrsonnet / refs/commits / 8fa5c73b5fe4

difftreelog

source

crates/fleet-shared/src/encoding.rs4.4 KiBsourcehistory
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";19// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.20const 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			// Always base64-encode encrypted fields.80			readable = None;81		}82		if Some(false) == readable.map(is_printable) {83			readable = None84		};85		// TODO: Check if text is readable, and has no unprintable characters?..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			// Complete base64 alphabet116			|| 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}