Skip to main content

dcrypt_internal/
zeroing.rs

1//! Safe-Rust memory clearing utilities.
2
3#[cfg(any(feature = "alloc", feature = "std"))]
4use alloc::{boxed::Box, string::String, vec, vec::Vec};
5use core::fmt;
6use core::hint::black_box;
7use core::ops::{Deref, DerefMut};
8use core::sync::atomic::{compiler_fence, Ordering};
9
10/// Overwrite the initialized representation of a value with its zero value.
11///
12/// This deliberately makes no claim about inaccessible allocator capacity. Secret
13/// containers in dcrypt use exact-size boxed slices so every initialized byte can
14/// be cleared using safe Rust.
15pub trait Zeroize {
16    fn zeroize(&mut self);
17}
18
19/// Marker for values whose `Drop` implementation invokes [`Zeroize`].
20pub trait ZeroizeOnDrop {}
21
22macro_rules! impl_zeroize_integer {
23    ($($ty:ty),+ $(,)?) => {$ (
24        impl Zeroize for $ty {
25            #[inline(never)]
26            fn zeroize(&mut self) {
27                *self = 0;
28                compiler_fence(Ordering::SeqCst);
29                black_box(self);
30            }
31        }
32    )+ };
33}
34
35impl_zeroize_integer!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
36
37impl Zeroize for bool {
38    #[inline(never)]
39    fn zeroize(&mut self) {
40        *self = false;
41        compiler_fence(Ordering::SeqCst);
42        black_box(self);
43    }
44}
45
46impl<T: Zeroize> Zeroize for [T] {
47    #[inline(never)]
48    fn zeroize(&mut self) {
49        for item in self.iter_mut() {
50            item.zeroize();
51        }
52        compiler_fence(Ordering::SeqCst);
53        black_box(self);
54    }
55}
56
57impl<T: Zeroize, const N: usize> Zeroize for [T; N] {
58    #[inline(never)]
59    fn zeroize(&mut self) {
60        self.as_mut_slice().zeroize();
61    }
62}
63
64impl<T: Zeroize> Zeroize for Option<T> {
65    fn zeroize(&mut self) {
66        if let Some(value) = self.as_mut() {
67            value.zeroize();
68        }
69        *self = None;
70    }
71}
72
73#[cfg(any(feature = "alloc", feature = "std"))]
74impl<T: Zeroize> Zeroize for Vec<T> {
75    fn zeroize(&mut self) {
76        self.as_mut_slice().zeroize();
77        self.clear();
78    }
79}
80
81#[cfg(any(feature = "alloc", feature = "std"))]
82impl<T: Zeroize> Zeroize for Box<[T]> {
83    fn zeroize(&mut self) {
84        self.as_mut().zeroize();
85    }
86}
87
88#[cfg(any(feature = "alloc", feature = "std"))]
89impl Zeroize for String {
90    fn zeroize(&mut self) {
91        let mut bytes = core::mem::take(self).into_bytes();
92        bytes.zeroize();
93    }
94}
95
96/// A wrapper that clears its initialized value when dropped.
97#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
98pub struct Zeroizing<T: Zeroize>(T);
99
100impl<T: Zeroize> fmt::Debug for Zeroizing<T> {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        formatter.write_str("Zeroizing([REDACTED])")
103    }
104}
105
106impl<T: Zeroize> Zeroizing<T> {
107    pub const fn new(value: T) -> Self {
108        Self(value)
109    }
110
111    /// Move the protected value out, leaving a zero/default value for `Drop`.
112    ///
113    /// This is primarily useful at an ownership boundary where an exact-size
114    /// boxed secret must become the caller's output without creating a second
115    /// secret allocation.
116    pub fn into_inner(mut self) -> T
117    where
118        T: Default,
119    {
120        core::mem::take(&mut self.0)
121    }
122}
123
124impl<const N: usize> Zeroizing<[u8; N]> {
125    /// Move a protected byte array out while leaving an all-zero replacement.
126    ///
127    /// This is the fixed-array counterpart to [`Self::into_inner`] for array
128    /// lengths whose standard-library `Default` implementation is unavailable
129    /// on the supported compiler baseline.
130    pub fn into_inner_array(mut self) -> [u8; N] {
131        core::mem::replace(&mut self.0, [0u8; N])
132    }
133}
134
135impl<T: Zeroize> Deref for Zeroizing<T> {
136    type Target = T;
137
138    fn deref(&self) -> &Self::Target {
139        &self.0
140    }
141}
142
143impl<T: Zeroize> DerefMut for Zeroizing<T> {
144    fn deref_mut(&mut self) -> &mut Self::Target {
145        &mut self.0
146    }
147}
148
149impl<T: Zeroize> AsRef<T> for Zeroizing<T> {
150    fn as_ref(&self) -> &T {
151        &self.0
152    }
153}
154
155impl<T: Zeroize> AsMut<T> for Zeroizing<T> {
156    fn as_mut(&mut self) -> &mut T {
157        &mut self.0
158    }
159}
160
161impl<T: Zeroize> Drop for Zeroizing<T> {
162    fn drop(&mut self) {
163        self.0.zeroize();
164    }
165}
166
167impl<T: Zeroize> Zeroize for Zeroizing<T> {
168    fn zeroize(&mut self) {
169        self.0.zeroize();
170    }
171}
172
173impl<T: Zeroize> ZeroizeOnDrop for Zeroizing<T> {}
174
175/// An exact-size byte allocation that clears every initialized byte on drop.
176///
177/// Unlike `Zeroizing<Vec<u8>>`, this type has no inaccessible spare capacity.
178/// New secret-returning APIs should prefer this representation.
179#[cfg(any(feature = "alloc", feature = "std"))]
180pub type ZeroizingBytes = Zeroizing<Box<[u8]>>;
181
182#[cfg(any(feature = "alloc", feature = "std"))]
183impl Zeroizing<Box<[u8]>> {
184    /// Borrow the exact-size protected allocation as bytes.
185    pub fn as_slice(&self) -> &[u8] {
186        &self.0
187    }
188
189    /// Mutably borrow the exact-size protected allocation as bytes.
190    pub fn as_mut_slice(&mut self) -> &mut [u8] {
191        &mut self.0
192    }
193
194    /// Exact-size allocations have no inaccessible spare capacity.
195    pub fn capacity(&self) -> usize {
196        self.0.len()
197    }
198}
199
200#[cfg(any(feature = "alloc", feature = "std"))]
201impl AsRef<[u8]> for Zeroizing<Box<[u8]>> {
202    fn as_ref(&self) -> &[u8] {
203        &self.0
204    }
205}
206
207#[cfg(any(feature = "alloc", feature = "std"))]
208impl AsMut<[u8]> for Zeroizing<Box<[u8]>> {
209    fn as_mut(&mut self) -> &mut [u8] {
210        &mut self.0
211    }
212}
213
214/// Allocate an exact-size boxed byte slice initialized to zero.
215///
216/// The temporary `Vec` contains only zeroes. Secret bytes must be written only
217/// after conversion to the exact-size boxed slice.
218#[cfg(any(feature = "alloc", feature = "std"))]
219pub fn boxed_bytes_zeroed(len: usize) -> Box<[u8]> {
220    vec![0u8; len].into_boxed_slice()
221}
222
223/// Copy bytes directly into exact-size owned storage.
224#[cfg(any(feature = "alloc", feature = "std"))]
225pub fn boxed_bytes_from_slice(data: &[u8]) -> Box<[u8]> {
226    let mut boxed = boxed_bytes_zeroed(data.len());
227    boxed.copy_from_slice(data);
228    boxed
229}
230
231/// Copy bytes into exact-size storage that clears itself on drop.
232#[cfg(any(feature = "alloc", feature = "std"))]
233pub fn zeroizing_bytes_from_slice(data: &[u8]) -> ZeroizingBytes {
234    Zeroizing::new(boxed_bytes_from_slice(data))
235}
236
237/// Explicitly overwrite every initialized byte in a slice.
238///
239/// The implementation uses safe-Rust writes plus `compiler_fence` and
240/// `black_box` as best-effort optimization barriers. It does not claim
241/// compiler-guaranteed physical erasure of registers, copies, or freed memory.
242pub fn secure_zero(data: &mut [u8]) {
243    data.zeroize();
244}
245
246/// Clone a slice into exact-size storage, then explicitly clear the source
247/// afterwards.
248///
249/// This function clones the contents of the slice and then invokes the same
250/// best-effort initialized-byte clearing used by [`secure_zero`].
251#[cfg(any(feature = "alloc", feature = "std"))]
252pub fn secure_clone_and_zero(data: &mut [u8]) -> Box<[u8]> {
253    let result = boxed_bytes_from_slice(data);
254    secure_zero(data);
255    result
256}
257
258/// Guard that invokes explicit initialized-byte clearing when dropped
259///
260/// `Drop` invokes [`secure_zero`] on the contained buffer. The same compiler
261/// and out-of-scope-copy limitations documented there apply.
262pub struct ZeroGuard<'a>(&'a mut [u8]);
263
264impl<'a> ZeroGuard<'a> {
265    /// Create a new guard that will zero the given data when dropped
266    pub fn new(data: &'a mut [u8]) -> Self {
267        Self(data)
268    }
269
270    /// Get a reference to the protected data
271    pub fn data(&self) -> &[u8] {
272        self.0
273    }
274
275    /// Get a mutable reference to the protected data
276    pub fn data_mut(&mut self) -> &mut [u8] {
277        self.0
278    }
279}
280
281impl Drop for ZeroGuard<'_> {
282    fn drop(&mut self) {
283        secure_zero(self.0);
284    }
285}
286
287#[cfg(all(test, any(feature = "alloc", feature = "std")))]
288mod tests {
289    use super::{
290        boxed_bytes_from_slice, boxed_bytes_zeroed, secure_clone_and_zero, Zeroize, Zeroizing,
291        ZeroizingBytes,
292    };
293
294    #[cfg(not(feature = "std"))]
295    use alloc::format;
296
297    #[test]
298    fn boxed_byte_helpers_use_exact_length_storage() {
299        let zeroed = boxed_bytes_zeroed(17);
300        assert_eq!(zeroed.len(), 17);
301        assert!(zeroed.iter().all(|byte| *byte == 0));
302
303        let copied = boxed_bytes_from_slice(&[1, 2, 3, 4]);
304        assert_eq!(&*copied, &[1, 2, 3, 4]);
305    }
306
307    #[test]
308    fn secure_clone_moves_secret_into_box_and_clears_source() {
309        let mut source = [0xA5; 8];
310        let copied = secure_clone_and_zero(&mut source);
311        assert_eq!(&*copied, &[0xA5; 8]);
312        assert_eq!(source, [0; 8]);
313    }
314
315    #[test]
316    fn zeroizing_bytes_can_be_cleared_in_place() {
317        let mut secret = ZeroizingBytes::new(boxed_bytes_from_slice(&[7, 8, 9]));
318        assert_eq!(format!("{secret:?}"), "Zeroizing([REDACTED])");
319        secret.zeroize();
320        assert_eq!(&**secret, &[0, 0, 0]);
321    }
322
323    #[test]
324    fn zeroizing_value_can_be_moved_out_without_copying() {
325        let bytes = ZeroizingBytes::new(boxed_bytes_from_slice(&[1, 2, 3]));
326        let inner = bytes.into_inner();
327        assert_eq!(&*inner, &[1, 2, 3]);
328    }
329
330    #[test]
331    fn arbitrary_length_byte_array_can_be_moved_out_safely() {
332        let bytes = Zeroizing::new([0x5a; 66]);
333        assert_eq!(bytes.into_inner_array(), [0x5a; 66]);
334    }
335}