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}