use nu_plugin::{ serve_plugin, EngineInterface, EvaluatedCall, MsgPackSerializer, Plugin, PluginCommand, SimplePluginCommand, }; use nu_protocol::{record, Category, Example, LabeledError, Signature, SyntaxShape, Type, Value}; mod helpers; #[cfg(test)] mod tests; /// Nushell plugin for KMS operations pub struct KmsPlugin; impl Plugin for KmsPlugin { fn version(&self) -> String { env!("CARGO_PKG_VERSION").into() } fn commands(&self) -> Vec>> { vec![ Box::new(KmsEncrypt), Box::new(KmsDecrypt), Box::new(KmsGenerateKey), Box::new(KmsStatus), ] } } /// Encrypt command pub struct KmsEncrypt; impl SimplePluginCommand for KmsEncrypt { type Plugin = KmsPlugin; fn name(&self) -> &str { "kms encrypt" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::String, Type::String) .required("data", SyntaxShape::String, "Data to encrypt") .named( "backend", SyntaxShape::String, "Backend: rustyvault, age, cosmian", Some('b'), ) .named("key", SyntaxShape::String, "Key ID or recipient", Some('k')) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Encrypt data using KMS backend" } fn examples(&self) -> Vec> { vec![ Example { example: "kms encrypt \"secret data\" --backend rustyvault", description: "Encrypt with RustyVault", result: None, }, Example { example: "kms encrypt \"data\" --backend age", description: "Encrypt with Age", result: None, }, ] } fn run( &self, _plugin: &KmsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let data: String = call.req(0)?; let backend_name: Option = call.get_flag("backend")?; let key: Option = call.get_flag("key")?; // Create tokio runtime for async operations let runtime = tokio::runtime::Runtime::new() .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?; // Detect or use specified backend let backend = runtime.block_on(async { if let Some(name) = backend_name { match name.as_str() { "rustyvault" => { let addr = std::env::var("RUSTYVAULT_ADDR") .unwrap_or("http://localhost:8200".to_string()); let token = std::env::var("RUSTYVAULT_TOKEN") .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?; helpers::Backend::new_rustyvault(&addr, &token) .await .map_err(|e| LabeledError::new(e)) } "age" => { let recipient = key .clone() .ok_or(LabeledError::new("Age requires --key recipient"))?; helpers::Backend::new_age(&recipient, None) .map_err(|e| LabeledError::new(e)) } backend @ _ => { let url = std::env::var("KMS_HTTP_URL") .unwrap_or("http://localhost:8081".to_string()); Ok(helpers::Backend::new_http_fallback(backend, &url)) } } } else { Ok(helpers::detect_backend().await) } })?; // Encrypt based on backend let encrypted = match backend { helpers::Backend::RustyVault { ref client } => { let key_name = key.unwrap_or("provisioning-main".to_string()); helpers::encrypt_rustyvault(client, &key_name, data.as_bytes()) .map_err(|e| LabeledError::new(e))? } helpers::Backend::Age { ref recipient, .. } => { helpers::encrypt_age(data.as_bytes(), recipient) .map_err(|e| LabeledError::new(e))? } helpers::Backend::HttpFallback { ref backend_name, ref url, } => runtime.block_on(async { helpers::encrypt_http(url, backend_name, data.as_bytes()) .await .map_err(|e| LabeledError::new(e)) })?, }; Ok(Value::string(encrypted, call.head)) } } /// Decrypt command pub struct KmsDecrypt; impl SimplePluginCommand for KmsDecrypt { type Plugin = KmsPlugin; fn name(&self) -> &str { "kms decrypt" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::String, Type::String) .required("encrypted", SyntaxShape::String, "Encrypted data") .named( "backend", SyntaxShape::String, "Backend: rustyvault, age, cosmian", Some('b'), ) .named( "key", SyntaxShape::String, "Key ID or private key path", Some('k'), ) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Decrypt data using KMS backend" } fn run( &self, _plugin: &KmsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let encrypted: String = call.req(0)?; let backend_name: Option = call.get_flag("backend")?; let key: Option = call.get_flag("key")?; // Create tokio runtime for async operations let runtime = tokio::runtime::Runtime::new() .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?; // Detect or use specified backend let backend = runtime.block_on(async { if let Some(name) = backend_name { match name.as_str() { "rustyvault" => { let addr = std::env::var("RUSTYVAULT_ADDR") .unwrap_or("http://localhost:8200".to_string()); let token = std::env::var("RUSTYVAULT_TOKEN") .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?; helpers::Backend::new_rustyvault(&addr, &token) .await .map_err(|e| LabeledError::new(e)) } "age" => { let identity = key .clone() .ok_or(LabeledError::new("Age requires --key identity_path"))?; helpers::Backend::new_age("", Some(identity)) .map_err(|e| LabeledError::new(e)) } backend @ _ => { let url = std::env::var("KMS_HTTP_URL") .unwrap_or("http://localhost:8081".to_string()); Ok(helpers::Backend::new_http_fallback(backend, &url)) } } } else { Ok(helpers::detect_backend().await) } })?; // Decrypt based on backend let decrypted = match backend { helpers::Backend::RustyVault { ref client } => { let key_name = key.unwrap_or("provisioning-main".to_string()); helpers::decrypt_rustyvault(client, &key_name, &encrypted) .map_err(|e| LabeledError::new(e))? } helpers::Backend::Age { ref identity, .. } => { let identity_path = identity.as_ref().ok_or(LabeledError::new( "Age requires identity path for decryption", ))?; helpers::decrypt_age(&encrypted, identity_path).map_err(|e| LabeledError::new(e))? } helpers::Backend::HttpFallback { ref backend_name, ref url, } => runtime.block_on(async { helpers::decrypt_http(url, backend_name, &encrypted) .await .map_err(|e| LabeledError::new(e)) })?, }; // Convert bytes to string let plaintext = String::from_utf8(decrypted) .map_err(|e| LabeledError::new(format!("Failed to convert to UTF-8: {}", e)))?; Ok(Value::string(plaintext, call.head)) } } /// Generate data key command pub struct KmsGenerateKey; impl SimplePluginCommand for KmsGenerateKey { type Plugin = KmsPlugin; fn name(&self) -> &str { "kms generate-key" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record([].into())) .named( "spec", SyntaxShape::String, "Key spec: AES128, AES256", Some('s'), ) .named("backend", SyntaxShape::String, "Backend", Some('b')) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Generate data encryption key" } fn run( &self, _plugin: &KmsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let key_spec: Option = call.get_flag("spec")?; let backend_name: Option = call.get_flag("backend")?; let key_spec = key_spec.unwrap_or("AES256".to_string()); // Create tokio runtime for async operations let runtime = tokio::runtime::Runtime::new() .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?; // Detect or use specified backend let backend = runtime.block_on(async { if let Some(name) = backend_name { match name.as_str() { "rustyvault" => { let addr = std::env::var("RUSTYVAULT_ADDR") .unwrap_or("http://localhost:8200".to_string()); let token = std::env::var("RUSTYVAULT_TOKEN") .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?; helpers::Backend::new_rustyvault(&addr, &token) .await .map_err(|e| LabeledError::new(e)) } "age" => Ok(helpers::Backend::new_age("age1placeholder", None) .map_err(|e| LabeledError::new(e))?), backend @ _ => { let url = std::env::var("KMS_HTTP_URL") .unwrap_or("http://localhost:8081".to_string()); Ok(helpers::Backend::new_http_fallback(backend, &url)) } } } else { Ok(helpers::detect_backend().await) } })?; // Generate key based on backend let (plaintext, ciphertext) = match backend { helpers::Backend::RustyVault { ref client } => { let key_name = "provisioning-main"; helpers::generate_data_key_rustyvault(client, key_name, &key_spec) .map_err(|e| LabeledError::new(e))? } helpers::Backend::Age { .. } => { // Age generates key pairs, not data keys helpers::generate_age_key() .map(|(secret, public)| (secret, public)) .map_err(|e| LabeledError::new(e))? } helpers::Backend::HttpFallback { ref backend_name, ref url, } => runtime.block_on(async { helpers::generate_data_key_http(url, backend_name, &key_spec) .await .map_err(|e| LabeledError::new(e)) })?, }; Ok(Value::record( record! { "plaintext" => Value::string(plaintext, call.head), "ciphertext" => Value::string(ciphertext, call.head), }, call.head, )) } } /// KMS status command pub struct KmsStatus; impl SimplePluginCommand for KmsStatus { type Plugin = KmsPlugin; fn name(&self) -> &str { "kms status" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record([].into())) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Check KMS backend status" } fn run( &self, _plugin: &KmsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { // Create tokio runtime for async operations let runtime = tokio::runtime::Runtime::new() .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?; let backend = runtime.block_on(helpers::detect_backend()); let (backend_type, available, config) = match backend { helpers::Backend::RustyVault { .. } => { let addr = std::env::var("RUSTYVAULT_ADDR").unwrap_or("not set".to_string()); ("rustyvault", true, format!("addr: {}", addr)) } helpers::Backend::Age { ref recipient, ref identity, } => { let identity_status = identity.as_ref().map(|_| "set").unwrap_or("not set"); ( "age", true, format!("recipient: {}, identity: {}", recipient, identity_status), ) } helpers::Backend::HttpFallback { ref backend_name, ref url, } => (backend_name.as_str(), true, format!("url: {}", url)), }; Ok(Value::record( record! { "backend" => Value::string(backend_type, call.head), "available" => Value::bool(available, call.head), "config" => Value::string(config, call.head), }, call.head, )) } } fn main() { serve_plugin(&KmsPlugin, MsgPackSerializer); }