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/// Volatile-clears an ordinary caller-owned byte buffer.
14///
15/// This delegates to the reviewed `sanitization` crate so the clear cannot be
16/// removed as an ordinary dead store.
17#[inline]
18pub fn sanitize_bytes(bytes: &mut [u8]) {
19    sanitization::wipe::bytes(bytes);
20}
21
22/// Volatile-clears one value through its reviewed field-wise sanitizer.
23///
24/// This is intended for fixed scalar bookkeeping and aggregates whose
25/// `SecureSanitize` implementation has been explicitly reviewed.
26#[inline]
27pub fn sanitize_value<T: sanitization::SecureSanitize + ?Sized>(value: &mut T) {
28    sanitization::SecureSanitize::secure_sanitize(value);
29}
30
31/// Volatile-clears an owned UTF-8 allocation's complete capacity.
32#[cfg(feature = "alloc")]
33#[inline]
34pub fn sanitize_string(value: &mut alloc::string::String) {
35    sanitization::wipe::string(value);
36}
37
38/// Caller-owned byte buffer that is volatile-cleared when dropped.
39///
40/// The full borrowed slice is cleared on success, error, or early return. This
41/// does not clear the source value or copies made by transports, operating
42/// systems, crash handlers, or remote services.
43pub struct SecretBuffer<'a> {
44    bytes: &'a mut [u8],
45}
46
47impl<'a> SecretBuffer<'a> {
48    /// Borrows a mutable byte slice until the guard is dropped.
49    #[must_use]
50    pub const fn new(bytes: &'a mut [u8]) -> Self {
51        Self { bytes }
52    }
53
54    /// Returns the guarded bytes for request construction.
55    #[must_use]
56    pub fn as_mut_slice(&mut self) -> &mut [u8] {
57        self.bytes
58    }
59
60    /// Returns the guarded bytes for transport.
61    #[must_use]
62    pub fn as_slice(&self) -> &[u8] {
63        self.bytes
64    }
65}
66
67impl Drop for SecretBuffer<'_> {
68    fn drop(&mut self) {
69        sanitize_bytes(self.bytes);
70    }
71}
72
73impl core::fmt::Debug for SecretBuffer<'_> {
74    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        formatter.write_str("SecretBuffer([redacted])")
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    #[cfg(feature = "alloc")]
82    extern crate alloc;
83
84    #[cfg(feature = "alloc")]
85    use super::SecretString;
86    #[cfg(feature = "alloc")]
87    use super::sanitize_string;
88    use super::{SecretBuffer, sanitize_bytes, sanitize_value};
89
90    #[test]
91    fn explicit_sanitization_clears_every_byte() {
92        let mut bytes = [0xa5_u8; 8];
93        sanitize_bytes(&mut bytes);
94        assert_eq!(bytes, [0; 8]);
95    }
96
97    #[test]
98    fn scalar_sanitization_uses_the_same_audited_boundary() {
99        let mut value = usize::MAX;
100        sanitize_value(&mut value);
101        assert_eq!(value, 0);
102    }
103
104    #[cfg(feature = "alloc")]
105    #[test]
106    fn owned_string_sanitization_clears_length_and_complete_capacity() {
107        let mut value = alloc::string::String::with_capacity(32);
108        value.push_str("sensitive key");
109        sanitize_string(&mut value);
110        assert!(value.is_empty());
111        assert_eq!(value.capacity(), 32);
112    }
113
114    #[test]
115    fn guard_clears_its_full_buffer_on_drop() {
116        let mut bytes = [0xa5_u8; 8];
117        {
118            let mut guarded = SecretBuffer::new(&mut bytes);
119            if let Some(first) = guarded.as_mut_slice().first_mut() {
120                *first = 0x42;
121            }
122            assert_eq!(guarded.as_slice().first(), Some(&0x42));
123        }
124        assert_eq!(bytes, [0; 8]);
125    }
126
127    #[test]
128    fn guard_clears_after_an_early_error() {
129        fn write_then_fail(output: &mut [u8]) -> Result<(), ()> {
130            let mut guarded = SecretBuffer::new(output);
131            if let Some(first) = guarded.as_mut_slice().first_mut() {
132                *first = 0x42;
133            }
134            Err(())
135        }
136
137        let mut bytes = [0xa5_u8; 8];
138        assert_eq!(write_then_fail(&mut bytes), Err(()));
139        assert_eq!(bytes, [0; 8]);
140    }
141
142    #[cfg(feature = "alloc")]
143    #[test]
144    fn reexported_secret_string_uses_scoped_access_and_redacted_debug() {
145        let secret = SecretString::from_string(alloc::string::String::from("temporary secret"));
146
147        assert_eq!(
148            secret.try_with_secret(|value| value == "temporary secret"),
149            Ok(true)
150        );
151        assert!(!alloc::format!("{secret:?}").contains("temporary secret"));
152    }
153}