Skip to main content

secure_types/
array.rs

1#[cfg(not(feature = "use_os"))]
2use alloc::{Layout, alloc, dealloc};
3
4use super::{Error, SecureVec};
5use core::{marker::PhantomData, mem, ptr::NonNull};
6use zeroize::Zeroize;
7
8#[cfg(feature = "use_os")]
9use super::{alloc, free};
10#[cfg(feature = "use_os")]
11use memsec::Prot;
12
13/// Unlocks the array's memory on construction and re-locks it on drop —
14/// including when the drop happens because the fn closure panicked.
15struct UnlockGuard<'a, T: Zeroize, const LENGTH: usize> {
16   array: &'a SecureArray<T, LENGTH>,
17}
18
19impl<'a, T: Zeroize, const LENGTH: usize> UnlockGuard<'a, T, LENGTH> {
20   fn new(array: &'a SecureArray<T, LENGTH>) -> Self {
21      let ok = array.unlock_memory();
22      debug_assert!(ok, "UnlockGuard::new: unlock_memory failed");
23      UnlockGuard { array }
24   }
25}
26
27impl<'a, T: Zeroize, const LENGTH: usize> Drop for UnlockGuard<'a, T, LENGTH> {
28   fn drop(&mut self) {
29      let ok = self.array.lock_memory();
30      debug_assert!(ok, "UnlockGuard::drop: lock_memory failed");
31   }
32}
33
34/// A fixed-size array allocated in a secure memory region.
35///
36/// ## Security Model
37///
38/// When compiled with the `use_os` feature (the default), it provides several layers of protection:
39/// - **Zeroization on Drop**: The memory is zeroized when the array is dropped.
40/// - **Memory Locking**: The underlying memory pages are locked using `mlock` & `madvise` for (Unix) or
41///   `VirtualLock` & `VirtualProtect` for (Windows) to prevent the OS from memory-dump/swap to disk or other processes accessing the memory.
42///
43/// In a `no_std` environment, it falls back to providing only the **zeroization-on-drop** guarantee.
44///
45/// # Program Termination
46///
47/// Direct indexing (e.g., `array[0]`) on a locked array will cause the operating system
48/// to terminate the process with an access violation error. Always use the provided
49/// scope methods (`unlock`, `unlock_mut`) for safe access.
50///
51/// # Notes
52///
53/// If you return a new allocated `[T; LENGTH]` from one of the unlock methods you are responsible for zeroizing the memory.
54///
55/// # Example
56///
57/// ```
58/// use secure_types::{SecureArray, Zeroize};
59///
60/// let exposed_key: &mut [u8; 32] = &mut [1u8; 32];
61/// let secure_key: SecureArray<u8, 32> = SecureArray::from_slice_mut(exposed_key).unwrap();
62///
63/// secure_key.unlock(|unlocked_slice| {
64///     assert_eq!(unlocked_slice.len(), 32);
65///     assert_eq!(unlocked_slice[0], 1);
66/// });
67///
68/// // Not recommended but if you allocate a new [u8; LENGTH] make sure to zeroize it
69/// let mut exposed = secure_key.unlock(|unlocked_slice| {
70///     [unlocked_slice[0], unlocked_slice[1], unlocked_slice[2]]
71/// });
72///
73/// // Do what you need to to do with the new array
74/// // When you are done with it, zeroize it
75/// exposed.zeroize();
76/// ```
77pub struct SecureArray<T, const LENGTH: usize>
78where
79   T: Zeroize,
80{
81   ptr: NonNull<T>,
82   _marker: PhantomData<T>,
83}
84
85unsafe impl<T: Zeroize + Send, const LENGTH: usize> Send for SecureArray<T, LENGTH> {}
86unsafe impl<T: Zeroize + Send + Sync, const LENGTH: usize> Sync for SecureArray<T, LENGTH> {}
87
88impl<T, const LENGTH: usize> SecureArray<T, LENGTH>
89where
90   T: Zeroize,
91{
92   /// Creates an empty (but allocated) SecureArray.
93   ///
94   /// The memory is allocated but not initialized, and it's the caller's responsibility to fill it.
95   pub fn empty() -> Result<Self, Error> {
96      let size = LENGTH * mem::size_of::<T>();
97      if size == 0 {
98         // Cannot create a zero-sized secure array
99         return Err(Error::LengthCannotBeZero);
100      }
101
102      let ptr = unsafe { alloc::<T>(size)? };
103
104      let secure_array = SecureArray {
105         ptr,
106         _marker: PhantomData,
107      };
108
109      let locked = secure_array.lock_memory();
110
111      #[cfg(feature = "use_os")]
112      if !locked {
113         return Err(Error::LockFailed);
114      }
115
116      Ok(secure_array)
117   }
118
119   /// Creates a new SecureArray from a `&mut [T; LENGTH]`.
120   ///
121   /// The passed slice is zeroized afterwards
122   pub fn from_slice_mut(content: &mut [T; LENGTH]) -> Result<Self, Error> {
123      let secure_array = match Self::empty() {
124         Ok(secure_array) => secure_array,
125         Err(e) => {
126            content.zeroize();
127            return Err(e);
128         }
129      };
130
131      let unlocked = secure_array.unlock_memory();
132
133      if !unlocked {
134         content.zeroize();
135         return Err(Error::UnlockFailed);
136      }
137
138      unsafe {
139         // Copy the data from the source array into the secure memory region
140         core::ptr::copy_nonoverlapping(
141            content.as_ptr(),
142            secure_array.ptr.as_ptr(),
143            LENGTH,
144         );
145      }
146
147      content.zeroize();
148
149      let locked = secure_array.lock_memory();
150
151      #[cfg(feature = "use_os")]
152      if !locked {
153         return Err(Error::LockFailed);
154      }
155
156      Ok(secure_array)
157   }
158
159   /// Creates a new SecureArray from a `&[T; LENGTH]`.
160   ///
161   /// The array is not zeroized, you are responsible for zeroizing it
162   pub fn from_slice(content: &[T; LENGTH]) -> Result<Self, Error> {
163      let secure_array = Self::empty()?;
164
165      let unlocked = secure_array.unlock_memory();
166
167      if !unlocked {
168         return Err(Error::UnlockFailed);
169      }
170
171      unsafe {
172         // Copy the data from the source array into the secure memory region
173         core::ptr::copy_nonoverlapping(
174            content.as_ptr(),
175            secure_array.ptr.as_ptr(),
176            LENGTH,
177         );
178      }
179
180      let locked = secure_array.lock_memory();
181
182      #[cfg(feature = "use_os")]
183      if !locked {
184         return Err(Error::LockFailed);
185      }
186
187      Ok(secure_array)
188   }
189
190   pub fn len(&self) -> usize {
191      LENGTH
192   }
193
194   pub fn is_empty(&self) -> bool {
195      self.len() == 0
196   }
197
198   pub(crate) fn lock_memory(&self) -> bool {
199      #[cfg(feature = "use_os")]
200      {
201         #[cfg(windows)]
202         {
203            super::mprotect(self.ptr, Prot::NoAccess)
204         }
205         #[cfg(unix)]
206         {
207            super::mprotect(self.ptr, Prot::NoAccess)
208         }
209      }
210      #[cfg(not(feature = "use_os"))]
211      {
212         true // No-op: always "succeeds"
213      }
214   }
215
216   pub(crate) fn unlock_memory(&self) -> bool {
217      #[cfg(feature = "use_os")]
218      {
219         #[cfg(windows)]
220         {
221            super::mprotect(self.ptr, Prot::ReadWrite)
222         }
223         #[cfg(unix)]
224         {
225            super::mprotect(self.ptr, Prot::ReadWrite)
226         }
227      }
228
229      #[cfg(not(feature = "use_os"))]
230      {
231         true // No-op: always "succeeds"
232      }
233   }
234
235   /// Immutable access to the array's data as a `&[T]`
236   pub fn unlock<F, R>(&self, f: F) -> R
237   where
238      F: FnOnce(&[T]) -> R,
239   {
240      let _guard = UnlockGuard::new(self);
241      let slice = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), LENGTH) };
242      let result = f(slice);
243      result
244   }
245
246   /// Mutable access to the array's data as a `&mut [T]`
247   pub fn unlock_mut<F, R>(&mut self, f: F) -> R
248   where
249      F: FnOnce(&mut [T]) -> R,
250   {
251      let _guard = UnlockGuard::new(self);
252      let slice = unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), LENGTH) };
253      let result = f(slice);
254      result
255   }
256
257   /// Securely erases the contents of the array by zeroizing the memory.
258   pub fn erase(&mut self) {
259      self.unlock_mut(|slice| {
260         for element in slice.iter_mut() {
261            element.zeroize();
262         }
263      });
264   }
265
266   /// Same as `SecureVec::init_from_clone`, for the fixed-size buffer.
267   /// `src.len()` must equal `LENGTH`.
268   pub(crate) fn init_from_clone(&mut self, src: &[T])
269   where
270      T: Clone,
271   {
272      debug_assert_eq!(src.len(), LENGTH);
273
274      let ok = self.unlock_memory();
275      debug_assert!(
276         ok,
277         "SecureArray::init_from_clone: unlock_memory failed"
278      );
279
280      unsafe {
281         let dst = self.ptr.as_ptr();
282         for (i, item) in src.iter().enumerate() {
283            core::ptr::write(dst.add(i), item.clone());
284         }
285      }
286      let ok = self.lock_memory();
287      debug_assert!(
288         ok,
289         "SecureArray::init_from_clone: lock_memory failed"
290      );
291   }
292}
293
294impl<T: Zeroize, const LENGTH: usize> core::ops::Index<usize> for SecureArray<T, LENGTH> {
295   type Output = T;
296   fn index(&self, index: usize) -> &Self::Output {
297      assert!(index < self.len(), "Index out of bounds");
298      unsafe {
299         let ptr = self.ptr.as_ptr().add(index);
300         &*ptr
301      }
302   }
303}
304
305impl<T: Zeroize, const LENGTH: usize> core::ops::IndexMut<usize> for SecureArray<T, LENGTH> {
306   fn index_mut(&mut self, index: usize) -> &mut Self::Output {
307      assert!(index < self.len(), "Index out of bounds");
308      unsafe {
309         let ptr = self.ptr.as_ptr().add(index);
310         &mut *ptr
311      }
312   }
313}
314
315impl<T: Zeroize, const LENGTH: usize> Drop for SecureArray<T, LENGTH> {
316   fn drop(&mut self) {
317      self.erase();
318      let ok = self.unlock_memory();
319      debug_assert!(ok, "SecureArray::drop: unlock_memory failed");
320
321      let size = LENGTH * mem::size_of::<T>();
322      if size == 0 {
323         return;
324      }
325
326      #[cfg(feature = "use_os")]
327      free(self.ptr);
328
329      #[cfg(not(feature = "use_os"))]
330      {
331         let layout = Layout::from_size_align_unchecked(size, mem::align_of::<T>());
332         dealloc(self.ptr.as_ptr() as *mut u8, layout);
333      }
334   }
335}
336
337impl<T: Clone + Zeroize, const LENGTH: usize> Clone for SecureArray<T, LENGTH> {
338   fn clone(&self) -> Self {
339      let mut new_array = Self::empty().unwrap();
340      self.unlock(|src_slice| {
341         new_array.init_from_clone(src_slice);
342      });
343      new_array
344   }
345}
346
347impl<const LENGTH: usize> TryFrom<SecureVec<u8>> for SecureArray<u8, LENGTH> {
348   type Error = Error;
349
350   /// Tries to convert a `SecureVec<u8>` into a `SecureArray<u8, LENGTH>`.
351   ///
352   /// This operation will only succeed if `vec.len() == LENGTH`.
353   ///
354   /// The `SecureVec` is consumed.
355   fn try_from(vec: SecureVec<u8>) -> Result<Self, Self::Error> {
356      if vec.len() != LENGTH {
357         return Err(Error::LengthMismatch);
358      }
359
360      let mut new_array = Self::empty()?;
361
362      vec.unlock_slice(|vec_slice| {
363         new_array.init_from_clone(vec_slice);
364      });
365
366      Ok(new_array)
367   }
368}
369
370#[cfg(feature = "serde")]
371impl<const LENGTH: usize> serde::Serialize for SecureArray<u8, LENGTH> {
372   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
373   where
374      S: serde::Serializer,
375   {
376      self.unlock(|slice| serializer.collect_seq(slice.iter()))
377   }
378}
379
380#[cfg(feature = "serde")]
381impl<'de, const LENGTH: usize> serde::Deserialize<'de> for SecureArray<u8, LENGTH> {
382   fn deserialize<D>(deserializer: D) -> Result<SecureArray<u8, LENGTH>, D::Error>
383   where
384      D: serde::Deserializer<'de>,
385   {
386      struct SecureArrayVisitor<const L: usize>;
387
388      impl<'de, const L: usize> serde::de::Visitor<'de> for SecureArrayVisitor<L> {
389         type Value = SecureArray<u8, L>;
390
391         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
392            write!(formatter, "a byte array of length {}", L)
393         }
394
395         fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
396         where
397            A: serde::de::SeqAccess<'de>,
398         {
399            let mut data: SecureVec<u8> =
400               SecureVec::new_with_capacity(L).map_err(serde::de::Error::custom)?;
401            while let Some(byte) = seq.next_element()? {
402               data.push(byte);
403            }
404
405            // Check that the deserialized data has the exact length required.
406            if data.len() != L {
407               return Err(serde::de::Error::invalid_length(
408                  data.len(),
409                  &self,
410               ));
411            }
412
413            SecureArray::try_from(data).map_err(serde::de::Error::custom)
414         }
415      }
416
417      deserializer.deserialize_bytes(SecureArrayVisitor::<LENGTH>)
418   }
419}
420
421#[cfg(all(test, feature = "use_os"))]
422mod tests {
423   use super::*;
424   use std::process::{Command, Stdio};
425   use std::sync::{Arc, Mutex};
426
427   #[test]
428   fn test_creation() {
429      let exposed_mut = &mut [1, 2, 3];
430      let array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed_mut).unwrap();
431      assert_eq!(array.len(), 3);
432
433      array.unlock(|slice| {
434         assert_eq!(slice, &[1, 2, 3]);
435      });
436
437      assert_eq!(exposed_mut, &[0u8; 3]);
438
439      let exposed = &[1, 2, 3];
440      let array: SecureArray<u8, 3> = SecureArray::from_slice(exposed).unwrap();
441      assert_eq!(array.len(), 3);
442
443      array.unlock(|slice| {
444         assert_eq!(slice, &[1, 2, 3]);
445      });
446
447      assert_eq!(exposed, &[1, 2, 3]);
448   }
449
450   #[test]
451   fn test_from_secure_vec() {
452      let vec: SecureVec<u8> = SecureVec::from_slice(&[1, 2, 3]).unwrap();
453      let array: SecureArray<u8, 3> = vec.try_into().unwrap();
454      assert_eq!(array.len(), 3);
455      array.unlock(|slice| {
456         assert_eq!(slice, &[1, 2, 3]);
457      });
458   }
459
460   #[test]
461   fn test_erase() {
462      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
463      let mut array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
464      array.erase();
465      array.unlock(|slice| {
466         assert_eq!(slice, &[0u8; 3]);
467      });
468   }
469
470   #[test]
471   #[should_panic]
472   fn test_length_cannot_be_zero() {
473      let secure_vec = SecureVec::new().unwrap();
474      let _secure_array: SecureArray<u8, 0> = SecureArray::try_from(secure_vec).unwrap();
475   }
476
477   #[test]
478   fn lock_unlock() {
479      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
480      let secure: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
481
482      let unlocked = secure.unlock_memory();
483      assert!(unlocked);
484
485      let locked = secure.lock_memory();
486      assert!(locked);
487   }
488
489   #[test]
490   fn test_clone() {
491      let mut array1: SecureArray<u8, 3> = SecureArray::empty().unwrap();
492      array1.unlock_mut(|slice| {
493         slice[0] = 1;
494         slice[1] = 2;
495         slice[2] = 3;
496      });
497
498      let array2 = array1.clone();
499
500      array2.unlock(|slice| {
501         assert_eq!(slice, &[1, 2, 3]);
502      });
503
504      array1.unlock(|slice| {
505         assert_eq!(slice, &[1, 2, 3]);
506      });
507   }
508
509   #[test]
510   fn test_thread_safety() {
511      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
512      let array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
513      let arc_array = Arc::new(Mutex::new(array));
514      let mut handles = Vec::new();
515
516      for _ in 0..5u8 {
517         let array_clone = Arc::clone(&arc_array);
518         let handle = std::thread::spawn(move || {
519            let mut guard = array_clone.lock().unwrap();
520            guard.unlock_mut(|slice| {
521               slice[0] += 1;
522            });
523         });
524         handles.push(handle);
525      }
526
527      for handle in handles {
528         handle.join().unwrap();
529      }
530
531      let final_array = arc_array.lock().unwrap();
532      final_array.unlock(|slice| {
533         assert_eq!(slice[0], 6);
534         assert_eq!(slice[1], 2);
535         assert_eq!(slice[2], 3);
536      });
537   }
538
539   #[test]
540   fn test_index_should_fail_when_locked() {
541      let arg = "CRASH_TEST_ARRAY_LOCKED";
542
543      if std::env::args().any(|a| a == arg) {
544         let exposed: &mut [u8; 3] = &mut [1, 2, 3];
545         let array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
546         let _value = core::hint::black_box(array[0]);
547
548         std::process::exit(1);
549      }
550
551      let child = Command::new(std::env::current_exe().unwrap())
552         .arg("array::tests::test_index_should_fail_when_locked")
553         .arg(arg)
554         .arg("--nocapture")
555         .stdout(Stdio::piped())
556         .stderr(Stdio::piped())
557         .spawn()
558         .expect("Failed to spawn child process");
559
560      let output = child.wait_with_output().expect("Failed to wait on child");
561      let status = output.status;
562
563      assert!(
564         !status.success(),
565         "Process exited successfully with code {:?}, but it should have crashed.",
566         status.code()
567      );
568
569      #[cfg(unix)]
570      {
571         use std::os::unix::process::ExitStatusExt;
572         let signal = status
573            .signal()
574            .expect("Process was not terminated by a signal on Unix.");
575         assert!(
576            signal == libc::SIGSEGV || signal == libc::SIGBUS,
577            "Process terminated with unexpected signal: {}",
578            signal
579         );
580         println!(
581            "Test passed: Process correctly terminated with signal {}.",
582            signal
583         );
584      }
585
586      #[cfg(windows)]
587      {
588         const STATUS_ACCESS_VIOLATION: i32 = 0xC0000005_u32 as i32;
589         assert_eq!(
590            status.code(),
591            Some(STATUS_ACCESS_VIOLATION),
592            "Process exited with unexpected code: {:x?}. Expected STATUS_ACCESS_VIOLATION.",
593            status.code()
594         );
595         eprintln!("Test passed: Process correctly terminated with STATUS_ACCESS_VIOLATION.");
596      }
597   }
598
599   #[test]
600   fn test_unlock_mut() {
601      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
602      let mut array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
603
604      array.unlock_mut(|slice| {
605         slice[1] = 100;
606      });
607
608      array.unlock(|slice| {
609         assert_eq!(slice, &[1, 100, 3]);
610      });
611   }
612
613   #[cfg(feature = "serde")]
614   #[test]
615   fn test_serde() {
616      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
617      let array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
618      let json_string = serde_json::to_string(&array).expect("Serialization failed");
619      let json_bytes = serde_json::to_vec(&array).expect("Serialization failed");
620
621      let deserialized_string: SecureArray<u8, 3> =
622         serde_json::from_str(&json_string).expect("Deserialization failed");
623
624      let deserialized_bytes: SecureArray<u8, 3> =
625         serde_json::from_slice(&json_bytes).expect("Deserialization failed");
626
627      deserialized_string.unlock(|slice| {
628         assert_eq!(slice, &[1, 2, 3]);
629      });
630
631      deserialized_bytes.unlock(|slice| {
632         assert_eq!(slice, &[1, 2, 3]);
633      });
634   }
635}