diff --git a/config/config.example.toml b/config/config.example.toml index 0e65c9cad5..7ef8eca00e 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -111,10 +111,6 @@ outgoing_enabled = true validity = 1 [api_keys] -# Key ID for the KMS managed key used to decrypt the API key hashing key -aws_key_id = "" -# The AWS region for the KMS managed key used to decrypt the API key hashing key -aws_region = "" # Base64-encoded (KMS encrypted) ciphertext of the API key hashing key kms_encrypted_hash_key = "" # Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating hashes of API keys @@ -190,10 +186,15 @@ max_read_count = 100 # Specifies the maximum number of entries that wo shutdown_interval = 1000 # Specifies how much time to wait, while waiting for threads to complete execution (in milliseconds) loop_interval = 500 # Specifies how much time to wait after checking all the possible streams in completed (in milliseconds) -# Filteration logic for list payment method, allowing use to limit payment methods based on the requirement country and currency +# Filtration logic for list payment method, allowing use to limit payment methods based on the requirement country and currency [pm_filters.stripe] # ^--- This can be any connector (can be multiple) paypal = { currency = "USD,INR", country = "US" } # ^ ^------- comma-separated values # ^------------------------------- any valid payment method type (can be multiple) (for cards this should be card_network) # If either currency or country isn't provided then, all possible values are accepted + +# KMS configuration. Only applicable when the `kms` feature flag is enabled. +[kms] +key_id = "" # The AWS key ID used by the KMS SDK for decrypting data. +region = "" # The AWS region used by the KMS SDK for decrypting data. diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 824ecce930..c66378d5b5 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -58,6 +58,8 @@ pub struct Settings { pub pm_filters: ConnectorFilters, pub bank_config: BankRedirectConfig, pub api_keys: ApiKeys, + #[cfg(feature = "kms")] + pub kms: Kms, } #[derive(Debug, Deserialize, Clone, Default)] @@ -187,10 +189,6 @@ pub struct EphemeralConfig { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct Jwekey { - #[cfg(feature = "kms")] - pub aws_key_id: String, - #[cfg(feature = "kms")] - pub aws_region: String, pub locker_key_identifier1: String, pub locker_key_identifier2: String, pub locker_encryption_key1: String, @@ -327,12 +325,6 @@ pub struct WebhooksSettings { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct ApiKeys { - #[cfg(feature = "kms")] - pub aws_key_id: String, - - #[cfg(feature = "kms")] - pub aws_region: String, - /// Base64-encoded (KMS encrypted) ciphertext of the key used for calculating hashes of API /// keys #[cfg(feature = "kms")] @@ -344,6 +336,14 @@ pub struct ApiKeys { pub hash_key: String, } +#[cfg(feature = "kms")] +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct Kms { + pub key_id: String, + pub region: String, +} + impl Settings { pub fn new() -> ApplicationResult { Self::with_config_path(None) @@ -417,8 +417,9 @@ impl Settings { .transpose()?; #[cfg(feature = "kv_store")] self.drainer.validate()?; - self.jwekey.validate()?; self.api_keys.validate()?; + #[cfg(feature = "kms")] + self.kms.validate()?; Ok(()) } diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index f3643c3b06..74552c5ef2 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -41,26 +41,6 @@ impl super::settings::Locker { } } -impl super::settings::Jwekey { - pub fn validate(&self) -> Result<(), ApplicationError> { - #[cfg(feature = "kms")] - common_utils::fp_utils::when(self.aws_key_id.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "AWS key ID must not be empty when KMS feature is enabled".into(), - )) - })?; - - #[cfg(feature = "kms")] - common_utils::fp_utils::when(self.aws_region.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "AWS region must not be empty when KMS feature is enabled".into(), - )) - })?; - - Ok(()) - } -} - impl super::settings::Server { pub fn validate(&self) -> Result<(), ApplicationError> { common_utils::fp_utils::when(self.host.is_default_or_empty(), || { @@ -190,25 +170,11 @@ impl super::settings::ApiKeys { use common_utils::fp_utils::when; #[cfg(feature = "kms")] - { - when(self.aws_key_id.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "API key AWS key ID must not be empty when KMS feature is enabled".into(), - )) - })?; - - when(self.aws_region.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "API key AWS region must not be empty when KMS feature is enabled".into(), - )) - })?; - - when(self.kms_encrypted_hash_key.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "API key hashing key must not be empty when KMS feature is enabled".into(), - )) - }) - } + return when(self.kms_encrypted_hash_key.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "API key hashing key must not be empty when KMS feature is enabled".into(), + )) + }); #[cfg(not(feature = "kms"))] when(self.hash_key.is_empty(), || { @@ -218,3 +184,22 @@ impl super::settings::ApiKeys { }) } } + +#[cfg(feature = "kms")] +impl super::settings::Kms { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when(self.key_id.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "KMS AWS key ID must not be empty".into(), + )) + })?; + + when(self.region.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "KMS AWS region must not be empty".into(), + )) + }) + } +} diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 9cda9e9562..f7cdfc6149 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -3,6 +3,8 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::{PeekInterface, StrongSecret}; use router_env::{instrument, tracing}; +#[cfg(feature = "kms")] +use crate::services::kms; use crate::{ configs::settings, consts, @@ -12,43 +14,40 @@ use crate::{ types::{api, storage, transformers::ForeignInto}, utils, }; -#[cfg(feature = "kms")] -use crate::{routes::metrics, services::kms}; -pub static HASH_KEY: tokio::sync::OnceCell> = +static HASH_KEY: tokio::sync::OnceCell> = tokio::sync::OnceCell::const_new(); pub async fn get_hash_key( api_key_config: &settings::ApiKeys, -) -> errors::RouterResult> { - #[cfg(feature = "kms")] - let hash_key = kms::KeyHandler::get_kms_decrypted_key( - &api_key_config.aws_region, - &api_key_config.aws_key_id, - api_key_config.kms_encrypted_hash_key.clone(), - ) - .await - .map_err(|error| { - metrics::AWS_KMS_FAILURES.add(&metrics::CONTEXT, 1, &[]); - error - }) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to KMS decrypt API key hashing key")?; + #[cfg(feature = "kms")] kms_config: &settings::Kms, +) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { + HASH_KEY + .get_or_try_init(|| async { + #[cfg(feature = "kms")] + let hash_key = kms::get_kms_client(kms_config) + .await + .decrypt(&api_key_config.kms_encrypted_hash_key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt API key hashing key")?; - #[cfg(not(feature = "kms"))] - let hash_key = &api_key_config.hash_key; + #[cfg(not(feature = "kms"))] + let hash_key = &api_key_config.hash_key; - <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from( - hex::decode(hash_key) + <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from( + hex::decode(hash_key) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("API key hash key has invalid hexadecimal data")? + .as_slice(), + ) .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("API key hash key has invalid hexadecimal data")? - .as_slice(), - ) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("The API hashing key has incorrect length") - .map(StrongSecret::new) + .attach_printable("The API hashing key has incorrect length") + .map(StrongSecret::new) + }) + .await } // Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility @@ -119,12 +118,16 @@ impl PlaintextApiKey { pub async fn create_api_key( store: &dyn StorageInterface, api_key_config: &settings::ApiKeys, + #[cfg(feature = "kms")] kms_config: &settings::Kms, api_key: api::CreateApiKeyRequest, merchant_id: String, ) -> RouterResponse { - let hash_key = HASH_KEY - .get_or_try_init(|| get_hash_key(api_key_config)) - .await?; + let hash_key = get_hash_key( + api_key_config, + #[cfg(feature = "kms")] + kms_config, + ) + .await?; let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH); let api_key = storage::ApiKeyNew { key_id: PlaintextApiKey::new_key_id(), @@ -248,10 +251,13 @@ mod tests { let settings = settings::Settings::new().expect("invalid settings"); let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH); - let hash_key = HASH_KEY - .get_or_try_init(|| get_hash_key(&settings.api_keys)) - .await - .unwrap(); + let hash_key = get_hash_key( + &settings.api_keys, + #[cfg(feature = "kms")] + &settings.kms, + ) + .await + .unwrap(); let hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek()); assert_ne!( diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index e72d87cf57..add5af5fc5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -313,6 +313,18 @@ pub enum VaultError { UnexpectedResponseError(bytes::Bytes), } +#[derive(Debug, thiserror::Error)] +pub enum KmsError { + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + #[error("Failed to KMS decrypt input data")] + DecryptionFailed, + #[error("Missing plaintext KMS decryption output")] + MissingPlaintextDecryptionOutput, + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, +} + #[derive(Debug, thiserror::Error)] pub enum ProcessTrackerError { #[error("An unexpected flow was specified")] diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 9929059d8c..b6669c2b67 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -220,10 +220,14 @@ pub async fn add_card_hs( ) -> errors::CustomResult { let locker = &state.conf.locker; let jwekey = &state.conf.jwekey; + + #[cfg(feature = "kms")] + let kms_config = &state.conf.kms; + let db = &*state.store; let merchant_id = &merchant_account.merchant_id; - let locker_id = merchant_account + let _ = merchant_account .locker_id .to_owned() .get_required_value("locker_id") @@ -234,9 +238,9 @@ pub async fn add_card_hs( locker, &card, &customer_id, - &req, - &locker_id, merchant_id, + #[cfg(feature = "kms")] + kms_config, ) .await?; @@ -249,10 +253,15 @@ pub async fn add_card_hs( .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting decrypted response payload")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting decrypted response payload")?; let stored_card_resp: payment_methods::StoreCardResp = decrypted_payload .parse_struct("StoreCardResp") .change_context(errors::VaultError::ResponseDeserializationFailed)?; @@ -363,12 +372,18 @@ pub async fn get_card_from_hs_locker<'a>( ) -> errors::CustomResult { let locker = &state.conf.locker; let jwekey = &state.conf.jwekey; + + #[cfg(feature = "kms")] + let kms_config = &state.conf.kms; + let request = payment_methods::mk_get_card_request_hs( jwekey, locker, customer_id, merchant_id, card_reference, + #[cfg(feature = "kms")] + kms_config, ) .await .change_context(errors::VaultError::FetchCardFailed) @@ -381,10 +396,15 @@ pub async fn get_card_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchCardFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(errors::VaultError::FetchCardFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchCardFailed)?; @@ -441,12 +461,18 @@ pub async fn delete_card_from_hs_locker<'a>( ) -> errors::RouterResult { let locker = &state.conf.locker; let jwekey = &state.conf.jwekey; + + #[cfg(feature = "kms")] + let kms_config = &state.conf.kms; + let request = payment_methods::mk_delete_card_request_hs( jwekey, locker, customer_id, merchant_id, card_reference, + #[cfg(feature = "kms")] + kms_config, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -458,10 +484,15 @@ pub async fn delete_card_from_hs_locker<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while executing call_connector_api for delete card"); let jwe_body: services::JweBody = response.get_response_inner("JweBody")?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting decrypted response payload for delete card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting decrypted response payload for delete card")?; let delete_card_resp: payment_methods::DeleteCardResp = decrypted_payload .parse_struct("DeleteCardResp") .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 5a16129d55..a84e5c8b31 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "kms")] use crate::services::kms; use crate::{ - configs::settings::{Jwekey, Locker}, + configs::settings, core::errors::{self, CustomResult}, headers, pii::{self, prelude::*, Secret}, @@ -147,35 +147,34 @@ pub fn get_dotted_jws(jws: encryption::JwsBody) -> String { } pub async fn get_decrypted_response_payload( - jwekey: &Jwekey, + jwekey: &settings::Jwekey, jwe_body: encryption::JweBody, + #[cfg(feature = "kms")] kms_config: &settings::Kms, ) -> CustomResult { #[cfg(feature = "kms")] - let public_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_encryption_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Fails to get public key of vault")?; + let public_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_encryption_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Fails to get public key of vault")?; + #[cfg(feature = "kms")] + let private_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_private_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting private key for signing jws")?; + #[cfg(not(feature = "kms"))] let public_key = jwekey.vault_encryption_key.to_owned(); - #[cfg(feature = "kms")] - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_private_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting private key for signing jws")?; #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.to_owned(); + let jwt = get_dotted_jwe(jwe_body); let key_id = basilisk_hs_key_id(); let alg = jwe::RSA_OAEP; - let jwe_decrypted = encryption::decrypt_jwe(jwekey, &jwt, key_id, key_id, private_key, alg) + let jwe_decrypted = encryption::decrypt_jwe(&jwt, key_id, key_id, private_key, alg) .await .change_context(errors::VaultError::SaveCardFailed) .attach_printable("Jwe Decryption failed for JweBody for vault")?; @@ -191,8 +190,9 @@ pub async fn get_decrypted_response_payload( } pub async fn mk_basilisk_req( - jwekey: &Jwekey, + jwekey: &settings::Jwekey, jws: &str, + #[cfg(feature = "kms")] kms_config: &settings::Kms, ) -> CustomResult { let jws_payload: Vec<&str> = jws.split('.').collect(); @@ -208,19 +208,19 @@ pub async fn mk_basilisk_req( let payload = utils::Encode::::encode_to_vec(&jws_body) .change_context(errors::VaultError::SaveCardFailed)?; + #[cfg(feature = "kms")] - let public_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_encryption_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Fails to get encryption key of vault")?; + let public_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_encryption_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Fails to get encryption key of vault")?; + #[cfg(not(feature = "kms"))] let public_key = jwekey.vault_encryption_key.to_owned(); - let jwe_encrypted = encryption::encrypt_jwe(jwekey, &payload, public_key) + let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) .await .change_context(errors::VaultError::SaveCardFailed) .attach_printable("Error on jwe encrypt")?; @@ -242,13 +242,12 @@ pub async fn mk_basilisk_req( } pub async fn mk_add_card_request_hs( - jwekey: &Jwekey, - locker: &Locker, + jwekey: &settings::Jwekey, + locker: &settings::Locker, card: &api::CardDetail, customer_id: &str, - _req: &api::PaymentMethodCreate, - _locker_id: &str, merchant_id: &str, + #[cfg(feature = "kms")] kms_config: &settings::Kms, ) -> CustomResult { let merchant_customer_id = if cfg!(feature = "sandbox") { format!("{customer_id}::{merchant_id}") @@ -271,15 +270,15 @@ pub async fn mk_add_card_request_hs( }; let payload = utils::Encode::>::encode_to_vec(&store_card_req) .change_context(errors::VaultError::RequestEncodingFailed)?; + #[cfg(feature = "kms")] - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_private_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting private key for signing jws")?; + let private_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_private_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting private key for signing jws")?; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.to_owned(); @@ -287,7 +286,13 @@ pub async fn mk_add_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req( + jwekey, + &jws, + #[cfg(feature = "kms")] + kms_config, + ) + .await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -366,7 +371,7 @@ pub fn mk_add_card_response( } pub fn mk_add_card_request( - locker: &Locker, + locker: &settings::Locker, card: &api::CardDetail, customer_id: &str, _req: &api::PaymentMethodCreate, @@ -399,11 +404,12 @@ pub fn mk_add_card_request( } pub async fn mk_get_card_request_hs( - jwekey: &Jwekey, - locker: &Locker, + jwekey: &settings::Jwekey, + locker: &settings::Locker, customer_id: &str, merchant_id: &str, card_reference: &str, + #[cfg(feature = "kms")] kms_config: &settings::Kms, ) -> CustomResult { let merchant_customer_id = if cfg!(feature = "sandbox") { format!("{customer_id}::{merchant_id}") @@ -417,15 +423,15 @@ pub async fn mk_get_card_request_hs( }; let payload = utils::Encode::>::encode_to_vec(&card_req_body) .change_context(errors::VaultError::RequestEncodingFailed)?; + #[cfg(feature = "kms")] - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_private_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting private key for signing jws")?; + let private_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_private_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting private key for signing jws")?; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.to_owned(); @@ -433,7 +439,13 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req( + jwekey, + &jws, + #[cfg(feature = "kms")] + kms_config, + ) + .await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -446,7 +458,7 @@ pub async fn mk_get_card_request_hs( } pub fn mk_get_card_request<'a>( - locker: &Locker, + locker: &settings::Locker, locker_id: &'a str, card_id: &'a str, ) -> CustomResult { @@ -484,11 +496,12 @@ pub fn mk_get_card_response(card: GetCardResponse) -> errors::RouterResult } pub async fn mk_delete_card_request_hs( - jwekey: &Jwekey, - locker: &Locker, + jwekey: &settings::Jwekey, + locker: &settings::Locker, customer_id: &str, merchant_id: &str, card_reference: &str, + #[cfg(feature = "kms")] kms_config: &settings::Kms, ) -> CustomResult { let merchant_customer_id = if cfg!(feature = "sandbox") { format!("{customer_id}::{merchant_id}") @@ -502,15 +515,14 @@ pub async fn mk_delete_card_request_hs( }; let payload = utils::Encode::>::encode_to_vec(&card_req_body) .change_context(errors::VaultError::RequestEncodingFailed)?; + #[cfg(feature = "kms")] - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &jwekey.aws_region, - &jwekey.aws_key_id, - jwekey.vault_private_key.to_string(), - ) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting private key for signing jws")?; + let private_key = kms::get_kms_client(kms_config) + .await + .decrypt(&jwekey.vault_private_key) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting private key for signing jws")?; #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.to_owned(); @@ -519,7 +531,13 @@ pub async fn mk_delete_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req( + jwekey, + &jws, + #[cfg(feature = "kms")] + kms_config, + ) + .await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -532,7 +550,7 @@ pub async fn mk_delete_card_request_hs( } pub fn mk_delete_card_request<'a>( - locker: &Locker, + locker: &settings::Locker, merchant_id: &'a str, card_id: &'a str, ) -> CustomResult { @@ -584,7 +602,7 @@ pub fn get_card_detail( //------------------------------------------------TokenizeService------------------------------------------------ pub fn mk_crud_locker_request( - locker: &Locker, + locker: &settings::Locker, path: &str, req: api::TokenizePayloadEncrypted, ) -> CustomResult { diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 0c994dc20e..6e809a0f68 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -6,7 +6,7 @@ use masking::PeekInterface; use router_env::{instrument, tracing}; use crate::{ - configs::settings::Jwekey, + configs::settings, core::errors::{self, CustomResult, RouterResult}, logger, routes, types::{ @@ -393,7 +393,7 @@ impl Vault { } //------------------------------------------------TokenizeService------------------------------------------------ -pub fn get_key_id(keys: &Jwekey) -> &str { +pub fn get_key_id(keys: &settings::Jwekey) -> &str { let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources if key_identifier == "1" { &keys.locker_key_identifier1 @@ -404,40 +404,30 @@ pub fn get_key_id(keys: &Jwekey) -> &str { #[cfg(feature = "basilisk")] async fn get_locker_jwe_keys( - keys: &Jwekey, + keys: &settings::Jwekey, + kms_config: &settings::Kms, ) -> CustomResult<(String, String), errors::EncryptionError> { let key_id = get_key_id(keys); - if key_id == keys.locker_key_identifier1 { - let public_key = kms::KeyHandler::get_kms_decrypted_key( - &keys.aws_region, - &keys.aws_key_id, - keys.locker_encryption_key1.to_string(), - ) - .await?; - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &keys.aws_region, - &keys.aws_key_id, - keys.locker_decryption_key1.to_string(), - ) - .await?; - Ok((public_key, private_key)) + let (encryption_key, decryption_key) = if key_id == keys.locker_key_identifier1 { + (&keys.locker_encryption_key1, &keys.locker_decryption_key1) } else if key_id == keys.locker_key_identifier2 { - let public_key = kms::KeyHandler::get_kms_decrypted_key( - &keys.aws_region, - &keys.aws_key_id, - keys.locker_encryption_key2.to_string(), - ) - .await?; - let private_key = kms::KeyHandler::get_kms_decrypted_key( - &keys.aws_region, - &keys.aws_key_id, - keys.locker_decryption_key2.to_string(), - ) - .await?; - Ok((public_key, private_key)) + (&keys.locker_encryption_key2, &keys.locker_decryption_key2) } else { - Err(errors::EncryptionError.into()) - } + return Err(errors::EncryptionError.into()); + }; + + let public_key = kms::get_kms_client(kms_config) + .await + .decrypt(encryption_key) + .await + .change_context(errors::EncryptionError)?; + let private_key = kms::get_kms_client(kms_config) + .await + .decrypt(decryption_key) + .await + .change_context(errors::EncryptionError)?; + + Ok((public_key, private_key)) } #[cfg(feature = "basilisk")] @@ -458,15 +448,14 @@ pub async fn create_tokenize( ) .change_context(errors::ApiErrorResponse::InternalServerError)?; - let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey) + let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey, &state.conf.kms) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Encryption key")?; - let encrypted_payload = - services::encrypt_jwe(&state.conf.jwekey, payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; + let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting Encrypt JWE response")?; let create_tokenize_request = api::TokenizePayloadEncrypted { payload: encrypted_payload, @@ -493,7 +482,6 @@ pub async fn create_tokenize( .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; let alg = jwe::RSA_OAEP_256; let decrypted_payload = services::decrypt_jwe( - &state.conf.jwekey, &resp.payload, get_key_id(&state.conf.jwekey), &resp.key_id, @@ -531,15 +519,14 @@ pub async fn get_tokenized_data( let payload = serde_json::to_string(&payload_to_be_encrypted) .map_err(|_x| errors::ApiErrorResponse::InternalServerError)?; - let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey) + let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey, &state.conf.kms) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Encryption key")?; - let encrypted_payload = - services::encrypt_jwe(&state.conf.jwekey, payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; + let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting Encrypt JWE response")?; let create_tokenize_request = api::TokenizePayloadEncrypted { payload: encrypted_payload, key_id: get_key_id(&state.conf.jwekey).to_string(), @@ -564,7 +551,6 @@ pub async fn get_tokenized_data( .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; let alg = jwe::RSA_OAEP_256; let decrypted_payload = services::decrypt_jwe( - &state.conf.jwekey, &resp.payload, get_key_id(&state.conf.jwekey), &resp.key_id, @@ -598,15 +584,14 @@ pub async fn delete_tokenized_data( let payload = serde_json::to_string(&payload_to_be_encrypted) .map_err(|_x| errors::ApiErrorResponse::InternalServerError)?; - let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey) + let (public_key, private_key) = get_locker_jwe_keys(&state.conf.jwekey, &state.conf.kms) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Encryption key")?; - let encrypted_payload = - services::encrypt_jwe(&state.conf.jwekey, payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; + let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting Encrypt JWE response")?; let create_tokenize_request = api::TokenizePayloadEncrypted { payload: encrypted_payload, key_id: get_key_id(&state.conf.jwekey).to_string(), @@ -631,7 +616,6 @@ pub async fn delete_tokenized_data( .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; let alg = jwe::RSA_OAEP_256; let decrypted_payload = services::decrypt_jwe( - &state.conf.jwekey, &resp.payload, get_key_id(&state.conf.jwekey), &resp.key_id, diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 1ebb092897..700c44a151 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -43,6 +43,8 @@ pub async fn api_key_create( api_keys::create_api_key( &*state.store, &state.conf.api_keys, + #[cfg(feature = "kms")] + &state.conf.kms, payload, merchant_id.clone(), ) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7f5c08133c..dee7235f65 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -55,7 +55,6 @@ impl AppState { } } - #[allow(unused_variables)] pub async fn new(conf: Settings) -> Self { Self::with_storage(conf, StorageImpl::Postgresql).await } diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 222c58476e..4173beb59a 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,6 +1,7 @@ pub mod api; pub mod authentication; pub mod encryption; +#[cfg(feature = "kms")] pub mod kms; pub mod logger; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 15e5fe2ff9..de437df04d 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -55,9 +55,12 @@ where let api_key = api_keys::PlaintextApiKey::from(api_key); let hash_key = { let config = state.conf(); - api_keys::HASH_KEY - .get_or_try_init(|| api_keys::get_hash_key(&config.api_keys)) - .await? + api_keys::get_hash_key( + &config.api_keys, + #[cfg(feature = "kms")] + &config.kms, + ) + .await? }; let hashed_api_key = api_key.keyed_hash(hash_key.peek()); diff --git a/crates/router/src/services/encryption.rs b/crates/router/src/services/encryption.rs index 7d74281a0c..fe328abe08 100644 --- a/crates/router/src/services/encryption.rs +++ b/crates/router/src/services/encryption.rs @@ -7,7 +7,6 @@ use ring::{aead::*, error::Unspecified}; use serde::{Deserialize, Serialize}; use crate::{ - configs::settings::Jwekey, core::errors::{self, CustomResult}, utils, }; @@ -119,7 +118,6 @@ pub fn decrypt(mut data: Vec, key: &[u8]) -> CustomResult CustomResult { @@ -133,15 +131,14 @@ pub async fn encrypt_jwe( .into_report() .change_context(errors::EncryptionError) .attach_printable("Error getting JweEncryptor")?; - let jwt = jwe::serialize_compact(payload, &src_header, &encrypter) + + jwe::serialize_compact(payload, &src_header, &encrypter) .into_report() .change_context(errors::EncryptionError) - .attach_printable("Error getting jwt string")?; - Ok(jwt) + .attach_printable("Error getting jwt string") } pub async fn decrypt_jwe( - _keys: &Jwekey, jwt: &str, key_id: &str, resp_key_id: &str, @@ -162,11 +159,11 @@ pub async fn decrypt_jwe( Err(report!(errors::EncryptionError).attach_printable("Missing ciphertext blob")) .attach_printable("key_id mismatch, Error authenticating response") })?; - let resp = String::from_utf8(dst_payload) + + String::from_utf8(dst_payload) .into_report() .change_context(errors::EncryptionError) - .attach_printable("Could not convert to UTF-8")?; - Ok(resp) + .attach_printable("Could not decode JWE payload from UTF-8") } pub async fn jws_sign_payload( @@ -240,7 +237,6 @@ mod tests { async fn test_jwe() { let conf = settings::Settings::new().unwrap(); let jwt = encrypt_jwe( - &conf.jwekey, "request_payload".as_bytes(), conf.jwekey.locker_encryption_key1.to_owned(), ) @@ -248,7 +244,6 @@ mod tests { .unwrap(); let alg = jwe::RSA_OAEP_256; let payload = decrypt_jwe( - &conf.jwekey, &jwt, &conf.jwekey.locker_key_identifier1, &conf.jwekey.locker_key_identifier1, diff --git a/crates/router/src/services/kms.rs b/crates/router/src/services/kms.rs index e5887a9840..30965aa7ed 100644 --- a/crates/router/src/services/kms.rs +++ b/crates/router/src/services/kms.rs @@ -1,73 +1,76 @@ -use crate::core::errors::{self, CustomResult}; +use aws_config::meta::region::RegionProviderChain; +use aws_sdk_kms::{types::Blob, Client, Region}; +use base64::Engine; +use error_stack::{IntoReport, ResultExt}; -pub struct KeyHandler; +use crate::{ + configs::settings, + consts, + core::errors::{self, CustomResult}, + logger, + routes::metrics, +}; -#[cfg(feature = "kms")] -mod aws_kms { - use aws_config::meta::region::RegionProviderChain; - use aws_sdk_kms::{types::Blob, Client, Region}; - use base64::Engine; - use error_stack::{report, IntoReport, ResultExt}; +static KMS_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); - use super::*; - use crate::{consts, logger}; +#[inline] +pub async fn get_kms_client(config: &settings::Kms) -> &KmsClient { + KMS_CLIENT.get_or_init(|| KmsClient::new(config)).await +} - impl KeyHandler { - // Fetching KMS decrypted key - // | Amazon KMS decryption - // This expect a base64 encoded input but we values are set via aws cli in env than cli - // already does that so we don't need to - pub async fn get_kms_decrypted_key( - aws_region: &str, - aws_key_id: &str, - kms_enc_key: String, - ) -> CustomResult { - let region_provider = - RegionProviderChain::first_try(Region::new(aws_region.to_owned())); - let shared_config = aws_config::from_env().region(region_provider).load().await; - let client = Client::new(&shared_config); - let data = consts::BASE64_ENGINE - .decode(kms_enc_key) - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error decoding from base64")?; - let blob = Blob::new(data); - let resp = client - .decrypt() - .key_id(aws_key_id) - .ciphertext_blob(blob) - .send() - .await - .map_err(|error| { - logger::error!(kms_sdk_error=?error, "Failed to KMS decrypt data"); - error - }) - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error decrypting kms encrypted data")?; - match resp.plaintext() { - Some(inner) => { - let bytes = inner.as_ref().to_vec(); - let res = String::from_utf8(bytes) - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Could not convert to UTF-8")?; - Ok(res) - } - None => Err(report!(errors::EncryptionError) - .attach_printable("Missing plaintext in response")), - } +pub struct KmsClient { + inner_client: Client, + key_id: String, +} + +impl KmsClient { + /// Constructs a new KMS client. + pub async fn new(config: &settings::Kms) -> 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(), } } -} -#[cfg(not(feature = "kms"))] -impl KeyHandler { - pub async fn get_kms_decrypted_key( - _aws_region: &str, - _aws_key_id: &str, - key: String, - ) -> CustomResult { - Ok(key) + /// 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 data = consts::BASE64_ENGINE + .decode(data) + .into_report() + .change_context(errors::KmsError::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!(kms_sdk_error=?error, "Failed to KMS decrypt data"); + metrics::AWS_KMS_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + .into_report() + .change_context(errors::KmsError::DecryptionFailed)?; + + decrypt_output + .plaintext + .ok_or(errors::KmsError::MissingPlaintextDecryptionOutput) + .into_report() + .and_then(|blob| { + String::from_utf8(blob.into_inner()) + .into_report() + .change_context(errors::KmsError::Utf8DecodingFailed) + }) } }