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 nix::sys::stat::Mode;6use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};8use std::fmt::{self, Display};9use std::fs::{self, File};10use std::io::{self, Cursor, Read, Write};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::path::Path;14use std::str::{from_utf8, FromStr};15use std::{collections::HashMap, path::PathBuf};16use tracing::{error, info, info_span, warn};17use tracing_subscriber::filter::LevelFilter;18use tracing_subscriber::EnvFilter;1920#[derive(Clone, Debug)]21struct SecretWrapper(Vec<u8>);22impl Display for SecretWrapper {23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {24 let encoded = z85::encode(&self.0);25 write!(f, "{encoded}")26 }27}28impl FromStr for SecretWrapper {29 type Err = z85::DecodeError;3031 fn from_str(s: &str) -> Result<Self, Self::Err> {32 z85::decode(s).map(Self)33 }34}35impl<'de> Deserialize<'de> for SecretWrapper {36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>37 where38 D: Deserializer<'de>,39 {40 let v = String::deserialize(deserializer)?;41 let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;42 Ok(Self(de))43 }44}4546#[derive(Parser)]47#[clap(author)]48enum Opts {49 50 Install { data: PathBuf },51 52 Reencrypt {53 #[clap(long)]54 secret: SecretWrapper,55 #[clap(long)]56 targets: Vec<String>,57 },58 59 Decrypt {60 #[clap(long)]61 secret: SecretWrapper,62 63 #[clap(long)]64 plaintext: bool,65 },66}6768#[derive(Deserialize)]69#[serde(rename_all = "camelCase")]70struct DataItem {71 group: String,72 mode: String,73 owner: String,7475 secret: Option<SecretWrapper>,76 public: Option<String>,7778 public_path: PathBuf,79 stable_public_path: PathBuf,8081 secret_path: PathBuf,82 stable_secret_path: PathBuf,83}8485type Data = HashMap<String, DataItem>;8687fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {88 let mut input = Cursor::new(&input.0);89 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;90 let decryptor = match decryptor {91 Decryptor::Recipients(r) => r,92 Decryptor::Passphrase(_) => bail!("should be recipients"),93 };94 let mut decryptor = decryptor95 .decrypt(iter::once(identity as &dyn age::Identity))96 .context("failed to decrypt, wrong key?")?;9798 let mut decrypted = Vec::new();99 decryptor100 .read_to_end(&mut decrypted)101 .context("failed to decrypt")?;102 Ok(decrypted)103}104fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {105 let recipients = targets106 .into_iter()107 .map(|t| {108 SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))109 })110 .collect::<Result<Vec<SshRecipient>>>()?;111 let recipients = recipients112 .into_iter()113 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)114 .collect::<Vec<_>>();115 let mut encrypted = vec![];116 let mut encryptor = Encryptor::with_recipients(recipients)117 .expect("recipients provided")118 .wrap_output(&mut encrypted)119 .expect("constructor should not fail");120 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");121 encryptor.finish().context("failed to finish encryption")?;122 Ok(SecretWrapper(encrypted))123}124125fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {126 if let Some(public) = &value.public {127 let mut hashed = File::create(&value.public_path)?;128 let stable_dir = value.stable_public_path.parent().expect("not root");129 let mut stable_temp =130 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;131 hashed.write_all(public.as_bytes())?;132 stable_temp.write_all(public.as_bytes())?;133 stable_temp.flush()?;134 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))135 .context("perm")?;136 fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))137 .context("perm")?;138139 stable_temp140 .persist(value.stable_public_path)141 .context("failed to persist")?;142 }143 if value.secret.is_none() {144 info!("no secret data found");145 return Ok(());146 }147 let secret = value.secret.as_ref().unwrap();148149 let mode = Mode::from_bits(150 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,151 )152 .context("failed to parse mode")?;153 let user = User::from_name(&value.owner)154 .context("failed to get user")?155 .ok_or_else(|| anyhow!("user not found"))?;156 let group = Group::from_name(&value.group)157 .context("failed to get group")?158 .ok_or_else(|| anyhow!("group not found"))?;159160 let stable_dir = value.stable_secret_path.parent().expect("not root");161 let mut stable_temp =162 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;163 let mut hashed = File::create(&value.secret_path)?;164165 166 let decrypted = decrypt(secret, identity)?;167 if decrypted.is_empty() {168 warn!("secret is decoded as empty, something is broken?");169 }170171 io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)172 .context("failed to write decrypted file")?;173 io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;174175 176 chown(stable_temp.path(), Some(user.uid), Some(group.gid))177 .context("failed to apply user/group")?;178 chown(&value.secret_path, Some(user.uid), Some(group.gid))179 .context("failed to apply user/group")?;180 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();181 fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();182 stable_temp183 .persist(value.stable_secret_path)184 .context("failed to persist")?;185186 Ok(())187}188189fn host_identity() -> anyhow::Result<SshIdentity> {190 let identity = SshIdentity::from_buffer(191 &mut Cursor::new(192 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,193 ),194 None,195 )196 .context("failed to parse identity")?;197 Ok(identity)198}199200fn install(data: &Path) -> anyhow::Result<()> {201 let data = fs::read(data).context("failed to read secrets data")?;202 let data_str = from_utf8(&data).context("failed to read data to string")?;203 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;204205 if !fs::metadata("/run/secrets")206 .map(|m| m.is_dir())207 .unwrap_or(false)208 {209 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;210 }211212 let identity = host_identity()?;213214 let mut failed = false;215 for (name, value) in data {216 let _span = info_span!("init", name = name);217 if let Err(e) = init_secret(&identity, value) {218 error!("{e}");219 failed = true;220 }221 }222 if failed {223 bail!("one or more secrets failed");224 }225226 Ok(())227}228229fn main() -> anyhow::Result<()> {230 tracing_subscriber::fmt()231 .with_env_filter(232 EnvFilter::builder()233 .with_default_directive(LevelFilter::INFO.into())234 .from_env_lossy(),235 )236 .without_time()237 .with_target(false)238 .init();239240 let opts = Opts::parse();241242 match opts {243 Opts::Install { data } => install(&data),244 Opts::Reencrypt { secret, targets } => {245 let identity = host_identity()?;246 let decrypted = decrypt(&secret, &identity).context("during decryption")?;247 let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;248249 println!("{encrypted}");250 Ok(())251 }252 Opts::Decrypt { secret, plaintext } => {253 let identity = host_identity()?;254 let decrypted = decrypt(&secret, &identity).context("during decryption")?;255256 if plaintext {257 let s = String::from_utf8(decrypted).context("output is not utf8")?;258 print!("{s}");259 } else {260 println!("{}", SecretWrapper(decrypted));261 }262 Ok(())263 }264 }265}