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