git.delta.rocks / jrsonnet / refs/commits / 837e795f702e

difftreelog

source

cmds/install-secrets/src/main.rs7.6 KiBsourcehistory
1use age::{ssh::Identity as SshIdentity, ssh::Recipient as SshRecipient, Decryptor};2use age::{Encryptor, Identity, Recipient};3use anyhow::{anyhow, bail, Context, Result};4use clap::Parser;5use log::{error, info, warn};6use nix::sys::stat::Mode;7use nix::unistd::{chown, Group, User};8use serde::{Deserialize, Deserializer};9use std::fmt::{self, Display};10use std::fs::{self, File};11use std::io::{self, Cursor, Read, Write};12use std::iter;13use std::os::unix::prelude::PermissionsExt;14use std::path::Path;15use std::str::{from_utf8, FromStr};16use std::{collections::HashMap, path::PathBuf};1718#[derive(Clone, Debug)]19struct SecretWrapper(Vec<u8>);20impl Display for SecretWrapper {21	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {22		let encoded = z85::encode(&self.0);23		write!(f, "{encoded}")24	}25}26impl FromStr for SecretWrapper {27	type Err = z85::DecodeError;2829	fn from_str(s: &str) -> Result<Self, Self::Err> {30		z85::decode(s).map(Self)31	}32}33impl<'de> Deserialize<'de> for SecretWrapper {34	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>35	where36		D: Deserializer<'de>,37	{38		let v = String::deserialize(deserializer)?;39		let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;40		Ok(Self(de))41	}42}4344#[derive(Parser)]45#[clap(author)]46enum Opts {47	/// Install secrets from json specification48	Install { data: PathBuf },49	/// Reencrypt secret using host key, outputting in z85 encoded string50	Reencrypt {51		#[clap(long)]52		secret: SecretWrapper,53		#[clap(long)]54		targets: Vec<String>,55	},56	/// Decrypt secret using host key, outputting in z85 encoded string57	Decrypt {58		#[clap(long)]59		secret: SecretWrapper,60		/// Shoult decoded output be printed as plaintext, instead of z85?61		#[clap(long)]62		plaintext: bool,63	},64}6566#[derive(Deserialize)]67#[serde(rename_all = "camelCase")]68struct DataItem {69	group: String,70	mode: String,71	owner: String,7273	secret: Option<SecretWrapper>,74	public: Option<String>,7576	public_path: PathBuf,77	stable_public_path: PathBuf,7879	secret_path: PathBuf,80	stable_secret_path: PathBuf,81}8283type Data = HashMap<String, DataItem>;8485fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {86	let mut input = Cursor::new(&input.0);87	let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;88	let decryptor = match decryptor {89		Decryptor::Recipients(r) => r,90		Decryptor::Passphrase(_) => bail!("should be recipients"),91	};92	let mut decryptor = decryptor93		.decrypt(iter::once(identity as &dyn age::Identity))94		.context("failed to decrypt, wrong key?")?;9596	let mut decrypted = Vec::new();97	decryptor98		.read_to_end(&mut decrypted)99		.context("failed to decrypt")?;100	Ok(decrypted)101}102fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {103	let recipients = targets104		.into_iter()105		.map(|t| {106			SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))107		})108		.collect::<Result<Vec<SshRecipient>>>()?;109	let recipients = recipients110		.into_iter()111		.map(|v| Box::new(v) as Box<dyn Recipient + Send>)112		.collect::<Vec<_>>();113	let mut encrypted = vec![];114	let mut encryptor = Encryptor::with_recipients(recipients)115		.expect("recipients provided")116		.wrap_output(&mut encrypted)117		.expect("constructor should not fail");118	io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");119	encryptor.finish().context("failed to finish encryption")?;120	Ok(SecretWrapper(encrypted))121}122123fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {124	if let Some(public) = &value.public {125		let mut hashed = File::create(&value.public_path)?;126		let stable_dir = value.stable_public_path.parent().expect("not root");127		let mut stable_temp =128			tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;129		hashed.write_all(public.as_bytes())?;130		stable_temp.write_all(public.as_bytes())?;131		stable_temp.flush()?;132		fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))133			.context("perm")?;134		fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))135			.context("perm")?;136137		stable_temp138			.persist(value.stable_public_path)139			.context("failed to persist")?;140	}141	if value.secret.is_none() {142		info!("no secret data found");143		return Ok(());144	}145	let secret = value.secret.as_ref().unwrap();146147	let mode = Mode::from_bits(148		u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,149	)150	.context("failed to parse mode")?;151	let user = User::from_name(&value.owner)152		.context("failed to get user")?153		.ok_or_else(|| anyhow!("user not found"))?;154	let group = Group::from_name(&value.group)155		.context("failed to get group")?156		.ok_or_else(|| anyhow!("group not found"))?;157158	let stable_dir = value.stable_secret_path.parent().expect("not root");159	let mut stable_temp =160		tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;161	let mut hashed = File::create(&value.secret_path)?;162163	// File is owned by root, and only root can modify it164	let decrypted = decrypt(&secret, identity)?;165	if decrypted.is_empty() {166		warn!("secret is decoded as empty, something is broken?");167	}168169	io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)170		.context("failed to write decrypted file")?;171	io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;172173	// Make file owned by specified user and group, then change mode174	chown(stable_temp.path(), Some(user.uid), Some(group.gid))175		.context("failed to apply user/group")?;176	chown(&value.secret_path, Some(user.uid), Some(group.gid))177		.context("failed to apply user/group")?;178	fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();179	fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();180	stable_temp181		.persist(value.stable_secret_path)182		.context("failed to persist")?;183184	Ok(())185}186187fn host_identity() -> anyhow::Result<SshIdentity> {188	let identity = SshIdentity::from_buffer(189		&mut Cursor::new(190			fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,191		),192		None,193	)194	.context("failed to parse identity")?;195	Ok(identity)196}197198fn install(data: &Path) -> anyhow::Result<()> {199	let data = fs::read(data).context("failed to read secrets data")?;200	let data_str = from_utf8(&data).context("failed to read data to string")?;201	let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;202203	if !fs::metadata("/run/secrets")204		.map(|m| m.is_dir())205		.unwrap_or(false)206	{207		fs::create_dir("/run/secrets").context("failed to create secrets directory")?;208	}209210	let identity = host_identity()?;211212	let mut failed = false;213	for (name, value) in data {214		info!("initializing secret {name}");215		if let Err(e) = init_secret(&identity, value) {216			error!(217				"{:?}",218				e.context(format!("failed to initialize secret {}", name))219			);220			failed = true;221		}222	}223	if failed {224		bail!("one or more secrets failed");225	}226227	Ok(())228}229230fn main() -> anyhow::Result<()> {231	env_logger::Builder::new()232		.filter_level(log::LevelFilter::Info)233		.init();234235	let opts = Opts::parse();236237	match opts {238		Opts::Install { data } => install(&data),239		Opts::Reencrypt { secret, targets } => {240			let identity = host_identity()?;241			let decrypted = decrypt(&secret, &identity).context("during decryption")?;242			let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;243244			println!("{encrypted}");245			Ok(())246		}247		Opts::Decrypt { secret, plaintext } => {248			let identity = host_identity()?;249			let decrypted = decrypt(&secret, &identity).context("during decryption")?;250251			if plaintext {252				let s = String::from_utf8(decrypted).context("output is not utf8")?;253				print!("{}", s);254			} else {255				println!("{}", SecretWrapper(decrypted));256			}257			Ok(())258		}259	}260}