diff --git a/Cargo.lock b/Cargo.lock index 49ccfc2c645..4bee1804f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2495,6 +2495,7 @@ dependencies = [ "hex", "hyper", "hyper-proxy", + "hyperswitch_interfaces", "masking", "once_cell", "router_env", @@ -3214,6 +3215,18 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyperswitch_interfaces" +version = "0.1.0" +dependencies = [ + "async-trait", + "common_utils", + "dyn-clone", + "masking", + "serde", + "thiserror", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -5186,6 +5199,7 @@ dependencies = [ "hex", "http", "hyper", + "hyperswitch_interfaces", "image", "infer 0.13.0", "josekit", diff --git a/crates/drainer/src/connection.rs b/crates/drainer/src/connection.rs index 788ff1456e2..e01b72150c5 100644 --- a/crates/drainer/src/connection.rs +++ b/crates/drainer/src/connection.rs @@ -3,7 +3,10 @@ use diesel::PgConnection; #[cfg(feature = "aws_kms")] use external_services::aws_kms::{self, decrypt::AwsKmsDecrypt}; #[cfg(feature = "hashicorp-vault")] -use external_services::hashicorp_vault::{self, decrypt::VaultFetch, Kv2}; +use external_services::hashicorp_vault::{ + core::{HashiCorpVault, Kv2}, + decrypt::VaultFetch, +}; #[cfg(not(feature = "aws_kms"))] use masking::PeekInterface; @@ -28,8 +31,8 @@ pub async fn redis_connection( pub async fn diesel_make_pg_pool( database: &Database, _test_transaction: bool, - #[cfg(feature = "aws_kms")] aws_kms_client: &'static aws_kms::AwsKmsClient, - #[cfg(feature = "hashicorp-vault")] hashicorp_client: &'static hashicorp_vault::HashiCorpVault, + #[cfg(feature = "aws_kms")] aws_kms_client: &'static aws_kms::core::AwsKmsClient, + #[cfg(feature = "hashicorp-vault")] hashicorp_client: &'static HashiCorpVault, ) -> PgPool { let password = database.password.clone(); #[cfg(feature = "hashicorp-vault")] diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index e6ca8d023a0..3918c756c10 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -34,10 +34,10 @@ impl Store { &config.master_database, test_transaction, #[cfg(feature = "aws_kms")] - external_services::aws_kms::get_aws_kms_client(&config.kms).await, + external_services::aws_kms::core::get_aws_kms_client(&config.kms).await, #[cfg(feature = "hashicorp-vault")] #[allow(clippy::expect_used)] - external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + external_services::hashicorp_vault::core::get_hashicorp_client(&config.hc_vault) .await .expect("Failed while getting hashicorp client"), ) diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index 17b0b68b324..de5af654540 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -14,7 +14,7 @@ use serde::Deserialize; use crate::errors; #[cfg(feature = "aws_kms")] -pub type Password = aws_kms::AwsKmsValue; +pub type Password = aws_kms::core::AwsKmsValue; #[cfg(not(feature = "aws_kms"))] pub type Password = masking::Secret; @@ -36,9 +36,9 @@ pub struct Settings { pub log: Log, pub drainer: DrainerSettings, #[cfg(feature = "aws_kms")] - pub kms: aws_kms::AwsKmsConfig, + pub kms: aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, + pub hc_vault: hashicorp_vault::core::HashiCorpVaultConfig, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 2b6bc22b2e9..58c81946017 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -35,5 +35,6 @@ hex = "0.4.3" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } +hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces" } masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } diff --git a/crates/external_services/src/aws_kms.rs b/crates/external_services/src/aws_kms.rs index cf21f36f22b..3e5353fd9e3 100644 --- a/crates/external_services/src/aws_kms.rs +++ b/crates/external_services/src/aws_kms.rs @@ -1,284 +1,7 @@ //! Interactions with the AWS KMS SDK -use std::time::Instant; +pub mod core; -use aws_config::meta::region::RegionProviderChain; -use aws_sdk_kms::{config::Region, primitives::Blob, Client}; -use base64::Engine; -use common_utils::errors::CustomResult; -use error_stack::{IntoReport, ResultExt}; -use masking::{PeekInterface, Secret}; -use router_env::logger; -/// decrypting data using the AWS KMS SDK. pub mod decrypt; -use crate::{consts, metrics}; - -static AWS_KMS_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); - -/// Returns a shared AWS KMS client, or initializes a new one if not previously initialized. -#[inline] -pub async fn get_aws_kms_client(config: &AwsKmsConfig) -> &'static AwsKmsClient { - AWS_KMS_CLIENT - .get_or_init(|| AwsKmsClient::new(config)) - .await -} - -/// Configuration parameters required for constructing a [`AwsKmsClient`]. -#[derive(Clone, Debug, Default, serde::Deserialize)] -#[serde(default)] -pub struct AwsKmsConfig { - /// The AWS key identifier of the KMS key used to encrypt or decrypt data. - pub key_id: String, - - /// The AWS region to send KMS requests to. - pub region: String, -} - -/// Client for AWS KMS operations. -#[derive(Debug)] -pub struct AwsKmsClient { - inner_client: Client, - key_id: String, -} - -impl AwsKmsClient { - /// Constructs a new AWS KMS client. - pub async fn new(config: &AwsKmsConfig) -> Self { - let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; - - Self { - inner_client: Client::new(&sdk_config), - key_id: config.key_id.clone(), - } - } - - /// Decrypts the provided base64-encoded encrypted data using the AWS KMS SDK. We assume that - /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and - /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in - /// a machine that is able to assume an IAM role. - pub async fn decrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { - let start = Instant::now(); - let data = consts::BASE64_ENGINE - .decode(data) - .into_report() - .change_context(AwsKmsError::Base64DecodingFailed)?; - let ciphertext_blob = Blob::new(data); - - let decrypt_output = self - .inner_client - .decrypt() - .key_id(&self.key_id) - .ciphertext_blob(ciphertext_blob) - .send() - .await - .map_err(|error| { - // Logging using `Debug` representation of the error as the `Display` - // representation does not hold sufficient information. - logger::error!(aws_kms_sdk_error=?error, "Failed to AWS KMS decrypt data"); - metrics::AWS_KMS_DECRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); - error - }) - .into_report() - .change_context(AwsKmsError::DecryptionFailed)?; - - let output = decrypt_output - .plaintext - .ok_or(AwsKmsError::MissingPlaintextDecryptionOutput) - .into_report() - .and_then(|blob| { - String::from_utf8(blob.into_inner()) - .into_report() - .change_context(AwsKmsError::Utf8DecodingFailed) - })?; - - let time_taken = start.elapsed(); - metrics::AWS_KMS_DECRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); - - Ok(output) - } - - /// Encrypts the provided String data using the AWS KMS SDK. We assume that - /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and - /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in - /// a machine that is able to assume an IAM role. - pub async fn encrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { - let start = Instant::now(); - let plaintext_blob = Blob::new(data.as_ref()); - - let encrypted_output = self - .inner_client - .encrypt() - .key_id(&self.key_id) - .plaintext(plaintext_blob) - .send() - .await - .map_err(|error| { - // Logging using `Debug` representation of the error as the `Display` - // representation does not hold sufficient information. - logger::error!(aws_kms_sdk_error=?error, "Failed to AWS KMS encrypt data"); - metrics::AWS_KMS_ENCRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); - error - }) - .into_report() - .change_context(AwsKmsError::EncryptionFailed)?; - - let output = encrypted_output - .ciphertext_blob - .ok_or(AwsKmsError::MissingCiphertextEncryptionOutput) - .into_report() - .map(|blob| consts::BASE64_ENGINE.encode(blob.into_inner()))?; - let time_taken = start.elapsed(); - metrics::AWS_KMS_ENCRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); - - Ok(output) - } -} - -/// Errors that could occur during AWS KMS operations. -#[derive(Debug, thiserror::Error)] -pub enum AwsKmsError { - /// An error occurred when base64 encoding input data. - #[error("Failed to base64 encode input data")] - Base64EncodingFailed, - - /// An error occurred when base64 decoding input data. - #[error("Failed to base64 decode input data")] - Base64DecodingFailed, - - /// An error occurred when AWS KMS decrypting input data. - #[error("Failed to AWS KMS decrypt input data")] - DecryptionFailed, - - /// An error occurred when AWS KMS encrypting input data. - #[error("Failed to AWS KMS encrypt input data")] - EncryptionFailed, - - /// The AWS KMS decrypted output does not include a plaintext output. - #[error("Missing plaintext AWS KMS decryption output")] - MissingPlaintextDecryptionOutput, - - /// The AWS KMS encrypted output does not include a ciphertext output. - #[error("Missing ciphertext AWS KMS encryption output")] - MissingCiphertextEncryptionOutput, - - /// An error occurred UTF-8 decoding AWS KMS decrypted output. - #[error("Failed to UTF-8 decode decryption output")] - Utf8DecodingFailed, - - /// The AWS KMS client has not been initialized. - #[error("The AWS KMS client has not been initialized")] - AwsKmsClientNotInitialized, -} - -impl AwsKmsConfig { - /// Verifies that the [`AwsKmsClient`] configuration is usable. - pub fn validate(&self) -> Result<(), &'static str> { - use common_utils::{ext_traits::ConfigExt, fp_utils::when}; - - when(self.key_id.is_default_or_empty(), || { - Err("KMS AWS key ID must not be empty") - })?; - - when(self.region.is_default_or_empty(), || { - Err("KMS AWS region must not be empty") - }) - } -} - -/// A wrapper around a AWS KMS value that can be decrypted. -#[derive(Clone, Debug, Default, serde::Deserialize, Eq, PartialEq)] -#[serde(transparent)] -pub struct AwsKmsValue(Secret); - -impl common_utils::ext_traits::ConfigExt for AwsKmsValue { - fn is_empty_after_trim(&self) -> bool { - self.0.peek().is_empty_after_trim() - } -} - -impl From for AwsKmsValue { - fn from(value: String) -> Self { - Self(Secret::new(value)) - } -} - -impl From> for AwsKmsValue { - fn from(value: Secret) -> Self { - Self(value) - } -} - -#[cfg(feature = "hashicorp-vault")] -#[async_trait::async_trait] -impl super::hashicorp_vault::decrypt::VaultFetch for AwsKmsValue { - async fn fetch_inner( - self, - client: &super::hashicorp_vault::HashiCorpVault, - ) -> error_stack::Result - where - for<'a> En: super::hashicorp_vault::Engine< - ReturnType<'a, String> = std::pin::Pin< - Box< - dyn std::future::Future< - Output = error_stack::Result< - String, - super::hashicorp_vault::HashiCorpError, - >, - > + Send - + 'a, - >, - >, - > + 'a, - { - self.0.fetch_inner::(client).await.map(AwsKmsValue) - } -} - -#[cfg(test)] -mod tests { - #![allow(clippy::expect_used)] - #[tokio::test] - async fn check_aws_kms_encryption() { - std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); - std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); - use super::*; - let config = AwsKmsConfig { - key_id: "YOUR AWS KMS KEY ID".to_string(), - region: "AWS REGION".to_string(), - }; - - let data = "hello".to_string(); - let binding = data.as_bytes(); - let kms_encrypted_fingerprint = AwsKmsClient::new(&config) - .await - .encrypt(binding) - .await - .expect("aws kms encryption failed"); - - println!("{}", kms_encrypted_fingerprint); - } - - #[tokio::test] - async fn check_aws_kms_decrypt() { - std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); - std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); - use super::*; - let config = AwsKmsConfig { - key_id: "YOUR AWS KMS KEY ID".to_string(), - region: "AWS REGION".to_string(), - }; - - // Should decrypt to hello - let data = "AWS KMS ENCRYPTED CIPHER".to_string(); - let binding = data.as_bytes(); - let kms_encrypted_fingerprint = AwsKmsClient::new(&config) - .await - .decrypt(binding) - .await - .expect("aws kms decryption failed"); - - println!("{}", kms_encrypted_fingerprint); - } -} +pub mod implementers; diff --git a/crates/external_services/src/aws_kms/core.rs b/crates/external_services/src/aws_kms/core.rs new file mode 100644 index 00000000000..c01bcbfb094 --- /dev/null +++ b/crates/external_services/src/aws_kms/core.rs @@ -0,0 +1,299 @@ +//! Interactions with the AWS KMS SDK + +use std::time::Instant; + +use aws_config::meta::region::RegionProviderChain; +use aws_sdk_kms::{config::Region, primitives::Blob, Client}; +use base64::Engine; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; +use router_env::logger; + +#[cfg(feature = "hashicorp-vault")] +use crate::hashicorp_vault; +use crate::{aws_kms::decrypt::AwsKmsDecrypt, consts, metrics}; + +pub(crate) static AWS_KMS_CLIENT: tokio::sync::OnceCell = + tokio::sync::OnceCell::const_new(); + +/// Returns a shared AWS KMS client, or initializes a new one if not previously initialized. +#[inline] +pub async fn get_aws_kms_client(config: &AwsKmsConfig) -> &'static AwsKmsClient { + AWS_KMS_CLIENT + .get_or_init(|| AwsKmsClient::new(config)) + .await +} + +/// Configuration parameters required for constructing a [`AwsKmsClient`]. +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct AwsKmsConfig { + /// The AWS key identifier of the KMS key used to encrypt or decrypt data. + pub key_id: String, + + /// The AWS region to send KMS requests to. + pub region: String, +} + +/// Client for AWS KMS operations. +#[derive(Debug, Clone)] +pub struct AwsKmsClient { + inner_client: Client, + key_id: String, +} + +impl AwsKmsClient { + /// Constructs a new AWS KMS client. + pub async fn new(config: &AwsKmsConfig) -> Self { + let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone())); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + + Self { + inner_client: Client::new(&sdk_config), + key_id: config.key_id.clone(), + } + } + + /// Decrypts the provided base64-encoded encrypted data using the AWS KMS SDK. We assume that + /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and + /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in + /// a machine that is able to assume an IAM role. + pub async fn decrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { + let start = Instant::now(); + let data = consts::BASE64_ENGINE + .decode(data) + .into_report() + .change_context(AwsKmsError::Base64DecodingFailed)?; + let ciphertext_blob = Blob::new(data); + + let decrypt_output = self + .inner_client + .decrypt() + .key_id(&self.key_id) + .ciphertext_blob(ciphertext_blob) + .send() + .await + .map_err(|error| { + // Logging using `Debug` representation of the error as the `Display` + // representation does not hold sufficient information. + logger::error!(aws_kms_sdk_error=?error, "Failed to AWS KMS decrypt data"); + metrics::AWS_KMS_DECRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + .into_report() + .change_context(AwsKmsError::DecryptionFailed)?; + + let output = decrypt_output + .plaintext + .ok_or(AwsKmsError::MissingPlaintextDecryptionOutput) + .into_report() + .and_then(|blob| { + String::from_utf8(blob.into_inner()) + .into_report() + .change_context(AwsKmsError::Utf8DecodingFailed) + })?; + + let time_taken = start.elapsed(); + metrics::AWS_KMS_DECRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); + + Ok(output) + } + + /// Encrypts the provided String data using the AWS KMS SDK. We assume that + /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and + /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in + /// a machine that is able to assume an IAM role. + pub async fn encrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { + let start = Instant::now(); + let plaintext_blob = Blob::new(data.as_ref()); + + let encrypted_output = self + .inner_client + .encrypt() + .key_id(&self.key_id) + .plaintext(plaintext_blob) + .send() + .await + .map_err(|error| { + // Logging using `Debug` representation of the error as the `Display` + // representation does not hold sufficient information. + logger::error!(aws_kms_sdk_error=?error, "Failed to AWS KMS encrypt data"); + metrics::AWS_KMS_ENCRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + .into_report() + .change_context(AwsKmsError::EncryptionFailed)?; + + let output = encrypted_output + .ciphertext_blob + .ok_or(AwsKmsError::MissingCiphertextEncryptionOutput) + .into_report() + .map(|blob| consts::BASE64_ENGINE.encode(blob.into_inner()))?; + let time_taken = start.elapsed(); + metrics::AWS_KMS_ENCRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); + + Ok(output) + } +} + +/// Errors that could occur during KMS operations. +#[derive(Debug, thiserror::Error)] +pub enum AwsKmsError { + /// An error occurred when base64 encoding input data. + #[error("Failed to base64 encode input data")] + Base64EncodingFailed, + + /// An error occurred when base64 decoding input data. + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + + /// An error occurred when AWS KMS decrypting input data. + #[error("Failed to AWS KMS decrypt input data")] + DecryptionFailed, + + /// An error occurred when AWS KMS encrypting input data. + #[error("Failed to AWS KMS encrypt input data")] + EncryptionFailed, + + /// The AWS KMS decrypted output does not include a plaintext output. + #[error("Missing plaintext AWS KMS decryption output")] + MissingPlaintextDecryptionOutput, + + /// The AWS KMS encrypted output does not include a ciphertext output. + #[error("Missing ciphertext AWS KMS encryption output")] + MissingCiphertextEncryptionOutput, + + /// An error occurred UTF-8 decoding AWS KMS decrypted output. + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, + + /// The AWS KMS client has not been initialized. + #[error("The AWS KMS client has not been initialized")] + AwsKmsClientNotInitialized, +} + +impl AwsKmsConfig { + /// Verifies that the [`AwsKmsClient`] configuration is usable. + pub fn validate(&self) -> Result<(), &'static str> { + use common_utils::{ext_traits::ConfigExt, fp_utils::when}; + + when(self.key_id.is_default_or_empty(), || { + Err("KMS AWS key ID must not be empty") + })?; + + when(self.region.is_default_or_empty(), || { + Err("KMS AWS region must not be empty") + }) + } +} + +/// A wrapper around a AWS KMS value that can be decrypted. +#[derive(Clone, Debug, Default, serde::Deserialize, Eq, PartialEq)] +#[serde(transparent)] +pub struct AwsKmsValue(Secret); + +impl common_utils::ext_traits::ConfigExt for AwsKmsValue { + fn is_empty_after_trim(&self) -> bool { + self.0.peek().is_empty_after_trim() + } +} + +impl From for AwsKmsValue { + fn from(value: String) -> Self { + Self(Secret::new(value)) + } +} + +impl From> for AwsKmsValue { + fn from(value: Secret) -> Self { + Self(value) + } +} + +#[cfg(feature = "hashicorp-vault")] +#[async_trait::async_trait] +impl hashicorp_vault::decrypt::VaultFetch for AwsKmsValue { + async fn fetch_inner( + self, + client: &hashicorp_vault::core::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: hashicorp_vault::core::Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result< + String, + hashicorp_vault::core::HashiCorpError, + >, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.0.fetch_inner::(client).await.map(AwsKmsValue) + } +} + +#[async_trait::async_trait] +impl AwsKmsDecrypt for &AwsKmsValue { + type Output = String; + async fn decrypt_inner( + self, + aws_kms_client: &AwsKmsClient, + ) -> CustomResult { + aws_kms_client + .decrypt(self.0.peek()) + .await + .attach_printable("Failed to decrypt AWS KMS value") + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + #[tokio::test] + async fn check_aws_kms_encryption() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = AwsKmsConfig { + key_id: "YOUR AWS KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + let data = "hello".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = AwsKmsClient::new(&config) + .await + .encrypt(binding) + .await + .expect("aws kms encryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } + + #[tokio::test] + async fn check_aws_kms_decrypt() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = AwsKmsConfig { + key_id: "YOUR AWS KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + // Should decrypt to hello + let data = "AWS KMS ENCRYPTED CIPHER".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = AwsKmsClient::new(&config) + .await + .decrypt(binding) + .await + .expect("aws kms decryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } +} diff --git a/crates/external_services/src/aws_kms/decrypt.rs b/crates/external_services/src/aws_kms/decrypt.rs index 9abd4fefbef..47edb8f958f 100644 --- a/crates/external_services/src/aws_kms/decrypt.rs +++ b/crates/external_services/src/aws_kms/decrypt.rs @@ -1,6 +1,7 @@ +//! Decrypting data using the AWS KMS SDK. use common_utils::errors::CustomResult; -use super::*; +use crate::aws_kms::core::{AwsKmsClient, AwsKmsError, AWS_KMS_CLIENT}; #[async_trait::async_trait] /// This trait performs in place decryption of the structure on which this is implemented @@ -26,17 +27,3 @@ pub trait AwsKmsDecrypt { self.decrypt_inner(client).await } } - -#[async_trait::async_trait] -impl AwsKmsDecrypt for &AwsKmsValue { - type Output = String; - async fn decrypt_inner( - self, - aws_kms_client: &AwsKmsClient, - ) -> CustomResult { - aws_kms_client - .decrypt(self.0.peek()) - .await - .attach_printable("Failed to decrypt AWS KMS value") - } -} diff --git a/crates/external_services/src/aws_kms/implementers.rs b/crates/external_services/src/aws_kms/implementers.rs new file mode 100644 index 00000000000..cc00dbf3468 --- /dev/null +++ b/crates/external_services/src/aws_kms/implementers.rs @@ -0,0 +1,41 @@ +//! Trait implementations for aws kms client + +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use hyperswitch_interfaces::{ + encryption_interface::{EncryptionError, EncryptionManagementInterface}, + secrets_interface::{SecretManagementInterface, SecretsManagementError}, +}; +use masking::{PeekInterface, Secret}; + +use crate::aws_kms::core::AwsKmsClient; + +#[async_trait::async_trait] +impl EncryptionManagementInterface for AwsKmsClient { + async fn encrypt(&self, input: &[u8]) -> CustomResult, EncryptionError> { + self.encrypt(input) + .await + .change_context(EncryptionError::EncryptionFailed) + .map(|val| val.into_bytes()) + } + + async fn decrypt(&self, input: &[u8]) -> CustomResult, EncryptionError> { + self.decrypt(input) + .await + .change_context(EncryptionError::DecryptionFailed) + .map(|val| val.into_bytes()) + } +} + +#[async_trait::async_trait] +impl SecretManagementInterface for AwsKmsClient { + async fn get_secret( + &self, + input: Secret, + ) -> CustomResult, SecretsManagementError> { + self.decrypt(input.peek()) + .await + .change_context(SecretsManagementError::FetchSecretFailed) + .map(Into::into) + } +} diff --git a/crates/external_services/src/hashicorp_vault.rs b/crates/external_services/src/hashicorp_vault.rs index e31c8f01392..5a3ba539689 100644 --- a/crates/external_services/src/hashicorp_vault.rs +++ b/crates/external_services/src/hashicorp_vault.rs @@ -1,215 +1,7 @@ //! Interactions with the HashiCorp Vault -use std::{collections::HashMap, future::Future, pin::Pin}; +pub mod core; -use error_stack::{Report, ResultExt}; -use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; - -/// Utilities for supporting decryption of data pub mod decrypt; -static HC_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); - -#[allow(missing_debug_implementations)] -/// A struct representing a connection to HashiCorp Vault. -pub struct HashiCorpVault { - /// The underlying client used for interacting with HashiCorp Vault. - client: VaultClient, -} - -/// Configuration for connecting to HashiCorp Vault. -#[derive(Clone, Debug, Default, serde::Deserialize)] -#[serde(default)] -pub struct HashiCorpVaultConfig { - /// The URL of the HashiCorp Vault server. - pub url: String, - /// The authentication token used to access HashiCorp Vault. - pub token: String, -} - -/// Asynchronously retrieves a HashiCorp Vault client based on the provided configuration. -/// -/// # Parameters -/// -/// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. -pub async fn get_hashicorp_client( - config: &HashiCorpVaultConfig, -) -> error_stack::Result<&'static HashiCorpVault, HashiCorpError> { - HC_CLIENT - .get_or_try_init(|| async { HashiCorpVault::new(config) }) - .await -} - -/// A trait defining an engine for interacting with HashiCorp Vault. -pub trait Engine: Sized { - /// The associated type representing the return type of the engine's operations. - type ReturnType<'b, T> - where - T: 'b, - Self: 'b; - /// Reads data from HashiCorp Vault at the specified location. - /// - /// # Parameters - /// - /// - `client`: A reference to the HashiCorpVault client. - /// - `location`: The location in HashiCorp Vault to read data from. - /// - /// # Returns - /// - /// A future representing the result of the read operation. - fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String>; -} - -/// An implementation of the `Engine` trait for the Key-Value version 2 (Kv2) engine. -#[derive(Debug)] -pub enum Kv2 {} - -impl Engine for Kv2 { - type ReturnType<'b, T: 'b> = - Pin> + Send + 'b>>; - fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String> { - Box::pin(async move { - let mut split = location.split(':'); - let mount = split.next().ok_or(HashiCorpError::IncompleteData)?; - let path = split.next().ok_or(HashiCorpError::IncompleteData)?; - let key = split.next().unwrap_or("value"); - - let mut output = - vaultrs::kv2::read::>(&client.client, mount, path) - .await - .map_err(Into::>::into) - .change_context(HashiCorpError::FetchFailed)?; - - Ok(output.remove(key).ok_or(HashiCorpError::ParseError)?) - }) - } -} - -impl HashiCorpVault { - /// Creates a new instance of HashiCorpVault based on the provided configuration. - /// - /// # Parameters - /// - /// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. - /// - pub fn new(config: &HashiCorpVaultConfig) -> error_stack::Result { - VaultClient::new( - VaultClientSettingsBuilder::default() - .address(&config.url) - .token(&config.token) - .build() - .map_err(Into::>::into) - .change_context(HashiCorpError::ClientCreationFailed) - .attach_printable("Failed while building vault settings")?, - ) - .map_err(Into::>::into) - .change_context(HashiCorpError::ClientCreationFailed) - .map(|client| Self { client }) - } - - /// Asynchronously fetches data from HashiCorp Vault using the specified engine. - /// - /// # Parameters - /// - /// - `data`: A String representing the location or identifier of the data in HashiCorp Vault. - /// - /// # Type Parameters - /// - /// - `En`: The engine type that implements the `Engine` trait. - /// - `I`: The type that can be constructed from the retrieved encoded data. - /// - pub async fn fetch(&self, data: String) -> error_stack::Result - where - for<'a> En: Engine< - ReturnType<'a, String> = Pin< - Box< - dyn Future> - + Send - + 'a, - >, - >, - > + 'a, - I: FromEncoded, - { - let output = En::read(self, data).await?; - I::from_encoded(output).ok_or(error_stack::report!(HashiCorpError::HexDecodingFailed)) - } -} - -/// A trait for types that can be constructed from encoded data in the form of a String. -pub trait FromEncoded: Sized { - /// Constructs an instance of the type from the provided encoded input. - /// - /// # Parameters - /// - /// - `input`: A String containing the encoded data. - /// - /// # Returns - /// - /// An `Option` representing the constructed instance if successful, or `None` otherwise. - /// - /// # Example - /// - /// ```rust - /// # use your_module::{FromEncoded, masking::Secret, Vec}; - /// let secret_instance = Secret::::from_encoded("encoded_secret_string".to_string()); - /// let vec_instance = Vec::::from_encoded("68656c6c6f".to_string()); - /// ``` - fn from_encoded(input: String) -> Option; -} - -impl FromEncoded for masking::Secret { - fn from_encoded(input: String) -> Option { - Some(input.into()) - } -} - -impl FromEncoded for Vec { - fn from_encoded(input: String) -> Option { - hex::decode(input).ok() - } -} - -/// An enumeration representing various errors that can occur in interactions with HashiCorp Vault. -#[derive(Debug, thiserror::Error)] -pub enum HashiCorpError { - /// Failed while creating hashicorp client - #[error("Failed while creating a new client")] - ClientCreationFailed, - - /// Failed while building configurations for hashicorp client - #[error("Failed while building configuration")] - ConfigurationBuildFailed, - - /// Failed while decoding data to hex format - #[error("Failed while decoding hex data")] - HexDecodingFailed, - - /// An error occurred when base64 decoding input data. - #[error("Failed to base64 decode input data")] - Base64DecodingFailed, - - /// An error occurred when KMS decrypting input data. - #[error("Failed to KMS decrypt input data")] - DecryptionFailed, - - /// The KMS decrypted output does not include a plaintext output. - #[error("Missing plaintext KMS decryption output")] - MissingPlaintextDecryptionOutput, - - /// An error occurred UTF-8 decoding KMS decrypted output. - #[error("Failed to UTF-8 decode decryption output")] - Utf8DecodingFailed, - - /// Incomplete data provided to fetch data from hasicorp - #[error("Provided information about the value is incomplete")] - IncompleteData, - - /// Failed while fetching data from vault - #[error("Failed while fetching data from the server")] - FetchFailed, - - /// Failed while parsing received data - #[error("Failed while parsing the response")] - ParseError, -} +pub mod implementers; diff --git a/crates/external_services/src/hashicorp_vault/core.rs b/crates/external_services/src/hashicorp_vault/core.rs new file mode 100644 index 00000000000..86908b9d3d6 --- /dev/null +++ b/crates/external_services/src/hashicorp_vault/core.rs @@ -0,0 +1,227 @@ +//! Interactions with the HashiCorp Vault + +use std::{collections::HashMap, future::Future, pin::Pin}; + +use common_utils::{ext_traits::ConfigExt, fp_utils::when}; +use error_stack::{Report, ResultExt}; +use masking::{PeekInterface, Secret}; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +static HC_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); + +#[allow(missing_debug_implementations)] +/// A struct representing a connection to HashiCorp Vault. +pub struct HashiCorpVault { + /// The underlying client used for interacting with HashiCorp Vault. + client: VaultClient, +} + +/// Configuration for connecting to HashiCorp Vault. +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct HashiCorpVaultConfig { + /// The URL of the HashiCorp Vault server. + pub url: String, + /// The authentication token used to access HashiCorp Vault. + pub token: Secret, +} + +impl HashiCorpVaultConfig { + /// Verifies that the [`HashiCorpVault`] configuration is usable. + pub fn validate(&self) -> Result<(), &'static str> { + when(self.url.is_default_or_empty(), || { + Err("HashiCorp vault url must not be empty") + })?; + + when(self.token.is_default_or_empty(), || { + Err("HashiCorp vault token must not be empty") + }) + } +} + +/// Asynchronously retrieves a HashiCorp Vault client based on the provided configuration. +/// +/// # Parameters +/// +/// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. +pub async fn get_hashicorp_client( + config: &HashiCorpVaultConfig, +) -> error_stack::Result<&'static HashiCorpVault, HashiCorpError> { + HC_CLIENT + .get_or_try_init(|| async { HashiCorpVault::new(config) }) + .await +} + +/// A trait defining an engine for interacting with HashiCorp Vault. +pub trait Engine: Sized { + /// The associated type representing the return type of the engine's operations. + type ReturnType<'b, T> + where + T: 'b, + Self: 'b; + /// Reads data from HashiCorp Vault at the specified location. + /// + /// # Parameters + /// + /// - `client`: A reference to the HashiCorpVault client. + /// - `location`: The location in HashiCorp Vault to read data from. + /// + /// # Returns + /// + /// A future representing the result of the read operation. + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String>; +} + +/// An implementation of the `Engine` trait for the Key-Value version 2 (Kv2) engine. +#[derive(Debug)] +pub enum Kv2 {} + +impl Engine for Kv2 { + type ReturnType<'b, T: 'b> = + Pin> + Send + 'b>>; + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String> { + Box::pin(async move { + let mut split = location.split(':'); + let mount = split.next().ok_or(HashiCorpError::IncompleteData)?; + let path = split.next().ok_or(HashiCorpError::IncompleteData)?; + let key = split.next().unwrap_or("value"); + + let mut output = + vaultrs::kv2::read::>(&client.client, mount, path) + .await + .map_err(Into::>::into) + .change_context(HashiCorpError::FetchFailed)?; + + Ok(output.remove(key).ok_or(HashiCorpError::ParseError)?) + }) + } +} + +impl HashiCorpVault { + /// Creates a new instance of HashiCorpVault based on the provided configuration. + /// + /// # Parameters + /// + /// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. + /// + pub fn new(config: &HashiCorpVaultConfig) -> error_stack::Result { + VaultClient::new( + VaultClientSettingsBuilder::default() + .address(&config.url) + .token(config.token.peek()) + .build() + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .attach_printable("Failed while building vault settings")?, + ) + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .map(|client| Self { client }) + } + + /// Asynchronously fetches data from HashiCorp Vault using the specified engine. + /// + /// # Parameters + /// + /// - `data`: A String representing the location or identifier of the data in HashiCorp Vault. + /// + /// # Type Parameters + /// + /// - `En`: The engine type that implements the `Engine` trait. + /// - `I`: The type that can be constructed from the retrieved encoded data. + /// + pub async fn fetch(&self, data: String) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + I: FromEncoded, + { + let output = En::read(self, data).await?; + I::from_encoded(output).ok_or(error_stack::report!(HashiCorpError::HexDecodingFailed)) + } +} + +/// A trait for types that can be constructed from encoded data in the form of a String. +pub trait FromEncoded: Sized { + /// Constructs an instance of the type from the provided encoded input. + /// + /// # Parameters + /// + /// - `input`: A String containing the encoded data. + /// + /// # Returns + /// + /// An `Option` representing the constructed instance if successful, or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// # use your_module::{FromEncoded, masking::Secret, Vec}; + /// let secret_instance = Secret::::from_encoded("encoded_secret_string".to_string()); + /// let vec_instance = Vec::::from_encoded("68656c6c6f".to_string()); + /// ``` + fn from_encoded(input: String) -> Option; +} + +impl FromEncoded for Secret { + fn from_encoded(input: String) -> Option { + Some(input.into()) + } +} + +impl FromEncoded for Vec { + fn from_encoded(input: String) -> Option { + hex::decode(input).ok() + } +} + +/// An enumeration representing various errors that can occur in interactions with HashiCorp Vault. +#[derive(Debug, thiserror::Error)] +pub enum HashiCorpError { + /// Failed while creating hashicorp client + #[error("Failed while creating a new client")] + ClientCreationFailed, + + /// Failed while building configurations for hashicorp client + #[error("Failed while building configuration")] + ConfigurationBuildFailed, + + /// Failed while decoding data to hex format + #[error("Failed while decoding hex data")] + HexDecodingFailed, + + /// An error occurred when base64 decoding input data. + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + + /// An error occurred when KMS decrypting input data. + #[error("Failed to KMS decrypt input data")] + DecryptionFailed, + + /// The KMS decrypted output does not include a plaintext output. + #[error("Missing plaintext KMS decryption output")] + MissingPlaintextDecryptionOutput, + + /// An error occurred UTF-8 decoding KMS decrypted output. + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, + + /// Incomplete data provided to fetch data from hasicorp + #[error("Provided information about the value is incomplete")] + IncompleteData, + + /// Failed while fetching data from vault + #[error("Failed while fetching data from the server")] + FetchFailed, + + /// Failed while parsing received data + #[error("Failed while parsing the response")] + ParseError, +} diff --git a/crates/external_services/src/hashicorp_vault/decrypt.rs b/crates/external_services/src/hashicorp_vault/decrypt.rs index 1bc1b6ffa16..650b219841f 100644 --- a/crates/external_services/src/hashicorp_vault/decrypt.rs +++ b/crates/external_services/src/hashicorp_vault/decrypt.rs @@ -1,7 +1,11 @@ +//! Utilities for supporting decryption of data + use std::{future::Future, pin::Pin}; use masking::ExposeInterface; +use crate::hashicorp_vault::core::{Engine, HashiCorpError, HashiCorpVault}; + /// A trait for types that can be asynchronously fetched and decrypted from HashiCorp Vault. #[async_trait::async_trait] pub trait VaultFetch: Sized { @@ -14,13 +18,13 @@ pub trait VaultFetch: Sized { /// async fn fetch_inner( self, - client: &super::HashiCorpVault, - ) -> error_stack::Result + client: &HashiCorpVault, + ) -> error_stack::Result where - for<'a> En: super::Engine< + for<'a> En: Engine< ReturnType<'a, String> = Pin< Box< - dyn Future> + dyn Future> + Send + 'a, >, @@ -32,13 +36,13 @@ pub trait VaultFetch: Sized { impl VaultFetch for masking::Secret { async fn fetch_inner( self, - client: &super::HashiCorpVault, - ) -> error_stack::Result + client: &HashiCorpVault, + ) -> error_stack::Result where - for<'a> En: super::Engine< + for<'a> En: Engine< ReturnType<'a, String> = Pin< Box< - dyn Future> + dyn Future> + Send + 'a, >, diff --git a/crates/external_services/src/hashicorp_vault/implementers.rs b/crates/external_services/src/hashicorp_vault/implementers.rs new file mode 100644 index 00000000000..52fec2bd8d3 --- /dev/null +++ b/crates/external_services/src/hashicorp_vault/implementers.rs @@ -0,0 +1,24 @@ +//! Trait implementations for Hashicorp vault client + +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use hyperswitch_interfaces::secrets_interface::{ + SecretManagementInterface, SecretsManagementError, +}; +use masking::{ExposeInterface, Secret}; + +use crate::hashicorp_vault::core::{HashiCorpVault, Kv2}; + +#[async_trait::async_trait] +impl SecretManagementInterface for HashiCorpVault { + async fn get_secret( + &self, + input: Secret, + ) -> CustomResult, SecretsManagementError> { + self.fetch::>(input.expose()) + .await + .map(|val| val.expose().to_owned()) + .change_context(SecretsManagementError::FetchSecretFailed) + .map(Into::into) + } +} diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index cdabdeb49ae..33ab7541974 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -13,6 +13,10 @@ pub mod file_storage; #[cfg(feature = "hashicorp-vault")] pub mod hashicorp_vault; +pub mod no_encryption; + +pub mod managers; + /// Crate specific constants #[cfg(feature = "aws_kms")] pub mod consts { diff --git a/crates/external_services/src/managers.rs b/crates/external_services/src/managers.rs new file mode 100644 index 00000000000..cb33d7bbf38 --- /dev/null +++ b/crates/external_services/src/managers.rs @@ -0,0 +1,5 @@ +//! Config and client managers + +pub mod encryption_management; + +pub mod secrets_management; diff --git a/crates/external_services/src/managers/encryption_management.rs b/crates/external_services/src/managers/encryption_management.rs new file mode 100644 index 00000000000..4612190926c --- /dev/null +++ b/crates/external_services/src/managers/encryption_management.rs @@ -0,0 +1,53 @@ +//! +//! Encryption management util module +//! + +use common_utils::errors::CustomResult; +use hyperswitch_interfaces::encryption_interface::{ + EncryptionError, EncryptionManagementInterface, +}; + +#[cfg(feature = "aws_kms")] +use crate::aws_kms; +use crate::no_encryption::core::NoEncryption; + +/// Enum representing configuration options for encryption management. +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(tag = "encryption_manager")] +#[serde(rename_all = "snake_case")] +pub enum EncryptionManagementConfig { + /// AWS KMS configuration + #[cfg(feature = "aws_kms")] + AwsKms { + /// AWS KMS config + aws_kms: aws_kms::core::AwsKmsConfig, + }, + + /// Variant representing no encryption + #[default] + NoEncryption, +} + +impl EncryptionManagementConfig { + /// Verifies that the client configuration is usable + pub fn validate(&self) -> Result<(), &'static str> { + match self { + #[cfg(feature = "aws_kms")] + Self::AwsKms { aws_kms } => aws_kms.validate(), + + Self::NoEncryption => Ok(()), + } + } + + /// Retrieves the appropriate encryption client based on the configuration. + pub async fn get_encryption_management_client( + &self, + ) -> CustomResult, EncryptionError> { + Ok(match self { + #[cfg(feature = "aws_kms")] + Self::AwsKms { aws_kms } => Box::new(aws_kms::core::AwsKmsClient::new(aws_kms).await), + + Self::NoEncryption => Box::new(NoEncryption), + }) + } +} diff --git a/crates/external_services/src/managers/secrets_management.rs b/crates/external_services/src/managers/secrets_management.rs new file mode 100644 index 00000000000..b79046b4c75 --- /dev/null +++ b/crates/external_services/src/managers/secrets_management.rs @@ -0,0 +1,72 @@ +//! +//! Secrets management util module +//! + +use common_utils::errors::CustomResult; +#[cfg(feature = "hashicorp-vault")] +use error_stack::ResultExt; +use hyperswitch_interfaces::secrets_interface::{ + SecretManagementInterface, SecretsManagementError, +}; + +#[cfg(feature = "aws_kms")] +use crate::aws_kms; +#[cfg(feature = "hashicorp-vault")] +use crate::hashicorp_vault; +use crate::no_encryption::core::NoEncryption; + +/// Enum representing configuration options for secrets management. +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(tag = "secrets_manager")] +#[serde(rename_all = "snake_case")] +pub enum SecretsManagementConfig { + /// AWS KMS configuration + #[cfg(feature = "aws_kms")] + AwsKms { + /// AWS KMS config + aws_kms: aws_kms::core::AwsKmsConfig, + }, + + /// HashiCorp-Vault configuration + #[cfg(feature = "hashicorp-vault")] + HashiCorpVault { + /// HC-Vault config + hc_vault: hashicorp_vault::core::HashiCorpVaultConfig, + }, + + /// Variant representing no encryption + #[default] + NoEncryption, +} + +impl SecretsManagementConfig { + /// Verifies that the client configuration is usable + pub fn validate(&self) -> Result<(), &'static str> { + match self { + #[cfg(feature = "aws_kms")] + Self::AwsKms { aws_kms } => aws_kms.validate(), + #[cfg(feature = "hashicorp-vault")] + Self::HashiCorpVault { hc_vault } => hc_vault.validate(), + Self::NoEncryption => Ok(()), + } + } + + /// Retrieves the appropriate secret management client based on the configuration. + pub async fn get_secret_management_client( + &self, + ) -> CustomResult, SecretsManagementError> { + match self { + #[cfg(feature = "aws_kms")] + Self::AwsKms { aws_kms } => { + Ok(Box::new(aws_kms::core::AwsKmsClient::new(aws_kms).await)) + } + #[cfg(feature = "hashicorp-vault")] + Self::HashiCorpVault { hc_vault } => { + hashicorp_vault::core::HashiCorpVault::new(hc_vault) + .change_context(SecretsManagementError::ClientCreationFailed) + .map(|inner| -> Box { Box::new(inner) }) + } + Self::NoEncryption => Ok(Box::new(NoEncryption)), + } + } +} diff --git a/crates/external_services/src/no_encryption.rs b/crates/external_services/src/no_encryption.rs new file mode 100644 index 00000000000..17c29618f89 --- /dev/null +++ b/crates/external_services/src/no_encryption.rs @@ -0,0 +1,7 @@ +//! +//! No encryption functionalities +//! + +pub mod core; + +pub mod implementers; diff --git a/crates/external_services/src/no_encryption/core.rs b/crates/external_services/src/no_encryption/core.rs new file mode 100644 index 00000000000..a55815c03f9 --- /dev/null +++ b/crates/external_services/src/no_encryption/core.rs @@ -0,0 +1,17 @@ +//! No encryption core functionalities + +/// No encryption type +#[derive(Debug, Clone)] +pub struct NoEncryption; + +impl NoEncryption { + /// Encryption functionality + pub fn encrypt(&self, data: impl AsRef<[u8]>) -> Vec { + data.as_ref().into() + } + + /// Decryption functionality + pub fn decrypt(&self, data: impl AsRef<[u8]>) -> Vec { + data.as_ref().into() + } +} diff --git a/crates/external_services/src/no_encryption/implementers.rs b/crates/external_services/src/no_encryption/implementers.rs new file mode 100644 index 00000000000..f67c7c85fd5 --- /dev/null +++ b/crates/external_services/src/no_encryption/implementers.rs @@ -0,0 +1,36 @@ +//! Trait implementations for No encryption client + +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use hyperswitch_interfaces::{ + encryption_interface::{EncryptionError, EncryptionManagementInterface}, + secrets_interface::{SecretManagementInterface, SecretsManagementError}, +}; +use masking::{ExposeInterface, Secret}; + +use crate::no_encryption::core::NoEncryption; + +#[async_trait::async_trait] +impl EncryptionManagementInterface for NoEncryption { + async fn encrypt(&self, input: &[u8]) -> CustomResult, EncryptionError> { + Ok(self.encrypt(input)) + } + + async fn decrypt(&self, input: &[u8]) -> CustomResult, EncryptionError> { + Ok(self.decrypt(input)) + } +} + +#[async_trait::async_trait] +impl SecretManagementInterface for NoEncryption { + async fn get_secret( + &self, + input: Secret, + ) -> CustomResult, SecretsManagementError> { + String::from_utf8(self.decrypt(input.expose())) + .map(Into::into) + .into_report() + .change_context(SecretsManagementError::FetchSecretFailed) + .attach_printable("Failed to convert decrypted value to UTF-8") + } +} diff --git a/crates/hyperswitch_interfaces/Cargo.toml b/crates/hyperswitch_interfaces/Cargo.toml new file mode 100644 index 00000000000..855cf63917f --- /dev/null +++ b/crates/hyperswitch_interfaces/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hyperswitch_interfaces" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +readme = "README.md" +license.workspace = true + +[dependencies] +async-trait = "0.1.68" +dyn-clone = "1.0.11" +serde = { version = "1.0.193", features = ["derive"] } +thiserror = "1.0.40" + +# First party crates +common_utils = { version = "0.1.0", path = "../common_utils" } +masking = { version = "0.1.0", path = "../masking" } diff --git a/crates/hyperswitch_interfaces/README.md b/crates/hyperswitch_interfaces/README.md new file mode 100644 index 00000000000..e96e3eabe2a --- /dev/null +++ b/crates/hyperswitch_interfaces/README.md @@ -0,0 +1,3 @@ +# Hyperswitch Interfaces + +This crate includes interfaces and its error types diff --git a/crates/hyperswitch_interfaces/src/encryption_interface.rs b/crates/hyperswitch_interfaces/src/encryption_interface.rs new file mode 100644 index 00000000000..1993019a213 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/encryption_interface.rs @@ -0,0 +1,29 @@ +//! Encryption related interface and error types + +#![warn(missing_docs, missing_debug_implementations)] + +use common_utils::errors::CustomResult; + +/// Trait defining the interface for encryption management +#[async_trait::async_trait] +pub trait EncryptionManagementInterface: Sync + Send + dyn_clone::DynClone { + /// Encrypt the given input data + async fn encrypt(&self, input: &[u8]) -> CustomResult, EncryptionError>; + + /// Decrypt the given input data + async fn decrypt(&self, input: &[u8]) -> CustomResult, EncryptionError>; +} + +dyn_clone::clone_trait_object!(EncryptionManagementInterface); + +/// Errors that may occur during above encryption functionalities +#[derive(Debug, thiserror::Error)] +pub enum EncryptionError { + /// An error occurred when encrypting input data. + #[error("Failed to encrypt input data")] + EncryptionFailed, + + /// An error occurred when decrypting input data. + #[error("Failed to decrypt input data")] + DecryptionFailed, +} diff --git a/crates/hyperswitch_interfaces/src/lib.rs b/crates/hyperswitch_interfaces/src/lib.rs new file mode 100644 index 00000000000..3f7b8d41c3e --- /dev/null +++ b/crates/hyperswitch_interfaces/src/lib.rs @@ -0,0 +1,7 @@ +//! Hyperswitch interface + +#![warn(missing_docs, missing_debug_implementations)] + +pub mod secrets_interface; + +pub mod encryption_interface; diff --git a/crates/hyperswitch_interfaces/src/secrets_interface.rs b/crates/hyperswitch_interfaces/src/secrets_interface.rs new file mode 100644 index 00000000000..761981bedda --- /dev/null +++ b/crates/hyperswitch_interfaces/src/secrets_interface.rs @@ -0,0 +1,36 @@ +//! Secrets management interface + +pub mod secret_handler; + +pub mod secret_state; + +use common_utils::errors::CustomResult; +use masking::Secret; + +/// Trait defining the interface for managing application secrets +#[async_trait::async_trait] +pub trait SecretManagementInterface: Send + Sync { + /// Given an input, encrypt/store the secret + // async fn store_secret( + // &self, + // input: Secret, + // ) -> CustomResult; + + /// Given an input, decrypt/retrieve the secret + async fn get_secret( + &self, + input: Secret, + ) -> CustomResult, SecretsManagementError>; +} + +/// Errors that may occur during secret management +#[derive(Debug, thiserror::Error)] +pub enum SecretsManagementError { + /// An error occurred when retrieving raw data. + #[error("Failed to fetch the raw data")] + FetchSecretFailed, + + /// Failed while creating kms client + #[error("Failed while creating a secrets management client")] + ClientCreationFailed, +} diff --git a/crates/hyperswitch_interfaces/src/secrets_interface/secret_handler.rs b/crates/hyperswitch_interfaces/src/secrets_interface/secret_handler.rs new file mode 100644 index 00000000000..a0915b8de69 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/secrets_interface/secret_handler.rs @@ -0,0 +1,21 @@ +//! Module containing trait for raw secret retrieval + +use common_utils::errors::CustomResult; + +use crate::secrets_interface::{ + secret_state::{RawSecret, SecretStateContainer, SecuredSecret}, + SecretManagementInterface, SecretsManagementError, +}; + +/// Trait defining the interface for retrieving a raw secret value, given a secured value +#[async_trait::async_trait] +pub trait SecretsHandler +where + Self: Sized, +{ + /// Construct `Self` with raw secret value and transitions its type from `SecuredSecret` to `RawSecret` + async fn convert_to_raw_secret( + value: SecretStateContainer, + kms_client: Box, + ) -> CustomResult, SecretsManagementError>; +} diff --git a/crates/hyperswitch_interfaces/src/secrets_interface/secret_state.rs b/crates/hyperswitch_interfaces/src/secrets_interface/secret_state.rs new file mode 100644 index 00000000000..6d3f75500af --- /dev/null +++ b/crates/hyperswitch_interfaces/src/secrets_interface/secret_state.rs @@ -0,0 +1,71 @@ +//! Module to manage encrypted and decrypted states for a given type. + +use std::marker::PhantomData; + +use serde::{Deserialize, Deserializer}; + +/// Trait defining the states of a secret +pub trait SecretState {} + +/// Decrypted state +#[derive(Debug, Clone, Deserialize)] +pub enum RawSecret {} + +/// Encrypted state +#[derive(Debug, Clone, Deserialize)] +pub enum SecuredSecret {} + +impl SecretState for RawSecret {} +impl SecretState for SecuredSecret {} + +/// Struct for managing the encrypted and decrypted states of a given type +#[derive(Debug, Clone, Default)] +pub struct SecretStateContainer { + inner: T, + marker: PhantomData, +} + +impl SecretStateContainer { + /// + /// Get the inner data while consuming self + /// + #[inline] + pub fn into_inner(self) -> T { + self.inner + } + + /// + /// Get the reference to inner value + /// + #[inline] + pub fn get_inner(&self) -> &T { + &self.inner + } +} + +impl<'de, T: Deserialize<'de>, S: SecretState> Deserialize<'de> for SecretStateContainer { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let val = Deserialize::deserialize(deserializer)?; + Ok(Self { + inner: val, + marker: PhantomData, + }) + } +} + +impl SecretStateContainer { + /// Transition the secret state from `SecuredSecret` to `RawSecret` + pub fn transition_state( + mut self, + decryptor_fn: impl FnOnce(T) -> T, + ) -> SecretStateContainer { + self.inner = decryptor_fn(self.inner); + SecretStateContainer { + inner: self.inner, + marker: PhantomData, + } + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 9b345217bb5..690a0676fa6 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -111,6 +111,7 @@ diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_ euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } pm_auth = { version = "0.1.0", path = "../pm_auth", package = "pm_auth" } external_services = { version = "0.1.0", path = "../external_services" } +hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces" } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } diff --git a/crates/router/src/configs/aws_kms.rs b/crates/router/src/configs/aws_kms.rs index 5e37f2a4dd6..9bb5fcc6f20 100644 --- a/crates/router/src/configs/aws_kms.rs +++ b/crates/router/src/configs/aws_kms.rs @@ -1,5 +1,8 @@ use common_utils::errors::CustomResult; -use external_services::aws_kms::{decrypt::AwsKmsDecrypt, AwsKmsClient, AwsKmsError}; +use external_services::aws_kms::{ + core::{AwsKmsClient, AwsKmsError}, + decrypt::AwsKmsDecrypt, +}; use masking::ExposeInterface; use crate::configs::settings; diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 71cd61ffa80..82237e7a887 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use api_models::{enums, payment_methods::RequiredFieldInfo}; #[cfg(feature = "aws_kms")] -use external_services::aws_kms::AwsKmsValue; +use external_services::aws_kms::core::AwsKmsValue; use super::settings::{ConnectorFields, Password, PaymentMethodType, RequiredFieldFinal}; diff --git a/crates/router/src/configs/hc_vault.rs b/crates/router/src/configs/hc_vault.rs index f20d8e79ed8..fa6596f7494 100644 --- a/crates/router/src/configs/hc_vault.rs +++ b/crates/router/src/configs/hc_vault.rs @@ -1,5 +1,6 @@ use external_services::hashicorp_vault::{ - decrypt::VaultFetch, Engine, HashiCorpError, HashiCorpVault, + core::{Engine, HashiCorpError, HashiCorpVault}, + decrypt::VaultFetch, }; use masking::ExposeInterface; diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 2572d764d06..13c3ba23e1d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -12,9 +12,15 @@ use config::{Environment, File}; use external_services::aws_kms; #[cfg(feature = "email")] use external_services::email::EmailSettings; -use external_services::file_storage::FileStorageConfig; #[cfg(feature = "hashicorp-vault")] use external_services::hashicorp_vault; +use external_services::{ + file_storage::FileStorageConfig, + managers::{ + encryption_management::EncryptionManagementConfig, + secrets_management::SecretsManagementConfig, + }, +}; use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use rust_decimal::Decimal; @@ -30,7 +36,7 @@ use crate::{ events::EventsConfig, }; #[cfg(feature = "aws_kms")] -pub type Password = aws_kms::AwsKmsValue; +pub type Password = aws_kms::core::AwsKmsValue; #[cfg(not(feature = "aws_kms"))] pub type Password = masking::Secret; @@ -89,10 +95,12 @@ pub struct Settings { pub bank_config: BankRedirectConfig, pub api_keys: ApiKeys, #[cfg(feature = "aws_kms")] - pub kms: aws_kms::AwsKmsConfig, + pub kms: aws_kms::core::AwsKmsConfig, pub file_storage: FileStorageConfig, + pub encryption_management: EncryptionManagementConfig, + pub secrets_management: SecretsManagementConfig, #[cfg(feature = "hashicorp-vault")] - pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, + pub hc_vault: hashicorp_vault::core::HashiCorpVaultConfig, pub tokenization: TokenizationConfig, pub connector_customer: ConnectorCustomer, #[cfg(feature = "dummy_connector")] @@ -368,11 +376,11 @@ pub struct Secrets { pub recon_admin_api_key: String, pub master_enc_key: Password, #[cfg(feature = "aws_kms")] - pub kms_encrypted_jwt_secret: aws_kms::AwsKmsValue, + pub kms_encrypted_jwt_secret: aws_kms::core::AwsKmsValue, #[cfg(feature = "aws_kms")] - pub kms_encrypted_admin_api_key: aws_kms::AwsKmsValue, + pub kms_encrypted_admin_api_key: aws_kms::core::AwsKmsValue, #[cfg(feature = "aws_kms")] - pub kms_encrypted_recon_admin_api_key: aws_kms::AwsKmsValue, + pub kms_encrypted_recon_admin_api_key: aws_kms::core::AwsKmsValue, } #[derive(Debug, Deserialize, Clone)] @@ -598,7 +606,7 @@ pub struct ApiKeys { /// Base64-encoded (KMS encrypted) ciphertext of the key used for calculating hashes of API /// keys #[cfg(feature = "aws_kms")] - pub kms_encrypted_hash_key: aws_kms::AwsKmsValue, + pub kms_encrypted_hash_key: aws_kms::core::AwsKmsValue, /// Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating /// hashes of API keys @@ -724,6 +732,14 @@ impl Settings { self.lock_settings.validate()?; self.events.validate()?; + + self.encryption_management + .validate() + .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?; + + self.secrets_management + .validate() + .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?; Ok(()) } } diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 162f8bed0f8..b694d1291a8 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -38,9 +38,9 @@ static HASH_KEY: tokio::sync::OnceCell errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { HASH_KEY .get_or_try_init(|| async { @@ -57,7 +57,7 @@ pub async fn get_hash_key( #[cfg(feature = "hashicorp-vault")] let hash_key = hash_key - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; @@ -153,9 +153,9 @@ impl PlaintextApiKey { #[instrument(skip_all)] pub async fn create_api_key( state: AppState, - #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::AwsKmsClient, + #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::core::AwsKmsClient, #[cfg(feature = "hashicorp-vault")] - hc_client: &external_services::hashicorp_vault::HashiCorpVault, + hc_client: &external_services::hashicorp_vault::core::HashiCorpVault, api_key: api::CreateApiKeyRequest, merchant_id: String, ) -> RouterResponse { @@ -590,9 +590,9 @@ mod tests { let hash_key = get_hash_key( &settings.api_keys, #[cfg(feature = "aws_kms")] - external_services::aws_kms::get_aws_kms_client(&settings.kms).await, + external_services::aws_kms::core::get_aws_kms_client(&settings.kms).await, #[cfg(feature = "hashicorp-vault")] - external_services::hashicorp_vault::get_hashicorp_client(&settings.hc_vault) + external_services::hashicorp_vault::core::get_hashicorp_client(&settings.hc_vault) .await .unwrap(), ) diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs index 43701c80786..733b565beed 100644 --- a/crates/router/src/core/blocklist/utils.rs +++ b/crates/router/src/core/blocklist/utils.rs @@ -48,7 +48,7 @@ pub async fn delete_entry_from_blocklist( })?; #[cfg(feature = "aws_kms")] - let decrypted_fingerprint = aws_kms::get_aws_kms_client(&state.conf.kms) + let decrypted_fingerprint = aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(blocklist_fingerprint.encrypted_fingerprint) .await @@ -244,7 +244,7 @@ pub async fn insert_entry_into_blocklist( })?; #[cfg(feature = "aws_kms")] - let decrypted_fingerprint = aws_kms::get_aws_kms_client(&state.conf.kms) + let decrypted_fingerprint = aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(blocklist_fingerprint.encrypted_fingerprint) .await diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 062c58e056c..479a0685a97 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -189,12 +189,13 @@ async fn create_applepay_session_token( common_merchant_identifier, ) = async { #[cfg(feature = "hashicorp-vault")] - let client = external_services::hashicorp_vault::get_hashicorp_client( - &state.conf.hc_vault, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while building hashicorp client")?; + let client = + external_services::hashicorp_vault::core::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while building hashicorp client")?; #[cfg(feature = "hashicorp-vault")] { @@ -206,7 +207,7 @@ async fn create_applepay_session_token( .apple_pay_merchant_cert .clone(), ) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(errors::ApiErrorResponse::InternalServerError)? .expose(), @@ -217,7 +218,7 @@ async fn create_applepay_session_token( .apple_pay_merchant_cert_key .clone(), ) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(errors::ApiErrorResponse::InternalServerError)? .expose(), @@ -228,7 +229,7 @@ async fn create_applepay_session_token( .common_merchant_identifier .clone(), ) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(errors::ApiErrorResponse::InternalServerError)? .expose(), @@ -260,7 +261,7 @@ async fn create_applepay_session_token( #[cfg(feature = "aws_kms")] let decrypted_apple_pay_merchant_cert = - aws_kms::get_aws_kms_client(&state.conf.kms) + aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(apple_pay_merchant_cert) .await @@ -269,7 +270,7 @@ async fn create_applepay_session_token( #[cfg(feature = "aws_kms")] let decrypted_apple_pay_merchant_cert_key = - aws_kms::get_aws_kms_client(&state.conf.kms) + aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(apple_pay_merchant_cert_key) .await @@ -280,7 +281,7 @@ async fn create_applepay_session_token( #[cfg(feature = "aws_kms")] let decrypted_merchant_identifier = - aws_kms::get_aws_kms_client(&state.conf.kms) + aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(common_merchant_identifier) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 7ec1f5e9213..b93d959eb8d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3537,16 +3537,17 @@ impl ApplePayData { ) -> CustomResult { let apple_pay_ppc = async { #[cfg(feature = "hashicorp-vault")] - let client = - external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) - .await - .change_context(errors::ApplePayDecryptionError::DecryptionFailed) - .attach_printable("Failed while creating client")?; + let client = external_services::hashicorp_vault::core::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; #[cfg(feature = "hashicorp-vault")] let output = masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc.clone()) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)? .expose(); @@ -3559,7 +3560,7 @@ impl ApplePayData { .await?; #[cfg(feature = "aws_kms")] - let cert_data = aws_kms::get_aws_kms_client(&state.conf.kms) + let cert_data = aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(&apple_pay_ppc) .await @@ -3620,16 +3621,17 @@ impl ApplePayData { let apple_pay_ppc_key = async { #[cfg(feature = "hashicorp-vault")] - let client = - external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) - .await - .change_context(errors::ApplePayDecryptionError::DecryptionFailed) - .attach_printable("Failed while creating client")?; + let client = external_services::hashicorp_vault::core::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; #[cfg(feature = "hashicorp-vault")] let output = masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone()) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed) .attach_printable("Failed while creating client")? @@ -3643,7 +3645,7 @@ impl ApplePayData { .await?; #[cfg(feature = "aws_kms")] - let decrypted_apple_pay_ppc_key = aws_kms::get_aws_kms_client(&state.conf.kms) + let decrypted_apple_pay_ppc_key = aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(&apple_pay_ppc_key) .await diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index f77d5120812..f00c452f3ea 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -349,14 +349,15 @@ async fn store_bank_details_in_payment_methods( let pm_auth_key = async { #[cfg(feature = "hashicorp-vault")] - let client = external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) - .await - .change_context(ApiErrorResponse::InternalServerError) - .attach_printable("Failed while creating client")?; + let client = + external_services::hashicorp_vault::core::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while creating client")?; #[cfg(feature = "hashicorp-vault")] let output = masking::Secret::new(state.conf.payment_method_auth.pm_auth_key.clone()) - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(ApiErrorResponse::InternalServerError)? .expose(); @@ -369,7 +370,7 @@ async fn store_bank_details_in_payment_methods( .await?; #[cfg(feature = "aws_kms")] - let pm_auth_key = aws_kms::get_aws_kms_client(&state.conf.kms) + let pm_auth_key = aws_kms::core::get_aws_kms_client(&state.conf.kms) .await .decrypt(pm_auth_key) .await diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index 0ed70e6e0c6..79513bfb308 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -13,7 +13,7 @@ pub async fn verify_merchant_creds_for_applepay( state: AppState, _req: &actix_web::HttpRequest, body: verifications::ApplepayMerchantVerificationRequest, - kms_config: &aws_kms::AwsKmsConfig, + kms_config: &aws_kms::core::AwsKmsConfig, merchant_id: String, ) -> CustomResult< services::ApplicationResponse, @@ -27,19 +27,19 @@ pub async fn verify_merchant_creds_for_applepay( let encrypted_key = &state.conf.applepay_merchant_configs.merchant_cert_key; let applepay_endpoint = &state.conf.applepay_merchant_configs.applepay_endpoint; - let applepay_internal_merchant_identifier = aws_kms::get_aws_kms_client(kms_config) + let applepay_internal_merchant_identifier = aws_kms::core::get_aws_kms_client(kms_config) .await .decrypt(encrypted_merchant_identifier) .await .change_context(api_error_response::ApiErrorResponse::InternalServerError)?; - let cert_data = aws_kms::get_aws_kms_client(kms_config) + let cert_data = aws_kms::core::get_aws_kms_client(kms_config) .await .decrypt(encrypted_cert) .await .change_context(api_error_response::ApiErrorResponse::InternalServerError)?; - let key_data = aws_kms::get_aws_kms_client(kms_config) + let key_data = aws_kms::core::get_aws_kms_client(kms_config) .await .decrypt(encrypted_key) .await diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index cf859d048dd..cf0f009a0b1 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -46,10 +46,10 @@ pub async fn api_key_create( |state, _, payload| async { #[cfg(feature = "aws_kms")] let aws_kms_client = - external_services::aws_kms::get_aws_kms_client(&state.clone().conf.kms).await; + external_services::aws_kms::core::get_aws_kms_client(&state.clone().conf.kms).await; #[cfg(feature = "hashicorp-vault")] - let hc_client = external_services::hashicorp_vault::get_hashicorp_client( + let hc_client = external_services::hashicorp_vault::core::get_hashicorp_client( &state.clone().conf.hc_vault, ) .await diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 651d3c0026f..84f8b62b57d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -13,6 +13,7 @@ use external_services::email::{ses::AwsSes, EmailService}; use external_services::file_storage::FileStorageInterface; #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] use external_services::hashicorp_vault::decrypt::VaultFetch; +use hyperswitch_interfaces::encryption_interface::EncryptionManagementInterface; #[cfg(all(feature = "olap", feature = "aws_kms"))] use masking::PeekInterface; use router_env::tracing_actix_web::RequestId; @@ -73,6 +74,7 @@ pub struct AppState { pub pool: crate::analytics::AnalyticsProvider, pub request_id: Option, pub file_storage_client: Box, + pub encryption_client: Box, } impl scheduler::SchedulerAppState for AppState { @@ -150,13 +152,20 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { + #[allow(clippy::expect_used)] + let encryption_client = conf + .encryption_management + .get_encryption_management_client() + .await + .expect("Failed to create encryption client"); + Box::pin(async move { #[cfg(feature = "aws_kms")] - let aws_kms_client = aws_kms::get_aws_kms_client(&conf.kms).await; + let aws_kms_client = aws_kms::core::get_aws_kms_client(&conf.kms).await; #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] #[allow(clippy::expect_used)] let hc_client = - external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + external_services::hashicorp_vault::core::get_hashicorp_client(&conf.hc_vault) .await .expect("Failed while creating hashicorp_client"); let testable = storage_impl == StorageImpl::PostgresqlTest; @@ -204,7 +213,7 @@ impl AppState { sqlx.password = sqlx .password .clone() - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .expect("Failed while fetching from hashicorp vault"); } @@ -230,7 +239,7 @@ impl AppState { { conf.connector_onboarding = conf .connector_onboarding - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .expect("Failed to decrypt connector onboarding credentials"); } @@ -254,7 +263,7 @@ impl AppState { conf.jwekey = conf .jwekey .clone() - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .expect("Failed to decrypt connector onboarding credentials"); } @@ -287,6 +296,7 @@ impl AppState { pool, request_id: None, file_storage_client, + encryption_client, } }) .await diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 220fde4631f..dc7bf14fecf 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -46,18 +46,19 @@ pub async fn get_store( test_transaction: bool, ) -> StorageResult { #[cfg(feature = "aws_kms")] - let aws_kms_client = aws_kms::get_aws_kms_client(&config.kms).await; + let aws_kms_client = aws_kms::core::get_aws_kms_client(&config.kms).await; #[cfg(feature = "hashicorp-vault")] - let hc_client = external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) - .await - .change_context(StorageError::InitializationError)?; + let hc_client = + external_services::hashicorp_vault::core::get_hashicorp_client(&config.hc_vault) + .await + .change_context(StorageError::InitializationError)?; let master_config = config.master_database.clone(); #[cfg(feature = "hashicorp-vault")] let master_config = master_config - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to fetch data from hashicorp vault")?; @@ -74,7 +75,7 @@ pub async fn get_store( #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] let replica_config = replica_config - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to fetch data from hashicorp vault")?; @@ -128,15 +129,15 @@ pub async fn get_store( #[allow(clippy::expect_used)] async fn get_master_enc_key( conf: &crate::configs::settings::Settings, - #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::AwsKmsClient, + #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::core::AwsKmsClient, #[cfg(feature = "hashicorp-vault")] - hc_client: &external_services::hashicorp_vault::HashiCorpVault, + hc_client: &external_services::hashicorp_vault::core::HashiCorpVault, ) -> StrongSecret> { let master_enc_key = conf.secrets.master_enc_key.clone(); #[cfg(feature = "hashicorp-vault")] let master_enc_key = master_enc_key - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .expect("Failed to fetch master enc key"); diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 308ceb2672b..990e0785306 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -226,9 +226,9 @@ where api_keys::get_hash_key( &config.api_keys, #[cfg(feature = "aws_kms")] - aws_kms::get_aws_kms_client(&config.kms).await, + aws_kms::core::get_aws_kms_client(&config.kms).await, #[cfg(feature = "hashicorp-vault")] - external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + external_services::hashicorp_vault::core::get_hashicorp_client(&config.hc_vault) .await .change_context(errors::ApiErrorResponse::InternalServerError)?, ) @@ -289,9 +289,9 @@ static ADMIN_API_KEY: tokio::sync::OnceCell> = pub async fn get_admin_api_key( secrets: &settings::Secrets, - #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::AwsKmsClient, + #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::core::AwsKmsClient, #[cfg(feature = "hashicorp-vault")] - hc_client: &external_services::hashicorp_vault::HashiCorpVault, + hc_client: &external_services::hashicorp_vault::core::HashiCorpVault, ) -> RouterResult<&'static StrongSecret> { ADMIN_API_KEY .get_or_try_init(|| async { @@ -308,7 +308,7 @@ pub async fn get_admin_api_key( #[cfg(feature = "hashicorp-vault")] let admin_api_key = masking::Secret::new(admin_api_key) - .fetch_inner::(hc_client) + .fetch_inner::(hc_client) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt admin API key")? @@ -369,9 +369,9 @@ where let admin_api_key = get_admin_api_key( &conf.secrets, #[cfg(feature = "aws_kms")] - aws_kms::get_aws_kms_client(&conf.kms).await, + aws_kms::core::get_aws_kms_client(&conf.kms).await, #[cfg(feature = "hashicorp-vault")] - external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + external_services::hashicorp_vault::core::get_hashicorp_client(&conf.hc_vault) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting admin api key")?, @@ -873,7 +873,7 @@ static JWT_SECRET: tokio::sync::OnceCell> = tokio::sync::On pub async fn get_jwt_secret( secrets: &settings::Secrets, - #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::AwsKmsClient, + #[cfg(feature = "aws_kms")] aws_kms_client: &aws_kms::core::AwsKmsClient, ) -> RouterResult<&'static StrongSecret> { JWT_SECRET .get_or_try_init(|| async { @@ -901,7 +901,7 @@ where let secret = get_jwt_secret( &conf.secrets, #[cfg(feature = "aws_kms")] - aws_kms::get_aws_kms_client(&conf.kms).await, + aws_kms::core::get_aws_kms_client(&conf.kms).await, ) .await? .peek() @@ -972,7 +972,7 @@ static RECON_API_KEY: tokio::sync::OnceCell> = #[cfg(feature = "recon")] pub async fn get_recon_admin_api_key( secrets: &settings::Secrets, - #[cfg(feature = "aws_kms")] kms_client: &aws_kms::AwsKmsClient, + #[cfg(feature = "aws_kms")] kms_client: &aws_kms::core::AwsKmsClient, ) -> RouterResult<&'static StrongSecret> { RECON_API_KEY .get_or_try_init(|| async { @@ -1013,7 +1013,7 @@ where let admin_api_key = get_recon_admin_api_key( &conf.secrets, #[cfg(feature = "aws_kms")] - aws_kms::get_aws_kms_client(&conf.kms).await, + aws_kms::core::get_aws_kms_client(&conf.kms).await, ) .await?; diff --git a/crates/router/src/services/jwt.rs b/crates/router/src/services/jwt.rs index 08db52e4020..05de1b4e11e 100644 --- a/crates/router/src/services/jwt.rs +++ b/crates/router/src/services/jwt.rs @@ -27,7 +27,7 @@ where let jwt_secret = authentication::get_jwt_secret( &settings.secrets, #[cfg(feature = "aws_kms")] - external_services::aws_kms::get_aws_kms_client(&settings.kms).await, + external_services::aws_kms::core::get_aws_kms_client(&settings.kms).await, ) .await .change_context(UserErrors::InternalServerError) diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index 057805a76f0..30ac4815efb 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -128,9 +128,9 @@ async fn waited_fetch_and_update_caches( state: &AppState, local_fetch_retry_delay: u64, local_fetch_retry_count: u64, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { for _n in 1..local_fetch_retry_count { sleep(Duration::from_millis(local_fetch_retry_delay)).await; @@ -192,9 +192,9 @@ pub async fn get_forex_rates( call_delay: i64, local_fetch_retry_delay: u64, local_fetch_retry_count: u64, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { if let Some(local_rates) = retrieve_forex_from_local().await { if local_rates.is_expired(call_delay) { @@ -234,9 +234,9 @@ async fn handler_local_no_data( call_delay: i64, _local_fetch_retry_delay: u64, _local_fetch_retry_count: u64, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(Some(data)) => { @@ -281,9 +281,9 @@ async fn handler_local_no_data( async fn successive_fetch_and_save_forex( state: &AppState, stale_redis_data: Option, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { match acquire_redis_lock(state).await { Ok(lock_acquired) => { @@ -351,9 +351,9 @@ async fn fallback_forex_redis_check( state: &AppState, redis_data: FxExchangeRatesCacheEntry, call_delay: i64, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { Some(redis_forex) => { @@ -381,9 +381,9 @@ async fn handler_local_expired( state: &AppState, call_delay: i64, local_rates: FxExchangeRatesCacheEntry, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(redis_data) => { @@ -427,14 +427,14 @@ async fn handler_local_expired( async fn fetch_forex_rates( state: &AppState, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> Result> { let forex_api_key = async { #[cfg(feature = "hashicorp-vault")] - let client = hashicorp_vault::get_hashicorp_client(hc_config) + let client = hashicorp_vault::core::get_hashicorp_client(hc_config) .await .change_context(ForexCacheError::AwsKmsDecryptionFailed)?; @@ -446,7 +446,7 @@ async fn fetch_forex_rates( .forex_api .api_key .clone() - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(ForexCacheError::AwsKmsDecryptionFailed)?; @@ -454,7 +454,7 @@ async fn fetch_forex_rates( } .await?; #[cfg(feature = "aws_kms")] - let forex_api_key = aws_kms::get_aws_kms_client(aws_kms_config) + let forex_api_key = aws_kms::core::get_aws_kms_client(aws_kms_config) .await .decrypt(forex_api_key.peek()) .await @@ -516,13 +516,13 @@ async fn fetch_forex_rates( pub async fn fallback_fetch_forex_rates( state: &AppState, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { let fallback_api_key = async { #[cfg(feature = "hashicorp-vault")] - let client = hashicorp_vault::get_hashicorp_client(hc_config) + let client = hashicorp_vault::core::get_hashicorp_client(hc_config) .await .change_context(ForexCacheError::AwsKmsDecryptionFailed)?; @@ -534,7 +534,7 @@ pub async fn fallback_fetch_forex_rates( .forex_api .fallback_api_key .clone() - .fetch_inner::(client) + .fetch_inner::(client) .await .change_context(ForexCacheError::AwsKmsDecryptionFailed)?; @@ -542,7 +542,7 @@ pub async fn fallback_fetch_forex_rates( } .await?; #[cfg(feature = "aws_kms")] - let fallback_forex_api_key = aws_kms::get_aws_kms_client(aws_kms_config) + let fallback_forex_api_key = aws_kms::core::get_aws_kms_client(aws_kms_config) .await .decrypt(fallback_api_key.peek()) .await @@ -691,9 +691,9 @@ pub async fn convert_currency( amount: i64, to_currency: String, from_currency: String, - #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::AwsKmsConfig, + #[cfg(feature = "aws_kms")] aws_kms_config: &aws_kms::core::AwsKmsConfig, #[cfg(feature = "hashicorp-vault")] - hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, + hc_config: &external_services::hashicorp_vault::core::HashiCorpVaultConfig, ) -> CustomResult { let rates = get_forex_rates( &state,