openzeppelin_relayer/models/
secret_string.rs1use std::{fmt, sync::Mutex};
11
12use secrets::SecretVec;
13use serde::{Deserialize, Serialize};
14use utoipa::ToSchema;
15use zeroize::Zeroizing;
16
17pub struct SecretString(Mutex<SecretVec<u8>>);
18
19impl Clone for SecretString {
20 fn clone(&self) -> Self {
21 let secret_vec = self.with_secret_vec(|secret_vec| secret_vec.clone());
22 Self(Mutex::new(secret_vec))
23 }
24}
25
26impl SecretString {
27 pub fn new(s: &str) -> Self {
31 let bytes = Zeroizing::new(s.as_bytes().to_vec());
32 let secret_vec = SecretVec::new(bytes.len(), |buffer| {
33 buffer.copy_from_slice(&bytes);
34 });
35 Self(Mutex::new(secret_vec))
36 }
37
38 fn with_secret_vec<F, R>(&self, f: F) -> R
42 where
43 F: FnOnce(&SecretVec<u8>) -> R,
44 {
45 let guard = match self.0.lock() {
46 Ok(guard) => guard,
47 Err(poisoned) => poisoned.into_inner(),
48 };
49
50 f(&guard)
51 }
52
53 pub fn as_str<F, R>(&self, f: F) -> R
58 where
59 F: FnOnce(&str) -> R,
60 {
61 self.with_secret_vec(|secret_vec| {
62 let bytes = secret_vec.borrow();
63 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
64 f(s)
65 })
66 }
67
68 pub fn to_str(&self) -> Zeroizing<String> {
73 self.with_secret_vec(|secret_vec| {
74 let bytes = secret_vec.borrow();
75 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
76 Zeroizing::new(s.to_string())
77 })
78 }
79
80 pub fn is_empty(&self) -> bool {
84 self.with_secret_vec(|secret_vec| secret_vec.is_empty())
85 }
86
87 pub fn has_minimum_length(&self, min_length: usize) -> bool {
91 self.with_secret_vec(|secret_vec| {
92 let bytes = secret_vec.borrow();
93 bytes.len() >= min_length
94 })
95 }
96
97 pub fn equals_str(&self, candidate: &str) -> bool {
98 self.with_secret_vec(|secret_vec| {
99 let self_bytes = secret_vec.borrow();
100 let candidate_bytes = candidate.as_bytes();
101
102 if self_bytes.len() != candidate_bytes.len() {
103 return false;
104 }
105
106 subtle::ConstantTimeEq::ct_eq(&*self_bytes, candidate_bytes).into()
107 })
108 }
109}
110
111impl Serialize for SecretString {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: serde::Serializer,
115 {
116 serializer.serialize_str("REDACTED")
117 }
118}
119
120impl<'de> Deserialize<'de> for SecretString {
121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122 where
123 D: serde::Deserializer<'de>,
124 {
125 let s = Zeroizing::new(String::deserialize(deserializer)?);
126
127 Ok(SecretString::new(&s))
128 }
129}
130
131impl PartialEq for SecretString {
132 fn eq(&self, other: &Self) -> bool {
133 self.with_secret_vec(|self_vec| {
134 other.with_secret_vec(|other_vec| {
135 let self_bytes = self_vec.borrow();
136 let other_bytes = other_vec.borrow();
137
138 self_bytes.len() == other_bytes.len()
139 && subtle::ConstantTimeEq::ct_eq(&*self_bytes, &*other_bytes).into()
140 })
141 })
142 }
143}
144
145impl fmt::Debug for SecretString {
146 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
147 write!(f, "SecretString(REDACTED)")
148 }
149}
150
151impl ToSchema for SecretString {
152 fn name() -> std::borrow::Cow<'static, str> {
153 "SecretString".into()
154 }
155}
156
157impl utoipa::PartialSchema for SecretString {
158 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
159 use utoipa::openapi::*;
160
161 RefOr::T(Schema::Object(
162 ObjectBuilder::new()
163 .schema_type(schema::Type::String)
164 .format(Some(schema::SchemaFormat::KnownFormat(
165 schema::KnownFormat::Password,
166 )))
167 .description(Some("A secret string value (content is protected)"))
168 .build(),
169 ))
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::sync::{Arc, Barrier};
177 use std::thread;
178
179 #[test]
180 fn test_new_creates_valid_secret_string() {
181 let secret = SecretString::new("test_secret_value");
182
183 secret.as_str(|s| {
184 assert_eq!(s, "test_secret_value");
185 });
186 }
187
188 #[test]
189 fn test_empty_string_is_handled_correctly() {
190 let empty = SecretString::new("");
191
192 assert!(empty.is_empty());
193
194 empty.as_str(|s| {
195 assert_eq!(s, "");
196 });
197 }
198
199 #[test]
200 fn test_to_str_creates_correct_zeroizing_copy() {
201 let secret = SecretString::new("temporary_copy");
202
203 let copy = secret.to_str();
204
205 assert_eq!(&*copy, "temporary_copy");
206 }
207
208 #[test]
209 fn test_is_empty_returns_correct_value() {
210 let empty = SecretString::new("");
211 let non_empty = SecretString::new("not empty");
212
213 assert!(empty.is_empty());
214 assert!(!non_empty.is_empty());
215 }
216
217 #[test]
218 fn test_serialization_redacts_content() {
219 let secret = SecretString::new("should_not_appear_in_serialized_form");
220
221 let serialized = serde_json::to_string(&secret).unwrap();
222
223 assert_eq!(serialized, "\"REDACTED\"");
224 assert!(!serialized.contains("should_not_appear_in_serialized_form"));
225 }
226
227 #[test]
228 fn test_deserialization_creates_valid_secret_string() {
229 let json_str = "\"deserialized_secret\"";
230
231 let deserialized: SecretString = serde_json::from_str(json_str).unwrap();
232
233 deserialized.as_str(|s| {
234 assert_eq!(s, "deserialized_secret");
235 });
236 }
237
238 #[test]
239 fn test_equality_comparison_works_correctly() {
240 let secret1 = SecretString::new("same_value");
241 let secret2 = SecretString::new("same_value");
242 let secret3 = SecretString::new("different_value");
243
244 assert_eq!(secret1, secret2);
245 assert_ne!(secret1, secret3);
246 }
247
248 #[test]
249 fn test_debug_output_redacts_content() {
250 let secret = SecretString::new("should_not_appear_in_debug");
251
252 let debug_str = format!("{secret:?}");
253
254 assert_eq!(debug_str, "SecretString(REDACTED)");
255 assert!(!debug_str.contains("should_not_appear_in_debug"));
256 }
257
258 #[test]
259 fn test_thread_safety() {
260 let secret = SecretString::new("shared_across_threads");
261 let num_threads = 10;
262 let barrier = Arc::new(Barrier::new(num_threads));
263 let mut handles = vec![];
264
265 for i in 0..num_threads {
266 let thread_secret = secret.clone();
267 let thread_barrier = barrier.clone();
268
269 let handle = thread::spawn(move || {
270 thread_barrier.wait();
272
273 thread_secret.as_str(|s| {
275 assert_eq!(s, "shared_across_threads");
276 });
277
278 assert!(!thread_secret.is_empty());
280 let copy = thread_secret.to_str();
281 assert_eq!(&*copy, "shared_across_threads");
282
283 i
285 });
286
287 handles.push(handle);
288 }
289
290 let mut completed_threads = vec![];
292 for handle in handles {
293 completed_threads.push(handle.join().unwrap());
294 }
295
296 completed_threads.sort();
298 assert_eq!(completed_threads, (0..num_threads).collect::<Vec<_>>());
299 }
300
301 #[test]
302 fn test_unicode_handling() {
303 let unicode_string = "こんにちは世界!";
304 let secret = SecretString::new(unicode_string);
305
306 secret.as_str(|s| {
307 assert_eq!(s, unicode_string);
308 assert_eq!(s.chars().count(), 8); });
310 }
311
312 #[test]
313 fn test_special_characters_handling() {
314 let special_chars = "!@#$%^&*()_+{}|:<>?~`-=[]\\;',./";
315 let secret = SecretString::new(special_chars);
316
317 secret.as_str(|s| {
318 assert_eq!(s, special_chars);
319 });
320 }
321
322 #[test]
323 fn test_very_long_string() {
324 let long_string = "a".repeat(100_000);
326 let secret = SecretString::new(&long_string);
327
328 secret.as_str(|s| {
329 assert_eq!(s.len(), 100_000);
330 assert_eq!(s, long_string);
331 });
332
333 assert_eq!(secret.0.lock().unwrap().len(), 100_000);
334 }
335
336 #[test]
337 fn test_has_minimum_length() {
338 let empty = SecretString::new("");
340 let short = SecretString::new("abc");
341 let medium = SecretString::new("abcdefghij"); let long = SecretString::new("abcdefghijklmnopqrst"); assert!(empty.has_minimum_length(0));
346 assert!(short.has_minimum_length(0));
347 assert!(medium.has_minimum_length(0));
348 assert!(long.has_minimum_length(0));
349
350 assert!(!empty.has_minimum_length(1));
352 assert!(short.has_minimum_length(1));
353 assert!(medium.has_minimum_length(1));
354 assert!(long.has_minimum_length(1));
355
356 assert!(empty.has_minimum_length(0));
358 assert!(short.has_minimum_length(3));
359 assert!(medium.has_minimum_length(10));
360 assert!(long.has_minimum_length(20));
361
362 assert!(!empty.has_minimum_length(1));
364 assert!(!short.has_minimum_length(4));
365 assert!(!medium.has_minimum_length(11));
366 assert!(!long.has_minimum_length(21));
367
368 assert!(!short.has_minimum_length(100));
370 assert!(!medium.has_minimum_length(100));
371 assert!(!long.has_minimum_length(100));
372 }
373}