refactor(core): inclusion of locker to store fingerprints (#3630)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in>
This commit is contained in:
Prajjwal Kumar
2024-02-21 13:37:25 +05:30
committed by GitHub
parent 4ae28e48cd
commit 7b0bce5558
20 changed files with 362 additions and 258 deletions

View File

@ -1,5 +1,6 @@
use common_enums::enums;
use common_utils::events::ApiEventMetric;
use masking::StrongSecret;
use utoipa::ToSchema;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
@ -10,6 +11,15 @@ pub enum BlocklistRequest {
ExtendedCardBin(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct GenerateFingerprintRequest {
pub card: Card,
pub hash_key: StrongSecret<String>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Card {
pub card_number: StrongSecret<String>,
}
pub type AddToBlocklistRequest = BlocklistRequest;
pub type DeleteFromBlocklistRequest = BlocklistRequest;
@ -22,6 +32,11 @@ pub struct BlocklistResponse {
pub created_at: time::PrimitiveDateTime,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GenerateFingerprintResponsePayload {
pub card_fingerprint: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct ToggleBlocklistResponse {
pub blocklist_guard_status: String,
@ -54,4 +69,7 @@ impl ApiEventMetric for BlocklistRequest {}
impl ApiEventMetric for BlocklistResponse {}
impl ApiEventMetric for ToggleBlocklistResponse {}
impl ApiEventMetric for ListBlocklistQuery {}
impl ApiEventMetric for GenerateFingerprintRequest {}
impl ApiEventMetric for ToggleBlocklistQuery {}
impl ApiEventMetric for GenerateFingerprintResponsePayload {}
impl ApiEventMetric for Card {}

View File

@ -160,6 +160,7 @@ pub struct PaymentAttempt {
pub unified_code: Option<String>,
pub unified_message: Option<String>,
pub mandate_data: Option<MandateDetails>,
pub fingerprint_id: Option<String>,
}
impl PaymentAttempt {
@ -238,6 +239,7 @@ pub struct PaymentAttemptNew {
pub unified_code: Option<String>,
pub unified_message: Option<String>,
pub mandate_data: Option<MandateDetails>,
pub fingerprint_id: Option<String>,
}
impl PaymentAttemptNew {
@ -270,6 +272,7 @@ pub enum PaymentAttemptUpdate {
capture_method: Option<storage_enums::CaptureMethod>,
surcharge_amount: Option<i64>,
tax_amount: Option<i64>,
fingerprint_id: Option<String>,
updated_by: String,
},
UpdateTrackers {
@ -307,6 +310,7 @@ pub enum PaymentAttemptUpdate {
surcharge_amount: Option<i64>,
tax_amount: Option<i64>,
merchant_connector_id: Option<String>,
fingerprint_id: Option<String>,
},
RejectUpdate {
status: storage_enums::AttemptStatus,

View File

@ -121,6 +121,7 @@ pub enum PaymentIntentUpdate {
amount_captured: Option<i64>,
return_url: Option<String>,
updated_by: String,
fingerprint_id: Option<String>,
incremental_authorization_allowed: Option<bool>,
},
MetadataUpdate {
@ -335,6 +336,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// currency,
status,
amount_captured,
fingerprint_id,
// customer_id,
return_url,
updated_by,
@ -344,6 +346,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// currency: Some(currency),
status: Some(status),
amount_captured,
fingerprint_id,
// customer_id,
return_url,
modified_at: Some(common_utils::date_time::now()),

View File

@ -65,6 +65,7 @@ pub struct PaymentAttempt {
pub unified_message: Option<String>,
pub net_amount: Option<i64>,
pub mandate_data: Option<storage_enums::MandateDetails>,
pub fingerprint_id: Option<String>,
}
impl PaymentAttempt {
@ -140,6 +141,7 @@ pub struct PaymentAttemptNew {
pub unified_message: Option<String>,
pub net_amount: Option<i64>,
pub mandate_data: Option<storage_enums::MandateDetails>,
pub fingerprint_id: Option<String>,
}
impl PaymentAttemptNew {
@ -177,6 +179,7 @@ pub enum PaymentAttemptUpdate {
capture_method: Option<storage_enums::CaptureMethod>,
surcharge_amount: Option<i64>,
tax_amount: Option<i64>,
fingerprint_id: Option<String>,
updated_by: String,
},
UpdateTrackers {
@ -212,6 +215,7 @@ pub enum PaymentAttemptUpdate {
amount_capturable: Option<i64>,
surcharge_amount: Option<i64>,
tax_amount: Option<i64>,
fingerprint_id: Option<String>,
updated_by: String,
merchant_connector_id: Option<String>,
},
@ -351,6 +355,7 @@ pub struct PaymentAttemptUpdateInternal {
encoded_data: Option<String>,
unified_code: Option<Option<String>>,
unified_message: Option<Option<String>>,
fingerprint_id: Option<String>,
}
impl PaymentAttemptUpdateInternal {
@ -411,6 +416,7 @@ impl PaymentAttemptUpdate {
encoded_data,
unified_code,
unified_message,
fingerprint_id,
} = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source);
PaymentAttempt {
amount: amount.unwrap_or(source.amount),
@ -452,6 +458,7 @@ impl PaymentAttemptUpdate {
encoded_data: encoded_data.or(source.encoded_data),
unified_code: unified_code.unwrap_or(source.unified_code),
unified_message: unified_message.unwrap_or(source.unified_message),
fingerprint_id: fingerprint_id.or(source.fingerprint_id),
..source
}
}
@ -476,6 +483,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
} => Self {
amount: Some(amount),
@ -494,6 +502,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
..Default::default()
},
@ -527,6 +536,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
merchant_connector_id,
surcharge_amount,
tax_amount,
fingerprint_id,
} => Self {
amount: Some(amount),
currency: Some(currency),
@ -549,6 +559,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
merchant_connector_id: merchant_connector_id.map(Some),
surcharge_amount,
tax_amount,
fingerprint_id,
..Default::default()
},
PaymentAttemptUpdate::VoidUpdate {

View File

@ -116,6 +116,7 @@ pub enum PaymentIntentUpdate {
ResponseUpdate {
status: storage_enums::IntentStatus,
amount_captured: Option<i64>,
fingerprint_id: Option<String>,
return_url: Option<String>,
updated_by: String,
incremental_authorization_allowed: Option<bool>,
@ -405,6 +406,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// currency,
status,
amount_captured,
fingerprint_id,
// customer_id,
return_url,
updated_by,
@ -414,6 +416,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// currency: Some(currency),
status: Some(status),
amount_captured,
fingerprint_id,
// customer_id,
return_url,
modified_at: Some(common_utils::date_time::now()),

View File

@ -689,6 +689,8 @@ diesel::table! {
unified_message -> Nullable<Varchar>,
net_amount -> Nullable<Int8>,
mandate_data -> Nullable<Jsonb>,
#[max_length = 64]
fingerprint_id -> Nullable<Varchar>,
}
}

View File

@ -68,6 +68,7 @@ pub struct PaymentAttemptBatchNew {
pub unified_message: Option<String>,
pub net_amount: Option<i64>,
pub mandate_data: Option<MandateDetails>,
pub fingerprint_id: Option<String>,
}
#[allow(dead_code)]
@ -122,6 +123,7 @@ impl PaymentAttemptBatchNew {
unified_message: self.unified_message,
net_amount: self.net_amount,
mandate_data: self.mandate_data,
fingerprint_id: self.fingerprint_id,
}
}
}

View File

@ -1,6 +1,28 @@
use api_models::blocklist;
use api_models::{blocklist, enums as api_enums};
use common_utils::{
ext_traits::{Encode, StringExt},
request::RequestContent,
};
use error_stack::ResultExt;
use josekit::jwe;
#[cfg(feature = "aws_kms")]
use masking::PeekInterface;
use masking::StrongSecret;
use router_env::{instrument, tracing};
use crate::types::{storage, transformers::ForeignFrom};
use crate::{
configs::settings,
core::{
errors::{self, CustomResult},
payment_methods::transformers as payment_methods,
},
headers, routes,
services::{api as services, encryption},
types::{storage, transformers::ForeignFrom},
utils::ConnectorResponseExt,
};
const LOCKER_FINGERPRINT_PATH: &str = "/cards/fingerprint";
impl ForeignFrom<storage::Blocklist> for blocklist::AddToBlocklistResponse {
fn foreign_from(from: storage::Blocklist) -> Self {
@ -11,3 +33,192 @@ impl ForeignFrom<storage::Blocklist> for blocklist::AddToBlocklistResponse {
}
}
}
async fn generate_fingerprint_request<'a>(
#[cfg(not(feature = "aws_kms"))] jwekey: &settings::Jwekey,
#[cfg(feature = "aws_kms")] jwekey: &settings::ActiveKmsSecrets,
locker: &settings::Locker,
payload: &blocklist::GenerateFingerprintRequest,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<services::Request, errors::VaultError> {
let payload = payload
.encode_to_vec()
.change_context(errors::VaultError::RequestEncodingFailed)?;
#[cfg(feature = "aws_kms")]
let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes();
#[cfg(not(feature = "aws_kms"))]
let private_key = jwekey.vault_private_key.as_bytes();
let jws = encryption::jws_sign_payload(&payload, &locker.locker_signing_key_id, private_key)
.await
.change_context(errors::VaultError::RequestEncodingFailed)?;
let jwe_payload = generate_jwe_payload_for_request(jwekey, &jws, locker_choice).await?;
let mut url = match locker_choice {
api_enums::LockerChoice::HyperswitchCardVault => locker.host.to_owned(),
};
url.push_str(LOCKER_FINGERPRINT_PATH);
let mut request = services::Request::new(services::Method::Post, &url);
request.add_header(headers::CONTENT_TYPE, "application/json".into());
request.set_body(RequestContent::Json(Box::new(jwe_payload)));
Ok(request)
}
async fn generate_jwe_payload_for_request(
#[cfg(feature = "aws_kms")] jwekey: &settings::ActiveKmsSecrets,
#[cfg(not(feature = "aws_kms"))] jwekey: &settings::Jwekey,
jws: &str,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<encryption::JweBody, errors::VaultError> {
let jws_payload: Vec<&str> = jws.split('.').collect();
let generate_jws_body = |payload: Vec<&str>| -> Option<encryption::JwsBody> {
Some(encryption::JwsBody {
header: payload.first()?.to_string(),
payload: payload.get(1)?.to_string(),
signature: payload.get(2)?.to_string(),
})
};
let jws_body =
generate_jws_body(jws_payload).ok_or(errors::VaultError::GenerateFingerprintFailed)?;
let payload = jws_body
.encode_to_vec()
.change_context(errors::VaultError::GenerateFingerprintFailed)?;
#[cfg(feature = "aws_kms")]
let public_key = match locker_choice {
api_enums::LockerChoice::HyperswitchCardVault => {
jwekey.jwekey.peek().vault_encryption_key.as_bytes()
}
};
#[cfg(not(feature = "aws_kms"))]
let public_key = match locker_choice {
api_enums::LockerChoice::HyperswitchCardVault => jwekey.vault_encryption_key.as_bytes(),
};
let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key)
.await
.change_context(errors::VaultError::SaveCardFailed)
.attach_printable("Error on jwe encrypt")?;
let jwe_payload: Vec<&str> = jwe_encrypted.split('.').collect();
let generate_jwe_body = |payload: Vec<&str>| -> Option<encryption::JweBody> {
Some(encryption::JweBody {
header: payload.first()?.to_string(),
iv: payload.get(2)?.to_string(),
encrypted_payload: payload.get(3)?.to_string(),
tag: payload.get(4)?.to_string(),
encrypted_key: payload.get(1)?.to_string(),
})
};
let jwe_body =
generate_jwe_body(jwe_payload).ok_or(errors::VaultError::GenerateFingerprintFailed)?;
Ok(jwe_body)
}
#[instrument(skip_all)]
pub async fn generate_fingerprint(
state: &routes::AppState,
card_number: StrongSecret<String>,
hash_key: StrongSecret<String>,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<blocklist::GenerateFingerprintResponsePayload, errors::VaultError> {
let payload = blocklist::GenerateFingerprintRequest {
card: blocklist::Card { card_number },
hash_key,
};
let generate_fingerprint_resp =
call_to_locker_for_fingerprint(state, &payload, locker_choice).await?;
Ok(generate_fingerprint_resp)
}
#[instrument(skip_all)]
async fn call_to_locker_for_fingerprint(
state: &routes::AppState,
payload: &blocklist::GenerateFingerprintRequest,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<blocklist::GenerateFingerprintResponsePayload, errors::VaultError> {
let locker = &state.conf.locker;
#[cfg(not(feature = "aws_kms"))]
let jwekey = &state.conf.jwekey;
#[cfg(feature = "aws_kms")]
let jwekey = &state.kms_secrets;
let request = generate_fingerprint_request(jwekey, locker, payload, locker_choice).await?;
let response = services::call_connector_api(state, request)
.await
.change_context(errors::VaultError::GenerateFingerprintFailed);
let jwe_body: encryption::JweBody = response
.get_response_inner("JweBody")
.change_context(errors::VaultError::GenerateFingerprintFailed)?;
let decrypted_payload =
decrypt_generate_fingerprint_response_payload(jwekey, jwe_body, Some(locker_choice))
.await
.change_context(errors::VaultError::GenerateFingerprintFailed)
.attach_printable("Error getting decrypted fingerprint response payload")?;
let generate_fingerprint_response: blocklist::GenerateFingerprintResponsePayload =
decrypted_payload
.parse_struct("GenerateFingerprintResponse")
.change_context(errors::VaultError::ResponseDeserializationFailed)?;
Ok(generate_fingerprint_response)
}
async fn decrypt_generate_fingerprint_response_payload(
#[cfg(not(feature = "aws_kms"))] jwekey: &settings::Jwekey,
#[cfg(feature = "aws_kms")] jwekey: &settings::ActiveKmsSecrets,
jwe_body: encryption::JweBody,
locker_choice: Option<api_enums::LockerChoice>,
) -> CustomResult<String, errors::VaultError> {
let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::HyperswitchCardVault);
#[cfg(feature = "aws_kms")]
let public_key = match target_locker {
api_enums::LockerChoice::HyperswitchCardVault => {
jwekey.jwekey.peek().vault_encryption_key.as_bytes()
}
};
#[cfg(feature = "aws_kms")]
let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes();
#[cfg(not(feature = "aws_kms"))]
let public_key = match target_locker {
api_enums::LockerChoice::HyperswitchCardVault => jwekey.vault_encryption_key.as_bytes(),
};
#[cfg(not(feature = "aws_kms"))]
let private_key = jwekey.vault_private_key.as_bytes();
let jwt = payment_methods::get_dotted_jwe(jwe_body);
let alg = jwe::RSA_OAEP;
let jwe_decrypted = encryption::decrypt_jwe(
&jwt,
encryption::KeyIdCheck::SkipKeyIdCheck,
private_key,
alg,
)
.await
.change_context(errors::VaultError::SaveCardFailed)
.attach_printable("Jwe Decryption failed for JweBody for vault")?;
let jws = jwe_decrypted
.parse_struct("JwsBody")
.change_context(errors::VaultError::ResponseDeserializationFailed)?;
let jws_body = payment_methods::get_dotted_jws(jws);
encryption::verify_sign(jws_body, public_key)
.change_context(errors::VaultError::SaveCardFailed)
.attach_printable("Jws Decryption failed for JwsBody for vault")
}

View File

@ -1,15 +1,11 @@
use api_models::blocklist as api_blocklist;
use common_enums::MerchantDecision;
use common_utils::{
crypto::{self, SignMessage},
errors::CustomResult,
};
use common_utils::errors::CustomResult;
use diesel_models::configs;
use error_stack::{IntoReport, ResultExt};
#[cfg(feature = "aws_kms")]
use external_services::aws_kms;
use masking::StrongSecret;
use super::{errors, AppState};
use super::{errors, transformers::generate_fingerprint, AppState};
use crate::{
consts,
core::{
@ -35,52 +31,13 @@ pub async fn delete_entry_from_blocklist(
delete_card_bin_blocklist_entry(state, &xbin, &merchant_id).await?
}
api_blocklist::DeleteFromBlocklistRequest::Fingerprint(fingerprint_id) => {
let blocklist_fingerprint = state
.store
.find_blocklist_fingerprint_by_merchant_id_fingerprint_id(
&merchant_id,
&fingerprint_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "blocklist record with given fingerprint id not found".to_string(),
})?;
#[cfg(feature = "aws_kms")]
let decrypted_fingerprint = aws_kms::core::get_aws_kms_client(&state.conf.kms)
.await
.decrypt(blocklist_fingerprint.encrypted_fingerprint)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to kms decrypt fingerprint")?;
#[cfg(not(feature = "aws_kms"))]
let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint;
let blocklist_entry = state
api_blocklist::DeleteFromBlocklistRequest::Fingerprint(fingerprint_id) => state
.store
.delete_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, &fingerprint_id)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "no blocklist record for the given fingerprint id was found"
.to_string(),
})?;
state
.store
.delete_blocklist_lookup_entry_by_merchant_id_fingerprint(
&merchant_id,
&decrypted_fingerprint,
)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "no blocklist record for the given fingerprint id was found"
.to_string(),
})?;
blocklist_entry
}
message: "no blocklist record for the given fingerprint id was found".to_string(),
})?,
};
Ok(blocklist_entry.foreign_into())
@ -232,57 +189,20 @@ pub async fn insert_entry_into_blocklist(
}
}
let blocklist_fingerprint = state
.store
.find_blocklist_fingerprint_by_merchant_id_fingerprint_id(
&merchant_id,
fingerprint_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "fingerprint not found".to_string(),
})?;
#[cfg(feature = "aws_kms")]
let decrypted_fingerprint = aws_kms::core::get_aws_kms_client(&state.conf.kms)
.await
.decrypt(blocklist_fingerprint.encrypted_fingerprint)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to kms decrypt encrypted fingerprint")?;
#[cfg(not(feature = "aws_kms"))]
let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint;
state
.store
.insert_blocklist_lookup_entry(
diesel_models::blocklist_lookup::BlocklistLookupNew {
merchant_id: merchant_id.clone(),
fingerprint: decrypted_fingerprint,
},
)
.await
.to_duplicate_response(errors::ApiErrorResponse::PreconditionFailed {
message: "the payment instrument associated with the given fingerprint is already in the blocklist".to_string(),
})
.attach_printable("failed to add fingerprint to blocklist lookup")?;
state
.store
.insert_blocklist_entry(storage::BlocklistNew {
merchant_id: merchant_id.clone(),
fingerprint_id: fingerprint_id.clone(),
data_kind: blocklist_fingerprint.data_kind,
data_kind: api_models::enums::enums::BlocklistDataKind::PaymentMethod,
metadata: None,
created_at: common_utils::date_time::now(),
})
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to add fingerprint to pm blocklist")?
.attach_printable("failed to add fingerprint to blocklist")?
}
};
Ok(blocklist_entry.foreign_into())
}
@ -330,17 +250,6 @@ async fn duplicate_check_insert_bin(
merchant_id: &str,
data_kind: common_enums::BlocklistDataKind,
) -> RouterResult<storage::Blocklist> {
let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?;
let bin_fingerprint = crypto::HmacSha512::sign_message(
&crypto::HmacSha512,
merchant_secret.clone().as_bytes(),
bin.as_bytes(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error in bin hash creation")?;
let encoded_fingerprint = hex::encode(bin_fingerprint.clone());
let blocklist_entry_result = state
.store
.find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin)
@ -363,17 +272,6 @@ async fn duplicate_check_insert_bin(
}
}
// Checking for duplicacy
state
.store
.insert_blocklist_lookup_entry(diesel_models::blocklist_lookup::BlocklistLookupNew {
merchant_id: merchant_id.to_string(),
fingerprint: encoded_fingerprint.clone(),
})
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error inserting blocklist lookup entry")?;
state
.store
.insert_blocklist_entry(storage::BlocklistNew {
@ -393,21 +291,6 @@ async fn delete_card_bin_blocklist_entry(
bin: &str,
merchant_id: &str,
) -> RouterResult<storage::Blocklist> {
let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?;
let bin_fingerprint = crypto::HmacSha512
.sign_message(merchant_secret.as_bytes(), bin.as_bytes())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error when hashing card bin")?;
let encoded_fingerprint = hex::encode(bin_fingerprint);
state
.store
.delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, &encoded_fingerprint)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "could not find a blocklist entry for the given bin".to_string(),
})?;
state
.store
.delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin)
@ -431,16 +314,16 @@ where
get_merchant_fingerprint_secret(state, merchant_id.as_str()).await?;
// Hashed Fingerprint to check whether or not this payment should be blocked.
let card_number_fingerprint = payment_data
.payment_method_data
.as_ref()
.and_then(|pm_data| match pm_data {
api_models::payments::PaymentMethodData::Card(card) => {
crypto::HmacSha512::sign_message(
&crypto::HmacSha512,
merchant_fingerprint_secret.as_bytes(),
card.card_number.clone().get_card_no().as_bytes(),
let card_number_fingerprint = if let Some(api_models::payments::PaymentMethodData::Card(card)) =
payment_data.payment_method_data.as_ref()
{
generate_fingerprint(
state,
StrongSecret::new(card.card_number.clone().get_card_no()),
StrongSecret::new(merchant_fingerprint_secret.clone()),
api_models::enums::LockerChoice::HyperswitchCardVault,
)
.await
.attach_printable("error in pm fingerprint creation")
.map_or_else(
|err| {
@ -449,10 +332,10 @@ where
},
Some,
)
}
_ => None,
})
.map(hex::encode);
.map(|payload| payload.card_fingerprint)
} else {
None
};
// Hashed Cardbin to check whether or not this payment should be blocked.
let card_bin_fingerprint = payment_data
@ -460,66 +343,43 @@ where
.as_ref()
.and_then(|pm_data| match pm_data {
api_models::payments::PaymentMethodData::Card(card) => {
crypto::HmacSha512::sign_message(
&crypto::HmacSha512,
merchant_fingerprint_secret.as_bytes(),
card.card_number.clone().get_card_isin().as_bytes(),
)
.attach_printable("error in card bin hash creation")
.map_or_else(
|err| {
logger::error!(error=?err);
None
},
Some,
)
Some(card.card_number.clone().get_card_isin())
}
_ => None,
})
.map(hex::encode);
});
// Hashed Extended Cardbin to check whether or not this payment should be blocked.
let extended_card_bin_fingerprint = payment_data
let extended_card_bin_fingerprint =
payment_data
.payment_method_data
.as_ref()
.and_then(|pm_data| match pm_data {
api_models::payments::PaymentMethodData::Card(card) => {
crypto::HmacSha512::sign_message(
&crypto::HmacSha512,
merchant_fingerprint_secret.as_bytes(),
card.card_number.clone().get_extended_card_bin().as_bytes(),
)
.attach_printable("error in extended card bin hash creation")
.map_or_else(
|err| {
logger::error!(error=?err);
None
},
Some,
)
Some(card.card_number.clone().get_extended_card_bin())
}
_ => None,
})
.map(hex::encode);
});
//validating the payment method.
let mut blocklist_futures = Vec::new();
if let Some(card_number_fingerprint) = card_number_fingerprint.as_ref() {
blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint(
blocklist_futures.push(db.find_blocklist_entry_by_merchant_id_fingerprint_id(
merchant_id,
card_number_fingerprint,
));
}
if let Some(card_bin_fingerprint) = card_bin_fingerprint.as_ref() {
blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint(
blocklist_futures.push(
db.find_blocklist_entry_by_merchant_id_fingerprint_id(
merchant_id,
card_bin_fingerprint,
));
),
);
}
if let Some(extended_card_bin_fingerprint) = extended_card_bin_fingerprint.as_ref() {
blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint(
blocklist_futures.push(db.find_blocklist_entry_by_merchant_id_fingerprint_id(
merchant_id,
extended_card_bin_fingerprint,
));
@ -538,7 +398,6 @@ where
}
}
}
if should_payment_be_blocked {
// Update db for attempt and intent status.
db.update_payment_intent(
@ -582,13 +441,12 @@ where
}
.into())
} else {
payment_data.payment_intent.fingerprint_id = generate_payment_fingerprint(
payment_data.payment_attempt.fingerprint_id = generate_payment_fingerprint(
state,
payment_data.payment_attempt.merchant_id.clone(),
payment_data.payment_method_data.clone(),
)
.await?;
Ok(false)
}
}
@ -598,17 +456,19 @@ pub async fn generate_payment_fingerprint(
merchant_id: String,
payment_method_data: Option<crate::types::api::PaymentMethodData>,
) -> CustomResult<Option<String>, errors::ApiErrorResponse> {
let db = &state.store;
let merchant_fingerprint_secret = get_merchant_fingerprint_secret(state, &merchant_id).await?;
let card_number_fingerprint = payment_method_data
.as_ref()
.and_then(|pm_data| match pm_data {
api_models::payments::PaymentMethodData::Card(card) => {
crypto::HmacSha512::sign_message(
&crypto::HmacSha512,
merchant_fingerprint_secret.as_bytes(),
card.card_number.clone().get_card_no().as_bytes(),
Ok(
if let Some(api_models::payments::PaymentMethodData::Card(card)) =
payment_method_data.as_ref()
{
generate_fingerprint(
state,
StrongSecret::new(card.card_number.clone().get_card_no()),
StrongSecret::new(merchant_fingerprint_secret),
api_models::enums::LockerChoice::HyperswitchCardVault,
)
.await
.attach_printable("error in pm fingerprint creation")
.map_or_else(
|err| {
@ -617,49 +477,10 @@ pub async fn generate_payment_fingerprint(
},
Some,
)
}
_ => None,
})
.map(hex::encode);
let mut fingerprint_id = None;
if let Some(encoded_hash) = card_number_fingerprint {
#[cfg(feature = "kms")]
let encrypted_fingerprint = kms::get_kms_client(&state.conf.kms)
.await
.encrypt(encoded_hash)
.await
.map_or_else(
|e| {
logger::error!(error=?e, "failed kms encryption of card fingerprint");
.map(|payload| payload.card_fingerprint)
} else {
logger::error!("failed to retrieve card fingerprint");
None
},
Some,
);
#[cfg(not(feature = "kms"))]
let encrypted_fingerprint = Some(encoded_hash);
if let Some(encrypted_fingerprint) = encrypted_fingerprint {
fingerprint_id = db
.insert_blocklist_fingerprint_entry(
diesel_models::blocklist_fingerprint::BlocklistFingerprintNew {
merchant_id,
fingerprint_id: utils::generate_id(consts::ID_LENGTH, "fingerprint"),
encrypted_fingerprint,
data_kind: common_enums::BlocklistDataKind::PaymentMethod,
created_at: common_utils::date_time::now(),
},
)
.await
.map_or_else(
|e| {
logger::error!(error=?e, "failed storing card fingerprint in db");
None
},
|fp| Some(fp.fingerprint_id),
);
}
}
Ok(fingerprint_id)
}

View File

@ -236,6 +236,8 @@ pub enum VaultError {
FetchPaymentMethodFailed,
#[error("Failed to save payment method in vault")]
SavePaymentMethodFailed,
#[error("Failed to generate fingerprint")]
GenerateFingerprintFailed,
}
#[derive(Debug, thiserror::Error)]

View File

@ -3202,6 +3202,7 @@ impl AttemptType {
unified_message: None,
net_amount: old_payment_attempt.amount,
mandate_data: old_payment_attempt.mandate_data,
fingerprint_id: None,
}
}

View File

@ -739,6 +739,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
let m_straight_through_algorithm = straight_through_algorithm.clone();
let m_error_code = error_code.clone();
let m_error_message = error_message.clone();
let m_fingerprint_id = payment_data.payment_attempt.fingerprint_id.clone();
let m_db = state.clone().store;
let surcharge_amount = payment_data
.surcharge_details
@ -774,6 +775,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
merchant_connector_id,
surcharge_amount,
tax_amount,
fingerprint_id: m_fingerprint_id,
},
storage_scheme,
)
@ -784,7 +786,6 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
);
let m_payment_data_payment_intent = payment_data.payment_intent.clone();
let m_fingerprint_id = payment_data.payment_intent.fingerprint_id.clone();
let m_customer_id = customer_id.clone();
let m_shipping_address_id = shipping_address.clone();
let m_billing_address_id = billing_address.clone();
@ -821,7 +822,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
metadata: m_metadata,
payment_confirm_source: header_payload.payment_confirm_source,
updated_by: m_storage_scheme,
fingerprint_id: m_fingerprint_id,
fingerprint_id: None,
session_expiry,
},
storage_scheme,

