Skip to content

Commit d597f23

Browse files
committed
feat(router): add support for azure key vault as secret manager
Implement Azure key vault support for secret management and encryption management in external_services. This can be used to securely storing sensitive configuration settings. Fixes juspay#6181
1 parent 74bbf4b commit d597f23

File tree

5 files changed

+319
-2
lines changed

5 files changed

+319
-2
lines changed

crates/external_services/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ aws_kms = ["dep:aws-config", "dep:aws-sdk-kms"]
1212
email = ["dep:aws-config"]
1313
aws_s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
1414
hashicorp-vault = ["dep:vaultrs"]
15+
azure_key_vault = ["dep:azure_identity", "dep:azure_security_keyvault_keys"]
1516
v1 = ["hyperswitch_interfaces/v1", "common_utils/v1"]
1617
dynamic_routing = ["dep:prost", "dep:tonic", "dep:tonic-reflection", "dep:tonic-types", "dep:api_models", "tokio/macros", "tokio/rt-multi-thread", "dep:tonic-build", "dep:router_env", "dep:hyper-util", "dep:http-body-util"]
1718

@@ -41,6 +42,8 @@ tonic-reflection = { version = "0.12.2", optional = true }
4142
tonic-types = { version = "0.12.2", optional = true }
4243
hyper-util = { version = "0.1.9", optional = true }
4344
http-body-util = { version = "0.1.2", optional = true }
45+
azure_identity = { git = "https://github.com/Azure/azure-sdk-for-rust.git", branch = "main", optional = true}
46+
azure_security_keyvault_keys = { git = "https://github.com/Azure/azure-sdk-for-rust.git", branch = "main", optional = true}
4447

4548

