cloud_sdk_sanitization/
lib.rs1#![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#[cfg(feature = "alloc")]
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[non_exhaustive]
17pub enum SecretStringAppendError {
18 TooLong,
20 CapacityOverflow,
22 Allocation,
24 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#[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#[inline]
91pub fn sanitize_bytes(bytes: &mut [u8]) {
92 sanitization::wipe::bytes(bytes);
93}
94
95#[inline]
100pub fn sanitize_value<T: sanitization::SecureSanitize + ?Sized>(value: &mut T) {
101 sanitization::SecureSanitize::secure_sanitize(value);
102}
103
104#[cfg(feature = "alloc")]
106#[inline]
107pub fn sanitize_string(value: &mut alloc::string::String) {
108 sanitization::wipe::string(value);
109}
110
111pub struct SecretBuffer<'a> {
117 bytes: &'a mut [u8],
118}
119
120impl<'a> SecretBuffer<'a> {
121 #[must_use]
123 pub const fn new(bytes: &'a mut [u8]) -> Self {
124 Self { bytes }
125 }
126
127 #[must_use]
129 pub fn as_mut_slice(&mut self) -> &mut [u8] {
130 self.bytes
131 }
132
133 #[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}