Skip to main content

secure_types/
string.rs

1use super::{Error, vec::SecureVec};
2use core::ops::Range;
3use zeroize::Zeroize;
4
5/// A securely allocated, growable UTF-8 string, just like `std::string::String`.
6///
7/// It is a wrapper around [SecureVec<u8>] and inherits all of its security guarantees.
8///
9/// Access to the string contents is provided through scoped methods like `unlock_str`,
10/// which ensure the memory is only unlocked for the briefest possible time.
11///
12/// # Notes
13///
14/// If you return a new allocated `String` from one of the unlock methods you are responsible for zeroizing the memory.
15///
16/// # Example
17///
18/// ```
19/// use secure_types::{SecureString, Zeroize};
20///
21/// // Create a SecureString
22/// let mut secret = SecureString::from("my_super_secret");
23///
24/// // The memory is locked here
25///
26/// // Safely append more data.
27/// secret.push_str("_password");
28///
29/// // The memory is locked here.
30///
31/// // Use a scope to safely access the content as a &str.
32/// secret.unlock_str(|exposed_str| {
33///     assert_eq!(exposed_str, "my_super_secret_password");
34/// });
35///
36/// // Not recommended but if you allocate a new String make sure to zeroize it
37/// let mut exposed = secret.unlock_str(|exposed_str| {
38///     String::from(exposed_str)
39/// });
40///
41/// // Do what you need to to do with the new string
42/// // When you are done with it, zeroize it
43/// exposed.zeroize();
44///
45/// // When `secret` is dropped, its data zeroized.
46/// ```
47#[derive(Clone)]
48pub struct SecureString {
49   vec: SecureVec<u8>,
50}
51
52impl SecureString {
53   pub fn new() -> Result<Self, Error> {
54      let vec = SecureVec::new()?;
55      Ok(SecureString { vec })
56   }
57
58   pub fn new_with_capacity(capacity: usize) -> Result<Self, Error> {
59      let vec = SecureVec::new_with_capacity(capacity)?;
60      Ok(SecureString { vec })
61   }
62
63   /// Creates a `SecureString` from a `SecureVec<u8>` without checking UTF-8.
64   ///
65   /// # Safety
66   /// The caller must guarantee `vec` holds valid UTF-8. Violating this breaks
67   /// the `SecureString` invariant and will make `unlock_str`/`char_len`/serde
68   /// panic.
69   pub unsafe fn from_utf8_unchecked(vec: SecureVec<u8>) -> SecureString {
70      SecureString { vec }
71   }
72
73   pub fn erase(&mut self) {
74      self.vec.erase();
75   }
76
77   /// Returns the length of the inner `SecureVec`
78   ///
79   /// If you want the character length use [`char_len`](Self::char_len)
80   pub fn byte_len(&self) -> usize {
81      self.vec.len()
82   }
83
84   pub fn is_empty(&self) -> bool {
85      self.vec.is_empty()
86   }
87
88   /// Removes the specified range from the string
89   ///
90   /// # Panics
91   /// Panics if the range is not on UTF-8 char boundaries.
92   pub fn drain(&mut self, range: Range<usize>) {
93      self.unlock_str(|s| {
94         assert!(
95            s.is_char_boundary(range.start) && s.is_char_boundary(range.end),
96            "SecureString::drain: range {:?} does not lie on UTF-8 char boundaries",
97            range
98         );
99      });
100      let _d = self.vec.drain(range);
101   }
102
103   /// Returns the number of chars in the string
104   ///
105   /// # Panics
106   /// Panics if the string is not valid UTF-8.
107   pub fn char_len(&self) -> usize {
108      self.unlock_str(|s| s.chars().count())
109   }
110
111   /// Returns the number of chars in the string
112   ///
113   /// # Safety
114   /// The caller must guarantee that the string is valid UTF-8.
115   pub fn char_len_unchecked(&self) -> usize {
116      self.unlock_str_unchecked(|s| s.chars().count())
117   }
118
119   /// Push a `&str` into the `SecureString`
120   pub fn push_str(&mut self, string: &str) {
121      let slice = string.as_bytes();
122      for s in slice.iter() {
123         self.vec.push(*s);
124      }
125   }
126
127   /// Immutable access as `&str`
128   ///
129   /// It uses the `from_utf8` function to check the validity of the internal
130   /// bytes. If the bytes are not valid UTF-8, the function panics.
131   pub fn unlock_str<F, R>(&self, f: F) -> R
132   where
133      F: FnOnce(&str) -> R,
134   {
135      self.vec.unlock_slice(|slice| {
136         let str = core::str::from_utf8(slice)
137            .expect("SecureString invariant violated: internal bytes are not valid UTF-8");
138         f(str)
139      })
140   }
141
142   /// Immutable access as `&str`
143   ///
144   /// It uses the `from_utf8_unchecked` function to bypass the validity check.
145   ///
146   /// # Safety
147   /// The caller must guarantee that the internal bytes are valid UTF-8.
148   pub fn unlock_str_unchecked<F, R>(&self, f: F) -> R
149   where
150      F: FnOnce(&str) -> R,
151   {
152      self.vec.unlock_slice(|slice| {
153         let str = unsafe { core::str::from_utf8_unchecked(slice) };
154         f(str)
155      })
156   }
157
158   /// Mutable access to the `SecureString`
159   ///
160   /// This method does not unlock the memory.
161   pub fn secure_mut<F, R>(&mut self, f: F) -> R
162   where
163      F: FnOnce(&mut SecureString) -> R,
164   {
165      f(self)
166   }
167
168   /// Inserts text at the given character index
169   ///
170   /// # Returns
171   ///
172   /// The number of characters inserted
173   ///
174   /// # Example
175   ///
176   /// ```
177   /// use secure_types::SecureString;
178   ///
179   /// let mut string = SecureString::from("GreekFeta");
180   /// string.insert_text_at_char_idx(9, "Cheese");
181   /// string.unlock_str(|str| {
182   ///     assert_eq!(str, "GreekFetaCheese");
183   /// });
184   /// ```
185   pub fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
186      let chars_to_insert_count = text_to_insert.chars().count();
187      if chars_to_insert_count == 0 {
188         return 0;
189      }
190
191      let bytes_to_insert = text_to_insert.as_bytes();
192      let insert_len = bytes_to_insert.len();
193
194      // Get the byte index corresponding to the character index
195      let byte_idx = self
196         .vec
197         .unlock_slice(|current_bytes| char_to_byte_idx(current_bytes, char_idx));
198
199      self.vec.reserve(insert_len);
200
201      let old_byte_len = self.vec.len();
202
203      // Perform the insertion in-place
204      self.vec.unlock_memory();
205      unsafe {
206         let ptr = self.vec.as_mut_ptr();
207
208         // Shift the "tail" of the string (from the insertion point to the end)
209         // to the right to make a gap for the new content.
210         if byte_idx < old_byte_len {
211            core::ptr::copy(
212               ptr.add(byte_idx),
213               ptr.add(byte_idx + insert_len),
214               old_byte_len - byte_idx,
215            );
216         }
217
218         // Copy the new text into the newly created gap.
219         core::ptr::copy_nonoverlapping(
220            bytes_to_insert.as_ptr(),
221            ptr.add(byte_idx),
222            insert_len,
223         );
224
225         self.vec.len += insert_len;
226      }
227
228      self.vec.lock_memory();
229
230      chars_to_insert_count
231   }
232
233   /// Deletes the text in the given character range
234   ///
235   /// # Example
236   ///
237   /// ```
238   /// use secure_types::SecureString;
239   ///
240   /// let mut string = SecureString::from("GreekFetaCheese");
241   /// string.delete_text_char_range(9..15);
242   /// string.unlock_str(|str| {
243   ///     assert_eq!(str, "GreekFeta");
244   /// });
245   /// ```
246   pub fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
247      if char_range.start >= char_range.end {
248         return;
249      }
250
251      let new_len = self.vec.unlock_slice_mut(|current_bytes| {
252         let current_text = unsafe { core::str::from_utf8_unchecked(current_bytes) };
253         let byte_start = char_to_byte_idx(current_text.as_bytes(), char_range.start);
254         let byte_end = char_to_byte_idx(current_text.as_bytes(), char_range.end);
255
256         if byte_start >= byte_end || byte_end > current_bytes.len() {
257            return 0;
258         }
259
260         let remove_len = byte_end - byte_start;
261         let old_total_len = current_bytes.len();
262
263         // Shift elements left
264         current_bytes.copy_within(byte_end..old_total_len, byte_start);
265
266         let new_len = old_total_len - remove_len;
267         // Zeroize the tail end that is now unused
268         for i in new_len..old_total_len {
269            current_bytes[i].zeroize();
270         }
271         new_len
272      });
273      self.vec.len = new_len;
274   }
275}
276
277#[cfg(feature = "use_os")]
278impl From<String> for SecureString {
279   /// Creates a new `SecureString` from a `String`.
280   ///
281   /// The `String` is zeroized afterwards.
282   fn from(s: String) -> SecureString {
283      let vec = SecureVec::from_vec(s.into_bytes()).unwrap();
284      SecureString { vec }
285   }
286}
287
288impl From<&str> for SecureString {
289   /// Creates a new `SecureString` from a `&str`.
290   ///
291   /// The `&str` is not zeroized, you are responsible for zeroizing it.
292   fn from(s: &str) -> SecureString {
293      let bytes = s.as_bytes();
294      // new_with_capacity bumps 0 -> 1 internally, so empty &str is fine.
295      let mut new_vec = SecureVec::new_with_capacity(bytes.len()).unwrap();
296      new_vec.init_from_clone(bytes);
297      SecureString { vec: new_vec }
298   }
299}
300
301impl TryFrom<SecureVec<u8>> for SecureString {
302   type Error = Error;
303
304   /// Creates a `SecureString` from a `SecureVec<u8>`, validating UTF-8.
305   ///
306   /// The `SecureVec` is consumed. On invalid UTF-8 it is dropped (and thus
307   /// zeroized) and `Error::InvalidUtf8` is returned.
308   fn try_from(vec: SecureVec<u8>) -> Result<Self, Self::Error> {
309      let valid = vec.unlock_slice(|slice| core::str::from_utf8(slice).is_ok());
310      if valid {
311         Ok(SecureString { vec })
312      } else {
313         Err(Error::InvalidUtf8)
314      }
315   }
316}
317
318#[cfg(feature = "serde")]
319impl serde::Serialize for SecureString {
320   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
321   where
322      S: serde::Serializer,
323   {
324      let res = self.unlock_str(|str| serializer.serialize_str(str));
325      res
326   }
327}
328
329#[cfg(feature = "serde")]
330impl<'de> serde::Deserialize<'de> for SecureString {
331   fn deserialize<D>(deserializer: D) -> Result<SecureString, D::Error>
332   where
333      D: serde::Deserializer<'de>,
334   {
335      struct SecureStringVisitor;
336      impl<'de> serde::de::Visitor<'de> for SecureStringVisitor {
337         type Value = SecureString;
338         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
339            write!(formatter, "an utf-8 encoded string")
340         }
341         fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
342         where
343            E: serde::de::Error,
344         {
345            Ok(SecureString::from(v))
346         }
347      }
348      deserializer.deserialize_string(SecureStringVisitor)
349   }
350}
351
352fn char_to_byte_idx(s_bytes: &[u8], char_idx: usize) -> usize {
353   core::str::from_utf8(s_bytes)
354      .ok()
355      .and_then(|s| s.char_indices().nth(char_idx).map(|(idx, _)| idx))
356      .unwrap_or(s_bytes.len()) // Fallback to end if char_idx is out of bounds
357}
358
359#[cfg(all(test, feature = "use_os"))]
360mod tests {
361   use super::*;
362
363   #[test]
364   fn test_creation() {
365      let hello_world = "Hello, world!";
366      let secure = SecureString::from(hello_world);
367
368      secure.unlock_str(|str| {
369         assert_eq!(str, hello_world);
370      });
371   }
372
373   #[test]
374   fn test_from_string() {
375      let hello_world = String::from("Hello, world!");
376      let string = SecureString::from(hello_world);
377
378      string.unlock_str(|str| {
379         assert_eq!(str, "Hello, world!");
380      });
381   }
382
383   #[test]
384   fn test_try_from_secure_vec() {
385      let hello_world = "Hello, world!".to_string();
386      let vec: SecureVec<u8> = SecureVec::from_slice(hello_world.as_bytes()).unwrap();
387
388      let string = SecureString::from(hello_world);
389      let string2 = SecureString::try_from(vec).unwrap();
390
391      string.unlock_str(|str| {
392         string2.unlock_str(|str2| {
393            assert_eq!(str, str2);
394         });
395      });
396
397      string.unlock_str_unchecked(|str| {
398         string2.unlock_str_unchecked(|str2| {
399            assert_eq!(str, str2);
400         });
401      });
402   }
403
404   #[test]
405   fn test_clone() {
406      let hello_world = "Hello, world!".to_string();
407      let secure1 = SecureString::from(hello_world.clone());
408      let secure2 = secure1.clone();
409
410      secure2.unlock_str(|str| {
411         assert_eq!(str, hello_world);
412      });
413
414      secure2.unlock_str_unchecked(|str| {
415         assert_eq!(str, hello_world);
416      });
417   }
418
419   #[test]
420   fn test_insert_text_at_char_idx() {
421      let hello_world = "My name is ";
422      let mut secure = SecureString::from(hello_world);
423      secure.insert_text_at_char_idx(12, "Mike");
424      secure.unlock_str(|str| {
425         assert_eq!(str, "My name is Mike");
426      });
427
428      secure.unlock_str_unchecked(|str| {
429         assert_eq!(str, "My name is Mike");
430      });
431   }
432
433   #[test]
434   fn test_delete_text_char_range() {
435      let hello_world = "My name is Mike";
436      let mut secure = SecureString::from(hello_world);
437      secure.delete_text_char_range(10..17);
438      secure.unlock_str(|str| {
439         assert_eq!(str, "My name is");
440      });
441
442      secure.unlock_str_unchecked(|str| {
443         assert_eq!(str, "My name is");
444      });
445   }
446
447   #[test]
448   fn test_drain() {
449      let hello_world = "Hello, world!";
450      let mut secure = SecureString::from(hello_world);
451      secure.drain(0..7);
452      secure.unlock_str(|str| {
453         assert_eq!(str, "world!");
454      });
455
456      secure.unlock_str_unchecked(|str| {
457         assert_eq!(str, "world!");
458      });
459   }
460
461   #[cfg(feature = "serde")]
462   #[test]
463   fn test_serde() {
464      let hello_world = "Hello, world!";
465      let secure = SecureString::from(hello_world);
466
467      let json_string = serde_json::to_string(&secure).expect("Serialization failed");
468      let json_bytes = serde_json::to_vec(&secure).expect("Serialization failed");
469
470      let deserialized_string: SecureString =
471         serde_json::from_str(&json_string).expect("Deserialization failed");
472
473      let deserialized_bytes: SecureString =
474         serde_json::from_slice(&json_bytes).expect("Deserialization failed");
475
476      deserialized_string.unlock_str(|str| {
477         assert_eq!(str, hello_world);
478      });
479
480      deserialized_string.unlock_str_unchecked(|str| {
481         assert_eq!(str, hello_world);
482      });
483
484      deserialized_bytes.unlock_str(|str| {
485         assert_eq!(str, hello_world);
486      });
487
488      deserialized_bytes.unlock_str_unchecked(|str| {
489         assert_eq!(str, hello_world);
490      });
491   }
492
493   #[test]
494   fn test_unlock_str() {
495      let hello_word = "Hello, world!";
496      let string = SecureString::from(hello_word);
497      let _exposed_string = string.unlock_str(|str| {
498         assert_eq!(str, hello_word);
499         String::from(str)
500      });
501
502      let _exposed_string = string.unlock_str_unchecked(|str| {
503         assert_eq!(str, hello_word);
504         String::from(str)
505      });
506   }
507
508   #[test]
509   fn test_push_str() {
510      let hello_world = "Hello, world!";
511
512      let mut string = SecureString::new().unwrap();
513      string.push_str(hello_world);
514      string.unlock_str(|str| {
515         assert_eq!(str, hello_world);
516      });
517
518      string.unlock_str_unchecked(|str| {
519         assert_eq!(str, hello_world);
520      });
521   }
522
523   #[test]
524   fn test_unlock_mut() {
525      let hello_world = "Hello, world!";
526      let mut string = SecureString::from("Hello, ");
527      string.secure_mut(|string| {
528         string.push_str("world!");
529      });
530
531      string.unlock_str(|str| {
532         assert_eq!(str, hello_world);
533      });
534
535      string.unlock_str_unchecked(|str| {
536         assert_eq!(str, hello_world);
537      });
538   }
539}