From 918101979829d0be3534ba8c9a5c3c9378b38608 Mon Sep 17 00:00:00 2001 From: Akseli Lukkarila Date: Tue, 8 Oct 2024 12:00:07 +0300 Subject: [PATCH] rust: implement prefix option and refactor vault factory methods (#527) * rust: implement prefix option and refactor vault factory methods * add missing `d` short alias for delete command * fix duplicate prefix in key name --- rust/Cargo.lock | 118 ++++++++--------- rust/Cargo.toml | 2 +- rust/src/cli.rs | 81 ++++-------- rust/src/errors.rs | 5 + rust/src/lib.rs | 7 +- rust/src/main.rs | 39 +++--- rust/src/value.rs | 39 +++++- rust/src/vault.rs | 322 +++++++++++++++++++++++---------------------- 8 files changed, 310 insertions(+), 303 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 12531087..7b08cd08 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -133,9 +133,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.7" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8191fb3091fa0561d1379ef80333c3c7191c6f0435d986e85821bcf7acbd1126" +checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6" dependencies = [ "aws-credential-types", "aws-runtime", @@ -201,9 +201,9 @@ dependencies = [ [[package]] name = "aws-sdk-cloudformation" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943017121d6b350cca4e83b42cb8d8ba7646937c2568326d22eb9e4419ebaa9f" +checksum = "110cb4838e4ac2c8d8e6fa851e66b95b1061becdce544b1c3ea37d23356c2e0b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "aws-sdk-kms" -version = "1.45.0" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0caf20b8855dbeb458552e6c8f8f9eb92b95e4a131725b93540ec73d60c38eb3" +checksum = "e33590e8d45206fdc4273ded8a1f292bcceaadd513037aa790fc67b237bc30ee" dependencies = [ "aws-credential-types", "aws-runtime", @@ -247,9 +247,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.52.0" +version = "1.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f571deb0a80c20d21d9f3e8418c1712af9ff4bf399d057e5549a934eca4844e2" +checksum = "e2f2a62020f3e06f9b352b2a23547f6e1d110b6bf1e18a6b588ae36114eaf6e2" dependencies = [ "ahash", "aws-credential-types", @@ -282,9 +282,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.44.0" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b90cfe6504115e13c41d3ea90286ede5aa14da294f3fe077027a6e83850843c" +checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb" dependencies = [ "aws-credential-types", "aws-runtime", @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.45.0" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167c0fad1f212952084137308359e8e4c4724d1c643038ce163f06de9662c1d0" +checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0" dependencies = [ "aws-credential-types", "aws-runtime", @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.44.0" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb5f98188ec1435b68097daa2a37d74b9d17c9caa799466338a8d1544e71b9d" +checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765" dependencies = [ "aws-credential-types", "aws-runtime", @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.24" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ "shlex", ] @@ -906,6 +906,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -917,36 +923,36 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-task", @@ -987,9 +993,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "group" @@ -1023,20 +1029,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" - [[package]] name = "heck" version = "0.5.0" @@ -1189,7 +1190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown", ] [[package]] @@ -1243,11 +1244,11 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", ] [[package]] @@ -1289,7 +1290,7 @@ dependencies = [ [[package]] name = "nitor-vault" -version = "0.9.0" +version = "0.10.0" dependencies = [ "aes-gcm", "anyhow", @@ -1333,21 +1334,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" @@ -1441,12 +1439,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - [[package]] name = "powerfmt" version = "0.2.0" @@ -1464,9 +1456,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -1617,9 +1609,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] @@ -1994,9 +1986,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ee0a29f8..f1aaaad2 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nitor-vault" -version = "0.9.0" +version = "0.10.0" edition = "2021" description = "Encrypted AWS key-value storage utility." license = "Apache-2.0" diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 0c2f45a2..6ffd7ae4 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -1,4 +1,3 @@ -use std::io::{stdin, Read}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; @@ -33,26 +32,7 @@ pub async fn store( } }; - let data = { - if let Some(value) = value_positional.or(value_argument) { - if value == "-" { - println!("Reading from stdin until EOF"); - read_data_from_stdin()? - } else { - Value::Utf8(value) - } - } else if let Some(path) = &file { - match path.as_str() { - "-" => { - println!("Reading from stdin until EOF"); - read_data_from_stdin()? - } - _ => read_data_from_path(path)?, - } - } else { - anyhow::bail!("No value or filename provided".red()) - } - }; + let value = read_value(value_positional, value_argument, file)?; if !overwrite && vault @@ -66,7 +46,7 @@ pub async fn store( ) } - Box::pin(vault.store(&key, data.as_bytes())) + Box::pin(vault.store(&key, value.as_bytes())) .await .with_context(|| format!("Failed to store key '{key}'").red()) } @@ -127,41 +107,6 @@ pub async fn exists(vault: &Vault, key: &str) -> Result<()> { }) } -/// Read data from filepath, supporting both UTF-8 and non-UTF-8 contents -fn read_data_from_path(path: &String) -> Result { - if let Ok(content) = std::fs::read_to_string(path) { - Ok(Value::Utf8(content)) - } else { - let binary_data = - std::fs::read(path).with_context(|| format!("Error reading file: '{path}'").red())?; - - Ok(Value::Binary(binary_data)) - } -} - -/// Read data from stdin, supporting both UTF-8 and non-UTF-8 input -fn read_data_from_stdin() -> Result { - let mut buffer = Vec::new(); - - let stdin = stdin(); - let mut stdin_lock = stdin.lock(); - - // Read raw bytes from stdin - stdin_lock - .read_to_end(&mut buffer) - .context("Failed to read from stdin")?; - - drop(stdin_lock); - - // Try to convert the raw bytes to a UTF-8 string - #[allow(clippy::option_if_let_else)] - // ^using `map_or` would require cloning buffer - match std::str::from_utf8(&buffer) { - Ok(valid_utf8) => Ok(Value::Utf8(valid_utf8.to_string())), - Err(_) => Ok(Value::Binary(buffer)), - } -} - /// Try to get the filename for the given filepath fn get_filename_from_path(path: &str) -> Result { let path = Path::new(path); @@ -193,9 +138,29 @@ fn resolve_output_file_path(outfile: Option) -> Result> format!("Failed to create directories for '{}'", parent.display()) })?; } - Ok(Some(path)) } else { Ok(None) } } + +fn read_value( + value_positional: Option, + value_argument: Option, + file: Option, +) -> Result { + Ok(if let Some(value) = value_positional.or(value_argument) { + if value == "-" { + Value::from_stdin()? + } else { + Value::Utf8(value) + } + } else if let Some(path) = file { + match path.as_str() { + "-" => Value::from_stdin()?, + _ => Value::from_path(path)?, + } + } else { + anyhow::bail!("No value or filename provided".red()) + }) +} diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 7ccdb119..941755e5 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -1,3 +1,4 @@ +use std::io; use std::string::FromUtf8Error; use aws_sdk_cloudformation::error::SdkError; @@ -66,4 +67,8 @@ pub enum VaultError { NoRegionError, #[error("Failed parsing Nonce from base64")] NonceParseError(#[from] base64::DecodeError), + #[error("Failed to read file: {0}")] + FileReadError(String, #[source] io::Error), + #[error("Failed to read from stdin")] + StdinReadError(#[from] io::Error), } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 27792357..fb3eda6e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -60,17 +60,16 @@ impl CloudFormationParams { } /// Get `CloudFormation` parameters based on config and stack name - async fn get_from_stack(config: &SdkConfig, stack: &str) -> Result { + async fn from_stack(config: &SdkConfig, stack: String) -> Result { let describe_stack_output = CloudFormationClient::new(config) .describe_stacks() - .stack_name(stack) + .stack_name(stack.clone()) .send() .await?; let stack_output = describe_stack_output .stacks() - .iter() - .next() + .first() .map(aws_sdk_cloudformation::types::Stack::outputs) .ok_or(VaultError::StackOutputsMissingError)?; diff --git a/rust/src/main.rs b/rust/src/main.rs index 87a0818c..d9c10b42 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -24,11 +24,15 @@ pub struct Args { #[arg(short, long, env = "VAULT_KEY")] pub key_arn: Option, + /// Optional prefix for key name + #[arg(short, long, env = "VAULT_PREFIX")] + pub prefix: Option, + /// Specify AWS region for the bucket #[arg(short, long, env = "AWS_REGION")] pub region: Option, - /// Optional CloudFormation stack to lookup key and bucket + /// Optional CloudFormation stack name to lookup key and bucket #[arg(long, env)] pub vault_stack: Option, @@ -45,7 +49,7 @@ pub enum Command { All {}, /// Delete an existing key from the store - #[command(short_flag('d'), long_flag("delete"))] + #[command(short_flag('d'), long_flag("delete"), alias("d"))] Delete { key: String }, /// Describe CloudFormation stack parameters for current configuration. @@ -91,13 +95,9 @@ pub enum Command { /// Key name key: Option, - /// Value to store + /// Value to store, use '-' for stdin value: Option, - /// Overwrite existing key - #[arg(short = 'w', long)] - overwrite: bool, - /// Value to store, use '-' for stdin #[arg( short, @@ -115,6 +115,10 @@ pub enum Command { conflicts_with_all = vec!["value", "value_opt"] )] file: Option, + + /// Overwrite existing key + #[arg(short = 'w', long)] + overwrite: bool, }, } @@ -122,18 +126,15 @@ pub enum Command { async fn main() -> Result<()> { let args = Args::parse(); - // Bucket and key were either given as args or found in env variables - let vault = if let (Some(bucket), key_arn) = (args.bucket.as_deref(), args.key_arn.as_deref()) { - Vault::from_cli_params(bucket, key_arn, args.region.as_deref()) - .await - // Note: `with_context` is used instead of the simpler `context` - // as it is lazily evaluated. - .with_context(|| "Failed to create vault from given params".red())? - } else { - Vault::new(args.vault_stack.as_deref(), args.region.as_deref()) - .await - .with_context(|| "Failed to create vault".red())? - }; + let vault = Vault::new( + args.vault_stack, + args.region, + args.bucket, + args.key_arn, + args.prefix, + ) + .await + .with_context(|| "Failed to create vault from given params".red())?; // Handle subcommands if let Some(command) = args.command { diff --git a/rust/src/value.rs b/rust/src/value.rs index 5c797313..2a6314a0 100644 --- a/rust/src/value.rs +++ b/rust/src/value.rs @@ -1,7 +1,9 @@ -use std::io::{BufWriter, Write}; +use std::io::{stdin, BufWriter, Read, Write}; use std::path::Path; use std::{fmt, io}; +use crate::errors::VaultError; + #[derive(Debug, Clone)] /// Vault supports storing arbitrary data that might not be valid UTF-8. /// Handle values as either UTF-8 or binary. @@ -11,6 +13,41 @@ pub enum Value { } impl Value { + /// Read data from given filepath. + /// Supports both UTF-8 and non-UTF-8 contents. + pub fn from_path(path: String) -> Result { + if let Ok(content) = std::fs::read_to_string(&path) { + Ok(Self::Utf8(content)) + } else { + let binary_data = + std::fs::read(&path).map_err(|e| VaultError::FileReadError(path, e))?; + + Ok(Self::Binary(binary_data)) + } + } + + /// Read data from stdin. + /// Supports both UTF-8 and non-UTF-8 input. + pub fn from_stdin() -> Result { + let mut buffer = Vec::new(); + + let stdin = stdin(); + let mut stdin_lock = stdin.lock(); + + // Read raw bytes from stdin + stdin_lock.read_to_end(&mut buffer)?; + + drop(stdin_lock); + + // Try to convert the raw bytes to a UTF-8 string + #[allow(clippy::option_if_let_else)] + // ^using `map_or` would require cloning buffer + match std::str::from_utf8(&buffer) { + Ok(valid_utf8) => Ok(Self::Utf8(valid_utf8.to_string())), + Err(_) => Ok(Self::Binary(buffer)), + } + } + /// Returns the data as a byte slice (`&[u8]`) #[must_use] pub fn as_bytes(&self) -> &[u8] { diff --git a/rust/src/vault.rs b/rust/src/vault.rs index 15c50423..23450daa 100644 --- a/rust/src/vault.rs +++ b/rust/src/vault.rs @@ -25,18 +25,34 @@ pub struct Vault { /// AWS region to use with Vault. /// Will fall back to default provider if nothing is specified. region: Region, + /// Prefix for key name + prefix: String, cloudformation_params: CloudFormationParams, s3: S3Client, kms: KmsClient, } impl Vault { + /// Construct Vault with defaults for an existing stack. + /// This will try reading environment variables for the config values, + /// and otherwise fall back to current AWS config. + /// + /// The Default trait can't be implemented for Vault since it can fail. + pub async fn default() -> Result { + Self::new(None, None, None, None, None).await + } + + /// Construct Vault with optional arguments for an existing stack. + /// This will try reading environment variables for the config values if they are `None`. pub async fn new( - vault_stack: Option<&str>, - region_opt: Option<&str>, + vault_stack: Option, + region: Option, + bucket: Option, + key: Option, + prefix: Option, ) -> Result { let config = aws_config::from_env() - .region(get_region_provider(region_opt)) + .region(get_region_provider(region)) .load() .await; @@ -47,48 +63,28 @@ impl Vault { // Check env variables directly in case the library is not used through the CLI. // These are also handled in the CLI, so they are documented in the CLI help. - let vault_stack_from_env = get_env_variable("VAULT_STACK"); - let vault_bucket_from_env = get_env_variable("VAULT_BUCKET"); - let vault_key_from_env = get_env_variable("VAULT_KEY"); - - let cloudformation_params = - if let (Some(bucket), Some(key)) = (vault_bucket_from_env, vault_key_from_env) { - CloudFormationParams::new(bucket, Some(key)) - } else { - let stack_name = vault_stack_from_env - .as_deref() - .or(vault_stack) - .unwrap_or("vault"); - - CloudFormationParams::get_from_stack(&config, stack_name).await? - }; - - Ok(Self { - region, - cloudformation_params, - s3: S3Client::new(&config), - kms: KmsClient::new(&config), - }) - } + let stack_name = vault_stack + .or_else(|| get_env_variable("VAULT_STACK")) + .unwrap_or_else(|| "vault".to_string()); + let bucket = bucket.or_else(|| get_env_variable("VAULT_BUCKET")); + let key = key.or_else(|| get_env_variable("VAULT_KEY")); + let mut prefix = prefix + .or_else(|| get_env_variable("VAULT_PREFIX")) + .unwrap_or_default(); + + if !prefix.is_empty() && !prefix.ends_with('/') { + prefix.push('/'); + } - pub async fn from_cli_params( - bucket: &str, - key_arn: Option<&str>, - region_opt: Option<&str>, - ) -> Result { - Self::from_params(CloudFormationParams::from(bucket, key_arn), region_opt).await - } + let cloudformation_params = if let (Some(bucket), Some(key)) = (bucket, key) { + CloudFormationParams::new(bucket, Some(key)) + } else { + CloudFormationParams::from_stack(&config, stack_name).await? + }; - pub async fn from_params( - cloudformation_params: CloudFormationParams, - region_opt: Option<&str>, - ) -> Result { - let config = aws_config::from_env() - .region(get_region_provider(region_opt)) - .load() - .await; Ok(Self { - region: config.region().ok_or(VaultError::NoRegionError)?.to_owned(), + region, + prefix, cloudformation_params, s3: S3Client::new(&config), kms: KmsClient::new(&config), @@ -126,99 +122,9 @@ impl Vault { self.cloudformation_params.clone() } - /// Encrypt data - async fn encrypt(&self, data: &[u8]) -> Result { - let key_dict = self - .kms - .generate_data_key() - .key_id( - self.cloudformation_params - .key_arn - .clone() - .ok_or(VaultError::KeyARNMissingError)?, - ) - .key_spec(DataKeySpec::Aes256) - .send() - .await?; - - let plaintext = key_dict - .plaintext() - .ok_or(VaultError::KMSDataKeyPlainTextMissingError)?; - - let aesgcm_cipher: AesGcm = - AesGcm::new_from_slice(plaintext.as_ref())?; - let nonce = create_random_nonce(); - let nonce = Nonce::from_slice(nonce.as_slice()); - let meta = Meta::aesgcm(nonce).to_json()?; - let aes_gcm_ciphertext = aesgcm_cipher - .encrypt( - nonce, - Payload { - msg: data, - aad: meta.as_bytes(), - }, - ) - .map_err(|_| VaultError::CiphertextEncryptionError)?; - - let data_key = key_dict - .ciphertext_blob() - .ok_or(VaultError::CiphertextEncryptionError)? - .to_owned() - .into_inner(); - - Ok(EncryptObject { - data_key, - aes_gcm_ciphertext, - meta, - }) - } - - /// Get S3 Object data for given key as a vec of bytes - async fn get_s3_object(&self, key: String) -> Result, VaultError> { - self.s3 - .get_object() - .bucket(self.cloudformation_params.bucket_name.clone()) - .key(&key) - .send() - .await? - .body - .collect() - .await - .map_err(|_| VaultError::S3GetObjectBodyError) - .map(aws_sdk_s3::primitives::AggregatedBytes::to_vec) - } - - /// Get decrypted data - async fn direct_decrypt(&self, encrypted_data: &[u8]) -> Result, VaultError> { - self.kms - .decrypt() - .ciphertext_blob(Blob::new(encrypted_data)) - .send() - .await? - .plaintext() - .map(|blob| blob.to_owned().into_inner()) - .ok_or(VaultError::KMSDataKeyPlainTextMissingError) - } - - /// Send PUT request with the given byte data - async fn put_s3_object( - &self, - body: ByteStream, - key: String, - ) -> Result { - Ok(self - .s3 - .put_object() - .bucket(&self.cloudformation_params.bucket_name) - .key(key) - .acl(aws_sdk_s3::types::ObjectCannedAcl::Private) - .body(body) - .send() - .await?) - } - /// Check if key already exists in bucket pub async fn exists(&self, name: &str) -> Result { + let name = self.full_key_name(name); match self .s3 .head_object() @@ -244,17 +150,16 @@ impl Vault { /// Store encrypted data in S3 pub async fn store(&self, name: &str, data: &[u8]) -> Result<(), VaultError> { let encrypted = self.encrypt(data).await?; - let first = self.put_s3_object( - ByteStream::from(encrypted.aes_gcm_ciphertext), - format!("{name}.aesgcm.encrypted"), - ); - let second = - self.put_s3_object(ByteStream::from(encrypted.data_key), format!("{name}.key")); - let third = self.put_s3_object( - ByteStream::from(encrypted.meta.into_bytes()), - format!("{name}.meta"), - ); - try_join!(first, second, third)?; + + let key = &self.full_key_name(name); + let keys = S3DataKeys::new(key); + + let put_cipher = + self.put_s3_object(keys.cipher, ByteStream::from(encrypted.aes_gcm_ciphertext)); + let put_key = self.put_s3_object(keys.key, ByteStream::from(encrypted.data_key)); + let put_meta = self.put_s3_object(keys.meta, ByteStream::from(encrypted.meta.into_bytes())); + + try_join!(put_cipher, put_key, put_meta)?; Ok(()) } @@ -265,8 +170,8 @@ impl Vault { return Err(VaultError::S3DeleteObjectKeyMissingError); } - let keys = S3DataKeys::new(name); - let identifiers = keys.to_object_identifiers()?; + let key = &self.full_key_name(name); + let identifiers = S3DataKeys::new(key).to_object_identifiers()?; self.s3 .delete_objects() .bucket(&self.cloudformation_params.bucket_name) @@ -281,17 +186,20 @@ impl Vault { /// If the data is valid UTF-8, it will be returned as a string. /// Otherwise, the raw bytes will be returned. pub async fn lookup(&self, name: &str) -> Result { - let keys = S3DataKeys::new(name); + let key = &self.full_key_name(name); + let keys = S3DataKeys::new(key); + let data_key = self.get_s3_object(keys.key); let cipher_text = self.get_s3_object(keys.cipher); let meta_add = self.get_s3_object(keys.meta); let (data_key, cipher_text, meta_add) = try_join!(data_key, cipher_text, meta_add)?; + let meta: Meta = serde_json::from_slice(&meta_add)?; let cipher: AesGcm = AesGcm::new_from_slice(self.direct_decrypt(&data_key).await?.as_slice())?; let nonce = base64::engine::general_purpose::STANDARD.decode(meta.nonce)?; let nonce = Nonce::from_slice(nonce.as_slice()); - let res = cipher + let decrypted_bytes = cipher .decrypt( nonce, Payload { @@ -301,11 +209,117 @@ impl Vault { ) .map_err(|_| VaultError::NonceDecryptError)?; - match String::from_utf8(res) { + match String::from_utf8(decrypted_bytes) { Ok(valid_string) => Ok(Value::Utf8(valid_string)), Err(from_utf8_error) => Ok(Value::Binary(from_utf8_error.into_bytes())), } } + + /// Get S3 Object data for given key as a vec of bytes + async fn get_s3_object(&self, key: String) -> Result, VaultError> { + self.s3 + .get_object() + .bucket(self.cloudformation_params.bucket_name.clone()) + .key(&key) + .send() + .await? + .body + .collect() + .await + .map_err(|_| VaultError::S3GetObjectBodyError) + .map(aws_sdk_s3::primitives::AggregatedBytes::to_vec) + } + + /// Get decrypted data + async fn direct_decrypt(&self, encrypted_data: &[u8]) -> Result, VaultError> { + self.kms + .decrypt() + .ciphertext_blob(Blob::new(encrypted_data)) + .send() + .await? + .plaintext() + .map(|blob| blob.to_owned().into_inner()) + .ok_or(VaultError::KMSDataKeyPlainTextMissingError) + } + + /// Encrypt data + async fn encrypt(&self, data: &[u8]) -> Result { + let key_dict = self + .kms + .generate_data_key() + .key_id( + self.cloudformation_params + .key_arn + .clone() + .ok_or(VaultError::KeyARNMissingError)?, + ) + .key_spec(DataKeySpec::Aes256) + .send() + .await?; + + let plaintext = key_dict + .plaintext() + .ok_or(VaultError::KMSDataKeyPlainTextMissingError)?; + + let aesgcm_cipher: AesGcm = + AesGcm::new_from_slice(plaintext.as_ref())?; + let nonce = create_random_nonce(); + let nonce = Nonce::from_slice(nonce.as_slice()); + let meta = Meta::aesgcm(nonce).to_json()?; + let aes_gcm_ciphertext = aesgcm_cipher + .encrypt( + nonce, + Payload { + msg: data, + aad: meta.as_bytes(), + }, + ) + .map_err(|_| VaultError::CiphertextEncryptionError)?; + + let data_key = key_dict + .ciphertext_blob() + .ok_or(VaultError::CiphertextEncryptionError)? + .to_owned() + .into_inner(); + + Ok(EncryptObject { + data_key, + aes_gcm_ciphertext, + meta, + }) + } + + /// Send PUT request with the given byte data + async fn put_s3_object( + &self, + key: String, + body: ByteStream, + ) -> Result { + Ok(self + .s3 + .put_object() + .bucket(&self.cloudformation_params.bucket_name) + .key(key) + .acl(aws_sdk_s3::types::ObjectCannedAcl::Private) + .body(body) + .send() + .await?) + } + + /// Add prefix to key if prefix has been specified. + fn full_key_name(&self, name: &str) -> String { + if self.prefix.is_empty() { + name.to_string() + } else { + format!("{}{}", self.prefix, name) + } + } +} + +impl fmt::Display for Vault { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "region: {}\n{}", self.region, self.cloudformation_params) + } } fn create_random_nonce() -> [u8; 12] { @@ -316,17 +330,11 @@ fn create_random_nonce() -> [u8; 12] { } /// Get AWS region from optional argument or fallback to default -fn get_region_provider(region: Option<&str>) -> RegionProviderChain { - RegionProviderChain::first_try(region.map(|r| Region::new(r.to_owned()))).or_default_provider() +fn get_region_provider(region: Option) -> RegionProviderChain { + RegionProviderChain::first_try(region.map(Region::new)).or_default_provider() } /// Return possible env variable value as Option fn get_env_variable(name: &str) -> Option { env::var(name).ok() } - -impl fmt::Display for Vault { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "region: {}\n{}", self.region, self.cloudformation_params) - } -}