Skip to main content

sanitization_bytes/
lib.rs

1#![no_std]
2#![deny(unsafe_code)]
3
4//! `bytes` integration wrappers for `sanitization`.
5//!
6//! This crate deliberately uses wrapper types instead of trait impls for
7//! external types. Rust's orphan rules prevent implementing
8//! `sanitization::SecureSanitize` directly for `bytes::BytesMut` here.
9
10use bytes::BytesMut;
11use core::fmt;
12use sanitization::{wipe, SecureSanitize};
13
14#[cfg(test)]
15extern crate std;
16
17/// Error returned when an append would exceed the fixed buffer capacity.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct CapacityError {
20    /// Current reported buffer capacity.
21    pub capacity: usize,
22    /// Current initialized length.
23    pub len: usize,
24    /// Additional bytes requested by the caller.
25    pub additional: usize,
26}
27
28impl fmt::Display for CapacityError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(
31            formatter,
32            "insufficient secret buffer capacity: capacity {}, len {}, additional {}",
33            self.capacity, self.len, self.additional
34        )
35    }
36}
37
38/// Clear-on-drop wrapper around [`BytesMut`].
39///
40/// Clearing expands the buffer to its reported capacity, volatile-clears that
41/// initialized view, then resets the length to zero. This covers the owned
42/// capacity exposed by `BytesMut`; it does not make claims about allocator
43/// internals outside that buffer.
44///
45/// # Security
46///
47/// This wrapper treats capacity as fixed after construction. Appending beyond
48/// capacity would force `BytesMut` to reallocate and free the old allocation
49/// while it still contains secret bytes. [`SecretBytesMut::extend_from_slice`]
50/// therefore returns [`CapacityError`] instead of growing implicitly. Allocate
51/// the maximum expected size up front with [`SecretBytesMut::with_capacity`].
52pub struct SecretBytesMut {
53    inner: BytesMut,
54}
55
56impl SecretBytesMut {
57    /// Create an empty secret byte buffer.
58    #[must_use]
59    #[inline]
60    pub fn new() -> Self {
61        Self {
62            inner: BytesMut::new(),
63        }
64    }
65
66    /// Allocate secret byte storage with at least `capacity` bytes.
67    #[must_use]
68    #[inline]
69    pub fn with_capacity(capacity: usize) -> Self {
70        Self {
71            inner: BytesMut::with_capacity(capacity),
72        }
73    }
74
75    /// Copy a slice into a new secret byte buffer.
76    #[must_use]
77    #[inline]
78    pub fn from_slice(bytes: &[u8]) -> Self {
79        let mut inner = BytesMut::with_capacity(bytes.len());
80        inner.extend_from_slice(bytes);
81        Self { inner }
82    }
83
84    /// Wrap an existing [`BytesMut`].
85    ///
86    /// Historical bytes in the incoming buffer's current spare capacity are
87    /// volatile-cleared immediately. Initialized bytes and their length remain
88    /// unchanged.
89    #[must_use]
90    #[inline]
91    pub fn from_bytes_mut(inner: BytesMut) -> Self {
92        let mut inner = inner;
93        let len = inner.len();
94        let capacity = inner.capacity();
95        inner.resize(capacity, 0);
96        wipe::bytes(&mut inner[len..]);
97        inner.truncate(len);
98        Self { inner }
99    }
100
101    /// Number of initialized bytes.
102    #[must_use]
103    #[inline]
104    pub fn len(&self) -> usize {
105        self.inner.len()
106    }
107
108    /// Returns true when there are no initialized bytes.
109    #[must_use]
110    #[inline]
111    pub fn is_empty(&self) -> bool {
112        self.inner.is_empty()
113    }
114
115    /// Reported capacity of the underlying [`BytesMut`].
116    #[must_use]
117    #[inline]
118    pub fn capacity(&self) -> usize {
119        self.inner.capacity()
120    }
121
122    /// Append bytes to the secret buffer without reallocating.
123    ///
124    /// Returns [`CapacityError`] if the append would exceed the current
125    /// capacity. This avoids leaving secret bytes in a freed old allocation
126    /// after an implicit `BytesMut` growth.
127    #[inline]
128    pub fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), CapacityError> {
129        let remaining = self.inner.capacity().saturating_sub(self.inner.len());
130        if bytes.len() > remaining {
131            return Err(CapacityError {
132                capacity: self.inner.capacity(),
133                len: self.inner.len(),
134                additional: bytes.len(),
135            });
136        }
137
138        self.inner.extend_from_slice(bytes);
139        Ok(())
140    }
141
142    /// Borrow initialized bytes.
143    #[must_use]
144    #[inline]
145    pub fn as_slice(&self) -> &[u8] {
146        self.inner.as_ref()
147    }
148
149    /// Run a closure with read-only access to initialized bytes.
150    #[inline]
151    pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
152        inspect(self.as_slice())
153    }
154
155    /// Run a closure with mutable access to initialized bytes.
156    #[inline]
157    pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [u8]) -> R) -> R {
158        edit(self.inner.as_mut())
159    }
160
161    /// Sanitize the reported capacity and clear the buffer.
162    #[inline]
163    pub fn clear_secret(&mut self) {
164        let capacity = self.inner.capacity();
165        self.inner.resize(capacity, 0);
166        wipe::bytes(self.inner.as_mut());
167        self.inner.clear();
168    }
169
170    /// Consume after first sanitizing all accessible capacity.
171    #[inline]
172    pub fn into_cleared(mut self) {
173        self.clear_secret();
174    }
175}
176
177impl Default for SecretBytesMut {
178    #[inline]
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184impl SecureSanitize for SecretBytesMut {
185    #[inline]
186    fn secure_sanitize(&mut self) {
187        self.clear_secret();
188    }
189}
190
191impl Drop for SecretBytesMut {
192    #[inline]
193    fn drop(&mut self) {
194        self.clear_secret();
195    }
196}
197
198impl fmt::Debug for SecretBytesMut {
199    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200        formatter
201            .debug_struct("SecretBytesMut")
202            .field("len", &self.len())
203            .field("capacity", &self.capacity())
204            .field("contents", &"<redacted>")
205            .finish()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn bytes_mut_wrapper_round_trip_and_clear() {
215        let mut secret = SecretBytesMut::with_capacity(8);
216
217        secret.extend_from_slice(b"token").unwrap();
218        secret.extend_from_slice(b"-v2").unwrap();
219
220        assert_eq!(secret.len(), 8);
221        assert_eq!(secret.with_secret(|bytes| bytes[0]), b't');
222
223        secret.with_secret_mut(|bytes| bytes[0] = b'T');
224        assert_eq!(secret.with_secret(|bytes| bytes[0]), b'T');
225
226        secret.clear_secret();
227        assert!(secret.is_empty());
228    }
229
230    #[test]
231    fn bytes_mut_wrapper_refuses_growth_past_capacity() {
232        let mut secret = SecretBytesMut::with_capacity(5);
233
234        secret.extend_from_slice(b"token").unwrap();
235
236        assert_eq!(
237            secret.extend_from_slice(b"-v2"),
238            Err(CapacityError {
239                capacity: 5,
240                len: 5,
241                additional: 3,
242            })
243        );
244        assert!(secret.with_secret(|bytes| bytes == b"token"));
245    }
246
247    #[test]
248    fn bytes_mut_wrapper_debug_is_redacted() {
249        let secret = SecretBytesMut::from_slice(b"token");
250        let rendered = std::format!("{secret:?}");
251
252        assert!(rendered.contains("redacted"));
253        assert!(!rendered.contains("token"));
254    }
255
256    #[test]
257    #[allow(unsafe_code)]
258    fn wrapping_clears_spare_capacity_and_preserves_live_bytes() {
259        let mut incoming = BytesMut::with_capacity(16);
260        incoming.extend_from_slice(b"historical-data");
261        incoming.truncate(5);
262        let capacity = incoming.capacity();
263
264        let mut wrapped = SecretBytesMut::from_bytes_mut(incoming);
265
266        assert_eq!(wrapped.as_slice(), b"histo");
267        assert_eq!(wrapped.capacity(), capacity);
268
269        let live_len = wrapped.len();
270        // SAFETY: `from_bytes_mut` initializes every byte through `resize`
271        // before truncating back to `live_len`, and `capacity` is the exact
272        // initialized bound established by that operation.
273        unsafe { wrapped.inner.set_len(capacity) };
274        assert!(wrapped.inner[live_len..].iter().all(|byte| *byte == 0));
275        // SAFETY: `live_len` was the valid initialized length on entry and is
276        // no greater than the current length.
277        unsafe { wrapped.inner.set_len(live_len) };
278    }
279}