View File

@ -604,6 +604,8 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
};
if router_data.status == enums::AttemptStatus::Charged {
payment_data.payment_intent.fingerprint_id =
payment_data.payment_attempt.fingerprint_id.clone();
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
}
@ -798,6 +800,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
return_url: router_data.return_url.clone(),
amount_captured,
updated_by: storage_scheme.to_string(),
fingerprint_id: payment_data.payment_attempt.fingerprint_id.clone(),
incremental_authorization_allowed: payment_data
.payment_intent
.incremental_authorization_allowed,

View File

@ -559,6 +559,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id: None,
updated_by: storage_scheme.to_string(),
},
storage_scheme,

View File

@ -114,6 +114,7 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec<String> {
let locker_host_rs = locker.host_rs.to_owned();
vec![
format!("{locker_host}/cards/add"),
format!("{locker_host}/cards/fingerprint"),
format!("{locker_host}/cards/retrieve"),
format!("{locker_host}/cards/delete"),
format!("{locker_host_rs}/cards/add"),

View File

@ -148,6 +148,7 @@ impl PaymentAttemptInterface for MockDb {
unified_code: payment_attempt.unified_code,
unified_message: payment_attempt.unified_message,
mandate_data: payment_attempt.mandate_data,
fingerprint_id: payment_attempt.fingerprint_id,
};
payment_attempts.push(payment_attempt.clone());
Ok(payment_attempt)

View File

@ -391,6 +391,7 @@ impl<T: DatabaseStore> PaymentAttemptInterface for KVRouterStore<T> {
unified_code: payment_attempt.unified_code.clone(),
unified_message: payment_attempt.unified_message.clone(),
mandate_data: payment_attempt.mandate_data.clone(),
fingerprint_id: payment_attempt.fingerprint_id.clone(),
};
let field = format!("pa_{}", created_attempt.attempt_id);
@ -1105,6 +1106,7 @@ impl DataModelExt for PaymentAttempt {
unified_code: self.unified_code,
unified_message: self.unified_message,
mandate_data: self.mandate_data.map(|d| d.to_storage_model()),
fingerprint_id: self.fingerprint_id,
}
}
@ -1163,6 +1165,7 @@ impl DataModelExt for PaymentAttempt {
mandate_data: storage_model
.mandate_data
.map(MandateDetails::from_storage_model),
fingerprint_id: storage_model.fingerprint_id,
}
}
}
@ -1219,6 +1222,7 @@ impl DataModelExt for PaymentAttemptNew {
unified_code: self.unified_code,
unified_message: self.unified_message,
mandate_data: self.mandate_data.map(|d| d.to_storage_model()),
fingerprint_id: self.fingerprint_id,
}
}
@ -1275,6 +1279,7 @@ impl DataModelExt for PaymentAttemptNew {
mandate_data: storage_model
.mandate_data
.map(MandateDetails::from_storage_model),
fingerprint_id: storage_model.fingerprint_id,
}
}
}
@ -1299,6 +1304,7 @@ impl DataModelExt for PaymentAttemptUpdate {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
} => DieselPaymentAttemptUpdate::Update {
amount,
@ -1315,6 +1321,7 @@ impl DataModelExt for PaymentAttemptUpdate {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
},
Self::UpdateTrackers {
@ -1373,6 +1380,7 @@ impl DataModelExt for PaymentAttemptUpdate {
amount_capturable,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
merchant_connector_id: connector_id,
} => DieselPaymentAttemptUpdate::ConfirmUpdate {
@ -1394,6 +1402,7 @@ impl DataModelExt for PaymentAttemptUpdate {
amount_capturable,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
merchant_connector_id: connector_id,
},
@ -1578,6 +1587,7 @@ impl DataModelExt for PaymentAttemptUpdate {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
} => Self::Update {
amount,
@ -1594,6 +1604,7 @@ impl DataModelExt for PaymentAttemptUpdate {
capture_method,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
},
DieselPaymentAttemptUpdate::UpdateTrackers {
@ -1641,6 +1652,7 @@ impl DataModelExt for PaymentAttemptUpdate {
amount_capturable,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
merchant_connector_id: connector_id,
} => Self::ConfirmUpdate {
@ -1662,6 +1674,7 @@ impl DataModelExt for PaymentAttemptUpdate {
amount_capturable,
surcharge_amount,
tax_amount,
fingerprint_id,
updated_by,
merchant_connector_id: connector_id,
},

View File

@ -925,12 +925,14 @@ impl DataModelExt for PaymentIntentUpdate {
Self::ResponseUpdate {
status,
amount_captured,
fingerprint_id,
return_url,
updated_by,
incremental_authorization_allowed,
} => DieselPaymentIntentUpdate::ResponseUpdate {
status,
amount_captured,
fingerprint_id,
return_url,
updated_by,
incremental_authorization_allowed,

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_attempt DROP COLUMN IF EXISTS fingerprint_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS fingerprint_id VARCHAR(64);