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    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                // Wait for all threads to be ready
271                thread_barrier.wait();
272
273                // Verify the secret content
274                thread_secret.as_str(|s| {
275                    assert_eq!(s, "shared_across_threads");
276                });
277
278                // Test other methods
279                assert!(!thread_secret.is_empty());
280                let copy = thread_secret.to_str();
281                assert_eq!(&*copy, "shared_across_threads");
282
283                // Return thread ID to verify all threads ran
284                i
285            });
286
287            handles.push(handle);
288        }
289
290        // Verify all threads completed successfully
291        let mut completed_threads = vec![];
292        for handle in handles {
293            completed_threads.push(handle.join().unwrap());
294        }
295
296        // Sort results to make comparison easier
297        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); // 7 Unicode characters + 1 ASCII
309        });
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        // Create a long string (100,000 characters)
325        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        // Create test strings of various lengths
339        let empty = SecretString::new("");
340        let short = SecretString::new("abc");
341        let medium = SecretString::new("abcdefghij"); // 10 characters
342        let long = SecretString::new("abcdefghijklmnopqrst"); // 20 characters
343
344        // Test with minimum length 0
345        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        // Test with minimum length 1
351        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        // Test with exact length matches
357        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        // Test with length exceeding the string
363        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        // Test with significantly larger minimum length
369        assert!(!short.has_minimum_length(100));
370        assert!(!medium.has_minimum_length(100));
371        assert!(!long.has_minimum_length(100));
372    }
373}