Skip to main content

secure_types/
array.rs

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