Skip to main content

secure_types/
string.rs

1use super::{
2   Error,
3   vec::{SecureVec, UnlockGuard},
4};
5use core::ops::Range;
6use zeroize::Zeroize;
7
8// `String` is only used by the serde visitor below; in a `no_std` build it has to come
9// from `alloc` (with `use_os` the prelude provides it).
10#[cfg(all(feature = "serde", not(feature = "use_os")))]
11use alloc::string::String;
12
13/// A securely allocated, growable UTF-8 string, just like `std::string::String`.
14///
15/// It is a wrapper around [SecureVec<u8>] and inherits all of its security guarantees.
16///
17/// Access to the string contents is provided through scoped methods like `unlock_str`,
18/// which ensure the memory is only unlocked for the briefest possible time.
19///
20/// # Thread Safety
21///
22/// Same as [`SecureVec`]: `Send` but not `Sync`. Share as `Arc<Mutex<SecureString>>`.
23///
24/// # Notes
25///
26/// If you return a new allocated `String` from one of the unlock methods you are responsible for zeroizing the memory.
27///
28/// # Example
29///
30/// ```
31/// use secure_types::{SecureString, Zeroize};
32///
33/// // Create a SecureString
34/// let mut secret = SecureString::from("my_super_secret");
35///
36/// // The memory is locked here
37///
38/// // Safely append more data.
39/// secret.push_str("_password");
40///
41/// // The memory is locked here.
42///
43/// // Use a scope to safely access the content as a &str.
44/// secret.unlock_str(|exposed_str| {
45///     assert_eq!(exposed_str, "my_super_secret_password");
46/// });
47///
48/// // Not recommended but if you allocate a new String make sure to zeroize it
49/// let mut exposed = secret.unlock_str(|exposed_str| {
50///     String::from(exposed_str)
51/// });
52///
53/// // Do what you need to to do with the new string
54/// // When you are done with it, zeroize it
55/// exposed.zeroize();
56///
57/// // When `secret` is dropped, its data zeroized.
58/// ```
59#[derive(Clone)]
60pub struct SecureString {
61   vec: SecureVec<u8>,
62}
63
64impl SecureString {
65   pub fn new() -> Result<Self, Error> {
66      let vec = SecureVec::new()?;
67      Ok(SecureString { vec })
68   }
69
70   pub fn new_with_capacity(capacity: usize) -> Result<Self, Error> {
71      let vec = SecureVec::new_with_capacity(capacity)?;
72      Ok(SecureString { vec })
73   }
74
75   /// Creates a `SecureString` from a `SecureVec<u8>` without checking UTF-8.
76   ///
77   /// # Safety
78   /// The caller must guarantee `vec` holds valid UTF-8. Violating this breaks
79   /// the `SecureString` invariant and will make `unlock_str`/`char_len`/serde
80   /// panic.
81   pub unsafe fn from_utf8_unchecked(vec: SecureVec<u8>) -> SecureString {
82      SecureString { vec }
83   }
84
85   pub fn erase(&mut self) {
86      self.vec.erase();
87   }
88
89   /// Returns the length of the inner `SecureVec`
90   ///
91   /// If you want the character length use [`char_len`](Self::char_len)
92   pub fn byte_len(&self) -> usize {
93      self.vec.len()
94   }
95
96   pub fn is_empty(&self) -> bool {
97      self.vec.is_empty()
98   }
99
100   /// Removes the specified byte range from the string.
101   ///
102   /// `range` is a **byte** range, not a character range: take it from `&str`
103   /// byte offsets. For a character range use
104   /// [`delete_text_char_range`](Self::delete_text_char_range).
105   ///
106   /// # Panics
107   /// Panics if the range is not on UTF-8 char boundaries.
108   pub fn drain(&mut self, range: Range<usize>) {
109      self.unlock_str(|s| {
110         assert!(
111            s.is_char_boundary(range.start) && s.is_char_boundary(range.end),
112            "SecureString::drain: range {:?} does not lie on UTF-8 char boundaries",
113            range
114         );
115      });
116      let _d = self.vec.drain(range);
117   }
118
119   /// Returns the number of chars in the string
120   ///
121   /// # Panics
122   /// Panics if the string is not valid UTF-8.
123   pub fn char_len(&self) -> usize {
124      self.unlock_str(|s| s.chars().count())
125   }
126
127   /// Returns the number of chars in the string
128   ///
129   /// # Safety
130   /// The caller must guarantee that the string is valid UTF-8.
131   pub fn char_len_unchecked(&self) -> usize {
132      self.unlock_str_unchecked(|s| s.chars().count())
133   }
134
135   /// Push a `&str` into the `SecureString`
136   pub fn push_str(&mut self, string: &str) {
137      let slice = string.as_bytes();
138      for s in slice.iter() {
139         self.vec.push(*s);
140      }
141   }
142
143   /// Immutable access as `&str`
144   ///
145   /// It uses the `from_utf8` function to check the validity of the internal
146   /// bytes. If the bytes are not valid UTF-8, the function panics.
147   pub fn unlock_str<F, R>(&self, f: F) -> R
148   where
149      F: FnOnce(&str) -> R,
150   {
151      self.vec.unlock_slice(|slice| {
152         let str = core::str::from_utf8(slice)
153            .expect("SecureString invariant violated: internal bytes are not valid UTF-8");
154         f(str)
155      })
156   }
157
158   /// Immutable access as `&str`
159   ///
160   /// It uses the `from_utf8_unchecked` function to bypass the validity check.
161   ///
162   /// # Safety
163   /// The caller must guarantee that the internal bytes are valid UTF-8.
164   pub fn unlock_str_unchecked<F, R>(&self, f: F) -> R
165   where
166      F: FnOnce(&str) -> R,
167   {
168      self.vec.unlock_slice(|slice| {
169         // SAFETY: this is the `unchecked` variant — the caller promised the
170         // internal bytes are valid UTF-8.
171         let str = unsafe { core::str::from_utf8_unchecked(slice) };
172         f(str)
173      })
174   }
175
176   /// Mutable access to the `SecureString`
177   ///
178   /// This method does not unlock the memory.
179   pub fn secure_mut<F, R>(&mut self, f: F) -> R
180   where
181      F: FnOnce(&mut SecureString) -> R,
182   {
183      f(self)
184   }
185
186   /// Inserts text at the given character index
187   ///
188   /// # Returns
189   ///
190   /// The number of characters inserted
191   ///
192   /// # Example
193   ///
194   /// ```
195   /// use secure_types::SecureString;
196   ///
197   /// let mut string = SecureString::from("GreekFeta");
198   /// string.insert_text_at_char_idx(9, "Cheese");
199   /// string.unlock_str(|str| {
200   ///     assert_eq!(str, "GreekFetaCheese");
201   /// });
202   /// ```
203   pub fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
204      let chars_to_insert_count = text_to_insert.chars().count();
205      if chars_to_insert_count == 0 {
206         return 0;
207      }
208
209      let bytes_to_insert = text_to_insert.as_bytes();
210      let insert_len = bytes_to_insert.len();
211
212      // Get the byte index corresponding to the character index
213      let byte_idx = self
214         .vec
215         .unlock_slice(|current_bytes| char_to_byte_idx(current_bytes, char_idx));
216
217      self.vec.reserve(insert_len);
218
219      let old_byte_len = self.vec.len();
220
221      // Taken before the guard borrows the vector: raw pointers do not keep the
222      // borrow alive, and the guard must stay alive while writing through it.
223      let ptr = self.vec.as_mut_ptr();
224
225      // Perform the insertion in-place. `UnlockGuard` re-locks the memory on
226      // drop, including when the block unwinds.
227      let new_byte_len = {
228         let _guard = UnlockGuard::new(&self.vec);
229
230         // SAFETY: the guard has unprotected `self.vec`'s live buffer for this
231         // block. `reserve(insert_len)` above guaranteed room for
232         // `old_byte_len + insert_len` bytes and `byte_idx <= old_byte_len`, so
233         // both the shifted tail and the inserted span stay inside the
234         // allocation. `ptr::copy` tolerates source/destination overlap; the
235         // `copy_nonoverlapping` source is `text_to_insert`, a distinct `&str`.
236         unsafe {
237            // Shift the "tail" of the string (from the insertion point to the end)
238            // to the right to make a gap for the new content.
239            if byte_idx < old_byte_len {
240               core::ptr::copy(
241                  ptr.add(byte_idx),
242                  ptr.add(byte_idx + insert_len),
243                  old_byte_len - byte_idx,
244               );
245            }
246
247            // Copy the new text into the newly created gap.
248            core::ptr::copy_nonoverlapping(
249               bytes_to_insert.as_ptr(),
250               ptr.add(byte_idx),
251               insert_len,
252            );
253         }
254
255         old_byte_len + insert_len
256      };
257
258      self.vec.len = new_byte_len;
259
260      chars_to_insert_count
261   }
262
263   /// Deletes the text in the given **character** range.
264   ///
265   /// `char_range` is a char range, not a byte range — unlike
266   /// [`drain`](Self::drain), which takes byte offsets.
267   ///
268   /// # Example
269   ///
270   /// ```
271   /// use secure_types::SecureString;
272   ///
273   /// let mut string = SecureString::from("GreekFetaCheese");
274   /// string.delete_text_char_range(9..15);
275   /// string.unlock_str(|str| {
276   ///     assert_eq!(str, "GreekFeta");
277   /// });
278   /// ```
279   pub fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
280      if char_range.start >= char_range.end {
281         return;
282      }
283
284      let new_len = self.vec.unlock_slice_mut(|current_bytes| {
285         // SAFETY: `SecureString` upholds the invariant that its bytes are valid
286         // UTF-8 — every constructor but the `unsafe` `from_utf8_unchecked`
287         // validates it. `current_bytes` is that buffer, unlocked by the guard.
288         let current_text = unsafe { core::str::from_utf8_unchecked(current_bytes) };
289         let byte_start = char_to_byte_idx(current_text.as_bytes(), char_range.start);
290         let byte_end = char_to_byte_idx(current_text.as_bytes(), char_range.end);
291
292         if byte_start >= byte_end || byte_end > current_bytes.len() {
293            return current_bytes.len();
294         }
295
296         let remove_len = byte_end - byte_start;
297         let old_total_len = current_bytes.len();
298
299         // Shift elements left
300         current_bytes.copy_within(byte_end..old_total_len, byte_start);
301
302         let new_len = old_total_len - remove_len;
303         // Zeroize the tail end that is now unused
304         for byte in current_bytes[new_len..old_total_len].iter_mut() {
305            byte.zeroize();
306         }
307         new_len
308      });
309      self.vec.len = new_len;
310   }
311}
312
313#[cfg(feature = "use_os")]
314impl From<String> for SecureString {
315   /// Creates a new `SecureString` from a `String`.
316   ///
317   /// The `String` is zeroized afterwards.
318   ///
319   /// # Panics
320   /// Panics if the secure allocation cannot be made or locked — `From` cannot
321   /// return an error. Use [`SecureVec::from_vec`] with
322   /// [`SecureString::try_from`] for a fallible path.
323   fn from(s: String) -> SecureString {
324      let vec = SecureVec::from_vec(s.into_bytes()).unwrap();
325      SecureString { vec }
326   }
327}
328
329impl From<&str> for SecureString {
330   /// Creates a new `SecureString` from a `&str`.
331   ///
332   /// The `&str` is not zeroized, you are responsible for zeroizing it.
333   ///
334   /// # Panics
335   /// Panics if the secure allocation cannot be made or locked — `From` cannot
336   /// return an error.
337   fn from(s: &str) -> SecureString {
338      let bytes = s.as_bytes();
339      // new_with_capacity bumps 0 -> 1 internally, so empty &str is fine.
340      let mut new_vec = SecureVec::new_with_capacity(bytes.len()).unwrap();
341      new_vec.init_from_clone(bytes);
342      SecureString { vec: new_vec }
343   }
344}
345
346impl TryFrom<SecureVec<u8>> for SecureString {
347   type Error = Error;
348
349   /// Creates a `SecureString` from a `SecureVec<u8>`, validating UTF-8.
350   ///
351   /// The `SecureVec` is consumed. On invalid UTF-8 it is dropped (and thus
352   /// zeroized) and `Error::InvalidUtf8` is returned.
353   fn try_from(vec: SecureVec<u8>) -> Result<Self, Self::Error> {
354      let valid = vec.unlock_slice(|slice| core::str::from_utf8(slice).is_ok());
355      if valid {
356         Ok(SecureString { vec })
357      } else {
358         Err(Error::InvalidUtf8)
359      }
360   }
361}
362
363#[cfg(feature = "serde")]
364impl serde::Serialize for SecureString {
365   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
366   where
367      S: serde::Serializer,
368   {
369      self.unlock_str(|str| serializer.serialize_str(str))
370   }
371}
372
373#[cfg(feature = "serde")]
374impl<'de> serde::Deserialize<'de> for SecureString {
375   fn deserialize<D>(deserializer: D) -> Result<SecureString, D::Error>
376   where
377      D: serde::Deserializer<'de>,
378   {
379      struct SecureStringVisitor;
380      impl<'de> serde::de::Visitor<'de> for SecureStringVisitor {
381         type Value = SecureString;
382         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
383            write!(formatter, "an utf-8 encoded string")
384         }
385         fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
386         where
387            E: serde::de::Error,
388         {
389            Ok(SecureString::from(v))
390         }
391
392         /// Formats that build an owned `String` (unescaping, normalization) hand it
393         /// over here. serde's default implementation would copy out of it and then
394         /// drop it with the plaintext still inside, so wipe it after the copy.
395         fn visit_string<E>(self, mut v: String) -> Result<Self::Value, E>
396         where
397            E: serde::de::Error,
398         {
399            let secure_string = SecureString::from(v.as_str());
400            v.zeroize();
401            Ok(secure_string)
402         }
403      }
404      deserializer.deserialize_string(SecureStringVisitor)
405   }
406}
407
408/// Maps a character index to a byte index.
409///
410/// An index at or past the end of the string resolves to the string's *length*, so callers
411/// clamp rather than fail: `insert_text_at_char_idx` appends and `delete_text_char_range`
412/// becomes a no-op. That has been the behaviour since the first release, and it matches what
413/// the callers expect (Zeus's text field behaves the same way over egui's `TextEdit`), so it
414/// is kept deliberately. Revisit if an out-of-range index should be rejected instead.
415fn char_to_byte_idx(s_bytes: &[u8], char_idx: usize) -> usize {
416   core::str::from_utf8(s_bytes)
417      .ok()
418      .and_then(|s| s.char_indices().nth(char_idx).map(|(idx, _)| idx))
419      .unwrap_or(s_bytes.len()) // Fallback to end if char_idx is out of bounds
420}