1use std::str::FromStr as _;23use age::Recipient;4use anyhow::{Result, anyhow, bail};5use futures::{StreamExt as _, TryStreamExt as _};6use itertools::Itertools as _;7use tracing::warn;89use crate::{fleetdata::SecretOwner, host::Config};1011impl Config {12 fn cached_host_key(&self, host: &str) -> Option<String> {13 let hosts = self.data.hosts.read().expect("no poisoning");14 let key = hosts.get(host).map(|h| &h.encryption_key);15 if let Some(key) = key16 && key.is_empty()17 {18 return None;19 }20 key.cloned()21 }22 pub fn update_key(&self, host: &str, key: String) {23 let mut hosts = self.data.hosts.write().expect("no poisoning");24 let host = hosts.entry(host.to_string()).or_default();25 host.encryption_key = key.trim().to_string();26 }2728 pub async fn host_key(&self, host: &str) -> anyhow::Result<String> {29 if let Some(key) = self.cached_host_key(host) {30 Ok(key)31 } else {32 warn!("Loading key for {}", host);33 let host = self.host(host)?;34 let mut cmd = host.cmd("cat").await?;35 cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub");36 let key = cmd.run_string().await?;37 self.update_key(&host.name, key.clone());38 Ok(key)39 }40 }41 pub async fn key(&self, owner: &SecretOwner) -> anyhow::Result<String> {42 if let Some(host) = owner.as_host() {43 self.host_key(host).await44 } else {45 bail!("only host keys supported for now")46 }47 }48 49 pub async fn recipient(&self, host: &SecretOwner) -> anyhow::Result<Box<dyn Recipient>> {50 let key = self.key(host).await?;51 age::ssh::Recipient::from_str(&key)52 .map_err(|e| anyhow!("parse recipient error: {e:?}"))53 .map(|v| Box::new(v) as Box<dyn Recipient>)54 }5556 pub async fn recipients(&self, hosts: Vec<SecretOwner>) -> Result<Vec<Box<dyn Recipient>>> {57 futures::stream::iter(hosts.iter())58 .then(|m| self.recipient(m))59 .try_collect::<Vec<_>>()60 .await61 }6263 #[allow(dead_code)]64 pub async fn orphaned_data(&self) -> Result<Vec<String>> {65 let mut out = Vec::new();66 let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();67 let hosts = self.data.hosts.read().expect("no poisoning");68 for hostname in hosts69 .iter()70 .filter(|(_, host)| !host.encryption_key.is_empty())71 .map(|(n, _)| n)72 {73 if !host_names.contains(hostname) {74 out.push(hostname.to_owned())75 }76 }7778 Ok(out)79 }80}