mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			338 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			338 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Personal Identifiable Information protection.
 | |
| 
 | |
| use std::{convert::AsRef, fmt, ops, str::FromStr};
 | |
| 
 | |
| use diesel::{
 | |
|     backend,
 | |
|     backend::Backend,
 | |
|     deserialize,
 | |
|     deserialize::FromSql,
 | |
|     prelude::*,
 | |
|     serialize::{Output, ToSql},
 | |
|     sql_types, AsExpression,
 | |
| };
 | |
| use masking::{Secret, Strategy, WithType};
 | |
| 
 | |
| use crate::{errors::ValidationError, validation::validate_email};
 | |
| 
 | |
| /// A string constant representing a redacted or masked value.
 | |
| pub const REDACTED: &str = "Redacted";
 | |
| 
 | |
| /// Type alias for serde_json value which has Secret Information
 | |
| pub type SecretSerdeValue = Secret<serde_json::Value>;
 | |
| 
 | |
| /// Card number
 | |
| #[derive(Debug)]
 | |
| pub struct CardNumber;
 | |
| 
 | |
| impl<T> Strategy<T> for CardNumber
 | |
| where
 | |
|     T: AsRef<str>,
 | |
| {
 | |
|     fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         let val_str: &str = val.as_ref();
 | |
| 
 | |
|         if val_str.len() < 15 || val_str.len() > 19 {
 | |
|             return WithType::fmt(val, f);
 | |
|         }
 | |
| 
 | |
|         write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6))
 | |
|     }
 | |
| }
 | |
| 
 | |
| /*
 | |
| /// Phone number
 | |
| #[derive(Debug)]
 | |
| pub struct PhoneNumber;
 | |
| 
 | |
| impl<T> Strategy<T> for PhoneNumber
 | |
| where
 | |
|     T: AsRef<str>,
 | |
| {
 | |
|     fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         let val_str: &str = val.as_ref();
 | |
| 
 | |
|         if val_str.len() < 10 || val_str.len() > 12 {
 | |
|             return WithType::fmt(val, f);
 | |
|         }
 | |
| 
 | |
|         write!(
 | |
|             f,
 | |
|             "{}{}{}",
 | |
|             &val_str[..2],
 | |
|             "*".repeat(val_str.len() - 5),
 | |
|             &val_str[(val_str.len() - 3)..]
 | |
|         )
 | |
|     }
 | |
| }
 | |
| */
 | |
| 
 | |
| /// Client secret
 | |
| #[derive(Debug)]
 | |
| pub struct ClientSecret;
 | |
| 
 | |
| impl<T> Strategy<T> for ClientSecret
 | |
| where
 | |
|     T: AsRef<str>,
 | |
