#![no_std]
#![deny(unsafe_code)]
use bytes::BytesMut;
use core::fmt;
use sanitization::{sanitize_bytes, SecureSanitize};
#[cfg(test)]
extern crate std;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CapacityError {
pub capacity: usize,
pub len: usize,
pub additional: usize,
}
impl fmt::Display for CapacityError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"insufficient secret buffer capacity: capacity {}, len {}, additional {}",
self.capacity, self.len, self.additional
)
}
}
pub struct SecretBytesMut {
inner: BytesMut,
}
impl SecretBytesMut {
#[must_use]
#[inline]
pub fn new() -> Self {
Self {
inner: BytesMut::new(),
}
}
#[must_use]
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: BytesMut::with_capacity(capacity),
}
}
#[must_use]
#[inline]
pub fn from_slice(bytes: &[u8]) -> Self {
let mut inner = BytesMut::with_capacity(bytes.len());
inner.extend_from_slice(bytes);
Self { inner }
}
#[must_use]
#[inline]
pub fn from_bytes_mut(inner: BytesMut) -> Self {
Self { inner }
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[must_use]
#[inline]
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
#[inline]
pub fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), CapacityError> {
let remaining = self.inner.capacity().saturating_sub(self.inner.len());
if bytes.len() > remaining {
return Err(CapacityError {
capacity: self.inner.capacity(),
len: self.inner.len(),
additional: bytes.len(),
});
}
self.inner.extend_from_slice(bytes);
Ok(())
}
#[must_use]
#[inline]
pub fn as_slice(&self) -> &[u8] {
self.inner.as_ref()
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
inspect(self.as_slice())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [u8]) -> R) -> R {
edit(self.inner.as_mut())
}
#[inline]
pub fn clear_secret(&mut self) {
let capacity = self.inner.capacity();
self.inner.resize(capacity, 0);
sanitize_bytes(self.inner.as_mut());
self.inner.clear();
}
#[inline]
pub fn into_cleared(mut self) {
self.clear_secret();
}
}
impl Default for SecretBytesMut {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl SecureSanitize for SecretBytesMut {
#[inline]
fn secure_sanitize(&mut self) {
self.clear_secret();
}
}
impl Drop for SecretBytesMut {
#[inline]
fn drop(&mut self) {
self.clear_secret();
}
}
impl fmt::Debug for SecretBytesMut {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretBytesMut")
.field("len", &self.len())
.field("capacity", &self.capacity())
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bytes_mut_wrapper_round_trip_and_clear() {
let mut secret = SecretBytesMut::with_capacity(8);
secret.extend_from_slice(b"token").unwrap();
secret.extend_from_slice(b"-v2").unwrap();
assert_eq!(secret.len(), 8);
assert_eq!(secret.with_secret(|bytes| bytes[0]), b't');
secret.with_secret_mut(|bytes| bytes[0] = b'T');
assert_eq!(secret.with_secret(|bytes| bytes[0]), b'T');
secret.clear_secret();
assert!(secret.is_empty());
}
#[test]
fn bytes_mut_wrapper_refuses_growth_past_capacity() {
let mut secret = SecretBytesMut::with_capacity(5);
secret.extend_from_slice(b"token").unwrap();
assert_eq!(
secret.extend_from_slice(b"-v2"),
Err(CapacityError {
capacity: 5,
len: 5,
additional: 3,
})
);
assert!(secret.with_secret(|bytes| bytes == b"token"));
}
#[test]
fn bytes_mut_wrapper_debug_is_redacted() {
let secret = SecretBytesMut::from_slice(b"token");
let rendered = std::format!("{secret:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("token"));
}
}