openzeppelin_relayer/models/
secret_string.rs

1//! SecretString - A container for sensitive string data
2//!
3//! This module provides a secure string implementation that protects sensitive
4//! data in memory and prevents it from being accidentally exposed through logs,
5//! serialization, or debug output.
6//!
7//! The `SecretString` type wraps a `SecretVec<u8>` and provides methods for
8//! securely handling string data, including zeroizing the memory when the
9//! string is dropped.
10use 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    /// Creates a new SecretString from a regular string
28    ///
29    /// The input string's content is copied into secure memory and protected.
30    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    /// Access the SecretVec with a provided function
39    ///
40    /// This is a private helper method to safely access the locked SecretVec
41    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    /// Access the secret string content with a provided function
54    ///
55    /// This method allows temporary access to the string content
56    /// without creating a copy of the string.
57    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    /// Create a temporary copy of the string content
69    ///
70    /// Returns a zeroizing string that will be securely erased when dropped.
71    /// Only use this when absolutely necessary as it creates a copy of the secret.
72    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    /// Check if the secret string is empty
81    ///
82    /// Returns true if the string contains no bytes.
83    pub fn is_empty(&self) -> bool {
84        self.with_secret_vec(|secret_vec| secret_vec.is_empty())
85    }
86
87    /// Check if the secret string meets a minimum length requirement
88    ///
89    /// Returns true if the string has at least the specified length.
90    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
98impl Serialize for SecretString {
99    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100    where
101        S: serde::Serializer,
102    {
103        serializer.serialize_str("REDACTED")
104    }
105}
106
107impl<'de> Deserialize<'de> for SecretString {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: serde::Deserializer<'de>,
111    {
112        let s = Zeroizing::new(String::deserialize(deserializer)?);
113
114        Ok(SecretString::new(&s))
115    }
116}
117
118impl PartialEq for SecretString {
119    fn eq(&self, other: &Self) -> bool {
120        self.with_secret_vec(|self_vec| {
121            other.with_secret_vec(|other_vec| {
122                let self_bytes = self_vec.borrow();
123                let other_bytes = other_vec.borrow();
124
125                self_bytes.len() == other_bytes.len()
126                    && subtle::ConstantTimeEq::ct_eq(&*self_bytes, &*other_bytes).into()
127            })
128        })
129    }
130}
131
132impl fmt::Debug for SecretString {
133    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134        write!(f, "SecretString(REDACTED)")
135    }
136}
137
138impl ToSchema for SecretString {
139    fn name() -> std::borrow::Cow<'static, str> {
140        "SecretString".into()
141    }
142}
143
144impl utoipa::PartialSchema for SecretString {
145    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
146        use utoipa::openapi::*;
147
148        RefOr::T(Schema::Object(
149            ObjectBuilder::new()
150                .schema_type(schema::Type::String)
151                .format(Some(schema::SchemaFormat::KnownFormat(
152                    schema::KnownFormat::Password,
153                )))
154                .description(Some("A secret string value (content is protected)"))
155                .build(),
156        ))
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use std::sync::{Arc, Barrier};
164    use std::thread;
165
166    #[test]
167    fn test_new_creates_valid_secret_string() {
168        let secret = SecretString::new("test_secret_value");
169
170        secret.as_str(|s| {
171            assert_eq!(s, "test_secret_value");
172        });
173    }
174
175    #[test]
176    fn test_empty_string_is_handled_correctly() {
177        let empty = SecretString::new("");
178
179        assert!(empty.is_empty());
180
181        empty.as_str(|s| {
182            assert_eq!(s, "");
183        });
184    }
185
186    #[test]
187    fn test_to_str_creates_correct_zeroizing_copy() {
188        let secret = SecretString::new("temporary_copy");
189
190        let copy = secret.to_str();
191
192        assert_eq!(&*copy, "temporary_copy");
193    }
194
195    #[test]
196    fn test_is_empty_returns_correct_value() {
197        let empty = SecretString::new("");
198        let non_empty = SecretString::new("not empty");
199
200        assert!(empty.is_empty());
201        assert!(!non_empty.is_empty());
202    }
203
204    #[test]
205    fn test_serialization_redacts_content() {
206        let secret = SecretString::new("should_not_appear_in_serialized_form");
207
208        let serialized = serde_json::to_string(&secret).unwrap();
209
210        assert_eq!(serialized, "\"REDACTED\"");
211        assert!(!serialized.contains("should_not_appear_in_serialized_form"));
212    }
213
214    #[test]
215    fn test_deserialization_creates_valid_secret_string() {
216        let json_str = "\"deserialized_secret\"";
217
218        let deserialized: SecretString = serde_json::from_str(json_str).unwrap();
219
220        deserialized.as_str(|s| {
221            assert_eq!(s, "deserialized_secret");
222        });
223    }
224
225    #[test]
226    fn test_equality_comparison_works_correctly() {
227        let secret1 = SecretString::new("same_value");
228        let secret2 = SecretString::new("same_value");
229        let secret3 = SecretString::new("different_value");
230
231        assert_eq!(secret1, secret2);
232        assert_ne!(secret1, secret3);
233    }
234
235    #[test]
236    fn test_debug_output_redacts_content() {
237        let secret = SecretString::new("should_not_appear_in_debug");
238
239        let debug_str = format!("{:?}", secret);
240
241        assert_eq!(debug_str, "SecretString(REDACTED)");
242        assert!(!debug_str.contains("should_not_appear_in_debug"));
243    }
244
245    #[test]
246    fn test_thread_safety() {
247        let secret = SecretString::new("shared_across_threads");
248        let num_threads = 10;
249        let barrier = Arc::new(Barrier::new(num_threads));
250        let mut handles = vec![];
251
252        for i in 0..num_threads {
253            let thread_secret = secret.clone();
254            let thread_barrier = barrier.clone();
255
256            let handle = thread::spawn(move || {
257                // Wait for all threads to be ready
258                thread_barrier.wait();
259
260                // Verify the secret content
261                thread_secret.as_str(|s| {
262                    assert_eq!(s, "shared_across_threads");
263                });
264
265                // Test other methods
266                assert!(!thread_secret.is_empty());
267                let copy = thread_secret.to_str();
268                assert_eq!(&*copy, "shared_across_threads");
269
270                // Return thread ID to verify all threads ran
271                i
272            });
273
274            handles.push(handle);
275        }
276
277        // Verify all threads completed successfully
278        let mut completed_threads = vec![];
279        for handle in handles {
280            completed_threads.push(handle.join().unwrap());
281        }
282
283        // Sort results to make comparison easier
284        completed_threads.sort();
285        assert_eq!(completed_threads, (0..num_threads).collect::<Vec<_>>());
286    }
287
288    #[test]
289    fn test_unicode_handling() {
290        let unicode_string = "こんにちは世界!";
291        let secret = SecretString::new(unicode_string);
292
293        secret.as_str(|s| {
294            assert_eq!(s, unicode_string);
295            assert_eq!(s.chars().count(), 8); // 7 Unicode characters + 1 ASCII
296        });
297    }
298
299    #[test]
300    fn test_special_characters_handling() {
301        let special_chars = "!@#$%^&*()_+{}|:<>?~`-=[]\\;',./";
302        let secret = SecretString::new(special_chars);
303
304        secret.as_str(|s| {
305            assert_eq!(s, special_chars);
306        });
307    }
308
309    #[test]
310    fn test_very_long_string() {
311        // Create a long string (100,000 characters)
312        let long_string = "a".repeat(100_000);
313        let secret = SecretString::new(&long_string);
314
315        secret.as_str(|s| {
316            assert_eq!(s.len(), 100_000);
317            assert_eq!(s, long_string);
318        });
319
320        assert_eq!(secret.0.lock().unwrap().len(), 100_000);
321    }
322
323    #[test]
324    fn test_has_minimum_length() {
325        // Create test strings of various lengths
326        let empty = SecretString::new("");
327        let short = SecretString::new("abc");
328        let medium = SecretString::new("abcdefghij"); // 10 characters
329        let long = SecretString::new("abcdefghijklmnopqrst"); // 20 characters
330
331        // Test with minimum length 0
332        assert!(empty.has_minimum_length(0));
333        assert!(short.has_minimum_length(0));
334        assert!(medium.has_minimum_length(0));
335        assert!(long.has_minimum_length(0));
336
337        // Test with minimum length 1
338        assert!(!empty.has_minimum_length(1));
339        assert!(short.has_minimum_length(1));
340        assert!(medium.has_minimum_length(1));
341        assert!(long.has_minimum_length(1));
342
343        // Test with exact length matches
344        assert!(empty.has_minimum_length(0));
345        assert!(short.has_minimum_length(3));
346        assert!(medium.has_minimum_length(10));
347        assert!(long.has_minimum_length(20));
348
349        // Test with length exceeding the string
350        assert!(!empty.has_minimum_length(1));
351        assert!(!short.has_minimum_length(4));
352        assert!(!medium.has_minimum_length(11));
353        assert!(!long.has_minimum_length(21));
354
355        // Test with significantly larger minimum length
356        assert!(!short.has_minimum_length(100));
357        assert!(!medium.has_minimum_length(100));
358        assert!(!long.has_minimum_length(100));
359    }
360}