| {
 | |
|     fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         let val_str: &str = val.as_ref();
 | |
| 
 | |
|         let client_secret_segments: Vec<&str> = val_str.split('_').collect();
 | |
| 
 | |
|         if client_secret_segments.len() != 4
 | |
|             || !client_secret_segments.contains(&"pay")
 | |
|             || !client_secret_segments.contains(&"secret")
 | |
|         {
 | |
|             return WithType::fmt(val, f);
 | |
|         }
 | |
|         write!(
 | |
|             f,
 | |
|             "{}_{}_{}",
 | |
|             client_secret_segments[0],
 | |
|             client_secret_segments[1],
 | |
|             "*".repeat(
 | |
|                 val_str.len()
 | |
|                     - (client_secret_segments[0].len() + client_secret_segments[1].len() + 2)
 | |
|             )
 | |
|         )
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Strategy for masking Email
 | |
| #[derive(Debug)]
 | |
| pub struct EmailStrategy;
 | |
| 
 | |
| impl<T> Strategy<T> for EmailStrategy
 | |
| where
 | |
|     T: AsRef<str> + std::fmt::Debug,
 | |
| {
 | |
|     fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         let val_str: &str = val.as_ref();
 | |
|         match val_str.split_once('@') {
 | |
|             Some((a, b)) => write!(f, "{}@{}", "*".repeat(a.len()), b),
 | |
|             None => WithType::fmt(val, f),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| /// Email address
 | |
| #[derive(
 | |
|     serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Default, AsExpression,
 | |
| )]
 | |
| #[diesel(sql_type = diesel::sql_types::Text)]
 | |
| pub struct Email(Secret<String, EmailStrategy>);
 | |
| 
 | |
| impl From<Secret<String, EmailStrategy>> for Email {
 | |
|     fn from(value: Secret<String, EmailStrategy>) -> Self {
 | |
|         Self(value)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl ops::Deref for Email {
 | |
|     type Target = Secret<String, EmailStrategy>;
 | |
| 
 | |
|     fn deref(&self) -> &Self::Target {
 | |
|         &self.0
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl ops::DerefMut for Email {
 | |
|     fn deref_mut(&mut self) -> &mut Self::Target {
 | |
|         &mut self.0
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<DB> Queryable<diesel::sql_types::Text, DB> for Email
 | |
| where
 | |
|     DB: Backend,
 | |
|     Self: FromSql<sql_types::Text, DB>,
 | |
| {
 | |
|     type Row = Self;
 | |
| 
 | |
|     fn build(row: Self::Row) -> deserialize::Result<Self> {
 | |
|         Ok(row)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<DB> FromSql<sql_types::Text, DB> for Email
 | |
| where
 | |
|     DB: Backend,
 | |
|     String: FromSql<sql_types::Text, DB>,
 | |
| {
 | |
|     fn from_sql(bytes: backend::RawValue<'_, DB>) -> deserialize::Result<Self> {
 | |
|         let val = String::from_sql(bytes)?;
 | |
|         Ok(Self::from_str(val.as_str())?)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<DB> ToSql<sql_types::Text, DB> for Email
 | |
| where
 | |
|     DB: Backend,
 | |
|     String: ToSql<sql_types::Text, DB>,
 | |
| {
 | |
|     fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
 | |
|         self.0.to_sql(out)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl FromStr for Email {
 | |
|     type Err = ValidationError;
 | |
| 
 | |
|     fn from_str(email: &str) -> Result<Self, Self::Err> {
 | |
|         if email.eq(REDACTED) {
 | |
|             return Ok(Self(Secret::new(email.to_string())));
 | |
|         }
 | |
|         match validate_email(email) {
 | |
|             Ok(_) => {
 | |
|                 let secret = Secret::<String, EmailStrategy>::new(email.to_string());
 | |
|                 Ok(Self(secret))
 | |
|             }
 | |
|             Err(_) => Err(ValidationError::InvalidValue {
 | |
|                 message: "Invalid email address format".into(),
 | |
|             }),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// IP address
 | |
| #[derive(Debug)]
 | |
| pub struct IpAddress;
 | |
| 
 | |
| impl<T> Strategy<T> for IpAddress
 | |
| where
 | |
|     T: AsRef<str>,
 | |
| {
 | |
|     fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         let val_str: &str = val.as_ref();
 | |
|         let segments: Vec<&str> = val_str.split('.').collect();
 | |
| 
 | |
|         if segments.len() != 4 {
 | |
|             return WithType::fmt(val, f);
 | |
|         }
 | |
| 
 | |
|         for seg in segments.iter() {
 | |
|             if seg.is_empty() || seg.len() > 3 {
 | |
|                 return WithType::fmt(val, f);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         write!(f, "{}.**.**.**", segments[0])
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod pii_masking_strategy_tests {
 | |
|     use std::str::FromStr;
 | |
| 
 | |
|     use masking::{ExposeInterface, Secret};
 | |
| 
 | |
|     use super::{CardNumber, ClientSecret, Email, IpAddress};
 | |
|     use crate::pii::{EmailStrategy, REDACTED};
 | |
| 
 | |
|     #[test]
 | |
|     fn test_valid_card_number_masking() {
 | |
|         let secret: Secret<String, CardNumber> = Secret::new("1234567890987654".to_string());
 | |
|         assert_eq!("123456**********", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_card_number_masking() {
 | |
|         let secret: Secret<String, CardNumber> = Secret::new("1234567890".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|     #[test]
 | |
|     fn test_valid_phone_number_masking() {
 | |
|         let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
 | |
|         assert_eq!("99*****299", format!("{}", secret));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_phone_number_masking() {
 | |
|         let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{}", secret));
 | |
| 
 | |
|         let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{}", secret));
 | |
|     }
 | |
|     */
 | |
| 
 | |
|     #[test]
 | |
|     fn test_valid_email_masking() {
 | |
|         let secret: Secret<String, EmailStrategy> = Secret::new("example@test.com".to_string());
 | |
|         assert_eq!("*******@test.com", format!("{secret:?}"));
 | |
| 
 | |
|         let secret: Secret<String, EmailStrategy> = Secret::new("username@gmail.com".to_string());
 | |
|         assert_eq!("********@gmail.com", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_email_masking() {
 | |
|         let secret: Secret<String, EmailStrategy> = Secret::new("myemailgmail.com".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
| 
 | |
|         let secret: Secret<String, EmailStrategy> = Secret::new("myemail$gmail.com".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_valid_newtype_email() {
 | |
|         let email_check: Result<Email, crate::errors::ValidationError> =
 | |
|             Email::from_str("example@abc.com");
 | |
|         assert!(email_check.is_ok());
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_newtype_email() {
 | |
|         let email_check: Result<Email, crate::errors::ValidationError> =
 | |
|             Email::from_str("example@abc@com");
 | |
|         assert!(email_check.is_err());
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_redacted_email() {
 | |
|         let email_result = Email::from_str(REDACTED);
 | |
|         assert!(email_result.is_ok());
 | |
|         if let Ok(email) = email_result {
 | |
|             let secret_value = email.0.expose();
 | |
|             assert_eq!(secret_value.as_str(), REDACTED);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_valid_ip_addr_masking() {
 | |
|         let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
 | |
|         assert_eq!("123.**.**.**", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_ip_addr_masking() {
 | |
|         let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
| 
 | |
|         let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
| 
 | |
|         let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_valid_client_secret_masking() {
 | |
|         let secret: Secret<String, ClientSecret> =
 | |
|             Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret_tLjTz9tAQxUVEFqfmOIP".to_string());
 | |
|         assert_eq!(
 | |
|             "pay_uszFB2QGe9MmLY65ojhT_***************************",
 | |
|             format!("{secret:?}")
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_invalid_client_secret_masking() {
 | |
|         let secret: Secret<String, IpAddress> =
 | |
|             Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret".to_string());
 | |
|         assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
 | |
|     }
 | |
| }
 | 
