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::{User, Group, chown};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 48 Install { data: PathBuf },49 50 Reencrypt {51 #[clap(long)]52 secret: SecretWrapper,53 #[clap(long)]54 targets: Vec<String>,55 },56 57 Decrypt {58 #[clap(long)]59 secret: SecretWrapper,60 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 164 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 174 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}