1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use log::{error, info, warn};5use nix::sys::stat::Mode;6use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};8use std::fs::{self, File};9use std::io::{self, Cursor, Read, Write};10use std::iter;11use std::os::unix::prelude::PermissionsExt;12use std::str::from_utf8;13use std::{collections::HashMap, path::PathBuf};1415#[derive(Parser)]16#[clap(author)]17struct Opts {18 data: PathBuf,19}2021#[derive(Deserialize)]22#[serde(rename_all = "camelCase")]23struct DataItem {24 group: String,25 mode: String,26 owner: String,2728 #[serde(deserialize_with = "from_z85")]29 secret: Option<Vec<u8>>,30 public: Option<String>,3132 public_path: PathBuf,33 stable_public_path: PathBuf,3435 secret_path: PathBuf,36 stable_secret_path: PathBuf,37}3839fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>40where41 D: Deserializer<'de>,42{43 use serde::de::Error;44 if let Some(v) = <Option<String>>::deserialize(deserializer)? {45 Ok(Some(46 z85::decode(v).map_err(|err| Error::custom(err.to_string()))?,47 ))48 } else {49 Ok(None)50 }51}5253type Data = HashMap<String, DataItem>;5455fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {56 if let Some(public) = &value.public {57 let mut hashed = File::create(&value.public_path)?;58 let stable_dir = value.stable_public_path.parent().expect("not root");59 let mut stable_temp =60 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;61 hashed.write_all(public.as_bytes())?;62 stable_temp.write_all(public.as_bytes())?;63 stable_temp.flush()?;64 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))65 .context("perm")?;66 fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))67 .context("perm")?;6869 stable_temp70 .persist(value.stable_public_path)71 .context("failed to persist")?;72 }73 if value.secret.is_none() {74 info!("no secret data found");75 return Ok(());76 }77 let secret = value.secret.as_ref().unwrap();7879 let mode = Mode::from_bits(80 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,81 )82 .context("failed to parse mode")?;83 let user = User::from_name(&value.owner)84 .context("failed to get user")?85 .ok_or_else(|| anyhow!("user not found"))?;86 let group = Group::from_name(&value.group)87 .context("failed to get group")?88 .ok_or_else(|| anyhow!("group not found"))?;8990 let stable_dir = value.stable_secret_path.parent().expect("not root");91 let mut stable_temp =92 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;93 let mut hashed = File::create(&value.secret_path)?;9495 96 let decrypted = {97 let mut input = Cursor::new(&secret);98 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;99 let decryptor = match decryptor {100 Decryptor::Recipients(r) => r,101 Decryptor::Passphrase(_) => bail!("should be recipients"),102 };103 let mut decryptor = decryptor104 .decrypt(iter::once(identity as &dyn age::Identity))105 .context("failed to decrypt, wrong key?")?;106107 let mut decrypted = Vec::new();108 decryptor109 .read_to_end(&mut decrypted)110 .context("failed to decrypt")?;111 decrypted112 };113 if decrypted.is_empty() {114 warn!("secret is decoded as empty, something is broken?");115 }116117 io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)118 .context("failed to write decrypted file")?;119 io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;120121 122 chown(stable_temp.path(), Some(user.uid), Some(group.gid))123 .context("failed to apply user/group")?;124 chown(&value.secret_path, Some(user.uid), Some(group.gid))125 .context("failed to apply user/group")?;126 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();127 fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();128 stable_temp129 .persist(value.stable_secret_path)130 .context("failed to persist")?;131132 Ok(())133}134135fn main() -> anyhow::Result<()> {136 env_logger::Builder::new()137 .filter_level(log::LevelFilter::Info)138 .init();139140 let opts = Opts::parse();141 let data = fs::read(&opts.data).context("failed to read secrets data")?;142 let data_str = from_utf8(&data).context("failed to read data to string")?;143 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;144145 if !fs::metadata("/run/secrets")146 .map(|m| m.is_dir())147 .unwrap_or(false)148 {149 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;150 }151152 let identity = age::ssh::Identity::from_buffer(153 &mut Cursor::new(154 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,155 ),156 None,157 )158 .context("failed to parse identity")?;159160 let mut failed = false;161 for (name, value) in data {162 info!("initializing secret {name}");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}