Skip to main content

cloud_sdk_sanitization/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4#[cfg(feature = "std")]
5extern crate std;
6
7#[cfg(feature = "alloc")]
8extern crate alloc;
9
10#[cfg(feature = "alloc")]
11pub use sanitization::SecretString;
12
13/// Failure while fallibly appending to protected UTF-8 storage.
14#[cfg(feature = "alloc")]
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[non_exhaustive]
17pub enum SecretStringAppendError {
18    /// The resulting public byte length exceeds the caller's bound.
19    TooLong,
20    /// The resulting public byte length overflowed `usize`.
21    CapacityOverflow,
22    /// Protected replacement storage could not be allocated.
23    Allocation,
24    /// The protected string's internal UTF-8 invariant was not satisfied.
25    InvalidUtf8,
26}
27
28#[cfg(feature = "alloc")]
29impl core::fmt::Display for SecretStringAppendError {
30    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31        formatter.write_str(match self {
32            Self::TooLong => "secret string length limit exceeded",
33            Self::CapacityOverflow => "secret string capacity overflowed",
34            Self::Allocation => "secret string allocation failed",
35            Self::InvalidUtf8 => "secret string UTF-8 invariant failed",
36        })
37    }
38}
39
40#[cfg(feature = "alloc")]
41impl core::error::Error for SecretStringAppendError {}
42
43/// Fallibly appends text to protected storage within a public byte bound.
44///
45/// Growth allocates and fills replacement storage before clearing the old
46/// allocation and swapping it out. Appends within existing capacity cannot
47/// allocate. The borrowed source is not cleared.
48#[cfg(feature = "alloc")]
49pub fn try_append_secret_string(
50    value: &mut SecretString,
51    text: &str,
52    maximum_bytes: usize,
53) -> Result<(), SecretStringAppendError> {
54    let required = value
55        .len()
56        .checked_add(text.len())
57        .ok_or(SecretStringAppendError::CapacityOverflow)?;
58    if required > maximum_bytes {
59        return Err(SecretStringAppendError::TooLong);
60    }
61    if required <= value.capacity() {
62        value.push_str(text);
63        return Ok(());
64    }
65
66    let capacity = value
67        .capacity()
68        .max(1)
69        .saturating_mul(2)
70        .max(required)
71        .min(maximum_bytes);
72    let mut replacement = SecretString::try_with_capacity(capacity)
73        .map_err(|_| SecretStringAppendError::Allocation)?;
74    value
75        .try_with_secret(|current| {
76            replacement.push_str(current);
77            replacement.push_str(text);
78        })
79        .map_err(|_| SecretStringAppendError::InvalidUtf8)?;
80
81    value.clear_secret();
82    core::mem::swap(value, &mut replacement);
83    Ok(())
84}
85
86/// Volatile-clears an ordinary caller-owned byte buffer.
87///
88/// This delegates to the reviewed `sanitization` crate so the clear cannot be
89/// removed as an ordinary dead store.
90#[inline]
91pub fn sanitize_bytes(bytes: &mut [u8]) {
92    sanitization::wipe::bytes(bytes);
93}
94
95/// Volatile-clears one value through its reviewed field-wise sanitizer.
96///
97/// This is intended for fixed scalar bookkeeping and aggregates whose
98/// `SecureSanitize` implementation has been explicitly reviewed.
99#[inline]
100pub fn sanitize_value<T: sanitization::SecureSanitize + ?Sized>(value: &mut T) {
101    sanitization::SecureSanitize::secure_sanitize(value);
102}
103
104/// Volatile-clears an owned UTF-8 allocation's complete capacity.
105#[cfg(feature = "alloc")]
106#[inline]
107pub fn sanitize_string(value: &mut alloc::string::String) {
108    sanitization::wipe::string(value);
109}
110
111/// Caller-owned byte buffer that is volatile-cleared when dropped.
112///
113/// The full borrowed slice is cleared on success, error, or early return. This
114/// does not clear the source value or copies made by transports, operating
115/// systems, crash handlers, or remote services.
116pub struct SecretBuffer<'a> {
117    bytes: &'a mut [u8],
118}
119
120impl<'a> SecretBuffer<'a> {
121    /// Borrows a mutable byte slice until the guard is dropped.
122    #[must_use]
123    pub const fn new(bytes: &'a mut [u8]) -> Self {
124        Self { bytes }
125    }
126
127    /// Returns the guarded bytes for request construction.
128    #[must_use]
129    pub fn as_mut_slice(&mut self) -> &mut [u8] {
130        self.bytes
131    }
132
133    /// Returns the guarded bytes for transport.
134    #[must_use]
135    pub fn as_slice(&self) -> &[u8] {
136        self.bytes
137    }
138}
139
140impl Drop for SecretBuffer<'_> {
141    fn drop(&mut self) {
142        sanitize_bytes(self.bytes);
143    }
144}
145
146impl core::fmt::Debug for SecretBuffer<'_> {
147    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148        formatter.write_str("SecretBuffer([redacted])")
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    #[cfg(feature = "alloc")]
155    extern crate alloc;
156
157    #[cfg(feature = "alloc")]
158    use super::SecretString;
159    #[cfg(feature = "alloc")]
160    use super::sanitize_string;
161    use super::{SecretBuffer, sanitize_bytes, sanitize_value};
162    #[cfg(feature = "alloc")]
163    use super::{SecretStringAppendError, try_append_secret_string};
164
165    #[test]
166    fn explicit_sanitization_clears_every_byte() {
167        let mut bytes = [0xa5_u8; 8];
168        sanitize_bytes(&mut bytes);
169        assert_eq!(bytes, [0; 8]);
170    }
171
172    #[test]
173    fn scalar_sanitization_uses_the_same_audited_boundary() {
174        let mut value = usize::MAX;
175        sanitize_value(&mut value);
176        assert_eq!(value, 0);
177    }
178
179    #[cfg(feature = "alloc")]
180    #[test]
181    fn owned_string_sanitization_clears_length_and_complete_capacity() {
182        let mut value = alloc::string::String::with_capacity(32);
183        value.push_str("sensitive key");
184        sanitize_string(&mut value);
185        assert!(value.is_empty());
186        assert_eq!(value.capacity(), 32);
187    }
188
189    #[test]
190    fn guard_clears_its_full_buffer_on_drop() {
191        let mut bytes = [0xa5_u8; 8];
192        {
193            let mut guarded = SecretBuffer::new(&mut bytes);
194            if let Some(first) = guarded.as_mut_slice().first_mut() {
195                *first = 0x42;
196            }
197            assert_eq!(guarded.as_slice().first(), Some(&0x42));
198        }
199        assert_eq!(bytes, [0; 8]);
200    }
201
202    #[test]
203    fn guard_clears_after_an_early_error() {
204        fn write_then_fail(output: &mut [u8]) -> Result<(), ()> {
205            let mut guarded = SecretBuffer::new(output);
206            if let Some(first) = guarded.as_mut_slice().first_mut() {
207                *first = 0x42;
208            }
209            Err(())
210        }
211
212        let mut bytes = [0xa5_u8; 8];
213        assert_eq!(write_then_fail(&mut bytes), Err(()));
214        assert_eq!(bytes, [0; 8]);
215    }
216
217    #[cfg(feature = "alloc")]
218    #[test]
219    fn reexported_secret_string_uses_scoped_access_and_redacted_debug() {
220        let secret = SecretString::from_string(alloc::string::String::from("temporary secret"));
221
222        assert_eq!(
223            secret.try_with_secret(|value| value == "temporary secret"),
224            Ok(true)
225        );
226        assert!(!alloc::format!("{secret:?}").contains("temporary secret"));
227    }
228
229    #[cfg(feature = "alloc")]
230    #[test]
231    fn protected_string_append_grows_fallibly_within_its_bound() {
232        let mut secret =
233            SecretString::try_with_capacity(2).unwrap_or_else(|_| SecretString::empty());
234        assert_eq!(try_append_secret_string(&mut secret, "ab", 8), Ok(()));
235        assert_eq!(try_append_secret_string(&mut secret, "cdef", 8), Ok(()));
236        assert_eq!(secret.try_with_secret(|text| text == "abcdef"), Ok(true));
237        assert_eq!(
238            try_append_secret_string(&mut secret, "ghi", 8),
239            Err(SecretStringAppendError::TooLong)
240        );
241        assert_eq!(secret.try_with_secret(|text| text == "abcdef"), Ok(true));
242    }
243}