1use std::fmt;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use subtle::ConstantTimeEq;
7
8const SECRET_MASK: &str = "***";
9
10#[derive(Clone, Default, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
14pub struct SecretString {
15 value: String,
16}
17
18impl SecretString {
19 pub fn new(value: impl Into<String>) -> Self {
21 Self {
22 value: value.into(),
23 }
24 }
25
26 pub fn expose(&self) -> &str {
28 &self.value
29 }
30
31 pub const fn is_empty(&self) -> bool {
33 self.value.is_empty()
34 }
35
36 pub const fn len(&self) -> usize {
38 self.value.len()
39 }
40}
41
42impl PartialEq for SecretString {
43 fn eq(&self, other: &Self) -> bool {
44 self.value.as_bytes().ct_eq(other.value.as_bytes()).into()
45 }
46}
47
48impl Eq for SecretString {}
49
50impl fmt::Display for SecretString {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 if self.value.is_empty() {
53 Ok(())
54 } else {
55 f.write_str(SECRET_MASK)
56 }
57 }
58}
59
60impl fmt::Debug for SecretString {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str("SecretString(***)")
63 }
64}
65
66impl Serialize for SecretString {
67 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
68 if self.value.is_empty() {
69 ser.serialize_str("")
70 } else {
71 ser.serialize_str(SECRET_MASK)
72 }
73 }
74}
75
76impl<'de> Deserialize<'de> for SecretString {
77 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
78 let s = String::deserialize(de)?;
79 Ok(Self::new(s))
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn display_masks_value() {
89 let s = SecretString::new("password123");
90 assert_eq!(format!("{s}"), "***");
91 }
92
93 #[test]
94 fn display_empty_is_empty() {
95 let s = SecretString::new("");
96 assert_eq!(format!("{s}"), "");
97 }
98
99 #[test]
100 fn debug_masks_value() {
101 let s = SecretString::new("password123");
102 assert_eq!(format!("{s:?}"), "SecretString(***)");
103 }
104
105 #[test]
106 fn expose_returns_plaintext() {
107 let s = SecretString::new("hunter2");
108 assert_eq!(s.expose(), "hunter2");
109 }
110
111 #[test]
112 fn json_serializes_masked() {
113 let s = SecretString::new("secret");
114 let json = serde_json::to_string(&s).unwrap();
115 assert_eq!(json, r#""***""#);
116 }
117
118 #[test]
119 fn json_deserializes_plaintext() {
120 let s: SecretString = serde_json::from_str(r#""actual_value""#).unwrap();
121 assert_eq!(s.expose(), "actual_value");
122 }
123
124 #[test]
125 fn equality_is_constant_time() {
126 let a = SecretString::new("same-value");
127 let b = SecretString::new("same-value");
128 let c = SecretString::new("different");
129 assert_eq!(a, b);
130 assert_ne!(a, c);
131 }
132}