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")]
8pub use sanitization::SecretString;
9
10/// Volatile-clears an ordinary caller-owned byte buffer.
11///
12/// This delegates to the reviewed `sanitization` crate so the clear cannot be
13/// removed as an ordinary dead store.
14#[inline]
15pub fn sanitize_bytes(bytes: &mut [u8]) {
16    sanitization::sanitize_bytes(bytes);
17}
18
19/// Caller-owned byte buffer that is volatile-cleared when dropped.
20///
21/// The full borrowed slice is cleared on success, error, or early return. This
22/// does not clear the source value or copies made by transports, operating
23/// systems, crash handlers, or remote services.
24pub struct SecretBuffer<'a> {
25    bytes: &'a mut [u8],
26}
27
28impl<'a> SecretBuffer<'a> {
29    /// Borrows a mutable byte slice until the guard is dropped.
30    #[must_use]
31    pub const fn new(bytes: &'a mut [u8]) -> Self {
32        Self { bytes }
33    }
34
35    /// Returns the guarded bytes for request construction.
36    #[must_use]
37    pub fn as_mut_slice(&mut self) -> &mut [u8] {
38        self.bytes
39    }
40
41    /// Returns the guarded bytes for transport.
42    #[must_use]
43    pub fn as_slice(&self) -> &[u8] {
44        self.bytes
45    }
46}
47
48impl Drop for SecretBuffer<'_> {
49    fn drop(&mut self) {
50        sanitize_bytes(self.bytes);
51    }
52}
53
54impl core::fmt::Debug for SecretBuffer<'_> {
55    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        formatter.write_str("SecretBuffer([redacted])")
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    #[cfg(feature = "alloc")]
63    extern crate alloc;
64
65    #[cfg(feature = "alloc")]
66    use super::SecretString;
67    use super::{SecretBuffer, sanitize_bytes};
68
69    #[test]
70    fn explicit_sanitization_clears_every_byte() {
71        let mut bytes = [0xa5_u8; 8];
72        sanitize_bytes(&mut bytes);
73        assert_eq!(bytes, [0; 8]);
74    }
75
76    #[test]
77    fn guard_clears_its_full_buffer_on_drop() {
78        let mut bytes = [0xa5_u8; 8];
79        {
80            let mut guarded = SecretBuffer::new(&mut bytes);
81            if let Some(first) = guarded.as_mut_slice().first_mut() {
82                *first = 0x42;
83            }
84            assert_eq!(guarded.as_slice().first(), Some(&0x42));
85        }
86        assert_eq!(bytes, [0; 8]);
87    }
88
89    #[test]
90    fn guard_clears_after_an_early_error() {
91        fn write_then_fail(output: &mut [u8]) -> Result<(), ()> {
92            let mut guarded = SecretBuffer::new(output);
93            if let Some(first) = guarded.as_mut_slice().first_mut() {
94                *first = 0x42;
95            }
96            Err(())
97        }
98
99        let mut bytes = [0xa5_u8; 8];
100        assert_eq!(write_then_fail(&mut bytes), Err(()));
101        assert_eq!(bytes, [0; 8]);
102    }
103
104    #[cfg(feature = "alloc")]
105    #[test]
106    fn reexported_secret_string_uses_scoped_access_and_redacted_debug() {
107        let secret = SecretString::from_string(alloc::string::String::from("temporary secret"));
108
109        assert_eq!(
110            secret.try_with_secret(|value| value == "temporary secret"),
111            Ok(true)
112        );
113        assert!(!alloc::format!("{secret:?}").contains("temporary secret"));
114    }
115}