1use std::{2 collections::{BTreeMap, HashMap},3 fs::{self, File},4 io::{self, Cursor, Read, Write},5 iter,6 os::unix::prelude::PermissionsExt,7 path::{Path, PathBuf},8 str::{from_utf8, FromStr},9};1011use age::{12 ssh::{Identity as SshIdentity, Recipient as SshRecipient},13 Decryptor, Encryptor, Identity, Recipient,14};15use anyhow::{anyhow, bail, ensure, Context, Result};16use clap::Parser;17use fleet_shared::SecretData;18use nix::unistd::{chown, Group, User};19use serde::Deserialize;20use tracing::{error, info_span};21use tracing_subscriber::{filter::LevelFilter, EnvFilter};2223#[derive(Parser)]24#[clap(author)]25enum Opts {26 27 Install { data: PathBuf },28 29 Reencrypt {30 #[clap(long)]31 secret: SecretData,32 #[clap(long)]33 targets: Vec<String>,34 },35 36 Decrypt {37 #[clap(long)]38 secret: SecretData,39 40 #[clap(long)]41 plaintext: bool,42 },43}4445#[derive(Deserialize)]46#[serde(rename_all = "camelCase")]47struct Part {48 raw: SecretData,49 path: PathBuf,50 stable_path: PathBuf,51}5253#[derive(Deserialize)]54#[serde(rename_all = "camelCase")]55struct DataItem {56 group: String,57 mode: String,58 owner: String,59 root_path: Option<PathBuf>,6061 #[serde(flatten)]62 parts: BTreeMap<String, Part>,63}6465type Data = HashMap<String, DataItem>;6667fn decrypt(input: &SecretData, identity: &dyn Identity) -> Result<Vec<u8>> {68 ensure!(input.encrypted, "passed data is not encrypted!");69 let mut input = Cursor::new(&input.data);70 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;71 let decryptor = match decryptor {72 Decryptor::Recipients(r) => r,73 Decryptor::Passphrase(_) => bail!("should be recipients"),74 };75 let mut decryptor = decryptor76 .decrypt(iter::once(identity as &dyn age::Identity))77 .context("failed to decrypt, wrong key?")?;7879 let mut decrypted = Vec::new();80 decryptor81 .read_to_end(&mut decrypted)82 .context("failed to decrypt")?;83 Ok(decrypted)84}85fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretData> {86 let recipients = targets87 .into_iter()88 .map(|t| {89 SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))90 })91 .collect::<Result<Vec<SshRecipient>>>()?;92 let recipients = recipients93 .into_iter()94 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)95 .collect::<Vec<_>>();96 let mut encrypted = vec![];97 let mut encryptor = Encryptor::with_recipients(recipients)98 .expect("recipients provided")99 .wrap_output(&mut encrypted)100 .expect("constructor should not fail");101 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");102 encryptor.finish().context("failed to finish encryption")?;103 Ok(SecretData {104 data: encrypted,105 encrypted: true,106 })107}108109fn init_part(identity: &dyn Identity, item: &DataItem, value: &Part) -> Result<()> {110 let stable_dir = value.stable_path.parent().expect("not root");111112 113 std::fs::create_dir_all(stable_dir)?;114115 let mut stable_temp =116 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;117 let mut hashed = File::create(&value.path)?;118119 let private = value.raw.encrypted;120 let data = if private {121 decrypt(&value.raw, identity)?122 } else {123 value.raw.data.to_owned()124 };125126 hashed.write_all(&data)?;127 hashed.flush()?;128 stable_temp.write_all(&data)?;129 stable_temp.flush()?;130131 let mode = if private {132 fs::Permissions::from_mode(133 u32::from_str_radix(&item.mode, 8).context("failed to parse mode as octal")?,134 )135 } else {136 fs::Permissions::from_mode(0o444)137 };138 fs::set_permissions(stable_temp.path(), mode.clone()).context("stable temp mode")?;139 fs::set_permissions(&value.path, mode).context("hashed mode")?;140141 142 143 if private {144 let user = User::from_name(&item.owner)145 .context("failed to get user")?146 .ok_or_else(|| anyhow!("user not found"))?;147 let group = Group::from_name(&item.group)148 .context("failed to get group")?149 .ok_or_else(|| anyhow!("group not found"))?;150151 chown(stable_temp.path(), Some(user.uid), Some(group.gid))152 .context("failed to apply user/group")?;153 chown(&value.path, Some(user.uid), Some(group.gid))154 .context("failed to apply user/group")?;155 }156157 stable_temp158 .persist(&value.stable_path)159 .context("stable persist")?;160 Ok(())161}162163fn init_secret(identity: &age::ssh::Identity, value: &DataItem) -> Result<()> {164 if let Some(root_path) = &value.root_path {165 if !fs::metadata(root_path).map(|m| m.is_dir()).unwrap_or(false) {166 fs::create_dir(root_path).context("failed to create secret directory")?;167 }168 }169 let mut errored = false;170 for (part_id, part) in value.parts.iter() {171 let _span = info_span!("part", part_id = part_id);172 if let Err(e) = init_part(identity, value, part) {173 error!("failed to init part {part_id}: {e}");174 errored = true;175 }176 }177178 ensure!(!errored, "some secret parts have failed to initialize");179 Ok(())180}181182fn host_identity() -> anyhow::Result<SshIdentity> {183 let identity = SshIdentity::from_buffer(184 &mut Cursor::new(185 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,186 ),187 None,188 )189 .context("failed to parse identity")?;190 Ok(identity)191}192193fn install(data: &Path) -> anyhow::Result<()> {194 let data = fs::read(data).context("failed to read secrets data")?;195 let data_str = from_utf8(&data).context("failed to read data to string")?;196 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;197198 if !fs::metadata("/run/secrets")199 .map(|m| m.is_dir())200 .unwrap_or(false)201 {202 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;203 }204205 let identity = host_identity()?;206207 let mut failed = false;208 for (name, value) in data {209 let _span = info_span!("init", name = name);210 if let Err(e) = init_secret(&identity, &value) {211 error!("secret failed to initialize: {e}");212 failed = true;213 }214 }215 if failed {216 bail!("one or more secrets failed");217 }218219 Ok(())220}221222fn main() -> anyhow::Result<()> {223 tracing_subscriber::fmt()224 .with_env_filter(225 EnvFilter::builder()226 .with_default_directive(LevelFilter::INFO.into())227 .from_env_lossy(),228 )229 .without_time()230 .with_target(false)231 .init();232233 let opts = Opts::parse();234235 match opts {236 Opts::Install { data } => install(&data),237 Opts::Reencrypt { secret, targets } => {238 let identity = host_identity()?;239 let decrypted = decrypt(&secret, &identity).context("during decryption")?;240 let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;241242 println!("{encrypted}");243 Ok(())244 }245 Opts::Decrypt { secret, plaintext } => {246 let identity = host_identity()?;247 let decrypted = decrypt(&secret, &identity).context("during decryption")?;248249 if plaintext {250 let s = String::from_utf8(decrypted).context("output is not utf8")?;251 print!("{s}");252 } else {253 println!(254 "{}",255 SecretData {256 data: decrypted,257 encrypted: false258 }259 );260 }261 Ok(())262 }263 }264}