From 20386bd169f5a7310b295989c85d41995bb9ea02 Mon Sep 17 00:00:00 2001 From: Sk Sakil Mostak Date: Mon, 3 Feb 2025 02:22:02 +0530 Subject: [PATCH] refactor: resolve comments (part1) --- Cargo.lock | 7 ++ crates/common_enums/src/enums.rs | 16 +-- crates/common_utils/Cargo.toml | 1 + crates/common_utils/src/custom_serde.rs | 16 ++- crates/common_utils/src/lib.rs | 10 ++ crates/masking/src/secret.rs | 7 ++ crates/router/src/configs/settings.rs | 7 +- crates/router/src/configs/validations.rs | 15 +++ crates/router/src/core/errors.rs | 4 +- crates/router/src/core/payments/helpers.rs | 135 ++++++++++----------- 10 files changed, 127 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dcfb88cc895..7e2ab6eccd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1528,6 +1528,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c6d128af408d8ebd08331f0331cf2cf20d19e6c44a7aec58791641ecc8c0b5" + [[package]] name = "base64-simd" version = "0.8.0" @@ -2044,6 +2050,7 @@ version = "0.1.0" dependencies = [ "async-trait", "base64 0.22.1", + "base64-serde", "blake3", "bytes 1.7.1", "common_enums", diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e7b093f6f60..7eaff9bfed0 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3651,21 +3651,7 @@ pub enum FeatureStatus { Supported, } -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - strum::Display, - strum::VariantNames, - strum::EnumIter, - strum::EnumString, - Deserialize, - Serialize, -)] -#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum GooglePayAuthMethod { /// Contain pan data only diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index dcdb4c760b1..3d31c7a6a98 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -26,6 +26,7 @@ payment_methods_v2 = [] [dependencies] async-trait = { version = "0.1.79", optional = true } base64 = "0.22.0" +base64-serde = "0.8.0" blake3 = { version = "1.5.1", features = ["serde"] } bytes = "1.6.0" diesel = "2.2.3" diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index 63ef30011f7..06087f082e0 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -203,8 +203,20 @@ pub mod timestamp { /// pub mod json_string { - use serde::de::{self, Deserialize, DeserializeOwned, Deserializer}; - use serde_json; + use serde::{ + de::{self, Deserialize, DeserializeOwned, Deserializer}, + ser::{self, Serialize, Serializer}, + }; + + /// Serialize a type to json_string format + pub fn serialize(value: &T, serializer: S) -> Result + where + T: Serialize, + S: Serializer, + { + let j = serde_json::to_string(value).map_err(ser::Error::custom)?; + j.serialize(serializer) + } /// Deserialize a string which is in json format pub fn deserialize<'de, T, D>(deserializer: D) -> Result diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 463ec3ee1b6..fd0c935035f 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -35,6 +35,8 @@ pub mod hashing; #[cfg(feature = "metrics")] pub mod metrics; +pub use base64_serializer::Base64Serializer; + /// Date-time utilities. pub mod date_time { #[cfg(feature = "async_ext")] @@ -296,6 +298,14 @@ pub trait DbConnectionParams { } } +// Can't add doc comments for macro invocations, neither does the macro allow it. +#[allow(missing_docs)] +mod base64_serializer { + use base64_serde::base64_serde_type; + + base64_serde_type!(pub Base64Serializer, crate::consts::BASE64_ENGINE); +} + #[cfg(test)] mod nanoid_tests { #![allow(clippy::unwrap_used)] diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index 0bd28c3af92..7c3c120e86e 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -156,3 +156,10 @@ where SecretValue::default().into() } } + +// Required by base64-serde to serialize Secret +impl AsRef<[T]> for Secret> { + fn as_ref(&self) -> &[T] { + self.peek().as_slice() + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index a6877c9286c..d721a4db967 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -759,7 +759,7 @@ pub struct PazeDecryptConfig { pub paze_private_key_passphrase: Secret, } -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct GooglePayDecryptConfig { pub google_pay_root_signing_keys: Secret, } @@ -917,6 +917,11 @@ impl Settings { .map(|x| x.get_inner().validate()) .transpose()?; + self.google_pay_decrypt_keys + .as_ref() + .map(|x| x.get_inner().validate()) + .transpose()?; + self.key_manager.get_inner().validate()?; #[cfg(feature = "email")] self.email diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 7040998ccf0..11aa4e0cf51 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -257,6 +257,21 @@ impl super::settings::PazeDecryptConfig { } } +impl super::settings::GooglePayDecryptConfig { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when( + self.google_pay_root_signing_keys.is_default_or_empty(), + || { + Err(ApplicationError::InvalidConfigurationValueError( + "google_pay_root_signing_keys must not be empty".into(), + )) + }, + ) + } +} + impl super::settings::KeyManagerConfig { pub fn validate(&self) -> Result<(), ApplicationError> { use common_utils::fp_utils::when; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 313b8a2b7c1..b4c1c1c2c03 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -248,8 +248,8 @@ pub enum PazeDecryptionError { pub enum GooglePayDecryptionError { #[error("Recipient ID not found")] RecipientIdNotFound, - #[error("Failed to fetch time delta for expiration")] - SystemTimeError, + #[error("Invalid expiration time")] + InvalidExpirationTime, #[error("Failed to base64 decode input data")] Base64DecodingFailed, #[error("Failed to decrypt input data")] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 760d71726c8..2a58ac653db 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -5432,34 +5432,46 @@ pub const PROTOCOL: &str = "ECv2"; // Structs for keys and the main decryptor pub struct GooglePayTokenDecryptor { root_signing_keys: Vec, - recipient_id: Option, + recipient_id: Option>, private_key: PKey, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EncryptedData { signature: String, intermediate_signing_key: IntermediateSigningKey, protocol_version: GooglePayProtocolVersion, - signed_message: String, + #[serde(with = "common_utils::custom_serde::json_string")] + signed_message: GooglePaySignedMessage, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +pub struct GooglePaySignedMessage { + #[serde(with = "common_utils::Base64Serializer")] + encrypted_message: masking::Secret>, + #[serde(with = "common_utils::Base64Serializer")] + ephemeral_public_key: masking::Secret>, + #[serde(with = "common_utils::Base64Serializer")] + tag: masking::Secret>, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct IntermediateSigningKey { signed_key: String, signatures: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GooglePaySignedKey { key_value: String, key_expiration: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GooglePayRootSigningKey { key_value: String, @@ -5467,14 +5479,6 @@ pub struct GooglePayRootSigningKey { protocol_version: GooglePayProtocolVersion, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GooglePaySignedMessage { - encrypted_message: String, - ephemeral_public_key: String, - tag: String, -} - #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub enum GooglePayProtocolVersion { #[serde(rename = "ECv2")] @@ -5493,35 +5497,47 @@ fn check_expiration_date_is_valid( Ok(current_ms < expiration_ms) } -// Construct little endian format of u32 in hexadecimal +// Construct little endian format of u32 fn get_little_endian_format(number: u32) -> Vec { number.to_le_bytes().to_vec() } // Filter and parse the root signing keys based on protocol version and expiration time -fn filter_root_signing_keys(root_keys: Vec) -> Vec { - let root_signing_keys: Vec = root_keys +fn filter_root_signing_keys( + root_keys: Vec, +) -> CustomResult, errors::GooglePayDecryptionError> { + let root_signing_keys = root_keys .into_iter() .map(|key| { - serde_json::from_value(key).unwrap_or_else(|_| GooglePayRootSigningKey { - key_value: "".to_string(), - key_expiration: "".to_string(), - protocol_version: GooglePayProtocolVersion::EcProtocolVersion2, - }) + serde_json::from_value::(key) + .change_context(errors::GooglePayDecryptionError::DeserializationFailed) }) - .collect(); + .collect::, _>>()?; - root_signing_keys + let filtered_root_signing_keys = root_signing_keys .iter() .filter(|key| { key.protocol_version == GooglePayProtocolVersion::EcProtocolVersion2 && matches!( - check_expiration_date_is_valid(&key.key_expiration), + check_expiration_date_is_valid(&key.key_expiration).inspect_err( + |err| logger::warn!( + "Failed to check expirattion due to invalid format: {:?}", + err + ) + ), Ok(true) ) }) .cloned() - .collect::>() + .collect::>(); + + logger::info!( + "Filtered {} out of {} root signing keys", + filtered_root_signing_keys.len(), + root_signing_keys.len() + ); + + Ok(filtered_root_signing_keys) } impl GooglePayTokenDecryptor { @@ -5537,21 +5553,18 @@ impl GooglePayTokenDecryptor { // create a private key from the decoded key let private_key = PKey::private_key_from_pkcs8(&decoded_key) .change_context(errors::GooglePayDecryptionError::KeyDeserializationFailed) - .attach_printable(format!( - "cannot convert private key from given data: {:?}", - decoded_key - ))?; + .attach_printable("cannot convert private key from decode_key")?; // parse the root signing keys let root_keys_vector: Vec = serde_json::from_str(&root_keys.expose()) .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; // parse and filter the root signing keys by protocol version - let filtered_root_signing_keys = filter_root_signing_keys(root_keys_vector); + let filtered_root_signing_keys = filter_root_signing_keys(root_keys_vector)?; Ok(Self { root_signing_keys: filtered_root_signing_keys, - recipient_id: recipient_id.map(|id| id.expose()), + recipient_id, private_key, }) } @@ -5571,45 +5584,15 @@ impl GooglePayTokenDecryptor { self.verify_signature(&encrypted_data)?; } - // load the signed message from the token - let signed_message: serde_json::Value = - serde_json::from_str(&encrypted_data.signed_message) - .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; - - // base64 decode the required fields - let ephemeral_public_key = BASE64_ENGINE - .decode( - signed_message - .get("ephemeralPublicKey") - .ok_or(errors::GooglePayDecryptionError::ParsingFailed)? - .as_str() - .ok_or(errors::GooglePayDecryptionError::DeserializationFailed)?, - ) - .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; - let tag = BASE64_ENGINE - .decode( - signed_message - .get("tag") - .ok_or(errors::GooglePayDecryptionError::ParsingFailed)? - .as_str() - .ok_or(errors::GooglePayDecryptionError::DeserializationFailed)?, - ) - .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; - let encrypted_message = BASE64_ENGINE - .decode( - signed_message - .get("encryptedMessage") - .ok_or(errors::GooglePayDecryptionError::ParsingFailed)? - .as_str() - .ok_or(errors::GooglePayDecryptionError::DeserializationFailed)?, - ) - .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; + let ephemeral_public_key = encrypted_data.signed_message.ephemeral_public_key.peek(); + let tag = encrypted_data.signed_message.tag.peek(); + let encrypted_message = encrypted_data.signed_message.encrypted_message.peek(); // derive the shared key - let shared_key = self.get_shared_key(&ephemeral_public_key)?; + let shared_key = self.get_shared_key(ephemeral_public_key)?; // derive the symmetric encryption key and MAC key - let derived_key = self.derive_key(&ephemeral_public_key, &shared_key)?; + let derived_key = self.derive_key(ephemeral_public_key, &shared_key)?; let symmetric_encryption_key = derived_key .get(..32) .ok_or(errors::GooglePayDecryptionError::ParsingFailed)?; // First 32 bytes for AES-256 @@ -5618,10 +5601,10 @@ impl GooglePayTokenDecryptor { .ok_or(errors::GooglePayDecryptionError::ParsingFailed)?; // Remaining bytes for HMAC // verify the HMAC of the message - self.verify_hmac(mac_key, &tag, &encrypted_message)?; + self.verify_hmac(mac_key, tag, encrypted_message)?; // decrypt the message - let decrypted = self.decrypt_message(symmetric_encryption_key, &encrypted_message)?; + let decrypted = self.decrypt_message(symmetric_encryption_key, encrypted_message)?; // parse the decrypted data let decrypted_data: serde_json::Value = serde_json::from_slice(&decrypted) @@ -5787,11 +5770,15 @@ impl GooglePayTokenDecryptor { let sender_id = String::from_utf8(SENDER_ID.to_vec()) .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + // parse the signed message to string + let signed_message = serde_json::to_string(&encrypted_data.signed_message) + .change_context(errors::GooglePayDecryptionError::SignedKeyParsingFailure)?; + // construct the signed data let signed_data = self.construct_signed_data_for_signature_verification( &sender_id, PROTOCOL, - &encrypted_data.signed_message, + &signed_message, )?; // hash the signed data @@ -5840,7 +5827,8 @@ impl GooglePayTokenDecryptor { let recipient_id = self .recipient_id .clone() - .ok_or(errors::GooglePayDecryptionError::RecipientIdNotFound)?; + .ok_or(errors::GooglePayDecryptionError::RecipientIdNotFound)? + .expose(); let length_of_sender_id = u32::try_from(sender_id.len()) .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; let length_of_recipient_id = u32::try_from(recipient_id.len()) @@ -5910,7 +5898,9 @@ impl GooglePayTokenDecryptor { let input_key_material = [ephemeral_public_key_bytes, shared_key].concat(); // initialize HKDF with SHA-256 as the hash function - let hkdf: Hkdf = Hkdf::new(Some(&[0u8; 32]), &input_key_material); // 32 zeroed bytes as salt + // Salt is not provided as per the Google Pay documentation + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#encrypt-spec + let hkdf: Hkdf = Hkdf::new(None, &input_key_material); // 32 zeroed bytes as salt // derive 64 bytes for the output key (symmetric encryption + MAC key) let mut output_key = vec![0u8; 64]; @@ -5922,6 +5912,7 @@ impl GooglePayTokenDecryptor { } // Verify the Hmac key + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#encrypt-spec fn verify_hmac( &self, mac_key: &[u8], @@ -5940,6 +5931,8 @@ impl GooglePayTokenDecryptor { encrypted_message: &[u8], ) -> CustomResult, errors::GooglePayDecryptionError> { //initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process. + // zero iv is being passed as specified in Google Pay documentation + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#decrypt-token let iv = [0u8; 16]; // extract the tag from the end of the encrypted message