#![no_std]
#![cfg_attr(not(feature = "unsafe-wipe"), forbid(unsafe_code))]
#![cfg_attr(feature = "unsafe-wipe", deny(unsafe_code))]
#![cfg_attr(feature = "unsafe-wipe", deny(unsafe_op_in_unsafe_fn))]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(test)]
extern crate std;
#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
#[cfg(not(target_has_atomic = "8"))]
use core::cell::Cell;
#[cfg(feature = "alloc")]
use core::str::Utf8Error;
#[cfg(target_has_atomic = "8")]
use core::sync::atomic::{fence, AtomicU8};
use core::{
fmt,
hint::black_box,
sync::atomic::{compiler_fence, Ordering},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LengthError {
pub expected: usize,
pub actual: usize,
}
pub trait SecureSanitize {
fn secure_sanitize(&mut self);
}
#[macro_export]
macro_rules! secure_sanitize_struct {
(
$(#[$attr:meta])*
$vis:vis struct $name:ident {
$(
$(#[$field_attr:meta])*
$field_vis:vis $field:ident: $field_ty:ty
),* $(,)?
}
) => {
$(#[$attr])*
$vis struct $name {
$(
$(#[$field_attr])*
$field_vis $field: $field_ty,
)*
}
impl $crate::SecureSanitize for $name {
#[inline]
fn secure_sanitize(&mut self) {
$(
$crate::SecureSanitize::secure_sanitize(&mut self.$field);
)*
}
}
};
}
#[macro_export]
macro_rules! secure_drop_struct {
(
$(#[$attr:meta])*
$vis:vis struct $name:ident {
$(
$(#[$field_attr:meta])*
$field_vis:vis $field:ident: $field_ty:ty
),* $(,)?
}
) => {
$crate::secure_sanitize_struct! {
$(#[$attr])*
$vis struct $name {
$(
$(#[$field_attr])*
$field_vis $field: $field_ty,
)*
}
}
impl Drop for $name {
#[inline]
fn drop(&mut self) {
$crate::SecureSanitize::secure_sanitize(self);
}
}
};
}
#[inline(never)]
pub fn sanitize_bytes_best_effort(bytes: &mut [u8]) {
compiler_fence(Ordering::SeqCst);
bytes.fill(0);
black_box(bytes);
compiler_fence(Ordering::SeqCst);
}
#[cfg(feature = "alloc")]
#[inline(never)]
fn sanitize_vec_capacity_best_effort(bytes: &mut Vec<u8>) {
sanitize_bytes_best_effort(bytes.as_mut_slice());
compiler_fence(Ordering::SeqCst);
for byte in bytes.spare_capacity_mut() {
byte.write(0);
}
black_box(bytes.spare_capacity_mut());
bytes.clear();
compiler_fence(Ordering::SeqCst);
}
impl SecureSanitize for [u8] {
#[inline(never)]
fn secure_sanitize(&mut self) {
sanitize_bytes_best_effort(self);
}
}
impl<const N: usize> SecureSanitize for [u8; N] {
#[inline(never)]
fn secure_sanitize(&mut self) {
sanitize_bytes_best_effort(self);
}
}
#[cfg(feature = "alloc")]
#[inline]
fn constant_time_eq_slices(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut diff = 0usize;
let mut index = 0;
while index < left.len() {
diff |= usize::from(left[index] ^ right[index]);
index += 1;
}
diff == 0
}
struct TemporaryBytes<'a, const N: usize> {
bytes: &'a mut [u8; N],
}
impl<const N: usize> Drop for TemporaryBytes<'_, N> {
#[inline]
fn drop(&mut self) {
sanitize_bytes_best_effort(self.bytes);
}
}
pub struct SecretBytes<const N: usize> {
#[cfg(target_has_atomic = "8")]
bytes: [AtomicU8; N],
#[cfg(not(target_has_atomic = "8"))]
bytes: [Cell<u8>; N],
}
impl<const N: usize> SecretBytes<N> {
#[must_use]
#[inline]
pub const fn zeroed() -> Self {
Self {
#[cfg(target_has_atomic = "8")]
bytes: [const { AtomicU8::new(0) }; N],
#[cfg(not(target_has_atomic = "8"))]
bytes: [const { Cell::new(0) }; N],
}
}
#[must_use]
#[inline]
pub fn from_array(mut bytes: [u8; N]) -> Self {
let mut secret = Self::zeroed();
for (index, byte) in bytes.iter().copied().enumerate() {
secret.store(index, byte);
}
secret.after_secret_write();
sanitize_bytes_best_effort(&mut bytes);
secret
}
#[must_use]
#[inline]
pub fn from_fn(mut make_byte: impl FnMut(usize) -> u8) -> Self {
let mut secret = Self::zeroed();
let mut index = 0;
while index < N {
secret.store(index, make_byte(index));
index += 1;
}
secret.after_secret_write();
secret
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[inline]
pub fn copy_from_slice(&mut self, source: &[u8]) -> Result<(), LengthError> {
if source.len() != N {
return Err(LengthError {
expected: N,
actual: source.len(),
});
}
for (index, byte) in source.iter().copied().enumerate() {
self.store(index, byte);
}
self.after_secret_write();
Ok(())
}
#[inline]
pub fn copy_to_slice(&self, destination: &mut [u8]) -> Result<(), LengthError> {
if destination.len() != N {
return Err(LengthError {
expected: N,
actual: destination.len(),
});
}
for (index, byte) in destination.iter_mut().enumerate() {
*byte = self.load(index);
}
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[must_use]
#[inline]
pub fn read_byte(&self, index: usize) -> Option<u8> {
if index < N {
Some(self.load(index))
} else {
None
}
}
#[inline]
pub fn write_byte(&mut self, index: usize, value: u8) -> Result<(), LengthError> {
if index >= N {
return Err(LengthError {
expected: N,
actual: index.saturating_add(1),
});
}
self.store(index, value);
self.after_secret_write();
Ok(())
}
#[inline]
pub fn expose_secret<R>(&self, inspect: impl FnOnce(&[u8; N]) -> R) -> R {
let mut temporary = [0; N];
let mut index = 0;
while index < N {
temporary[index] = self.load(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
let guard = TemporaryBytes {
bytes: &mut temporary,
};
let result = inspect(guard.bytes);
sanitize_bytes_best_effort(guard.bytes);
result
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
if other.len() != N {
return false;
}
let mut diff = 0usize;
let mut index = 0;
while index < N {
diff |= usize::from(self.load(index) ^ other[index]);
index += 1;
}
diff == 0
}
#[must_use]
#[inline]
pub fn constant_time_eq_secret(&self, other: &Self) -> bool {
let mut diff = 0usize;
let mut index = 0;
while index < N {
diff |= usize::from(self.load(index) ^ other.load(index));
index += 1;
}
diff == 0
}
#[inline(never)]
pub fn secure_clear(&mut self) {
compiler_fence(Ordering::SeqCst);
#[cfg(target_has_atomic = "8")]
{
for byte in &self.bytes {
byte.store(0, Ordering::SeqCst);
}
fence(Ordering::SeqCst);
}
#[cfg(not(target_has_atomic = "8"))]
{
for byte in &self.bytes {
byte.set(0);
}
}
compiler_fence(Ordering::SeqCst);
}
#[inline]
fn load(&self, index: usize) -> u8 {
#[cfg(target_has_atomic = "8")]
{
self.bytes[index].load(Ordering::SeqCst)
}
#[cfg(not(target_has_atomic = "8"))]
{
self.bytes[index].get()
}
}
#[inline]
fn store(&mut self, index: usize, value: u8) {
#[cfg(target_has_atomic = "8")]
{
self.bytes[index].store(value, Ordering::SeqCst);
}
#[cfg(not(target_has_atomic = "8"))]
{
self.bytes[index].set(value);
}
}
#[inline]
fn after_secret_write(&self) {
#[cfg(target_has_atomic = "8")]
fence(Ordering::SeqCst);
compiler_fence(Ordering::SeqCst);
}
}
impl<const N: usize> Default for SecretBytes<N> {
#[inline]
fn default() -> Self {
Self::zeroed()
}
}
impl<const N: usize> Drop for SecretBytes<N> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> SecureSanitize for SecretBytes<N> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> fmt::Debug for SecretBytes<N> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretBytes")
.field("len", &N)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HeapWipeMode {
BestEffort,
#[cfg(feature = "unsafe-wipe")]
Volatile,
}
#[cfg(feature = "alloc")]
pub struct SecretVec {
inner: Vec<u8>,
wipe: HeapWipeMode,
}
#[cfg(feature = "alloc")]
impl SecretVec {
#[must_use]
#[inline]
pub const fn new(inner: Vec<u8>) -> Self {
Self {
inner,
wipe: HeapWipeMode::BestEffort,
}
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub const fn new_volatile(inner: Vec<u8>) -> Self {
Self {
inner,
wipe: HeapWipeMode::Volatile,
}
}
#[must_use]
#[inline]
pub const fn empty() -> Self {
Self::new(Vec::new())
}
#[must_use]
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self::new(Vec::with_capacity(capacity))
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub fn with_capacity_volatile(capacity: usize) -> Self {
Self::new_volatile(Vec::with_capacity(capacity))
}
#[must_use]
#[inline]
pub fn from_slice(bytes: &[u8]) -> Self {
Self::new(Vec::from(bytes))
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub fn from_slice_volatile(bytes: &[u8]) -> Self {
Self::new_volatile(Vec::from(bytes))
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
inspect(self.inner.as_slice())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [u8]) -> R) -> R {
edit(self.inner.as_mut_slice())
}
#[inline]
pub fn extend_from_slice(&mut self, bytes: &[u8]) {
self.grow_for(bytes.len());
self.inner.extend_from_slice(bytes);
}
#[inline(never)]
pub fn clear_secret(&mut self) {
match self.wipe {
HeapWipeMode::BestEffort => {
sanitize_vec_capacity_best_effort(&mut self.inner);
}
#[cfg(feature = "unsafe-wipe")]
HeapWipeMode::Volatile => {
crate::unsafe_wipe::volatile_sanitize_vec(&mut self.inner);
}
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
constant_time_eq_slices(self.inner.as_slice(), other)
}
#[inline]
pub fn into_cleared(mut self) {
self.clear_secret();
}
fn grow_for(&mut self, additional: usize) {
let required = self.inner.len().saturating_add(additional);
if required <= self.inner.capacity() {
return;
}
let mut replacement = Vec::with_capacity(required);
replacement.extend_from_slice(self.inner.as_slice());
self.clear_secret();
self.inner = replacement;
}
}
#[cfg(feature = "alloc")]
impl Drop for SecretVec {
#[inline]
fn drop(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl SecureSanitize for SecretVec {
#[inline]
fn secure_sanitize(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl fmt::Debug for SecretVec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretVec")
.field("len", &self.inner.len())
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "alloc")]
pub struct SecretString {
inner: Vec<u8>,
wipe: HeapWipeMode,
}
#[cfg(feature = "alloc")]
impl SecretString {
#[must_use]
#[inline]
pub fn new(inner: String) -> Self {
Self {
inner: inner.into_bytes(),
wipe: HeapWipeMode::BestEffort,
}
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub fn new_volatile(inner: String) -> Self {
Self {
inner: inner.into_bytes(),
wipe: HeapWipeMode::Volatile,
}
}
#[must_use]
#[inline]
pub const fn empty() -> Self {
Self {
inner: Vec::new(),
wipe: HeapWipeMode::BestEffort,
}
}
#[must_use]
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
wipe: HeapWipeMode::BestEffort,
}
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub fn with_capacity_volatile(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
wipe: HeapWipeMode::Volatile,
}
}
#[must_use]
#[inline]
pub fn from_secret_str(text: &str) -> Self {
Self {
inner: Vec::from(text.as_bytes()),
wipe: HeapWipeMode::BestEffort,
}
}
#[cfg(feature = "unsafe-wipe")]
#[must_use]
#[inline]
pub fn from_secret_str_volatile(text: &str) -> Self {
Self {
inner: Vec::from(text.as_bytes()),
wipe: HeapWipeMode::Volatile,
}
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn try_with_secret<R>(&self, inspect: impl FnOnce(&str) -> R) -> Result<R, Utf8Error> {
core::str::from_utf8(self.inner.as_slice()).map(inspect)
}
#[inline]
pub fn with_secret_bytes<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
inspect(self.inner.as_slice())
}
#[inline]
pub fn push_str(&mut self, text: &str) {
self.grow_for(text.len());
self.inner.extend_from_slice(text.as_bytes());
}
#[inline(never)]
pub fn clear_secret(&mut self) {
match self.wipe {
HeapWipeMode::BestEffort => {
sanitize_vec_capacity_best_effort(&mut self.inner);
}
#[cfg(feature = "unsafe-wipe")]
HeapWipeMode::Volatile => {
crate::unsafe_wipe::volatile_sanitize_vec(&mut self.inner);
}
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &str) -> bool {
constant_time_eq_slices(self.inner.as_slice(), other.as_bytes())
}
#[inline]
pub fn into_cleared(mut self) {
self.clear_secret();
}
fn grow_for(&mut self, additional: usize) {
let required = self.inner.len().saturating_add(additional);
if required <= self.inner.capacity() {
return;
}
let mut replacement = Vec::with_capacity(required);
replacement.extend_from_slice(self.inner.as_slice());
self.clear_secret();
self.inner = replacement;
}
}
#[cfg(feature = "alloc")]
impl Drop for SecretString {
#[inline]
fn drop(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl SecureSanitize for SecretString {
#[inline]
fn secure_sanitize(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretString")
.field("len", &self.inner.len())
.field("contents", &"<redacted>")
.finish()
}
}
pub struct Secret<T: SecureSanitize> {
inner: T,
}
impl<T: SecureSanitize> Secret<T> {
#[must_use]
#[inline]
pub const fn new(inner: T) -> Self {
Self { inner }
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&T) -> R) -> R {
inspect(&self.inner)
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut T) -> R) -> R {
edit(&mut self.inner)
}
#[inline]
pub fn into_cleared(mut self) {
self.inner.secure_sanitize();
}
}
impl<T: SecureSanitize> Drop for Secret<T> {
#[inline]
fn drop(&mut self) {
self.inner.secure_sanitize();
}
}
impl<T: SecureSanitize> fmt::Debug for Secret<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Secret")
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "unsafe-wipe")]
#[allow(unsafe_code)]
pub mod unsafe_wipe {
use core::{
ptr,
sync::atomic::{compiler_fence, fence, Ordering},
};
#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
pub trait VolatileSanitize {
fn volatile_sanitize(&mut self);
}
#[inline(never)]
pub fn volatile_sanitize_bytes(bytes: &mut [u8]) {
volatile_wipe_raw(bytes.as_mut_ptr(), bytes.len());
}
#[inline(never)]
pub fn volatile_sanitize_array<const N: usize>(bytes: &mut [u8; N]) {
volatile_sanitize_bytes(bytes);
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn volatile_sanitize_vec(bytes: &mut Vec<u8>) {
volatile_wipe_raw(bytes.as_mut_ptr(), bytes.capacity());
bytes.clear();
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn volatile_sanitize_string(text: &mut String) {
volatile_wipe_raw(text.as_mut_ptr(), text.capacity());
text.clear();
}
impl VolatileSanitize for [u8] {
#[inline(never)]
fn volatile_sanitize(&mut self) {
volatile_sanitize_bytes(self);
}
}
impl<const N: usize> VolatileSanitize for [u8; N] {
#[inline(never)]
fn volatile_sanitize(&mut self) {
volatile_sanitize_array(self);
}
}
#[cfg(feature = "alloc")]
impl VolatileSanitize for Vec<u8> {
#[inline(never)]
fn volatile_sanitize(&mut self) {
volatile_sanitize_vec(self);
}
}
#[cfg(feature = "alloc")]
impl VolatileSanitize for String {
#[inline(never)]
fn volatile_sanitize(&mut self) {
volatile_sanitize_string(self);
}
}
pub struct VolatileOnDrop<T: VolatileSanitize> {
inner: T,
}
impl<T: VolatileSanitize> VolatileOnDrop<T> {
#[must_use]
#[inline]
pub const fn new(inner: T) -> Self {
Self { inner }
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&T) -> R) -> R {
inspect(&self.inner)
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut T) -> R) -> R {
edit(&mut self.inner)
}
#[inline]
pub fn into_cleared(mut self) {
self.inner.volatile_sanitize();
}
}
impl<T: VolatileSanitize> Drop for VolatileOnDrop<T> {
#[inline]
fn drop(&mut self) {
self.inner.volatile_sanitize();
}
}
impl<T: VolatileSanitize> core::fmt::Debug for VolatileOnDrop<T> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("VolatileOnDrop")
.field("contents", &"<redacted>")
.finish()
}
}
#[inline(never)]
fn volatile_wipe_raw(ptr: *mut u8, len: usize) {
compiler_fence(Ordering::SeqCst);
let mut offset = 0;
while offset < len {
unsafe {
ptr::write_volatile(ptr.add(offset), 0);
}
offset += 1;
}
compiler_fence(Ordering::SeqCst);
fence(Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_bytes_round_trip_and_clear() {
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
let mut out = [0; 4];
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
secret.secure_clear();
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [0, 0, 0, 0]);
}
#[test]
fn length_errors_are_explicit() {
let mut secret = SecretBytes::<4>::zeroed();
assert_eq!(
secret.copy_from_slice(&[1, 2]).err(),
Some(LengthError {
expected: 4,
actual: 2
})
);
}
#[test]
fn equality_does_not_short_circuit_on_first_byte() {
let left = SecretBytes::<4>::from_array([9, 8, 7, 6]);
let same = SecretBytes::<4>::from_array([9, 8, 7, 6]);
let different = SecretBytes::<4>::from_array([0, 8, 7, 6]);
assert!(left.constant_time_eq(&[9, 8, 7, 6]));
assert!(!left.constant_time_eq(&[9, 8, 7]));
assert!(!left.constant_time_eq(&[0, 8, 7, 6]));
assert!(left.constant_time_eq_secret(&same));
assert!(!left.constant_time_eq_secret(&different));
}
#[test]
fn debug_output_is_redacted() {
let secret = SecretBytes::<3>::from_array([b'a', b'b', b'c']);
let rendered = std::format!("{secret:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("abc"));
}
#[test]
fn generic_secret_uses_closure_access() {
let mut secret = Secret::new([1, 2, 3, 4]);
assert_eq!(secret.with_secret(|bytes| bytes[0]), 1);
secret.with_secret_mut(|bytes| bytes[0] = 9);
assert_eq!(secret.with_secret(|bytes| bytes[0]), 9);
secret.into_cleared();
}
#[test]
fn secure_sanitize_struct_macro_covers_all_fields() {
crate::secure_sanitize_struct! {
struct MacroCredentials {
private_key: SecretBytes<4>,
nonce: SecretBytes<2>,
}
}
let mut credentials = MacroCredentials {
private_key: SecretBytes::from_array([1, 2, 3, 4]),
nonce: SecretBytes::from_array([5, 6]),
};
credentials.secure_sanitize();
assert!(credentials.private_key.constant_time_eq(&[0, 0, 0, 0]));
assert!(credentials.nonce.constant_time_eq(&[0, 0]));
}
#[test]
fn secure_drop_struct_macro_generates_sanitize_and_drop() {
crate::secure_drop_struct! {
struct DropCredentials {
private_key: SecretBytes<4>,
nonce: SecretBytes<2>,
}
}
let mut credentials = DropCredentials {
private_key: SecretBytes::from_array([1, 2, 3, 4]),
nonce: SecretBytes::from_array([5, 6]),
};
credentials.secure_sanitize();
assert!(credentials.private_key.constant_time_eq(&[0, 0, 0, 0]));
assert!(credentials.nonce.constant_time_eq(&[0, 0]));
{
let credentials = DropCredentials {
private_key: SecretBytes::from_array([1, 2, 3, 4]),
nonce: SecretBytes::from_array([5, 6]),
};
let _ = &credentials;
}
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_round_trip_and_clear() {
let mut secret = SecretVec::from_slice(&[1, 2, 3]);
assert_eq!(secret.with_secret(|bytes| bytes.len()), 3);
assert!(secret.constant_time_eq(&[1, 2, 3]));
assert!(!secret.constant_time_eq(&[1, 2]));
secret.extend_from_slice(&[4]);
assert_eq!(secret.with_secret(|bytes| bytes[3]), 4);
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_round_trip_and_clear() {
let mut secret = SecretString::from_secret_str("secret");
assert_eq!(secret.try_with_secret(|text| text.len()), Ok(6));
secret.push_str("-token");
assert_eq!(
secret.try_with_secret(|text| text.ends_with("token")),
Ok(true)
);
assert!(secret.constant_time_eq("secret-token"));
assert!(!secret.constant_time_eq("secret"));
let rendered = std::format!("{secret:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("secret-token"));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "unsafe-wipe")]
#[test]
fn volatile_wipe_clears_slice_when_feature_enabled() {
let mut bytes = [0xA5; 16];
crate::unsafe_wipe::volatile_sanitize_bytes(&mut bytes);
assert_eq!(bytes, [0; 16]);
}
#[cfg(all(feature = "unsafe-wipe", feature = "alloc"))]
#[test]
fn volatile_wipe_clears_alloc_types_when_enabled() {
let mut bytes = std::vec![0xBB; 8];
let mut text = std::string::String::from("secret");
crate::unsafe_wipe::volatile_sanitize_vec(&mut bytes);
crate::unsafe_wipe::volatile_sanitize_string(&mut text);
assert!(bytes.is_empty());
assert!(text.is_empty());
}
#[cfg(feature = "unsafe-wipe")]
#[test]
fn volatile_on_drop_wrapper_is_explicit() {
let mut secret = crate::unsafe_wipe::VolatileOnDrop::new([1, 2, 3, 4]);
assert_eq!(secret.with_secret(|bytes| bytes[2]), 3);
secret.with_secret_mut(|bytes| bytes[2] = 9);
assert_eq!(secret.with_secret(|bytes| bytes[2]), 9);
secret.into_cleared();
}
#[cfg(all(feature = "unsafe-wipe", feature = "alloc"))]
#[test]
fn heap_secrets_can_opt_into_volatile_mode() {
let mut bytes = SecretVec::from_slice_volatile(&[1, 2, 3]);
let mut text = SecretString::from_secret_str_volatile("secret");
assert_eq!(bytes.with_secret(|secret| secret[0]), 1);
assert_eq!(text.try_with_secret(|secret| secret.len()), Ok(6));
bytes.clear_secret();
text.clear_secret();
assert!(bytes.is_empty());
assert!(text.is_empty());
}
}