1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use log::{error, warn};5use nix::fcntl::{renameat2, RenameFlags};6use nix::sys::stat::Mode;7use nix::unistd::{chown, Group, User};8use serde::{Deserialize, Deserializer};9use std::fs::{self, DirBuilder, File};10use std::io::{self, Cursor, Read, Write};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::str::from_utf8;14use std::{15 collections::HashMap,16 os::unix::fs::DirBuilderExt,17 path::{Path, PathBuf},18};1920#[derive(Parser)]21#[clap(author)]22struct Opts {23 data: PathBuf,24}2526#[derive(Deserialize)]27#[serde(rename_all = "camelCase")]28struct DataItem {29 group: String,30 mode: String,31 owner: String,3233 #[serde(deserialize_with = "from_z85")]34 secret: Option<Vec<u8>>,35 public: Option<String>,3637 public_path: PathBuf,38 stable_public_path: PathBuf,3940 secret_path: PathBuf,41 stable_secret_path: PathBuf,42}4344fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>45where46 D: Deserializer<'de>,47{48 use serde::de::Error;49 if let Some(v) = <Option<String>>::deserialize(deserializer)? {50 Ok(Some(51 z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,52 ))53 } else {54 Ok(None)55 }56}5758type Data = HashMap<String, DataItem>;5960fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {61 if let Some(public) = &value.public {62 let mut hashed = File::create(&value.public_path)?;63 let mut stable_dir = value.stable_public_path.parent().expect("not root");64 let mut stable_temp =65 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;66 hashed.write_all(public.as_bytes())?;67 stable_temp.write_all(public.as_bytes())?;68 stable_temp.flush()?;69 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))70 .context("perm")?;71 fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))72 .context("perm")?;7374 stable_temp75 .persist(value.stable_public_path)76 .context("failed to persist")?;77 }78 if value.secret.is_none() {79 return Ok(());80 }81 let secret = value.secret.as_ref().unwrap();8283 let mode = Mode::from_bits(84 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,85 )86 .context("failed to parse mode")?;87 let user = User::from_name(&value.owner)88 .context("failed to get user")?89 .ok_or_else(|| anyhow!("user not found"))?;90 let group = Group::from_name(&value.group)91 .context("failed to get group")?92 .ok_or_else(|| anyhow!("group not found"))?;9394 let mut stable_dir = value.stable_secret_path.parent().expect("not root");95 let mut stable_temp =96 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;97 let mut hashed = File::create(&value.secret_path)?;9899 100 let decrypted = {101 let mut input = Cursor::new(&secret);102 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;103 let decryptor = match decryptor {104 Decryptor::Recipients(r) => r,105 Decryptor::Passphrase(_) => bail!("should be recipients"),106 };107 let mut decryptor = decryptor108 .decrypt(iter::once(identity as &dyn age::Identity))109 .context("failed to decrypt, wrong key?")?;110111 let mut decrypted = Vec::new();112 decryptor113 .read_to_end(&mut decrypted)114 .context("failed to decrypt")?;115 decrypted116 };117118 io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)119 .context("failed to write decrypted file")?;120 io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;121122 123 chown(stable_temp.path(), Some(user.uid), Some(group.gid))124 .context("failed to apply user/group")?;125 chown(&value.secret_path, Some(user.uid), Some(group.gid))126 .context("failed to apply user/group")?;127 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();128 fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();129 stable_temp130 .persist(value.stable_secret_path)131 .context("failed to persist")?;132133 Ok(())134}135136fn main() -> anyhow::Result<()> {137 env_logger::Builder::new()138 .filter_level(log::LevelFilter::Info)139 .init();140141 let opts = Opts::parse();142 let data = fs::read(&opts.data).context("failed to read secrets data")?;143 let data_str = from_utf8(&data).context("failed to read data to string")?;144 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;145146 if !fs::metadata("/run/secrets")147 .map(|m| m.is_dir())148 .unwrap_or(false)149 {150 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;151 }152153 let identity = age::ssh::Identity::from_buffer(154 &mut Cursor::new(155 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,156 ),157 None,158 )159 .context("failed to parse identity")?;160161 let mut failed = false;162 for (name, value) in data {163 if let Err(e) = init_secret(&identity, value) {164 error!(165 "{:?}",166 e.context(format!("failed to initialize secret {}", name))167 );168 failed = true;169 }170 }171 if failed {172 bail!("one or more secrets failed");173 }174175 Ok(())176}