use crate::{credential, pty::CommandExt as _};
use akv_cli::{cache::ClientCache, jose::Jwe, ErrorKind, Result};
use azure_security_keyvault_keys::{
models::{KeyClientUnwrapKeyOptions, KeyOperationParameters},
KeyClient, ResourceId as KeyResourceId,
};
use azure_security_keyvault_secrets::{
models::SecretClientGetSecretOptions, ResourceId as SecretResourceId, SecretClient,
};
use clap::Parser;
use std::{
collections::HashMap,
env,
io::{BufRead, BufReader},
path::PathBuf,
process::{exit, Command},
sync::Arc,
};
use tokio::sync::Mutex;
use tracing::Level;
const MASK: &str = "<concealed by akv>";
#[derive(Debug, Parser)]
#[command(arg_required_else_help = true)]
pub struct Args {
#[arg(long, value_name = "PATH")]
env_file: Vec<PathBuf>,
#[arg(long)]
no_masking: bool,
#[arg(
allow_hyphen_values = true,
hide = true,
required = true,
trailing_var_arg = true
)]
args: Vec<String>,
}
impl Args {
#[tracing::instrument(level = Level::INFO, skip(self), err)]
pub async fn run(&self) -> Result<()> {
for path in &self.env_file {
dotenvy::from_path_override(path)?;
}
let credential = credential()?;
let key_client_cache = ClientCache::new();
let secret_client_cache = ClientCache::new();
let secrets = Arc::new(Mutex::new(HashMap::<String, String>::new()));
for (name, value) in env::vars_os() {
let Ok(value) = value.into_string() else {
continue;
};
if let Ok(id) = value.parse::<SecretResourceId>() {
tracing::debug!("replacing environment variable {name:?} from {value}");
let mut secrets = secrets.lock().await;
if let Some(secret) = secrets.get(&value) {
env::set_var(&name, secret.as_str());
continue;
}
let client = secret_client_cache
.get(&id.vault_url, |endpoint| {
SecretClient::new(endpoint, credential.clone(), None)
})
.await?;
let secret = client
.get_secret(
&id.name,
Some(SecretClientGetSecretOptions {
secret_version: id.version.as_deref().map(Into::into),
..Default::default()
}),
)
.await?
.into_model()?;
tracing::debug!("retrieved {:?}", &secret);
let Some(secret) = secret.value else {
continue;
};
env::set_var(&name, secret.as_str());
secrets.insert(value, secret);
continue;
}
if let Ok(jwe) = value.parse::<Jwe>() {
tracing::debug!("decrypting environment variable {name:?}");
let mut secrets = secrets.lock().await;
if let Some(secret) = secrets.get(&value) {
env::set_var(&name, secret.as_str());
continue;
}
let plaintext = jwe
.decrypt(async |kid, alg, cek| {
let KeyResourceId {
vault_url,
name,
version,
..
} = kid.parse()?;
let version = version.as_deref().unwrap_or_default();
let client = key_client_cache
.get(vault_url, |endpoint| {
KeyClient::new(endpoint, credential.clone(), None)
})
.await?;
let params = KeyOperationParameters {
algorithm: Some(alg.try_into()?),
value: Some(cek.into()),
..Default::default()
};
client
.unwrap_key(
&name,
params.try_into()?,
Some(KeyClientUnwrapKeyOptions {
key_version: Some(version.into()),
..Default::default()
}),
)
.await?
.into_model()?
.try_into()
})
.await?;
let Ok(plaintext) = String::from_utf8(plaintext.to_vec()) else {
tracing::warn!(target: "akv", "cannot decrypt {name:?} into valid string");
continue;
};
env::set_var(&name, plaintext.as_str());
secrets.insert(value, plaintext);
continue;
}
}
let secrets: Vec<String> = secrets
.lock()
.await
.values()
.map(ToOwned::to_owned)
.collect();
if self.no_masking {
let mut args = self.args.iter();
let program = args.next().ok_or_else(|| {
akv_cli::Error::with_message(ErrorKind::InvalidData, "command required")
})?;
let mut cmd = Command::new(program);
let mut process = cmd.args(args).spawn()?;
if let Some(code) = process.wait()?.code() {
exit(code);
}
return Ok(());
}
let mut args = self.args.iter();
let program = args.next().ok_or_else(|| {
akv_cli::Error::with_message(ErrorKind::InvalidData, "command required")
})?;
let mut cmd = Command::new(program);
cmd.args(args);
let (mut process, pty) = cmd.spawn_pty()?;
let pipe = tokio::spawn({
let pty = pty.clone();
async move {
let reader = BufReader::new(pty);
let lines = reader.lines().fuse();
for line in lines {
let Ok(line) = line else {
continue;
};
let masked = mask_secrets(&line, &secrets);
println!("{masked}");
}
}
});
let status = process.wait();
drop(pty);
let _ = pipe.await;
if let Some(code) = status?.code() {
exit(code);
}
Ok(())
}
}
fn mask_secrets(line: &str, secrets: &Vec<String>) -> String {
let mut masked = line.to_string();
for secret in secrets {
masked = masked.replace(secret, MASK);
}
masked
}