From 75ee6327820fe31ff2c379250eae3e7974e6ae6c Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:49:10 +0530 Subject: [PATCH] feat(apple_pay): add support for pre decrypted apple pay token (#2056) Co-authored-by: Sangamesh --- .typos.toml | 2 + Cargo.lock | 125 ++++++++ config/config.example.toml | 6 + config/development.toml | 6 + crates/api_models/src/payments.rs | 29 +- crates/router/Cargo.toml | 2 + crates/router/src/configs/settings.rs | 14 +- .../src/connector/bluesnap/transformers.rs | 50 ++-- .../braintree_graphql_transformers.rs | 7 +- .../src/connector/checkout/transformers.rs | 73 ++++- .../src/connector/mollie/transformers.rs | 26 +- .../src/connector/payme/transformers.rs | 15 +- .../src/connector/square/transformers.rs | 34 ++- .../router/src/connector/stax/transformers.rs | 16 +- .../src/connector/stripe/transformers.rs | 129 +++++++-- crates/router/src/connector/utils.rs | 4 +- crates/router/src/consts.rs | 4 + crates/router/src/core/errors.rs | 18 ++ crates/router/src/core/payments.rs | 270 ++++++++++++++++-- crates/router/src/core/payments/flows.rs | 3 +- .../src/core/payments/flows/approve_flow.rs | 4 +- .../src/core/payments/flows/authorize_flow.rs | 6 +- .../src/core/payments/flows/cancel_flow.rs | 4 +- .../src/core/payments/flows/capture_flow.rs | 4 +- .../payments/flows/complete_authorize_flow.rs | 4 +- .../src/core/payments/flows/psync_flow.rs | 4 +- .../src/core/payments/flows/reject_flow.rs | 4 +- .../src/core/payments/flows/session_flow.rs | 270 +++++++++++++----- .../src/core/payments/flows/verify_flow.rs | 6 +- crates/router/src/core/payments/helpers.rs | 196 ++++++++++++- .../router/src/core/payments/tokenization.rs | 13 +- .../router/src/core/payments/transformers.rs | 22 +- crates/router/src/types.rs | 27 +- crates/router/tests/connectors/utils.rs | 4 +- 34 files changed, 1174 insertions(+), 227 deletions(-) diff --git a/.typos.toml b/.typos.toml index ace2acb90b..8ed1c84a5b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -22,6 +22,8 @@ bottm = "bottm" # name of a css class for nexinets ui test klick = "klick" # Swedish word for clicks optin = "optin" # Boku preflow name optin_id = "optin_id" # Boku's id for optin flow +deriver = "deriver" +Deriver = "Deriver" [default.extend-words] aci = "aci" # Name of a connector diff --git a/Cargo.lock b/Cargo.lock index f5377afa40..bddc9e50df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,45 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.22", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -1689,6 +1728,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "data_models" version = "0.1.0" @@ -1734,6 +1779,20 @@ dependencies = [ "byteorder", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "derive_deref" version = "1.1.1" @@ -1856,6 +1915,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -3226,6 +3296,15 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -4087,6 +4166,7 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", + "openssl", "qrcode", "rand 0.8.5", "redis_interface", @@ -4119,6 +4199,7 @@ dependencies = [ "utoipa-swagger-ui", "uuid", "wiremock", + "x509-parser", ] [[package]] @@ -4231,6 +4312,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.20" @@ -4849,6 +4939,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -5456,6 +5558,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unidecode" version = "0.3.0" @@ -5954,6 +6062,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.22", +] + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/config/config.example.toml b/config/config.example.toml index 547aef7afe..97a50672cf 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -403,3 +403,9 @@ adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_ba [multiple_api_version_supported_connectors] supported_connectors = "braintree" + +[applepay_decrypt_keys] +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index dd6ecae2a4..f1363d2235 100644 --- a/config/development.toml +++ b/config/development.toml @@ -414,3 +414,9 @@ payout_eligibility = true [multiple_api_version_supported_connectors] supported_connectors = "braintree" + +[applepay_decrypt_keys] +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c5267c9ab8..2e637e13c0 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2363,8 +2363,15 @@ pub struct ApplepayConnectorMetadataRequest { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ApplepaySessionTokenData { - #[serde(rename = "apple_pay")] - pub data: ApplePayMetadata, + #[serde(flatten)] + pub data: ApplepaySessionTokenMetadata, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ApplepaySessionTokenMetadata { + ApplePayCombined(ApplePayCombinedMetadata), + ApplePay(ApplePayMetadata), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -2373,6 +2380,19 @@ pub struct ApplePayMetadata { pub session_token_data: SessionTokenInfo, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ApplePayCombinedMetadata { + Simplified { + payment_request_data: PaymentRequestMetadata, + session_token_data: SessionTokenForSimplifiedApplePay, + }, + Manual { + payment_request_data: PaymentRequestMetadata, + session_token_data: SessionTokenInfo, + }, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentRequestMetadata { pub supported_networks: Vec, @@ -2390,6 +2410,11 @@ pub struct SessionTokenInfo { pub initiative_context: String, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct SessionTokenForSimplifiedApplePay { + pub initiative_context: String, +} + #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] #[serde(tag = "wallet_name")] #[serde(rename_all = "snake_case")] diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 51bfcddac4..84ce469353 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -84,6 +84,8 @@ url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } +openssl = "0.10.55" +x509-parser = "0.15.0" # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 168a8bc3f0..20e33783e4 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -94,8 +94,8 @@ pub struct Settings { pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig, #[cfg(feature = "payouts")] pub payouts: Payouts, + pub applepay_decrypt_keys: ApplePayDecryptConifg, pub multiple_api_version_supported_connectors: MultipleApiVersionSupportedConnectors, - #[cfg(all(feature = "olap", feature = "kms"))] pub applepay_merchant_configs: ApplepayMerchantConfigs, } @@ -614,16 +614,24 @@ pub struct FileUploadConfig { #[derive(Debug, Deserialize, Clone, Default)] pub struct DelayedSessionConfig { - #[serde(deserialize_with = "delayed_session_deser")] + #[serde(deserialize_with = "deser_to_get_connectors")] pub connectors_with_delayed_session_response: HashSet, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ApplePayDecryptConifg { + pub apple_pay_ppc: String, + pub apple_pay_ppc_key: String, + pub apple_pay_merchant_cert: String, + pub apple_pay_merchant_cert_key: String, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct ConnectorRequestReferenceIdConfig { pub merchant_ids_send_payment_id_as_connector_request_id: HashSet, } -fn delayed_session_deser<'a, D>( +fn deser_to_get_connectors<'a, D>( deserializer: D, ) -> Result, D::Error> where diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index e38a4c9342..d2be16530a 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -348,11 +348,22 @@ impl TryFrom<&types::PaymentsSessionRouterData> for BluesnapCreateWalletToken { "ApplepaySessionTokenData", ) .change_context(errors::ConnectorError::ParsingFailed)?; + let session_token_data = match applepay_metadata.data { + payments::ApplepaySessionTokenMetadata::ApplePay(apple_pay_data) => { + Ok(apple_pay_data.session_token_data) + } + payments::ApplepaySessionTokenMetadata::ApplePayCombined(_apple_pay_combined_data) => { + Err(errors::ConnectorError::FlowNotSupported { + flow: "apple pay combined".to_string(), + connector: "bluesnap".to_string(), + }) + } + }?; Ok(Self { wallet_type: "APPLE_PAY".to_string(), validation_url: consts::APPLEPAY_VALIDATION_URL.to_string().into(), - domain_name: applepay_metadata.data.session_token_data.initiative_context, - display_name: Some(applepay_metadata.data.session_token_data.display_name), + domain_name: session_token_data.initiative_context, + display_name: Some(session_token_data.display_name), }) } } @@ -383,6 +394,18 @@ impl TryFrom { + Err(errors::ConnectorError::FlowNotSupported { + flow: "apple pay combined".to_string(), + connector: "bluesnap".to_string(), + }) + } + payments::ApplepaySessionTokenMetadata::ApplePay(apple_pay) => { + Ok((apple_pay.payment_request_data, apple_pay.session_token_data)) + } + }?; + Ok(Self { response: Ok(types::PaymentsResponseData::SessionResponse { session_token: types::api::SessionToken::ApplePay(Box::new( @@ -395,28 +418,13 @@ impl TryFrom for BraintreePaymentsRequest { query, variables: VariablePaymentInput { input: PaymentInput { - payment_method_id: item.get_payment_method_token()?, + payment_method_id: match item.get_payment_method_token()? { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, transaction: TransactionBody { amount: utils::to_currency_base_unit( item.request.amount, diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index c0e50d402e..25403568a7 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1,6 +1,6 @@ use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, Secret}; +use masking::{ExposeInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -11,6 +11,7 @@ use crate::{ core::errors, services, types::{self, api, storage::enums, transformers::ForeignFrom}, + utils::OptionExt, }; #[derive(Debug, Serialize)] @@ -21,6 +22,13 @@ pub enum TokenRequest { Applepay(CheckoutApplePayData), } +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "type", content = "token_data")] +pub enum PreDecryptedTokenRequest { + Applepay(Box), +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CheckoutGooglePayData { @@ -155,6 +163,19 @@ pub struct WalletSource { pub enum PaymentSource { Card(CardSource), Wallets(WalletSource), + ApplePayPredecrypt(Box), +} + +#[derive(Debug, Serialize)] +pub struct ApplePayPredecrypt { + token: Secret, + #[serde(rename = "type")] + decrypt_type: String, + token_type: String, + expiry_month: Secret, + expiry_year: Secret, + eci: Option>, + cryptogram: Secret, } #[derive(Debug, Serialize)] @@ -230,13 +251,57 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest { Ok(a) } api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(_) - | api_models::payments::WalletData::ApplePay(_) => { + api_models::payments::WalletData::GooglePay(_) => { Ok(PaymentSource::Wallets(WalletSource { source_type: CheckoutSourceTypes::Token, - token: item.get_payment_method_token()?, + token: match item.get_payment_method_token()? { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, })) } + api_models::payments::WalletData::ApplePay(_) => { + let payment_method_token = item + .payment_method_token + .to_owned() + .get_required_value("payment_token") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let decrypt_data = match payment_method_token { + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + types::PaymentMethodToken::ApplePayDecrypt(data) => data, + }; + let expiry_year_4_digit = Secret::new(format!( + "20{:?}", + decrypt_data + .application_expiration_date + .peek() + .get(0..2) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + )); + let expiry_month = Secret::new( + decrypt_data + .application_expiration_date + .peek() + .get(2..4) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_owned(), + ); + Ok(PaymentSource::ApplePayPredecrypt(Box::new( + ApplePayPredecrypt { + token: decrypt_data.application_primary_account_number, + decrypt_type: "network_token".to_string(), + token_type: "applepay".to_string(), + expiry_month, + expiry_year: expiry_year_4_digit, + eci: decrypt_data.payment_data.eci_indicator, + cryptogram: decrypt_data.payment_data.online_payment_cryptogram, + }, + ))) + } api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index c7738ed5b4..d0036d3c2f 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -131,13 +131,21 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for MolliePaymentsRequest { let redirect_url = item.request.get_return_url()?; let payment_method_data = match item.request.capture_method.unwrap_or_default() { enums::CaptureMethod::Automatic => match &item.request.payment_method_data { - api_models::payments::PaymentMethodData::Card(_) => Ok( - PaymentMethodData::CreditCard(Box::new(CreditCardMethodData { - billing_address: get_billing_details(item)?, - shipping_address: get_shipping_details(item)?, - card_token: Some(Secret::new(item.get_payment_method_token()?)), - })), - ), + api_models::payments::PaymentMethodData::Card(_) => { + let pm_token = item.get_payment_method_token()?; + Ok(PaymentMethodData::CreditCard(Box::new( + CreditCardMethodData { + billing_address: get_billing_details(item)?, + shipping_address: get_shipping_details(item)?, + card_token: Some(Secret::new(match pm_token { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + })), + }, + ))) + } api_models::payments::PaymentMethodData::BankRedirect(ref redirect_data) => { PaymentMethodData::try_from(redirect_data) } @@ -472,7 +480,9 @@ impl ) -> Result { Ok(Self { status: storage_enums::AttemptStatus::Pending, - payment_method_token: Some(item.response.card_token.clone().expose()), + payment_method_token: Some(types::PaymentMethodToken::Token( + item.response.card_token.clone().expose(), + )), response: Ok(types::PaymentsResponseData::TokenizationResponse { token: item.response.card_token.expose(), }), diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 4cf87bde26..1917947d70 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -642,11 +642,12 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { .connector_transaction_id .clone() .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; - let buyer_key = match item.payment_method_token.clone() { - Some(key) => key, - None => Err(errors::ConnectorError::MissingRequiredField { - field_name: "buyer_key", - })?, + let pm_token = item.get_payment_method_token()?; + let buyer_key = match pm_token { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } }; Ok(Self { buyer_email, @@ -833,7 +834,9 @@ impl item: types::ResponseRouterData, ) -> Result { Ok(Self { - payment_method_token: Some(item.response.buyer_key.clone()), + payment_method_token: Some(types::PaymentMethodToken::Token( + item.response.buyer_key.clone(), + )), response: Ok(types::PaymentsResponseData::TokenizationResponse { token: item.response.buyer_key, }), diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 76b8efb2d2..69e5c52f9b 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -238,19 +238,27 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { let autocomplete = item.request.is_auto_capture()?; match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(_) => Ok(Self { - idempotency_key: Secret::new(item.attempt_id.clone()), - source_id: Secret::new(item.get_payment_method_token()?), - amount_money: SquarePaymentsAmountData { - amount: item.request.amount, - currency: item.request.currency, - }, - autocomplete, - external_details: SquarePaymentsRequestExternalDetails { - source: "Hyperswitch".to_string(), - source_type: "Card".to_string(), - }, - }), + api::PaymentMethodData::Card(_) => { + let pm_token = item.get_payment_method_token()?; + Ok(Self { + idempotency_key: Secret::new(item.attempt_id.clone()), + source_id: Secret::new(match pm_token { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }), + amount_money: SquarePaymentsAmountData { + amount: item.request.amount, + currency: item.request.currency, + }, + autocomplete, + external_details: SquarePaymentsRequestExternalDetails { + source: "Hyperswitch".to_string(), + source_type: "Card".to_string(), + }, + }) + } api::PaymentMethodData::BankDebit(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::PayLater(_) diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index e36f66abb7..42c7c02a36 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -38,25 +38,37 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(_) => { + let pm_token = item.get_payment_method_token()?; let pre_auth = !item.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, is_refundable: true, pre_auth, - payment_method_id: Secret::new(item.get_payment_method_token()?), + payment_method_id: Secret::new(match pm_token { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }), }) } api::PaymentMethodData::BankDebit( api_models::payments::BankDebitData::AchBankDebit { .. }, ) => { + let pm_token = item.get_payment_method_token()?; let pre_auth = !item.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, is_refundable: true, pre_auth, - payment_method_id: Secret::new(item.get_payment_method_token()?), + payment_method_id: Secret::new(match pm_token { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }), }) } api::PaymentMethodData::BankDebit(_) diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 8a566e72d8..ef23f4ddab 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -7,7 +7,7 @@ use common_utils::{ pii::{self, Email}, }; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, ExposeOptionInterface, Secret}; +use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -434,6 +434,23 @@ pub enum StripeWallet { WechatpayPayment(WechatpayPayment), AlipayPayment(AlipayPayment), Cashapp(CashappPayment), + ApplePayPredecryptToken(Box), +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +pub struct StripeApplePayPredecrypt { + #[serde(rename = "card[number]")] + number: Secret, + #[serde(rename = "card[exp_year]")] + exp_year: Secret, + #[serde(rename = "card[exp_month]")] + exp_month: Secret, + #[serde(rename = "card[cryptogram]")] + cryptogram: Secret, + #[serde(rename = "card[eci]")] + eci: Option>, + #[serde(rename = "card[tokenization_method]")] + tokenization_method: String, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -1201,6 +1218,7 @@ fn create_stripe_payment_method( experience: Option<&enums::PaymentExperience>, payment_method_data: &api_models::payments::PaymentMethodData, auth_type: enums::AuthenticationType, + payment_method_token: Option, ) -> Result< ( StripePaymentMethodData, @@ -1271,18 +1289,71 @@ fn create_stripe_payment_method( )) } payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - payments::WalletData::ApplePay(applepay_data) => Ok(( - StripePaymentMethodData::Wallet(StripeWallet::ApplepayToken(StripeApplePay { - pk_token: applepay_data - .get_applepay_decoded_payment_data() - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - pk_token_instrument_name: applepay_data.payment_method.pm_type.to_owned(), - pk_token_payment_network: applepay_data.payment_method.network.to_owned(), - pk_token_transaction_id: applepay_data.transaction_identifier.to_owned(), - })), - None, - StripeBillingAddress::default(), - )), + payments::WalletData::ApplePay(applepay_data) => { + let mut apple_pay_decrypt_data = + if let Some(types::PaymentMethodToken::ApplePayDecrypt(decrypt_data)) = + payment_method_token + { + let expiry_year_4_digit = Secret::new(format!( + "20{:?}", + decrypt_data + .clone() + .application_expiration_date + .peek() + .get(0..2) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + )); + let exp_month = Secret::new( + decrypt_data + .clone() + .application_expiration_date + .peek() + .get(2..4) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_owned(), + ); + + Some(StripePaymentMethodData::Wallet( + StripeWallet::ApplePayPredecryptToken(Box::new( + StripeApplePayPredecrypt { + number: decrypt_data.clone().application_primary_account_number, + exp_year: expiry_year_4_digit, + exp_month, + eci: decrypt_data.payment_data.eci_indicator, + cryptogram: decrypt_data.payment_data.online_payment_cryptogram, + tokenization_method: "apple_pay".to_string(), + }, + )), + )) + } else { + None + }; + + if apple_pay_decrypt_data.is_none() { + apple_pay_decrypt_data = Some(StripePaymentMethodData::Wallet( + StripeWallet::ApplepayToken(StripeApplePay { + pk_token: applepay_data + .get_applepay_decoded_payment_data() + .change_context(errors::ConnectorError::RequestEncodingFailed)?, + pk_token_instrument_name: applepay_data + .payment_method + .pm_type + .to_owned(), + pk_token_payment_network: applepay_data + .payment_method + .network + .to_owned(), + pk_token_transaction_id: applepay_data + .transaction_identifier + .to_owned(), + }), + )); + }; + let pmd = apple_pay_decrypt_data + .ok_or(errors::ConnectorError::MissingApplePayTokenData)?; + Ok((pmd, None, StripeBillingAddress::default())) + } + payments::WalletData::WeChatPayQr(_) => Ok(( StripePaymentMethodData::Wallet(StripeWallet::WechatpayPayment(WechatpayPayment { client: WechatClient::Web, @@ -1539,6 +1610,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { item.request.payment_experience.as_ref(), &item.request.payment_method_data, item.auth_type, + item.payment_method_token.clone(), )?; validate_shipping_address_against_payment_method( @@ -1558,17 +1630,25 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { }; payment_data = match item.request.payment_method_data { - payments::PaymentMethodData::Wallet(payments::WalletData::ApplePay(_)) => Some( - StripePaymentMethodData::Wallet(StripeWallet::ApplepayPayment(ApplepayPayment { - token: Secret::new( - item.payment_method_token - .to_owned() - .get_required_value("payment_token") - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - ), - payment_method_types: StripePaymentMethodType::Card, - })), - ), + payments::PaymentMethodData::Wallet(payments::WalletData::ApplePay(_)) => { + let payment_method_token = item + .payment_method_token + .to_owned() + .get_required_value("payment_token") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let payment_method_token = match payment_method_token { + types::PaymentMethodToken::Token(payment_method_token) => payment_method_token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }; + Some(StripePaymentMethodData::Wallet( + StripeWallet::ApplepayPayment(ApplepayPayment { + token: Secret::new(payment_method_token), + payment_method_types: StripePaymentMethodType::Card, + }), + )) + } _ => payment_data, }; @@ -1737,6 +1817,7 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { None, &item.request.payment_method_data, item.auth_type, + item.payment_method_token.clone(), )?; Ok(Self { token_data: payment_data.0, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index fb2242e43f..727875d001 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -65,7 +65,7 @@ pub trait RouterData { where T: serde::de::DeserializeOwned; fn is_three_ds(&self) -> bool; - fn get_payment_method_token(&self) -> Result; + fn get_payment_method_token(&self) -> Result; fn get_customer_id(&self) -> Result; fn get_connector_customer_id(&self) -> Result; fn get_preprocessing_id(&self) -> Result; @@ -156,7 +156,7 @@ impl RouterData for types::RouterData Result { + fn get_payment_method_token(&self) -> Result { self.payment_method_token .clone() .ok_or_else(missing_field_err("payment_method_token")) diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index d432bfeec8..adde413730 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -40,4 +40,8 @@ pub(crate) const APPLEPAY_VALIDATION_URL: &str = // The base64 image data will be appended to it to image data source pub(crate) const QR_IMAGE_DATA_SOURCE_STRING: &str = "data:image/png;base64"; +// OID (Object Identifier) for the merchant ID field extension. +#[cfg(feature = "kms")] +pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32"; + pub(crate) const METRICS_HOST_TAG_NAME: &str = "host"; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 35d76247ec..1e9b58ca96 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -119,6 +119,8 @@ pub enum ConnectorError { MissingConnectorTransactionID, #[error("Missing connector refund ID")] MissingConnectorRefundID, + #[error("Missing apple pay tokenization data")] + MissingApplePayTokenData, #[error("Webhooks not implemented for this connector")] WebhooksNotImplemented, #[error("Failed to decode webhook event body")] @@ -250,6 +252,22 @@ pub enum WebhooksFlowError { MissingRequiredField { field_name: &'static str }, } +#[derive(Debug, thiserror::Error)] +pub enum ApplePayDecryptionError { + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + #[error("Failed to decrypt input data")] + DecryptionFailed, + #[error("Certificate parsing failed")] + CertificateParsingFailed, + #[error("Certificate parsing failed")] + MissingMerchantId, + #[error("Key Deserialization failure")] + KeyDeserializationFailed, + #[error("Failed to Derive a shared secret key")] + DerivingSharedSecretKeyFailed, +} + impl ConnectorError { pub fn is_connector_timeout(&self) -> bool { self == &Self::RequestTimeoutReceived diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 3c86ef3284..0163564dc4 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -13,6 +13,8 @@ use common_utils::{ext_traits::AsyncExt, pii}; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; use error_stack::{IntoReport, ResultExt}; use futures::future::join_all; +#[cfg(feature = "kms")] +use helpers::ApplePayData; use masking::Secret; use router_env::{instrument, tracing}; use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, utils as pt_utils}; @@ -133,9 +135,13 @@ where _ => None, }; - let (mut payment_data, tokenization_action) = - get_connector_tokenization_action(state, &operation, payment_data, &validate_result) - .await?; + payment_data = tokenize_in_router_when_confirm_false( + state, + &operation, + &mut payment_data, + &validate_result, + ) + .await?; let updated_customer = call_create_connector_customer_if_required( state, @@ -160,9 +166,8 @@ where &mut payment_data, &customer, call_connector_action, - tokenization_action, updated_customer, - validate_result.requeue, + &validate_result, schedule_time, ) .await?; @@ -547,9 +552,8 @@ pub async fn call_connector_service( payment_data: &mut PaymentData, customer: &Option, call_connector_action: CallConnectorAction, - tokenization_action: TokenizationAction, updated_customer: Option, - requeue: bool, + validate_result: &operations::ValidateResult<'_>, schedule_time: Option, ) -> RouterResult> where @@ -567,6 +571,33 @@ where { let stime_connector = Instant::now(); + let pm_data = payment_data.clone(); + + let connector_name = pm_data + .payment_attempt + .connector + .as_ref() + .get_required_value("connector")?; + + let merchant_connector_account = construct_connector_label_and_get_mca( + state, + merchant_account, + payment_data, + connector_name, + key_store, + ) + .await?; + + let (mut payment_data, tokenization_action) = + get_connector_tokenization_action_when_confirm_true( + state, + operation, + payment_data, + validate_result, + &merchant_connector_account, + ) + .await?; + let mut router_data = payment_data .construct_router_data( state, @@ -574,6 +605,7 @@ where merchant_account, key_store, customer, + &merchant_connector_account, ) .await?; @@ -587,18 +619,50 @@ where &call_connector_action, ); + // Tokenization Action will be DecryptApplePayToken, only when payment method type is Apple Pay + // and the connector supports Apple Pay predecrypt + #[cfg(feature = "kms")] + if matches!( + tokenization_action, + TokenizationAction::DecryptApplePayToken + | TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt + ) { + let apple_pay_data = match payment_data.payment_method_data.clone() { + Some(api_models::payments::PaymentMethodData::Wallet( + api_models::payments::WalletData::ApplePay(wallet_data), + )) => Some( + ApplePayData::token_json(api_models::payments::WalletData::ApplePay(wallet_data)) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .decrypt(state) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, + ), + _ => None, + }; + + let apple_pay_predecrypt = apple_pay_data + .parse_value::("ApplePayPredecryptData") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + router_data.payment_method_token = Some(router_types::PaymentMethodToken::ApplePayDecrypt( + Box::new(apple_pay_predecrypt), + )); + } + let pm_token = router_data .add_payment_method_token(state, &connector, &tokenization_action) .await?; - if let Some(payment_method_token) = pm_token { - router_data.payment_method_token = Some(payment_method_token); + if let Some(payment_method_token) = pm_token.clone() { + router_data.payment_method_token = Some(router_types::PaymentMethodToken::Token( + payment_method_token, + )); }; (router_data, should_continue_further) = complete_preprocessing_steps_if_required( state, &connector, - payment_data, + &payment_data, router_data, operation, should_continue_further, @@ -624,13 +688,13 @@ where (None, false) }; - if should_add_task_to_process_tracker(payment_data) { + if should_add_task_to_process_tracker(&payment_data) { operation .to_domain()? .add_task_to_process_tracker( state, &payment_data.payment_attempt, - requeue, + validate_result.requeue, schedule_time, ) .await @@ -641,7 +705,7 @@ where // Update the payment trackers just before calling the connector // Since the request is already built in the previous step, // there should be no error in request construction from hyperswitch end - (_, *payment_data) = operation + (_, payment_data) = operation .to_update_tracker()? .update_trackers( &*state.store, @@ -711,8 +775,25 @@ where for session_connector_data in connectors.iter() { let connector_id = session_connector_data.connector.connector.id(); + + let merchant_connector_account = construct_connector_label_and_get_mca( + state, + merchant_account, + &mut payment_data, + &session_connector_data.connector.connector_name.to_string(), + key_store, + ) + .await?; + let router_data = payment_data - .construct_router_data(state, connector_id, merchant_account, key_store, customer) + .construct_router_data( + state, + connector_id, + merchant_account, + key_store, + customer, + &merchant_connector_account, + ) .await?; let res = router_data.decide_flows( @@ -793,6 +874,15 @@ where match connector_name { Some(connector_name) => { + let merchant_connector_account = construct_connector_label_and_get_mca( + state, + merchant_account, + payment_data, + &connector_name, + key_store, + ) + .await?; + let connector = api::ConnectorData::get_connector_by_name( &state.conf.connectors, &connector_name, @@ -823,6 +913,7 @@ where merchant_account, key_store, customer, + &merchant_connector_account, ) .await?; @@ -919,6 +1010,35 @@ pub fn is_preprocessing_required_for_wallets(connector_name: String) -> bool { connector_name == *"trustpay" || connector_name == *"payme" } +pub async fn construct_connector_label_and_get_mca<'a, F>( + state: &'a AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, + connector_id: &str, + key_store: &domain::MerchantKeyStore, +) -> RouterResult +where + F: Clone, +{ + let connector_label = helpers::get_connector_label( + payment_data.payment_intent.business_country, + &payment_data.payment_intent.business_label, + payment_data.payment_attempt.business_sub_label.as_ref(), + connector_id, + ); + + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + merchant_account.merchant_id.as_str(), + &connector_label, + payment_data.creds_identifier.to_owned(), + key_store, + ) + .await?; + + Ok(merchant_connector_account) +} + fn is_payment_method_tokenization_enabled_for_connector( state: &AppState, connector_name: &str, @@ -941,6 +1061,47 @@ fn is_payment_method_tokenization_enabled_for_connector( .unwrap_or(false)) } +fn is_apple_pay_predecrypt( + payment_method_type: &Option, + merchant_connector_account: &Option, +) -> RouterResult { + Ok(payment_method_type + .map(|pmt| match pmt { + api_models::enums::PaymentMethodType::ApplePay => { + check_apple_pay_metadata(merchant_connector_account) + } + _ => Ok(false), + }) + .transpose()? + .unwrap_or(false)) +} + +fn check_apple_pay_metadata( + merchant_connector_account: &Option, +) -> RouterResult { + let apple_pay_predecrypt = merchant_connector_account + .clone() + .and_then(|mca| { + let metadata = mca.get_metadata(); + metadata.and_then(|apple_pay_metadata| { + let parsed_metadata: Result = + apple_pay_metadata.parse_value("ApplepaySessionTokenData"); + + parsed_metadata.ok().map(|metadata| match metadata.data { + api_models::payments::ApplepaySessionTokenMetadata::ApplePayCombined( + apple_pay_combined, + ) => match apple_pay_combined { + api_models::payments::ApplePayCombinedMetadata::Simplified { .. } => true, + api_models::payments::ApplePayCombinedMetadata::Manual { .. } => false, + }, + api_models::payments::ApplepaySessionTokenMetadata::ApplePay(_) => false, + }) + }) + }) + .unwrap_or(false); + Ok(apple_pay_predecrypt) +} + fn is_payment_method_type_allowed_for_connector( current_pm_type: &Option, pm_type_filter: Option, @@ -961,11 +1122,16 @@ async fn decide_payment_method_tokenize_action( payment_method: &storage::enums::PaymentMethod, pm_parent_token: Option<&String>, is_connector_tokenization_enabled: bool, + is_apple_pay_predecrypt_supported: bool, ) -> RouterResult { match pm_parent_token { None => { - if is_connector_tokenization_enabled { + if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported { + Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt) + } else if is_connector_tokenization_enabled { Ok(TokenizationAction::TokenizeInConnectorAndRouter) + } else if is_apple_pay_predecrypt_supported { + Ok(TokenizationAction::DecryptApplePayToken) } else { Ok(TokenizationAction::TokenizeInRouter) } @@ -993,8 +1159,12 @@ async fn decide_payment_method_tokenize_action( match connector_token_option { Some(connector_token) => Ok(TokenizationAction::ConnectorToken(connector_token)), None => { - if is_connector_tokenization_enabled { - Ok(TokenizationAction::TokenizeInConnector) + if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported { + Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt) + } else if is_connector_tokenization_enabled { + Ok(TokenizationAction::TokenizeInConnectorAndRouter) + } else if is_apple_pay_predecrypt_supported { + Ok(TokenizationAction::DecryptApplePayToken) } else { Ok(TokenizationAction::TokenizeInRouter) } @@ -1011,14 +1181,17 @@ pub enum TokenizationAction { TokenizeInConnectorAndRouter, ConnectorToken(String), SkipConnectorTokenization, + DecryptApplePayToken, + TokenizeInConnectorAndApplepayPreDecrypt, } #[allow(clippy::too_many_arguments)] -pub async fn get_connector_tokenization_action( +pub async fn get_connector_tokenization_action_when_confirm_true( state: &AppState, operation: &BoxedOperation<'_, F, Req>, - mut payment_data: PaymentData, + payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult<(PaymentData, TokenizationAction)> where F: Send + Clone, @@ -1036,13 +1209,17 @@ where .unwrap_or(false); let payment_data_and_tokenization_action = match connector { - Some(_) if is_mandate => (payment_data, TokenizationAction::SkipConnectorTokenization), + Some(_) if is_mandate => ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ), Some(connector) if is_operation_confirm(&operation) => { let payment_method = &payment_data .payment_attempt .payment_method .get_required_value("payment_method")?; let payment_method_type = &payment_data.payment_attempt.payment_method_type; + let is_connector_tokenization_enabled = is_payment_method_tokenization_enabled_for_connector( state, @@ -1051,12 +1228,18 @@ where payment_method_type, )?; + let is_apple_pay_predecrypt = is_apple_pay_predecrypt( + payment_method_type, + &Some(merchant_connector_account.clone()), + )?; + let payment_method_action = decide_payment_method_tokenize_action( state, &connector, payment_method, payment_data.token.as_ref(), is_connector_tokenization_enabled, + is_apple_pay_predecrypt, ) .await?; @@ -1064,7 +1247,7 @@ where TokenizationAction::TokenizeInRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, &mut payment_data, validate_result.storage_scheme) + .make_pm_data(state, payment_data, validate_result.storage_scheme) .await?; payment_data.payment_method_data = payment_method_data; TokenizationAction::SkipConnectorTokenization @@ -1074,7 +1257,7 @@ where TokenizationAction::TokenizeInConnectorAndRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, &mut payment_data, validate_result.storage_scheme) + .make_pm_data(state, payment_data, validate_result.storage_scheme) .await?; payment_data.payment_method_data = payment_method_data; @@ -1087,22 +1270,47 @@ where TokenizationAction::SkipConnectorTokenization => { TokenizationAction::SkipConnectorTokenization } + TokenizationAction::DecryptApplePayToken => { + TokenizationAction::DecryptApplePayToken + } + TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt => { + TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt + } }; - (payment_data, connector_tokenization_action) - } - _ => { - let (_operation, payment_method_data) = operation - .to_domain()? - .make_pm_data(state, &mut payment_data, validate_result.storage_scheme) - .await?; - payment_data.payment_method_data = payment_method_data; - (payment_data, TokenizationAction::SkipConnectorTokenization) + (payment_data.to_owned(), connector_tokenization_action) } + _ => ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ), }; Ok(payment_data_and_tokenization_action) } +pub async fn tokenize_in_router_when_confirm_false( + state: &AppState, + operation: &BoxedOperation<'_, F, Req>, + payment_data: &mut PaymentData, + validate_result: &operations::ValidateResult<'_>, +) -> RouterResult> +where + F: Send + Clone, +{ + // On confirm is false and only router related + let payment_data = if !is_operation_confirm(operation) { + let (_operation, payment_method_data) = operation + .to_domain()? + .make_pm_data(state, payment_data, validate_result.storage_scheme) + .await?; + payment_data.payment_method_data = payment_method_data; + payment_data + } else { + payment_data + }; + Ok(payment_data.to_owned()) +} + #[derive(Clone)] pub enum CallConnectorAction { Trigger, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 0de210fddd..a9731d0483 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -14,7 +14,7 @@ use crate::{ connector, core::{ errors::{ConnectorError, CustomResult, RouterResult}, - payments, + payments::{self, helpers}, }, routes::AppState, services, @@ -30,6 +30,7 @@ pub trait ConstructFlowSpecificData { merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult>; } diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index faa15d2731..24f7e05e7b 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -4,7 +4,7 @@ use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, - payments::{self, access_token, transformers, PaymentData}, + payments::{self, access_token, helpers, transformers, PaymentData}, }, routes::AppState, services, @@ -23,6 +23,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -31,6 +32,7 @@ impl merchant_account, key_store, customer, + merchant_connector_account, ) .await } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index dd7f050bbf..7793a7eb0d 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -6,7 +6,9 @@ use crate::{ core::{ errors::{self, ConnectorErrorExt, RouterResult}, mandate, - payments::{self, access_token, customers, tokenization, transformers, PaymentData}, + payments::{ + self, access_token, customers, helpers, tokenization, transformers, PaymentData, + }, }, logger, routes::{metrics, AppState}, @@ -29,6 +31,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult< types::RouterData< api::Authorize, @@ -43,6 +46,7 @@ impl merchant_account, key_store, customer, + merchant_connector_account, ) .await } diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 911791f499..3a3ac1b5b0 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -4,7 +4,7 @@ use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ errors::{ConnectorErrorExt, RouterResult}, - payments::{self, access_token, transformers, PaymentData}, + payments::{self, access_token, helpers, transformers, PaymentData}, }, routes::{metrics, AppState}, services, @@ -22,6 +22,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -30,6 +31,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -31,6 +32,7 @@ impl merchant_account, key_store, customer, + merchant_connector_account, ) .await } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 1b6f163c1b..6fbbb01e1a 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -4,7 +4,7 @@ use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ errors::{self, ConnectorErrorExt, RouterResult}, - payments::{self, access_token, transformers, PaymentData}, + payments::{self, access_token, helpers, transformers, PaymentData}, }, routes::AppState, services, @@ -27,6 +27,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult< types::RouterData< api::CompleteAuthorize, @@ -44,6 +45,7 @@ impl merchant_account, key_store, customer, + merchant_connector_account, ) .await } diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index f39ee13395..82a7f67286 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -6,7 +6,7 @@ use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ errors::{ApiErrorResponse, ConnectorErrorExt, RouterResult}, - payments::{self, access_token, transformers, PaymentData}, + payments::{self, access_token, helpers, transformers, PaymentData}, }, routes::AppState, services, @@ -24,6 +24,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult< types::RouterData, > { @@ -34,6 +35,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -30,6 +31,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -36,6 +39,7 @@ impl merchant_account, key_store, customer, + merchant_connector_account, ) .await } @@ -92,30 +96,12 @@ fn get_applepay_metadata( }) } -fn mk_applepay_session_request( +fn build_apple_pay_session_request( state: &routes::AppState, - router_data: &types::PaymentsSessionRouterData, + request: payment_types::ApplepaySessionRequest, + apple_pay_merchant_cert: String, + apple_pay_merchant_cert_key: String, ) -> RouterResult { - let applepay_metadata = get_applepay_metadata(router_data.connector_meta_data.clone())?; - let request = payment_types::ApplepaySessionRequest { - merchant_identifier: applepay_metadata - .data - .session_token_data - .merchant_identifier - .clone(), - display_name: applepay_metadata - .data - .session_token_data - .display_name - .clone(), - initiative: applepay_metadata.data.session_token_data.initiative.clone(), - initiative_context: applepay_metadata - .data - .session_token_data - .initiative_context - .clone(), - }; - let applepay_session_request = types::RequestBody::log_and_get_request_body( &request, utils::Encode::::encode_to_string_of_json, @@ -135,16 +121,8 @@ fn mk_applepay_session_request( "application/json".to_string().into(), )]) .body(Some(applepay_session_request)) - .add_certificate(Some( - applepay_metadata - .data - .session_token_data - .certificate - .clone(), - )) - .add_certificate_key(Some( - applepay_metadata.data.session_token_data.certificate_keys, - )) + .add_certificate(Some(apple_pay_merchant_cert)) + .add_certificate_key(Some(apple_pay_merchant_cert_key)) .build(); Ok(session_request) } @@ -167,53 +145,128 @@ async fn create_applepay_session_token( payment_types::NextActionCall::Confirm, ) } else { - let applepay_metadata = get_applepay_metadata(router_data.connector_meta_data.clone())?; + // Get the apple pay metadata + let apple_pay_metadata = get_applepay_metadata(router_data.connector_meta_data.clone())?; - let amount_info = payment_types::AmountInfo { - label: applepay_metadata.data.payment_request_data.label, - total_type: Some("final".to_string()), - amount: router_data - .request - .currency - .to_currency_base_unit(router_data.request.amount) - .into_report() - .change_context(errors::ApiErrorResponse::PreconditionFailed { - message: "Failed to convert currency to base unit".to_string(), - })?, + // Get payment request data , apple pay session request and merchant keys + let ( + payment_request_data, + apple_pay_session_request, + apple_pay_merchant_cert, + apple_pay_merchant_cert_key, + ) = match apple_pay_metadata.data { + payment_types::ApplepaySessionTokenMetadata::ApplePayCombined( + apple_pay_combined_metadata, + ) => match apple_pay_combined_metadata { + payment_types::ApplePayCombinedMetadata::Simplified { + payment_request_data, + session_token_data, + } => { + #[cfg(feature = "kms")] + let decrypted_apple_pay_merchant_cert = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Apple pay merchant certificate decryption failed")?; + + #[cfg(feature = "kms")] + let decrypted_apple_pay_merchant_cert_key = + kms::get_kms_client(&state.conf.kms) + .await + .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Apple pay merchant certificate key decryption failed", + )?; + + #[cfg(feature = "kms")] + let decrypted_merchant_identifier = kms::get_kms_client(&state.conf.kms) + .await + .decrypt( + &state + .conf + .applepay_merchant_configs + .common_merchant_identifier, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Apple pay merchant identifier decryption failed")?; + + #[cfg(not(feature = "kms"))] + let decrypted_merchant_identifier = &state + .conf + .applepay_merchant_configs + .common_merchant_identifier; + + let apple_pay_session_request = get_session_request_for_simplified_apple_pay( + decrypted_merchant_identifier.to_string(), + session_token_data, + ); + + #[cfg(not(feature = "kms"))] + let decrypted_apple_pay_merchant_cert = + &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert; + + #[cfg(not(feature = "kms"))] + let decrypted_apple_pay_merchant_cert_key = + &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key; + + ( + payment_request_data, + apple_pay_session_request, + decrypted_apple_pay_merchant_cert.to_owned(), + decrypted_apple_pay_merchant_cert_key.to_owned(), + ) + } + payment_types::ApplePayCombinedMetadata::Manual { + payment_request_data, + session_token_data, + } => { + let apple_pay_session_request = + get_session_request_for_manual_apple_pay(session_token_data.clone()); + ( + payment_request_data, + apple_pay_session_request, + session_token_data.certificate.clone(), + session_token_data.certificate_keys, + ) + } + }, + payment_types::ApplepaySessionTokenMetadata::ApplePay(apple_pay_metadata) => { + let apple_pay_session_request = get_session_request_for_manual_apple_pay( + apple_pay_metadata.session_token_data.clone(), + ); + ( + apple_pay_metadata.payment_request_data, + apple_pay_session_request, + apple_pay_metadata.session_token_data.certificate.clone(), + apple_pay_metadata.session_token_data.certificate_keys, + ) + } }; - let applepay_payment_request = payment_types::ApplePayPaymentRequest { - country_code: router_data - .request - .country - .to_owned() - .get_required_value("country_code") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "country_code", - })?, - currency_code: router_data.request.currency, - total: amount_info, - merchant_capabilities: Some( - applepay_metadata - .data - .payment_request_data - .merchant_capabilities, - ), - supported_networks: Some( - applepay_metadata - .data - .payment_request_data - .supported_networks, - ), - merchant_identifier: Some( - applepay_metadata - .data - .session_token_data - .merchant_identifier, - ), - }; + // Get amount info for apple pay + let amount_info = get_apple_pay_amount_info( + payment_request_data.label.as_str(), + router_data.request.to_owned(), + )?; - let applepay_session_request = mk_applepay_session_request(state, router_data)?; + // Get apple pay payment request + let applepay_payment_request = get_apple_pay_payment_request( + amount_info, + payment_request_data, + router_data.request.to_owned(), + apple_pay_session_request.merchant_identifier.as_str(), + )?; + + let applepay_session_request = build_apple_pay_session_request( + state, + apple_pay_session_request, + apple_pay_merchant_cert, + apple_pay_merchant_cert_key, + )?; let response = services::call_connector_api(state, applepay_session_request).await; // logging the error if present in session call response @@ -254,6 +307,71 @@ async fn create_applepay_session_token( } } +fn get_session_request_for_simplified_apple_pay( + apple_pay_merchant_identifier: String, + session_token_data: payment_types::SessionTokenForSimplifiedApplePay, +) -> payment_types::ApplepaySessionRequest { + payment_types::ApplepaySessionRequest { + merchant_identifier: apple_pay_merchant_identifier, + display_name: "Apple pay".to_string(), + initiative: "web".to_string(), + initiative_context: session_token_data.initiative_context, + } +} + +fn get_session_request_for_manual_apple_pay( + session_token_data: payment_types::SessionTokenInfo, +) -> payment_types::ApplepaySessionRequest { + payment_types::ApplepaySessionRequest { + merchant_identifier: session_token_data.merchant_identifier.clone(), + display_name: session_token_data.display_name.clone(), + initiative: session_token_data.initiative.clone(), + initiative_context: session_token_data.initiative_context, + } +} + +fn get_apple_pay_amount_info( + label: &str, + session_data: types::PaymentsSessionData, +) -> RouterResult { + let amount_info = payment_types::AmountInfo { + label: label.to_string(), + total_type: Some("final".to_string()), + amount: session_data + .currency + .to_currency_base_unit(session_data.amount) + .into_report() + .change_context(errors::ApiErrorResponse::PreconditionFailed { + message: "Failed to convert currency to base unit".to_string(), + })?, + }; + + Ok(amount_info) +} + +fn get_apple_pay_payment_request( + amount_info: payment_types::AmountInfo, + payment_request_data: payment_types::PaymentRequestMetadata, + session_data: types::PaymentsSessionData, + merchant_identifier: &str, +) -> RouterResult { + let applepay_payment_request = payment_types::ApplePayPaymentRequest { + country_code: session_data + .country + .to_owned() + .get_required_value("country_code") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "country_code", + })?, + currency_code: session_data.currency, + total: amount_info, + merchant_capabilities: Some(payment_request_data.merchant_capabilities), + supported_networks: Some(payment_request_data.supported_networks), + merchant_identifier: Some(merchant_identifier.to_string()), + }; + Ok(applepay_payment_request) +} + fn create_apple_pay_session_response( router_data: &types::PaymentsSessionRouterData, session_response: Option, diff --git a/crates/router/src/core/payments/flows/verify_flow.rs b/crates/router/src/core/payments/flows/verify_flow.rs index 9e9af95a29..c18e25d467 100644 --- a/crates/router/src/core/payments/flows/verify_flow.rs +++ b/crates/router/src/core/payments/flows/verify_flow.rs @@ -5,7 +5,9 @@ use crate::{ core::{ errors::{self, ConnectorErrorExt, RouterResult}, mandate, - payments::{self, access_token, customers, tokenization, transformers, PaymentData}, + payments::{ + self, access_token, customers, helpers, tokenization, transformers, PaymentData, + }, }, routes::AppState, services, @@ -23,6 +25,7 @@ impl ConstructFlowSpecificData, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { transformers::construct_payment_router_data::( state, @@ -31,6 +34,7 @@ impl ConstructFlowSpecificData Result> { - let decoded_certificate = consts::BASE64_ENGINE + let decoded_certificate = BASE64_ENGINE .decode(encoded_certificate) .into_report() .change_context(errors::ApiClientError::CertificateDecodeFailed)?; - let decoded_certificate_key = consts::BASE64_ENGINE + let decoded_certificate_key = BASE64_ENGINE .decode(encoded_certificate_key) .into_report() .change_context(errors::ApiClientError::CertificateDecodeFailed)?; @@ -2383,6 +2395,7 @@ impl MerchantConnectorAccountType { Self::CacheVal(val) => val.metadata.to_owned(), } } + pub fn get_connector_account_details(&self) -> serde_json::Value { match self { Self::DbVal(val) => val.connector_account_details.peek().to_owned(), @@ -3006,3 +3019,180 @@ pub fn validate_customer_access( } Ok(()) } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ApplePayData { + version: masking::Secret, + data: masking::Secret, + signature: masking::Secret, + header: ApplePayHeader, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayHeader { + ephemeral_public_key: masking::Secret, + public_key_hash: masking::Secret, + transaction_id: masking::Secret, +} + +#[cfg(feature = "kms")] +impl ApplePayData { + pub fn token_json( + wallet_data: api_models::payments::WalletData, + ) -> CustomResult { + let json_wallet_data: Self = + connector::utils::WalletData::get_wallet_token_as_json(&wallet_data)?; + Ok(json_wallet_data) + } + + pub async fn decrypt( + &self, + state: &AppState, + ) -> CustomResult { + let merchant_id = self.merchant_id(state).await?; + let shared_secret = self.shared_secret(state).await?; + let symmetric_key = self.symmetric_key(&merchant_id, &shared_secret)?; + let decrypted = self.decrypt_ciphertext(&symmetric_key)?; + let parsed_decrypted: serde_json::Value = serde_json::from_str(&decrypted) + .into_report() + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; + Ok(parsed_decrypted) + } + + pub async fn merchant_id( + &self, + state: &AppState, + ) -> CustomResult { + let cert_data = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; + + let base64_decode_cert_data = BASE64_ENGINE + .decode(cert_data) + .into_report() + .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; + + // Parsing the certificate using x509-parser + let (_, certificate) = parse_x509_certificate(&base64_decode_cert_data) + .into_report() + .change_context(errors::ApplePayDecryptionError::CertificateParsingFailed) + .attach_printable("Error parsing apple pay PPC")?; + + // Finding the merchant ID extension + let apple_pay_m_id = certificate + .extensions() + .iter() + .find(|extension| { + extension + .oid + .to_string() + .eq(consts::MERCHANT_ID_FIELD_EXTENSION_ID) + }) + .map(|ext| { + let merchant_id = String::from_utf8_lossy(ext.value) + .trim() + .trim_start_matches('@') + .to_string(); + + merchant_id + }) + .ok_or(errors::ApplePayDecryptionError::MissingMerchantId) + .into_report() + .attach_printable("Unable to find merchant ID extension in the certificate")?; + + Ok(apple_pay_m_id) + } + + pub async fn shared_secret( + &self, + state: &AppState, + ) -> CustomResult, errors::ApplePayDecryptionError> { + let public_ec_bytes = BASE64_ENGINE + .decode(self.header.ephemeral_public_key.peek().as_bytes()) + .into_report() + .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; + + let public_key = PKey::public_key_from_der(&public_ec_bytes) + .into_report() + .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) + .attach_printable("Failed to deserialize the public key")?; + + let decrypted_apple_pay_ppc_key = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc_key) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; + + // Create PKey objects from EcKey + let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes()) + .into_report() + .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) + .attach_printable("Failed to deserialize the private key")?; + + // Create the Deriver object and set the peer public key + let mut deriver = Deriver::new(&private_key) + .into_report() + .change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed) + .attach_printable("Failed to create a deriver for the private key")?; + + deriver + .set_peer(&public_key) + .into_report() + .change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed) + .attach_printable("Failed to set the peer key for the secret derivation")?; + + // Compute the shared secret + let shared_secret = deriver + .derive_to_vec() + .into_report() + .change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed) + .attach_printable("Final key derivation failed")?; + Ok(shared_secret) + } + + pub fn symmetric_key( + &self, + merchant_id: &str, + shared_secret: &[u8], + ) -> CustomResult, errors::ApplePayDecryptionError> { + let kdf_algorithm = b"\x0did-aes256-GCM"; + let kdf_party_v = hex::decode(merchant_id) + .into_report() + .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; + let kdf_party_u = b"Apple"; + let kdf_info = [&kdf_algorithm[..], kdf_party_u, &kdf_party_v[..]].concat(); + + let mut hash = openssl::sha::Sha256::new(); + hash.update(b"\x00\x00\x00"); + hash.update(b"\x01"); + hash.update(shared_secret); + hash.update(&kdf_info[..]); + let symmetric_key = hash.finish(); + Ok(symmetric_key.to_vec()) + } + + pub fn decrypt_ciphertext( + &self, + symmetric_key: &[u8], + ) -> CustomResult { + let data = BASE64_ENGINE + .decode(self.data.peek().as_bytes()) + .into_report() + .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; + let iv = [0u8; 16]; //Initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process. + let ciphertext = &data[..data.len() - 16]; + let tag = &data[data.len() - 16..]; + let cipher = Cipher::aes_256_gcm(); + let decrypted_data = decrypt_aead(cipher, symmetric_key, Some(&iv), &[], ciphertext, tag) + .into_report() + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; + let decrypted = String::from_utf8(decrypted_data) + .into_report() + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; + + Ok(decrypted) + } +} diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index e50883ccff..ebffee3d1a 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -44,10 +44,18 @@ where .unwrap_or(false); let connector_token = if token_store { - let token = resp + let tokens = resp .payment_method_token .to_owned() .get_required_value("payment_token")?; + let token = match tokens { + types::PaymentMethodToken::Token(connector_token) => connector_token, + types::PaymentMethodToken::ApplePayDecrypt(_) => { + Err(errors::ApiErrorResponse::NotSupported { + message: "Apple Pay Decrypt token is not supported".to_string(), + })? + } + }; Some((connector, token)) } else { None @@ -235,7 +243,8 @@ pub async fn add_payment_method_token( pm_token_request_data: types::PaymentMethodTokenizationData, ) -> RouterResult> { match tokenization_action { - payments::TokenizationAction::TokenizeInConnector => { + payments::TokenizationAction::TokenizeInConnector + | payments::TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt => { let connector_integration: services::BoxedConnectorIntegration< '_, api::PaymentMethodToken, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 768ff74042..d1cde60ca8 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -32,8 +32,9 @@ pub async fn construct_payment_router_data<'a, F, T>( payment_data: PaymentData, connector_id: &str, merchant_account: &domain::MerchantAccount, - key_store: &domain::MerchantKeyStore, + _key_store: &domain::MerchantKeyStore, customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult> where T: TryFrom>, @@ -42,22 +43,7 @@ where error_stack::Report: From<>>::Error>, { - let (merchant_connector_account, payment_method, router_data); - let connector_label = helpers::get_connector_label( - payment_data.payment_intent.business_country, - &payment_data.payment_intent.business_label, - payment_data.payment_attempt.business_sub_label.as_ref(), - connector_id, - ); - - merchant_connector_account = helpers::get_merchant_connector_account( - state, - merchant_account.merchant_id.as_str(), - &connector_label, - payment_data.creds_identifier.to_owned(), - key_store, - ) - .await?; + let (payment_method, router_data); fp_utils::when(merchant_connector_account.is_disabled(), || { Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) @@ -153,7 +139,7 @@ where access_token: None, session_token: None, reference_id: None, - payment_method_token: payment_data.pm_token, + payment_method_token: payment_data.pm_token.map(types::PaymentMethodToken::Token), connector_customer: payment_data.connector_customer_id, recurring_mandate_payment_data: payment_data.recurring_mandate_payment_data, connector_request_reference_id: core_utils::get_connector_request_reference_id( diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index aa2319ef57..9f0b77a7d7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -241,7 +241,7 @@ pub struct RouterData { pub access_token: Option, pub session_token: Option, pub reference_id: Option, - pub payment_method_token: Option, + pub payment_method_token: Option, pub recurring_mandate_payment_data: Option, pub preprocessing_id: Option, /// This is the balance amount for gift cards or voucher @@ -274,6 +274,31 @@ pub struct RouterData { pub connector_http_status_code: Option, } +#[derive(Debug, Clone, serde::Deserialize)] +pub enum PaymentMethodToken { + Token(String), + ApplePayDecrypt(Box), +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPredecryptData { + pub application_primary_account_number: Secret, + pub application_expiration_date: Secret, + pub currency_code: Secret, + pub transaction_amount: Secret, + pub device_manufacturer_identifier: Secret, + pub payment_data_type: Secret, + pub payment_data: ApplePayCryptogramData, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayCryptogramData { + pub online_payment_cryptogram: Secret, + pub eci_indicator: Option>, +} + #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index c59bb73f2d..eac50482a6 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -497,7 +497,9 @@ pub trait ConnectorActions: Connector { access_token: info.clone().and_then(|a| a.access_token), session_token: None, reference_id: None, - payment_method_token: info.clone().and_then(|a| a.payment_method_token), + payment_method_token: info + .clone() + .and_then(|a| a.payment_method_token.map(types::PaymentMethodToken::Token)), connector_customer: info.clone().and_then(|a| a.connector_customer), recurring_mandate_payment_data: None,