mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
refactor: introducing hyperswitch_interface crates (#3536)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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<AwsKmsClient> = 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<String, AwsKmsError> {
|
||||
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<String, AwsKmsError> {
|
||||
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<String>);
|
||||
|
||||
impl common_utils::ext_traits::ConfigExt for AwsKmsValue {
|
||||
fn is_empty_after_trim(&self) -> bool {
|
||||
self.0.peek().is_empty_after_trim()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AwsKmsValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self(Secret::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Secret<String>> for AwsKmsValue {
|
||||
fn from(value: Secret<String>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hashicorp-vault")]
|
||||
#[async_trait::async_trait]
|
||||
impl super::hashicorp_vault::decrypt::VaultFetch for AwsKmsValue {
|
||||
async fn fetch_inner<En>(
|
||||
self,
|
||||
client: &super::hashicorp_vault::HashiCorpVault,
|
||||
) -> error_stack::Result<Self, super::hashicorp_vault::HashiCorpError>
|
||||
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::<En>(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;
|
||||
|
||||
299
crates/external_services/src/aws_kms/core.rs
Normal file
299
crates/external_services/src/aws_kms/core.rs
Normal file
@ -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<AwsKmsClient> =
|
||||
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<String, AwsKmsError> {
|
||||
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<String, AwsKmsError> {
|
||||
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<String>);
|
||||
|
||||
impl common_utils::ext_traits::ConfigExt for AwsKmsValue {
|
||||
fn is_empty_after_trim(&self) -> bool {
|
||||
self.0.peek().is_empty_after_trim()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AwsKmsValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self(Secret::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Secret<String>> for AwsKmsValue {
|
||||
fn from(value: Secret<String>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hashicorp-vault")]
|
||||
#[async_trait::async_trait]
|
||||
impl hashicorp_vault::decrypt::VaultFetch for AwsKmsValue {
|
||||
async fn fetch_inner<En>(
|
||||
self,
|
||||
client: &hashicorp_vault::core::HashiCorpVault,
|
||||
) -> error_stack::Result<Self, hashicorp_vault::core::HashiCorpError>
|
||||
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::<En>(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<Self::Output, AwsKmsError> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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<Self::Output, AwsKmsError> {
|
||||
aws_kms_client
|
||||
.decrypt(self.0.peek())
|
||||
.await
|
||||
.attach_printable("Failed to decrypt AWS KMS value")
|
||||
}
|
||||
}
|
||||
|
||||
41
crates/external_services/src/aws_kms/implementers.rs
Normal file
41
crates/external_services/src/aws_kms/implementers.rs
Normal file
@ -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<Vec<u8>, EncryptionError> {
|
||||
self.encrypt(input)
|
||||
.await
|
||||
.change_context(EncryptionError::EncryptionFailed)
|
||||
.map(|val| val.into_bytes())
|
||||
}
|
||||
|
||||
async fn decrypt(&self, input: &[u8]) -> CustomResult<Vec<u8>, 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<String>,
|
||||
) -> CustomResult<Secret<String>, SecretsManagementError> {
|
||||
self.decrypt(input.peek())
|
||||
.await
|
||||
.change_context(SecretsManagementError::FetchSecretFailed)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
@ -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<HashiCorpVault> = 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<Box<dyn Future<Output = error_stack::Result<T, HashiCorpError>> + 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::<HashMap<String, String>>(&client.client, mount, path)
|
||||
.await
|
||||
.map_err(Into::<Report<_>>::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<Self, HashiCorpError> {
|
||||
VaultClient::new(
|
||||
VaultClientSettingsBuilder::default()
|
||||
.address(&config.url)
|
||||
.token(&config.token)
|
||||
.build()
|
||||
.map_err(Into::<Report<_>>::into)
|
||||
.change_context(HashiCorpError::ClientCreationFailed)
|
||||
.attach_printable("Failed while building vault settings")?,
|
||||
)
|
||||
.map_err(Into::<Report<_>>::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<En, I>(&self, data: String) -> error_stack::Result<I, HashiCorpError>
|
||||
where
|
||||
for<'a> En: Engine<
|
||||
ReturnType<'a, String> = Pin<
|
||||
Box<
|
||||
dyn Future<Output = error_stack::Result<String, HashiCorpError>>
|
||||
+ 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<Self>` representing the constructed instance if successful, or `None` otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use your_module::{FromEncoded, masking::Secret, Vec};
|
||||
/// let secret_instance = Secret::<String>::from_encoded("encoded_secret_string".to_string());
|
||||
/// let vec_instance = Vec::<u8>::from_encoded("68656c6c6f".to_string());
|
||||
/// ```
|
||||
fn from_encoded(input: String) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl FromEncoded for masking::Secret<String> {
|
||||
fn from_encoded(input: String) -> Option<Self> {
|
||||
Some(input.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromEncoded for Vec<u8> {
|
||||
fn from_encoded(input: String) -> Option<Self> {
|
||||
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;
|
||||
|
||||
227
crates/external_services/src/hashicorp_vault/core.rs
Normal file
227
crates/external_services/src/hashicorp_vault/core.rs
Normal file
@ -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<HashiCorpVault> = 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<String>,
|
||||
}
|
||||
|
||||
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<Box<dyn Future<Output = error_stack::Result<T, HashiCorpError>> + 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::<HashMap<String, String>>(&client.client, mount, path)
|
||||
.await
|
||||
.map_err(Into::<Report<_>>::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<Self, HashiCorpError> {
|
||||
VaultClient::new(
|
||||
VaultClientSettingsBuilder::default()
|
||||
.address(&config.url)
|
||||
.token(config.token.peek())
|
||||
.build()
|
||||
.map_err(Into::<Report<_>>::into)
|
||||
.change_context(HashiCorpError::ClientCreationFailed)
|
||||
.attach_printable("Failed while building vault settings")?,
|
||||
)
|
||||
.map_err(Into::<Report<_>>::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<En, I>(&self, data: String) -> error_stack::Result<I, HashiCorpError>
|
||||
where
|
||||
for<'a> En: Engine<
|
||||
ReturnType<'a, String> = Pin<
|
||||
Box<
|
||||
dyn Future<Output = error_stack::Result<String, HashiCorpError>>
|
||||
+ 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<Self>` representing the constructed instance if successful, or `None` otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use your_module::{FromEncoded, masking::Secret, Vec};
|
||||
/// let secret_instance = Secret::<String>::from_encoded("encoded_secret_string".to_string());
|
||||
/// let vec_instance = Vec::<u8>::from_encoded("68656c6c6f".to_string());
|
||||
/// ```
|
||||
fn from_encoded(input: String) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl FromEncoded for Secret<String> {
|
||||
fn from_encoded(input: String) -> Option<Self> {
|
||||
Some(input.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromEncoded for Vec<u8> {
|
||||
fn from_encoded(input: String) -> Option<Self> {
|
||||
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,
|
||||
}
|
||||
@ -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<En>(
|
||||
self,
|
||||
client: &super::HashiCorpVault,
|
||||
) -> error_stack::Result<Self, super::HashiCorpError>
|
||||
client: &HashiCorpVault,
|
||||
) -> error_stack::Result<Self, HashiCorpError>
|
||||
where
|
||||
for<'a> En: super::Engine<
|
||||
for<'a> En: Engine<
|
||||
ReturnType<'a, String> = Pin<
|
||||
Box<
|
||||
dyn Future<Output = error_stack::Result<String, super::HashiCorpError>>
|
||||
dyn Future<Output = error_stack::Result<String, HashiCorpError>>
|
||||
+ Send
|
||||
+ 'a,
|
||||
>,
|
||||
@ -32,13 +36,13 @@ pub trait VaultFetch: Sized {
|
||||
impl VaultFetch for masking::Secret<String> {
|
||||
async fn fetch_inner<En>(
|
||||
self,
|
||||
client: &super::HashiCorpVault,
|
||||
) -> error_stack::Result<Self, super::HashiCorpError>
|
||||
client: &HashiCorpVault,
|
||||
) -> error_stack::Result<Self, HashiCorpError>
|
||||
where
|
||||
for<'a> En: super::Engine<
|
||||
for<'a> En: Engine<
|
||||
ReturnType<'a, String> = Pin<
|
||||
Box<
|
||||
dyn Future<Output = error_stack::Result<String, super::HashiCorpError>>
|
||||
dyn Future<Output = error_stack::Result<String, HashiCorpError>>
|
||||
+ Send
|
||||
+ 'a,
|
||||
>,
|
||||
|
||||
24
crates/external_services/src/hashicorp_vault/implementers.rs
Normal file
24
crates/external_services/src/hashicorp_vault/implementers.rs
Normal file
@ -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<String>,
|
||||
) -> CustomResult<Secret<String>, SecretsManagementError> {
|
||||
self.fetch::<Kv2, Secret<String>>(input.expose())
|
||||
.await
|
||||
.map(|val| val.expose().to_owned())
|
||||
.change_context(SecretsManagementError::FetchSecretFailed)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
5
crates/external_services/src/managers.rs
Normal file
5
crates/external_services/src/managers.rs
Normal file
@ -0,0 +1,5 @@
|
||||
//! Config and client managers
|
||||
|
||||
pub mod encryption_management;
|
||||
|
||||
pub mod secrets_management;
|
||||
@ -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<Box<dyn EncryptionManagementInterface>, 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),
|
||||
})
|
||||
}
|
||||
}
|
||||
72
crates/external_services/src/managers/secrets_management.rs
Normal file
72
crates/external_services/src/managers/secrets_management.rs
Normal file
@ -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<Box<dyn SecretManagementInterface>, 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<dyn SecretManagementInterface> { Box::new(inner) })
|
||||
}
|
||||
Self::NoEncryption => Ok(Box::new(NoEncryption)),
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/external_services/src/no_encryption.rs
Normal file
7
crates/external_services/src/no_encryption.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//!
|
||||
//! No encryption functionalities
|
||||
//!
|
||||
|
||||
pub mod core;
|
||||
|
||||
pub mod implementers;
|
||||
17
crates/external_services/src/no_encryption/core.rs
Normal file
17
crates/external_services/src/no_encryption/core.rs
Normal file
@ -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<u8> {
|
||||
data.as_ref().into()
|
||||
}
|
||||
|
||||
/// Decryption functionality
|
||||
pub fn decrypt(&self, data: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
data.as_ref().into()
|
||||
}
|
||||
}
|
||||
36
crates/external_services/src/no_encryption/implementers.rs
Normal file
36
crates/external_services/src/no_encryption/implementers.rs
Normal file
@ -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<Vec<u8>, EncryptionError> {
|
||||
Ok(self.encrypt(input))
|
||||
}
|
||||
|
||||
async fn decrypt(&self, input: &[u8]) -> CustomResult<Vec<u8>, EncryptionError> {
|
||||
Ok(self.decrypt(input))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SecretManagementInterface for NoEncryption {
|
||||
async fn get_secret(
|
||||
&self,
|
||||
input: Secret<String>,
|
||||
) -> CustomResult<Secret<String>, 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user