424 lines
15 KiB
Rust
Raw Normal View History

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<Box<dyn PluginCommand<Plugin = Self>>> {
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<Example<'_>> {
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<Value, LabeledError> {
let data: String = call.req(0)?;
let backend_name: Option<String> = call.get_flag("backend")?;
let key: Option<String> = 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<Value, LabeledError> {
let encrypted: String = call.req(0)?;
let backend_name: Option<String> = call.get_flag("backend")?;
let key: Option<String> = 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<Value, LabeledError> {
let key_spec: Option<String> = call.get_flag("spec")?;
let backend_name: Option<String> = 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<Value, LabeledError> {
// 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);
}