Files
Chethan Rao 20b52f11c3 chore: address Rust 1.88.0 clippy lints (#8498)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
2025-07-02 06:43:07 +00:00

457 lines
14 KiB
Rust

//! Structure describing secret.
use std::{fmt, marker::PhantomData};
use crate::{strategy::Strategy, PeekInterface, StrongSecret};
/// Secret thing.
///
/// To get access to value use method `expose()` of trait [`crate::ExposeInterface`].
///
/// ## Masking
/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a zero-variant
/// enum and pass this enum as a second generic parameter to [`Secret`] while defining it.
/// [`Secret`] will take care of applying the masking strategy on the inner secret when being
/// displayed.
///
/// ## Masking Example
///
/// ```
/// use masking::Strategy;
/// use masking::Secret;
/// use std::fmt;
///
/// enum MyStrategy {}
///
/// impl<T> Strategy<T> for MyStrategy
/// where
/// T: fmt::Display
/// {
/// fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// write!(f, "{}", val.to_string().to_ascii_lowercase())
/// }
/// }
///
/// let my_secret: Secret<String, MyStrategy> = Secret::new("HELLO".to_string());
///
/// assert_eq!("hello", &format!("{:?}", my_secret));
/// ```
pub struct Secret<Secret, MaskingStrategy = crate::WithType>
where
MaskingStrategy: Strategy<Secret>,
{
pub(crate) inner_secret: Secret,
pub(crate) masking_strategy: PhantomData<MaskingStrategy>,
}
impl<SecretValue, MaskingStrategy> Secret<SecretValue, MaskingStrategy>
where
MaskingStrategy: Strategy<SecretValue>,
{
/// Take ownership of a secret value
pub fn new(secret: SecretValue) -> Self {
Self {
inner_secret: secret,
masking_strategy: PhantomData,
}
}
/// Zip 2 secrets with the same masking strategy into one
pub fn zip<OtherSecretValue>(
self,
other: Secret<OtherSecretValue, MaskingStrategy>,
) -> Secret<(SecretValue, OtherSecretValue), MaskingStrategy>
where
MaskingStrategy: Strategy<OtherSecretValue> + Strategy<(SecretValue, OtherSecretValue)>,
{
(self.inner_secret, other.inner_secret).into()
}
/// consume self and modify the inner value
pub fn map<OtherSecretValue>(
self,
f: impl FnOnce(SecretValue) -> OtherSecretValue,
) -> Secret<OtherSecretValue, MaskingStrategy>
where
MaskingStrategy: Strategy<OtherSecretValue>,
{
f(self.inner_secret).into()
}
/// Convert to [`StrongSecret`]
pub fn into_strong(self) -> StrongSecret<SecretValue, MaskingStrategy>
where
SecretValue: zeroize::DefaultIsZeroes,
{
StrongSecret::new(self.inner_secret)
}
/// Convert to [`Secret`] with a reference to the inner secret
pub fn as_ref(&self) -> Secret<&SecretValue, MaskingStrategy>
where
MaskingStrategy: for<'a> Strategy<&'a SecretValue>,
{
Secret::new(self.peek())
}
}
impl<SecretValue, MaskingStrategy> PeekInterface<SecretValue>
for Secret<SecretValue, MaskingStrategy>
where
MaskingStrategy: Strategy<SecretValue>,
{
fn peek(&self) -> &SecretValue {
&self.inner_secret
}
fn peek_mut(&mut self) -> &mut SecretValue {
&mut self.inner_secret
}
}
impl<SecretValue, MaskingStrategy> From<SecretValue> for Secret<SecretValue, MaskingStrategy>
where
MaskingStrategy: Strategy<SecretValue>,
{
fn from(secret: SecretValue) -> Self {
Self::new(secret)
}
}
impl<SecretValue, MaskingStrategy> Clone for Secret<SecretValue, MaskingStrategy>
where
SecretValue: Clone,
MaskingStrategy: Strategy<SecretValue>,
{
fn clone(&self) -> Self {
Self {
inner_secret: self.inner_secret.clone(),
masking_strategy: PhantomData,
}
}
}
impl<SecretValue, MaskingStrategy> PartialEq for Secret<SecretValue, MaskingStrategy>
where
Self: PeekInterface<SecretValue>,
SecretValue: PartialEq,
MaskingStrategy: Strategy<SecretValue>,
{
fn eq(&self, other: &Self) -> bool {
self.peek().eq(other.peek())
}
}
impl<SecretValue, MaskingStrategy> Eq for Secret<SecretValue, MaskingStrategy>
where
Self: PeekInterface<SecretValue>,
SecretValue: Eq,
MaskingStrategy: Strategy<SecretValue>,
{
}
impl<SecretValue, MaskingStrategy> fmt::Debug for Secret<SecretValue, MaskingStrategy>
where
MaskingStrategy: Strategy<SecretValue>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
MaskingStrategy::fmt(&self.inner_secret, f)
}
}
impl<SecretValue, MaskingStrategy> Default for Secret<SecretValue, MaskingStrategy>
where
SecretValue: Default,
MaskingStrategy: Strategy<SecretValue>,
{
fn default() -> Self {
SecretValue::default().into()
}
}
// Required by base64-serde to serialize Secret of Vec<u8> which contains the base64 decoded value
impl AsRef<[u8]> for Secret<Vec<u8>> {
fn as_ref(&self) -> &[u8] {
self.peek().as_slice()
}
}
/// Strategy for masking JSON values
#[cfg(feature = "serde")]
pub enum JsonMaskStrategy {}
#[cfg(feature = "serde")]
impl Strategy<serde_json::Value> for JsonMaskStrategy {
fn fmt(value: &serde_json::Value, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match value {
serde_json::Value::Object(map) => {
write!(f, "{{")?;
let mut first = true;
for (key, val) in map {
if !first {
write!(f, ", ")?;
}
first = false;
write!(f, "\"{key}\":")?;
Self::fmt(val, f)?;
}
write!(f, "}}")
}
serde_json::Value::Array(arr) => {
write!(f, "[")?;
let mut first = true;
for val in arr {
if !first {
write!(f, ", ")?;
}
first = false;
Self::fmt(val, f)?;
}
write!(f, "]")
}
serde_json::Value::String(s) => {
// For strings, we show a masked version that gives a hint about the content
let masked = if s.len() <= 2 {
"**".to_string()
} else if s.len() <= 6 {
format!("{}**", &s[0..1])
} else {
// For longer strings, show first and last character with length in between
format!(
"{}**{}**{}",
&s[0..1],
s.len() - 2,
&s[s.len() - 1..s.len()]
)
};
write!(f, "\"{masked}\"")
}
serde_json::Value::Number(n) => {
// For numbers, we can show the order of magnitude
if n.is_i64() || n.is_u64() {
let num_str = n.to_string();
let masked_num = "*".repeat(num_str.len());
write!(f, "{masked_num}")
} else if n.is_f64() {
// For floats, just use a generic mask
write!(f, "**.**")
} else {
write!(f, "0")
}
}
serde_json::Value::Bool(b) => {
// For booleans, we can show a hint about which one it is
write!(f, "{}", if *b { "**true" } else { "**false" })
}
serde_json::Value::Null => write!(f, "null"),
}
}
}
#[cfg(feature = "proto_tonic")]
impl<T> prost::Message for Secret<T, crate::WithType>
where
T: prost::Message + Default + Clone,
{
fn encode_raw(&self, buf: &mut impl bytes::BufMut) {
self.peek().encode_raw(buf);
}
fn merge_field(
&mut self,
tag: u32,
wire_type: prost::encoding::WireType,
buf: &mut impl bytes::Buf,
ctx: prost::encoding::DecodeContext,
) -> Result<(), prost::DecodeError> {
if tag == 1 {
self.peek_mut().merge_field(tag, wire_type, buf, ctx)
} else {
prost::encoding::skip_field(wire_type, tag, buf, ctx)
}
}
fn encoded_len(&self) -> usize {
self.peek().encoded_len()
}
fn clear(&mut self) {
self.peek_mut().clear();
}
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod tests {
use serde_json::json;
use super::*;
#[test]
#[allow(clippy::expect_used)]
fn test_json_mask_strategy() {
// Create a sample JSON with different types for testing
let original = json!({ "user": { "name": "John Doe", "email": "john@example.com", "age": 35, "verified": true }, "card": { "number": "4242424242424242", "cvv": 123, "amount": 99.99 }, "tags": ["personal", "premium"], "null_value": null, "short": "hi" });
// Apply the JsonMaskStrategy
let secret = Secret::<_, JsonMaskStrategy>::new(original.clone());
let masked_str = format!("{secret:?}");
// Get specific values from original
let original_obj = original.as_object().expect("Original should be an object");
let user_obj = original_obj["user"]
.as_object()
.expect("User should be an object");
let name = user_obj["name"].as_str().expect("Name should be a string");
let email = user_obj["email"]
.as_str()
.expect("Email should be a string");
let age = user_obj["age"].as_i64().expect("Age should be a number");
let verified = user_obj["verified"]
.as_bool()
.expect("Verified should be a boolean");
let card_obj = original_obj["card"]
.as_object()
.expect("Card should be an object");
let card_number = card_obj["number"]
.as_str()
.expect("Card number should be a string");
let cvv = card_obj["cvv"].as_i64().expect("CVV should be a number");
let tags = original_obj["tags"]
.as_array()
.expect("Tags should be an array");
let tag1 = tags
.first()
.and_then(|v| v.as_str())
.expect("First tag should be a string");
// Now explicitly verify the masking patterns for each value type
// 1. String masking - pattern: first char + ** + length - 2 + ** + last char
let expected_name_mask = format!(
"\"{}**{}**{}\"",
&name[0..1],
name.len() - 2,
&name[name.len() - 1..]
);
let expected_email_mask = format!(
"\"{}**{}**{}\"",
&email[0..1],
email.len() - 2,
&email[email.len() - 1..]
);
let expected_card_mask = format!(
"\"{}**{}**{}\"",
&card_number[0..1],
card_number.len() - 2,
&card_number[card_number.len() - 1..]
);
let expected_tag1_mask = if tag1.len() <= 2 {
"\"**\"".to_string()
} else if tag1.len() <= 6 {
format!("\"{}**\"", &tag1[0..1])
} else {
format!(
"\"{}**{}**{}\"",
&tag1[0..1],
tag1.len() - 2,
&tag1[tag1.len() - 1..]
)
};
let expected_short_mask = "\"**\"".to_string(); // For "hi"
// 2. Number masking
let expected_age_mask = "*".repeat(age.to_string().len()); // Repeat * for the number of digits
let expected_cvv_mask = "*".repeat(cvv.to_string().len());
// 3. Boolean masking
let expected_verified_mask = if verified { "**true" } else { "**false" };
// Check that the masked output includes the expected masked patterns
assert!(
masked_str.contains(&expected_name_mask),
"Name not masked correctly. Expected: {expected_name_mask}"
);
assert!(
masked_str.contains(&expected_email_mask),
"Email not masked correctly. Expected: {expected_email_mask}",
);
assert!(
masked_str.contains(&expected_card_mask),
"Card number not masked correctly. Expected: {expected_card_mask}",
);
assert!(
masked_str.contains(&expected_tag1_mask),
"Tag not masked correctly. Expected: {expected_tag1_mask}",
);
assert!(
masked_str.contains(&expected_short_mask),
"Short string not masked correctly. Expected: {expected_short_mask}",
);
assert!(
masked_str.contains(&expected_age_mask),
"Age not masked correctly. Expected: {expected_age_mask}",
);
assert!(
masked_str.contains(&expected_cvv_mask),
"CVV not masked correctly. Expected: {expected_cvv_mask}",
);
assert!(
masked_str.contains(expected_verified_mask),
"Boolean not masked correctly. Expected: {expected_verified_mask}",
);
// Check structure preservation
assert!(
masked_str.contains("\"user\""),
"Structure not preserved - missing user object"
);
assert!(
masked_str.contains("\"card\""),
"Structure not preserved - missing card object"
);
assert!(
masked_str.contains("\"tags\""),
"Structure not preserved - missing tags array"
);
assert!(
masked_str.contains("\"null_value\":null"),
"Null value not preserved correctly"
);
// Additional security checks to ensure no original values are exposed
assert!(
!masked_str.contains(name),
"Original name value exposed in masked output"
);
assert!(
!masked_str.contains(email),
"Original email value exposed in masked output"
);
assert!(
!masked_str.contains(card_number),
"Original card number exposed in masked output"
);
assert!(
!masked_str.contains(&age.to_string()),
"Original age value exposed in masked output"
);
assert!(
!masked_str.contains(&cvv.to_string()),
"Original CVV value exposed in masked output"
);
assert!(
!masked_str.contains(tag1),
"Original tag value exposed in masked output"
);
assert!(
!masked_str.contains("hi"),
"Original short string value exposed in masked output"
);
}
}