4649
# First party crates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//! Interactions with the AZURE KEY VAULT SDK
2+
3+
pub mod core;
4+
5+
pub mod implementers;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//! Interactions with the AZURE KEY VAULT SDK
2+
3+
use std::sync::Arc;
4+
5+
use azure_identity::DefaultAzureCredential;
6+
use azure_security_keyvault_keys::{
7+
KeyClient,
8+
models::{KeyOperationsParameters, JsonWebKeyEncryptionAlgorithm},
9+
};
10+
use crate::{consts, metrics};
11+
use base64::Engine;
12+
13+
use std::time::Instant;
14+
use common_utils::errors::CustomResult;
15+
use error_stack::{report, ResultExt};
16+
use router_env::logger;
17+
18+
19+
/// Configuration parameters required for constructing a [`AzureKeyVaultClient`].
20+
#[derive(Clone, Debug, Default, serde::Deserialize)]
21+
#[serde(default)]
22+
pub struct AzureKeyVaultConfig {
23+
/// key name of Azure Key vault used to encrypt or decrypt data
24+
pub key_name: String,
25+
/// The Azure vault url of the Key vault.
26+
pub vault_url: String,
27+
/// version of the key name
28+
pub version: String,
29+
}
30+
31+
impl AzureKeyVaultConfig {
32+
/// Verifies that the [`AzureKeyVaultClient`] configuration is usable.
33+
pub fn validate(&self) -> Result<(), &'static str> {
34+
use common_utils::{ext_traits::ConfigExt, fp_utils::when};
35+
36+
when(self.key_name.is_default_or_empty(), || {
37+
Err("Azure Key Vault key name must not be empty")
38+
})?;
39+
40+
when(self.vault_url.is_default_or_empty(), || {
41+
Err("Azure Key Vault url must not be empty")
42+
})
43+
}
44+
}
45+
46+
/// Client for AZURE KEY VAULT operations.
47+
#[derive(Clone)]
48+
pub struct AzureKeyVaultClient {
49+
inner_client: Arc<KeyClient>,
50+
key_name: String,
51+
version: String,
52+
}
53+
54+
impl AzureKeyVaultClient {
55+
/// Constructs a new Azure Key Vault client.
56+
pub async fn new(config: &AzureKeyVaultConfig) -> Result<Self, AzureKeyVaultError> {
57+
let credential = DefaultAzureCredential::new()
58+
.map_err(|_| AzureKeyVaultError::AzureKeyVaultClientInitializationFailed)?;
59+
60+
Ok(Self {
61+
inner_client: Arc::new(
62+
KeyClient::new(
63+
&config.vault_url,
64+
credential.clone(),
65+
None
66+
)
67+
.map_err(
68+
|_| AzureKeyVaultError::AzureKeyVaultClientInitializationFailed)?
69+
),
70+
key_name: config.key_name.clone(),
71+
version: config.version.clone(),
72+
})
73+
}
74+
/// Decrypts the provided base64-encoded encrypted data using the AZURE KEY VAULT SDK. We assume that
75+
/// the SDK has the values required to interact with the AZURE KEY VAULT APIs (`AZURE_TENANT_ID`,
76+
/// `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET`) either set in environment variables, or that the
77+
/// SDK is running in a machine that is able to assume an Azure AD role.
78+
pub async fn decrypt(&self, data: impl AsRef<[u8]>) -> CustomResult<String, AzureKeyVaultError> {
79+
let start = Instant::now();
80+
81+
let data = consts::BASE64_ENGINE
82+
.decode(data)
83+
.change_context(AzureKeyVaultError::Base64DecodingFailed)?;
84+
85+
let decrypt_params = KeyOperationsParameters {
86+
algorithm: Some(JsonWebKeyEncryptionAlgorithm::RsaOaep),
87+
value: Some(data),
88+
..Default::default()
89+
};
90+
let decrypted_output = self.inner_client
91+
.decrypt(&self.key_name, &self.version , decrypt_params.clone().try_into().unwrap(), None)
92+
.await
93+
.inspect_err(|error| {
94+
logger::error!(azure_key_vault_error=?error, "Failed to Azure Key Vault decrypt data");
95+
metrics::AZURE_KEY_VAULT_DECRYPTION_FAILURES.add(1, &[]);
96+
})
97+
.change_context(AzureKeyVaultError::DecryptionFailed)?
98+
.into_body()
99+
.await
100+
.inspect_err(|error| {
101+
logger::error!(azure_key_vault_error=?error, "Failed to Azure Key Vault decrypt data");
102+
metrics::AZURE_KEY_VAULT_DECRYPTION_FAILURES.add(1, &[]);
103+
})
104+
.change_context(AzureKeyVaultError::DecryptionFailed)?;
105+
106+
let output = decrypted_output
107+
.result
108+
.ok_or(report!(AzureKeyVaultError::MissingPlaintextDecryptionOutput))
109+
.and_then(|bytes|
110+
String::from_utf8(bytes)
111+
.change_context(AzureKeyVaultError::Utf8DecodingFailed)
112+
)?;
113+
114+
let time_taken = start.elapsed();
115+
metrics::AZURE_KEY_VAULT_DECRYPT_TIME.record(time_taken.as_secs_f64(), &[]);
116+
117+
Ok(output)
118+
}
119+
120+
/// Encrypts the provided String using the AZURE KEY VAULT SDK and returns base64-encoded encrypted data.
121+
/// We assume that the SDK has the values required to interact with the AZURE KEY VAULT APIs (`AZURE_TENANT_ID`,
122+
/// `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET`) either set in environment variables, or that the
123+
/// SDK is running in a machine that is able to assume an Azure AD role.
124+
pub async fn encrypt(&self, data: impl AsRef<[u8]>) -> CustomResult<String, AzureKeyVaultError> {
125+
let start = Instant::now();
126+
127+
let encrypt_params = KeyOperationsParameters {
128+
algorithm: Some(JsonWebKeyEncryptionAlgorithm::RsaOaep),
129+
value: Some(data.as_ref().to_vec()),
130+
..Default::default()
131+
};
132+
133+
let encrypted_output = self
134+
.inner_client
135+
.encrypt(&self.key_name, &self.version, encrypt_params.clone().try_into().unwrap(), None)
136+
.await
137+
.inspect_err(|error| {
138+
logger::error!(azure_key_vault_error=?error, "Failed to Azure Key Vault decrypt data");
139+
metrics::AZURE_KEY_VAULT_ENCRYPTION_FAILURES.add(1, &[]);
140+
})
141+
.change_context(AzureKeyVaultError::EncryptionFailed)?
142+
.into_body()
143+
.await
144+
.inspect_err(|error| {
145+
logger::error!(azure_key_vault_error=?error, "Failed to Azure Key Vault decrypt data");
146+
metrics::AZURE_KEY_VAULT_ENCRYPTION_FAILURES.add(1, &[]);
147+
})
148+
.change_context(AzureKeyVaultError::EncryptionFailed)?;
149+
150+
let output = encrypted_output
151+
.result
152+
.ok_or(AzureKeyVaultError::MissingCiphertextEncryptionOutput)
153+
.map(|bytes| consts::BASE64_ENGINE.encode(bytes))?;
154+
155+
let time_taken = start.elapsed();
156+
metrics::AZURE_KEY_VAULT_ENCRYPT_TIME.record(time_taken.as_secs_f64(), &[]);
157+
158+
Ok(output)
159+
}
160+
161+
162+
}
163+
164+
165+
/// Errors that could occur during AZURE KEY VAULT operations.
166+
#[derive(Debug, thiserror::Error)]
167+
pub enum AzureKeyVaultError {
168+
/// An error occurred when base64 encoding input data.
169+
#[error("Failed to base64 encode input data")]
170+
Base64EncodingFailed,
171+
172+
/// An error occurred when base64 decoding input data.
173+
#[error("Failed to base64 decode input data")]
174+
Base64DecodingFailed,
175+
176+
/// An error occurred when AZURE KEY VAULT decrypting input data.
177+
#[error("Failed to Azure Key Vault decrypt input data")]
178+
DecryptionFailed,
179+
180+
/// An error occurred when AZURE KEY VAULT encrypting input data.
181+
#[error("Failed to Azure Key Vault encrypt input data")]
182+
EncryptionFailed,
183+
184+
/// The AZURE KEY VAULT decrypted output does not include a plaintext output.
185+
#[error("Missing plaintext AZURE KEY VAULT decryption output")]
186+
MissingPlaintextDecryptionOutput,
187+
188+
/// The AZURE KEY VAULT encrypted output does not include a ciphertext output.
189+
#[error("Missing ciphertext AZURE KEY VAULT encryption output")]
190+
MissingCiphertextEncryptionOutput,
191+
192+
/// An error occurred UTF-8 decoding AZURE KEY VAULT decrypted output.
193+
#[error("Failed to UTF-8 decode decryption output")]
194+
Utf8DecodingFailed,
195+
196+
/// The AZURE KEY VAULT client has not been initialized.
197+
#[error("The AZURE KEY VAULT client has not been initialized")]
198+
AzureKeyVaultClientInitializationFailed,
199+
}
200+
201+
202+
#[cfg(test)]
203+
mod tests {
204+
#![allow(clippy::expect_used, clippy::print_stdout)]
205+
#[tokio::test]
206+
async fn check_azure_key_vault_encryption() {
207+
std::env::set_var("AZURE_CLIENT_ID", "YOUR-CLIENT-ID");
208+
std::env::set_var("AZURE_TENANT_ID", "YOUR-TENANT-ID");
209+
std::env::set_var("AZURE_CLIENT_SECRET", "YOUR-CLIENT-SECRET");
210+
use super::*;
211+
let config = AzureKeyVaultConfig {
212+
key_name: "YOUR AZURE KEY VAULT KEY NAME".to_string(),
213+
vault_url: "YOUR AZURE KEY VAULT URL".to_string(),
214+
version: "".to_string(),
215+
};
216+
217+
let data = "hello".to_string();
218+
let binding = data.as_bytes();
219+
let encrypted_fingerprint = AzureKeyVaultClient::new(&config)
220+
.await
221+
.expect("azure key vault client initialization failed")
222+
.encrypt(binding)
223+
.await
224+
.expect("azure key vault encryption failed");
225+
226+
println!("{}", encrypted_fingerprint);
227+
}
228+
229+
#[tokio::test]
230+
async fn check_azure_key_vault_decrypt() {
231+
std::env::set_var("AZURE_CLIENT_ID", "YOUR-CLIENT-ID");
232+
std::env::set_var("AZURE_TENANT_ID", "YOUR-TENANT-ID");
233+
std::env::set_var("AZURE_CLIENT_SECRET", "YOUR-CLIENT-SECRET");
234+
use super::*;
235+
let config = AzureKeyVaultConfig {
236+
key_name: "YOUR AZURE KEY VAULT KEY NAME".to_string(),
237+
vault_url: "YOUR AZURE KEY VAULT URL".to_string(),
238+
version: "".to_string(),
239+
};
240+
241+
// Should decrypt to hello
242+
let data = "AZURE KEY VAULT ENCRYPTED CIPHER".to_string();
243+
let binding = data.as_bytes();
244+
let decrypted_fingerprint = AzureKeyVaultClient::new(&config)
245+
.await
246+
.expect("azure key vault client initialization failed")
247+
.encrypt(binding)
248+
.await
249+
.expect("azure key vault decryption failed");
250+
251+
println!("{}", decrypted_fingerprint);
252+
}
253+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//! Trait implementations for azure key vault
2+
3+
use common_utils::errors::CustomResult;
4+
use error_stack::ResultExt;
5+
use hyperswitch_interfaces::{
6+
encryption_interface::{EncryptionError, EncryptionManagementInterface},
7+
secrets_interface::{SecretManagementInterface, SecretsManagementError},
8+
};
9+
use masking::{PeekInterface, Secret};
10+
11+
use crate::azure_key_vault::core::AzureKeyVaultClient;
12+
13+
#[async_trait::async_trait]
14+
impl EncryptionManagementInterface for AzureKeyVaultClient {
15+
async fn encrypt(&self, input: &[u8]) -> CustomResult<Vec<u8>, EncryptionError> {
16+
self.encrypt(input)
17+
.await
18+
.change_context(EncryptionError::EncryptionFailed)
19+
.map(|val| val.into_bytes())
20+
}
21+
22+
async fn decrypt(&self, input: &[u8]) -> CustomResult<Vec<u8>, EncryptionError> {
23+
self.decrypt(input)
24+
.await
25+
.change_context(EncryptionError::DecryptionFailed)
26+
.map(|val| val.into_bytes())
27+
}
28+
}
29+
30+
#[async_trait::async_trait]
31+
impl SecretManagementInterface for AzureKeyVaultClient {
32+
async fn get_secret(
33+
&self,
34+
input: Secret<String>,
35+
) -> CustomResult<Secret<String>, SecretsManagementError> {
36+
self.decrypt(input.peek())
37+
.await
38+
.change_context(SecretsManagementError::FetchSecretFailed)
39+
.map(Into::into)
40+
}
41+
}

crates/external_services/src/lib.rs

+17-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ pub mod email;
88
#[cfg(feature = "aws_kms")]
99
pub mod aws_kms;
1010

11+
#[cfg(feature = "azure_key_vault")]
12+
pub mod azure_key_vault;
13+
1114
pub mod file_storage;
1215
#[cfg(feature = "hashicorp-vault")]
1316
pub mod hashicorp_vault;
@@ -20,15 +23,15 @@ pub mod grpc_client;
2023
pub mod managers;
2124

2225
/// Crate specific constants
23-
#[cfg(feature = "aws_kms")]
26+
#[cfg(feature = "aws_kms", feature = "azure_key_vault")]
2427
pub mod consts {
2528
/// General purpose base64 engine
2629
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
2730
base64::engine::general_purpose::STANDARD;
2831
}
2932

3033
/// Metrics for interactions with external systems.
31-
#[cfg(feature = "aws_kms")]
34+
#[cfg(feature = "aws_kms", feature = "azure_key_vault")]
3235
pub mod metrics {
3336
use router_env::{counter_metric, global_meter, histogram_metric_f64};
3437

@@ -43,4 +46,16 @@ pub mod metrics {
4346
histogram_metric_f64!(AWS_KMS_DECRYPT_TIME, GLOBAL_METER); // Histogram for AWS KMS decryption time (in sec)
4447
#[cfg(feature = "aws_kms")]
4548
histogram_metric_f64!(AWS_KMS_ENCRYPT_TIME, GLOBAL_METER); // Histogram for AWS KMS encryption time (in sec)
49+
50+
51+
#[cfg(feature = "azure_key_vault")]
52+
counter_metric!(AZURE_KEY_VAULT_DECRYPTION_FAILURES, GLOBAL_METER); // No. of Azure Key Vault Decryption failures
53+
#[cfg(feature = "azure_key_vault")]
54+
counter_metric!(AZURE_KEY_VAULT_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of Azure Key Vault Encryption failures
55+
56+
#[cfg(feature = "azure_key_vault")]
57+
histogram_metric_f64!(AZURE_KEY_VAULT_DECRYPT_TIME, GLOBAL_METER); // Histogram for Azure Key Vault decryption time (in sec)
58+
59+
#[cfg(feature = "azure_key_vault")]
60+
histogram_metric_f64!(AZURE_KEY_VAULT_ENCRYPT_TIME, GLOBAL_METER); // Histogram for Azure Key Vault encryption time (in sec)
4661
}

0 commit comments

Comments
 (0)