Skip to main content

sanitization_arrayvec/
lib.rs

1#![no_std]
2#![deny(unsafe_code)]
3#![deny(unsafe_op_in_unsafe_fn)]
4
5//! `arrayvec` integration wrappers for `sanitization`.
6//!
7//! This crate deliberately uses wrapper types instead of trait impls for
8//! external types. Rust's orphan rules prevent implementing
9//! `sanitization::SecureSanitize` directly for `arrayvec::ArrayVec` here.
10
11use arrayvec::{ArrayVec, CapacityError};
12use core::fmt;
13use sanitization::SecureSanitize;
14
15#[cfg(test)]
16extern crate std;
17
18mod backing_wipe {
19    use arrayvec::ArrayVec;
20
21    pub(super) fn wipe_spare<T, const CAP: usize>(inner: &mut ArrayVec<T, CAP>) {
22        let spare = inner.spare_capacity_mut();
23        sanitization::wipe::maybe_uninit(spare);
24    }
25
26    #[cfg(test)]
27    #[allow(unsafe_code)]
28    pub(super) unsafe fn spare_is_zero_after_wipe<T, const CAP: usize>(
29        inner: &mut ArrayVec<T, CAP>,
30    ) -> bool {
31        let spare = inner.spare_capacity_mut();
32        let byte_len = core::mem::size_of_val(spare);
33        if byte_len == 0 {
34            return true;
35        }
36
37        // SAFETY: The caller guarantees every spare byte was initialized by
38        // `wipe_spare` after the most recent operation that changed the live
39        // range. Reading those initialized bytes as `u8` is valid.
40        let bytes = unsafe { core::slice::from_raw_parts(spare.as_ptr().cast::<u8>(), byte_len) };
41        bytes.iter().all(|byte| *byte == 0)
42    }
43}
44
45/// Error returned when [`SecretArrayVec::push_or_sanitize`] rejects and clears
46/// an element because the vector is full.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct SanitizedCapacityError;
49
50impl fmt::Display for SanitizedCapacityError {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        formatter.write_str("secret array vector is full; rejected element was sanitized")
53    }
54}
55
56impl core::error::Error for SanitizedCapacityError {}
57
58/// Clear-on-drop wrapper around [`ArrayVec`].
59///
60/// Live elements are sanitized and dropped before the complete resulting
61/// `MaybeUninit<T>` spare region is volatile-cleared. This covers inline bytes
62/// left by earlier push, pop, truncate, clear, reuse, or wrapping operations
63/// without raw-zeroing a live `T`.
64pub struct SecretArrayVec<T: SecureSanitize, const CAP: usize> {
65    inner: ArrayVec<T, CAP>,
66}
67
68impl<T: SecureSanitize, const CAP: usize> SecretArrayVec<T, CAP> {
69    /// Create an empty secret array vector.
70    #[must_use]
71    #[inline]
72    pub const fn new() -> Self {
73        Self {
74            inner: ArrayVec::new_const(),
75        }
76    }
77
78    /// Wrap an existing [`ArrayVec`].
79    ///
80    /// Historical bytes in the incoming vector's current spare capacity are
81    /// cleared immediately. Live elements remain unchanged.
82    #[must_use]
83    #[inline]
84    pub fn from_arrayvec(inner: ArrayVec<T, CAP>) -> Self {
85        let mut secret = Self { inner };
86        secret.wipe_spare();
87        secret
88    }
89
90    /// Number of initialized elements.
91    #[must_use]
92    #[inline]
93    pub fn len(&self) -> usize {
94        self.inner.len()
95    }
96
97    /// Maximum number of elements.
98    #[must_use]
99    #[inline]
100    pub const fn capacity(&self) -> usize {
101        CAP
102    }
103
104    /// Returns true when there are no initialized elements.
105    #[must_use]
106    #[inline]
107    pub fn is_empty(&self) -> bool {
108        self.inner.is_empty()
109    }
110
111    /// Push one sanitizable element.
112    ///
113    /// If the vector is full, [`CapacityError`] returns the original element
114    /// unchanged, matching `arrayvec` semantics. Callers remain responsible for
115    /// sanitizing or securely reusing that rejected value. Use
116    /// [`SecretArrayVec::push_or_sanitize`] when rejection must consume and
117    /// clear the element instead.
118    #[inline]
119    pub fn push(&mut self, value: T) -> Result<(), CapacityError<T>> {
120        self.inner.try_push(value)
121    }
122
123    /// Push an element, consuming and sanitizing it if the vector is full.
124    ///
125    /// The error intentionally carries no `T`, preventing callers from
126    /// mistaking a sanitized value for the original secret.
127    #[inline]
128    pub fn push_or_sanitize(&mut self, value: T) -> Result<(), SanitizedCapacityError> {
129        match self.inner.try_push(value) {
130            Ok(()) => Ok(()),
131            Err(error) => {
132                let mut rejected = error.element();
133                rejected.secure_sanitize();
134                Err(SanitizedCapacityError)
135            }
136        }
137    }
138
139    /// Remove and return the last element.
140    ///
141    /// The returned value remains secret-bearing and is the caller's
142    /// responsibility. The stale inline slot left by the move is
143    /// volatile-cleared before this method returns.
144    #[inline]
145    pub fn pop(&mut self) -> Option<T> {
146        let value = self.inner.pop();
147        self.wipe_spare();
148        value
149    }
150
151    /// Shorten the vector to `new_len`.
152    ///
153    /// Removed elements are sanitized before their destructors run. The
154    /// complete spare region, including historical bytes from earlier
155    /// operations, is then volatile-cleared.
156    #[inline]
157    pub fn truncate(&mut self, new_len: usize) {
158        let len = self.inner.len();
159        if new_len < len {
160            self.inner.as_mut_slice()[new_len..].secure_sanitize();
161            self.inner.truncate(new_len);
162        }
163        self.wipe_spare();
164    }
165
166    /// Borrow initialized elements.
167    #[must_use]
168    #[inline]
169    pub fn as_slice(&self) -> &[T] {
170        self.inner.as_slice()
171    }
172
173    /// Mutably borrow initialized elements.
174    #[must_use]
175    #[inline]
176    pub fn as_mut_slice(&mut self) -> &mut [T] {
177        self.inner.as_mut_slice()
178    }
179
180    /// Run a closure with read-only access to initialized elements.
181    #[inline]
182    pub fn with_secret<R>(&self, inspect: impl FnOnce(&[T]) -> R) -> R {
183        inspect(self.as_slice())
184    }
185
186    /// Run a closure with mutable access to initialized elements.
187    #[inline]
188    pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [T]) -> R) -> R {
189        edit(self.as_mut_slice())
190    }
191
192    /// Sanitize and drop all live elements, then clear all inline backing bytes.
193    #[inline]
194    pub fn clear_secret(&mut self) {
195        self.inner.as_mut_slice().secure_sanitize();
196        self.inner.clear();
197        self.wipe_spare();
198    }
199
200    /// Consume after first sanitizing all live elements.
201    #[inline]
202    pub fn into_cleared(mut self) {
203        self.clear_secret();
204    }
205
206    #[inline]
207    fn wipe_spare(&mut self) {
208        backing_wipe::wipe_spare(&mut self.inner);
209    }
210}
211
212impl<T: SecureSanitize, const CAP: usize> Default for SecretArrayVec<T, CAP> {
213    #[inline]
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl<T: SecureSanitize, const CAP: usize> SecureSanitize for SecretArrayVec<T, CAP> {
220    #[inline]
221    fn secure_sanitize(&mut self) {
222        self.clear_secret();
223    }
224}
225
226impl<T: SecureSanitize, const CAP: usize> Drop for SecretArrayVec<T, CAP> {
227    #[inline]
228    fn drop(&mut self) {
229        self.clear_secret();
230    }
231}
232
233impl<T: SecureSanitize, const CAP: usize> fmt::Debug for SecretArrayVec<T, CAP> {
234    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235        formatter
236            .debug_struct("SecretArrayVec")
237            .field("len", &self.len())
238            .field("capacity", &CAP)
239            .field("contents", &"<redacted>")
240            .finish()
241    }
242}
243
244#[cfg(test)]
245#[allow(unsafe_code)]
246mod tests {
247    use super::*;
248    use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
249    use sanitization::SecretBytes;
250    use std::sync::Arc;
251
252    fn assert_backing_is_zero<T: SecureSanitize, const CAP: usize>(
253        secrets: &mut SecretArrayVec<T, CAP>,
254    ) {
255        // SAFETY: Tests call this only immediately after a wrapper operation
256        // that invokes `wipe_spare`, which initializes every byte in the
257        // current spare region to zero.
258        assert!(unsafe { backing_wipe::spare_is_zero_after_wipe(&mut secrets.inner) });
259    }
260
261    #[test]
262    fn empty_arrayvec_wipes_never_initialized_spare() {
263        let mut secrets = SecretArrayVec::<u8, 1>::new();
264
265        secrets.clear_secret();
266
267        assert_backing_is_zero(&mut secrets);
268    }
269
270    #[test]
271    fn arrayvec_wrapper_clears_live_elements() {
272        let mut secrets = SecretArrayVec::<SecretBytes<4>, 2>::new();
273
274        secrets.push(SecretBytes::from_array([1, 2, 3, 4])).unwrap();
275        secrets.push(SecretBytes::from_array([5, 6, 7, 8])).unwrap();
276
277        assert_eq!(secrets.len(), 2);
278        assert!(secrets.with_secret(|items| items[0].constant_time_eq(&[1, 2, 3, 4])));
279
280        secrets.clear_secret();
281
282        assert!(secrets.is_empty());
283        assert_backing_is_zero(&mut secrets);
284    }
285
286    #[test]
287    fn arrayvec_wrapper_debug_is_redacted() {
288        let mut secrets = SecretArrayVec::<SecretBytes<4>, 2>::new();
289        secrets.push(SecretBytes::from_array([1, 2, 3, 4])).unwrap();
290
291        let rendered = std::format!("{secrets:?}");
292
293        assert!(rendered.contains("redacted"));
294        assert!(!rendered.contains("1, 2, 3, 4"));
295    }
296
297    #[test]
298    fn arrayvec_wrapper_push_returns_original_rejected_element() {
299        let mut secrets = SecretArrayVec::<[u8; 4], 0>::new();
300
301        let error = secrets.push([1, 2, 3, 4]).unwrap_err();
302
303        assert_eq!(error.element(), [1, 2, 3, 4]);
304    }
305
306    #[test]
307    fn arrayvec_wrapper_can_sanitize_rejected_elements() {
308        struct Probe(Arc<AtomicBool>);
309
310        impl SecureSanitize for Probe {
311            fn secure_sanitize(&mut self) {
312                self.0.store(true, Ordering::Release);
313            }
314        }
315
316        let sanitized = Arc::new(AtomicBool::new(false));
317        let mut secrets = SecretArrayVec::<Probe, 0>::new();
318
319        assert_eq!(
320            secrets.push_or_sanitize(Probe(Arc::clone(&sanitized))),
321            Err(SanitizedCapacityError)
322        );
323        assert!(sanitized.load(Ordering::Acquire));
324    }
325
326    #[test]
327    fn arrayvec_wrapper_pop_clears_the_historical_slot() {
328        let mut secrets = SecretArrayVec::<[u8; 4], 2>::new();
329        secrets.push([0xA5; 4]).unwrap();
330
331        let mut removed = secrets.pop().unwrap();
332
333        assert_eq!(removed, [0xA5; 4]);
334        assert!(secrets.is_empty());
335        assert_backing_is_zero(&mut secrets);
336        removed.secure_sanitize();
337    }
338
339    #[test]
340    fn arrayvec_wrapper_truncate_sanitizes_before_drop_and_wipes_spare() {
341        struct Probe {
342            bytes: [u8; 4],
343            sanitized: Arc<AtomicBool>,
344            dropped_zeroed: Arc<AtomicBool>,
345        }
346
347        impl SecureSanitize for Probe {
348            fn secure_sanitize(&mut self) {
349                self.bytes.secure_sanitize();
350                self.sanitized.store(true, Ordering::Release);
351            }
352        }
353
354        impl Drop for Probe {
355            fn drop(&mut self) {
356                self.dropped_zeroed
357                    .store(self.bytes == [0; 4], Ordering::Release);
358            }
359        }
360
361        let sanitized = Arc::new(AtomicBool::new(false));
362        let dropped_zeroed = Arc::new(AtomicBool::new(false));
363        let mut secrets = SecretArrayVec::<Probe, 2>::new();
364        secrets
365            .push(Probe {
366                bytes: [9; 4],
367                sanitized: Arc::clone(&sanitized),
368                dropped_zeroed: Arc::clone(&dropped_zeroed),
369            })
370            .unwrap();
371
372        secrets.truncate(0);
373
374        assert!(sanitized.load(Ordering::Acquire));
375        assert!(dropped_zeroed.load(Ordering::Acquire));
376        assert_backing_is_zero(&mut secrets);
377    }
378
379    #[test]
380    fn arrayvec_wrapper_clears_historical_spare_when_wrapping() {
381        let mut inner = ArrayVec::<[u8; 4], 2>::new();
382        inner.push([0xC3; 4]);
383        let mut removed = inner.pop().unwrap();
384
385        let mut secrets = SecretArrayVec::from_arrayvec(inner);
386
387        assert_backing_is_zero(&mut secrets);
388        removed.secure_sanitize();
389    }
390
391    #[test]
392    fn arrayvec_wrapper_clears_complete_backing_after_reuse() {
393        let mut secrets = SecretArrayVec::<[u8; 4], 3>::new();
394        secrets.push([1; 4]).unwrap();
395        secrets.push([2; 4]).unwrap();
396        let mut removed = secrets.pop().unwrap();
397        secrets.push([3; 4]).unwrap();
398
399        secrets.clear_secret();
400
401        assert!(secrets.is_empty());
402        assert_backing_is_zero(&mut secrets);
403        removed.secure_sanitize();
404    }
405
406    #[test]
407    fn arrayvec_wrapper_handles_zero_sized_drop_types() {
408        struct ZeroSized;
409
410        static SANITIZED: AtomicUsize = AtomicUsize::new(0);
411        static DROPPED: AtomicUsize = AtomicUsize::new(0);
412
413        impl SecureSanitize for ZeroSized {
414            fn secure_sanitize(&mut self) {
415                SANITIZED.fetch_add(1, Ordering::SeqCst);
416            }
417        }
418
419        impl Drop for ZeroSized {
420            fn drop(&mut self) {
421                DROPPED.fetch_add(1, Ordering::SeqCst);
422            }
423        }
424
425        SANITIZED.store(0, Ordering::SeqCst);
426        DROPPED.store(0, Ordering::SeqCst);
427        let mut secrets = SecretArrayVec::<ZeroSized, 4>::new();
428        secrets.push(ZeroSized).unwrap();
429        secrets.push(ZeroSized).unwrap();
430
431        secrets.clear_secret();
432
433        assert_eq!(SANITIZED.load(Ordering::SeqCst), 2);
434        assert_eq!(DROPPED.load(Ordering::SeqCst), 2);
435        assert_backing_is_zero(&mut secrets);
436    }
437}