git.delta.rocks / jrsonnet / refs/heads / trunk

difftreelog

source

crates/fleet-shared/src/encoding.rs3.9 KiBsourcehistory
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";17// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.18const 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			// Always base64-encode encrypted fields.72			readable = None;73		}74		if Some(false) == readable.map(is_printable) {75			readable = None76		};77		// TODO: Check if text is readable, and has no unprintable characters?..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			// Complete base64 alphabet108			|| 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}