#![no_std]
#![deny(unsafe_code)]
#![deny(unsafe_op_in_unsafe_fn)]
#[cfg(all(
feature = "memory-lock",
target_arch = "wasm32",
not(feature = "wasm-compat")
))]
compile_error!(
"sanitization: memory-lock on wasm32 requires the wasm-compat feature; WASM has no mlock/mprotect, so this is an explicit reduced-guarantee compatibility backend"
);
#[cfg(all(feature = "guard-pages", target_arch = "wasm32"))]
compile_error!(
"sanitization: the guard-pages feature is not supported on wasm32 because WASM linear memory has no page protection or mprotect equivalent"
);
#[cfg(all(
feature = "canary-check",
not(feature = "random-canary"),
target_arch = "wasm32"
))]
compile_error!(
"sanitization: canary-check on wasm32 requires random-canary because deterministic WASM canaries have no ASLR-backed entropy"
);
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(any(test, feature = "std"))]
extern crate std;
#[cfg(feature = "alloc")]
use alloc::{boxed::Box, string::String, vec::Vec};
#[cfg(feature = "alloc")]
use core::str::Utf8Error;
use core::{
fmt,
hint::black_box,
marker::PhantomData,
mem,
sync::atomic::{compiler_fence, Ordering},
};
#[cfg(feature = "derive")]
pub use sanitization_derive::{SecureSanitize, SecureSanitizeOnDrop};
#[cfg(feature = "random-canary")]
#[allow(unsafe_code)]
mod canary_random {
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "windows",
))]
use core::ffi::c_void;
#[cfg(any(target_os = "linux", target_os = "android"))]
use core::arch::asm;
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_arch = "x86_64"
))]
const SYS_GETRANDOM: usize = 318;
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_arch = "aarch64"
))]
const SYS_GETRANDOM: usize = 278;
#[cfg(target_os = "windows")]
const BCRYPT_USE_SYSTEM_PREFERRED_RNG: u32 = 0x0000_0002;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
unsafe extern "C" {
fn arc4random_buf(buf: *mut c_void, nbytes: usize);
}
#[cfg(target_os = "windows")]
#[link(name = "bcrypt")]
unsafe extern "system" {
fn BCryptGenRandom(
algorithm: *mut c_void,
buffer: *mut u8,
buffer_len: u32,
flags: u32,
) -> i32;
}
#[cfg(all(target_os = "wasi", target_env = "p1"))]
#[link(wasm_import_module = "wasi_snapshot_preview1")]
unsafe extern "C" {
#[link_name = "random_get"]
fn wasi_random_get(buf: *mut u8, buf_len: usize) -> u16;
}
pub(crate) fn fill(bytes: &mut [u8]) -> Result<(), i32> {
if bytes.is_empty() {
return Ok(());
}
fill_inner(bytes)
}
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
fn fill_inner(bytes: &mut [u8]) -> Result<(), i32> {
fill_with_getrandom_syscall(bytes)
}
#[cfg(all(
target_os = "android",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
fn fill_inner(bytes: &mut [u8]) -> Result<(), i32> {
fill_with_getrandom_syscall(bytes)
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn fill_inner(bytes: &mut [u8]) -> Result<(), i32> {
unsafe { arc4random_buf(bytes.as_mut_ptr().cast::<c_void>(), bytes.len()) };
Ok(())
}
#[cfg(target_os = "windows")]
fn fill_inner(bytes: &mut [u8]) -> Result<(), i32> {
for chunk in bytes.chunks_mut(u32::MAX as usize) {
let status = unsafe {
BCryptGenRandom(
core::ptr::null_mut(),
chunk.as_mut_ptr(),
chunk.len() as u32,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
if status != 0 {
return Err(status);
}
}
Ok(())
}
#[cfg(all(target_os = "wasi", target_env = "p1"))]
fn fill_inner(bytes: &mut [u8]) -> Result<(), i32> {
let errno = unsafe { wasi_random_get(bytes.as_mut_ptr(), bytes.len()) };
if errno == 0 {
Ok(())
} else {
Err(errno as i32)
}
}
#[cfg(not(any(
all(
any(target_os = "linux", target_os = "android"),
target_arch = "x86_64"
),
all(
any(target_os = "linux", target_os = "android"),
target_arch = "aarch64"
),
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "windows",
all(target_os = "wasi", target_env = "p1"),
)))]
fn fill_inner(_bytes: &mut [u8]) -> Result<(), i32> {
Err(0)
}
#[cfg(all(
any(target_os = "linux", target_os = "android"),
any(target_arch = "x86_64", target_arch = "aarch64")
))]
fn fill_with_getrandom_syscall(bytes: &mut [u8]) -> Result<(), i32> {
let mut filled = 0;
while filled < bytes.len() {
let ptr = bytes[filled..].as_mut_ptr() as usize;
let len = bytes.len() - filled;
let ret = raw_syscall3(SYS_GETRANDOM, ptr, len, 0);
if syscall_failed(ret) {
let errno = (-ret) as i32;
if errno == 4 {
continue;
}
return Err(errno);
}
if ret == 0 {
return Err(0);
}
filled += ret as usize;
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn syscall_failed(ret: isize) -> bool {
(-4095..=-1).contains(&ret)
}
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_arch = "x86_64"
))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
let ret: isize;
unsafe {
asm!(
"syscall",
inlateout("rax") number as isize => ret,
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
}
ret
}
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_arch = "aarch64"
))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
let ret: isize;
unsafe {
asm!(
"svc 0",
inlateout("x0") arg1 as isize => ret,
in("x1") arg2,
in("x2") arg3,
in("x8") number,
options(nostack)
);
}
ret
}
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
#[allow(unsafe_code)]
mod linux_aarch64_page_size {
use core::{
arch::asm,
sync::atomic::{AtomicUsize, Ordering},
};
const AT_NULL: usize = 0;
const AT_PAGESZ: usize = 6;
const AUXV_ENTRY_SIZE: usize = core::mem::size_of::<usize>() * 2;
const CONSERVATIVE_PAGE_GRANULE: usize = 65_536;
const MIN_PAGE_GRANULE: usize = 4096;
const AT_FDCWD: usize = usize::MAX - 99;
const O_RDONLY: usize = 0;
const SYS_OPENAT: usize = 56;
const SYS_CLOSE: usize = 57;
const SYS_READ: usize = 63;
static DETECTED_PAGE_GRANULE: AtomicUsize = AtomicUsize::new(0);
pub(crate) fn detect_page_granule() -> usize {
let cached = DETECTED_PAGE_GRANULE.load(Ordering::Acquire);
if cached != 0 {
return cached;
}
let detected = read_auxv_page_size().unwrap_or(CONSERVATIVE_PAGE_GRANULE);
DETECTED_PAGE_GRANULE.store(detected, Ordering::Release);
detected
}
fn read_auxv_page_size() -> Option<usize> {
let path = b"/proc/self/auxv\0";
let fd = raw_syscall4(SYS_OPENAT, AT_FDCWD, path.as_ptr() as usize, O_RDONLY, 0);
if syscall_failed(fd) {
return None;
}
let fd = fd as usize;
let mut pending = [0_u8; AUXV_ENTRY_SIZE];
let mut pending_len = 0;
let mut buffer = [0_u8; 256];
loop {
let read = raw_syscall3(SYS_READ, fd, buffer.as_mut_ptr() as usize, buffer.len());
if read == -4 {
continue;
}
if syscall_failed(read) || read == 0 {
let _ = raw_syscall1(SYS_CLOSE, fd);
return None;
}
for byte in buffer[..read as usize].iter().copied() {
pending[pending_len] = byte;
pending_len += 1;
if pending_len == AUXV_ENTRY_SIZE {
let (key, value) = parse_auxv_entry(&pending);
pending_len = 0;
if key == AT_PAGESZ {
let _ = raw_syscall1(SYS_CLOSE, fd);
return valid_page_granule(value).then_some(value);
}
if key == AT_NULL {
let _ = raw_syscall1(SYS_CLOSE, fd);
return None;
}
}
}
}
}
fn parse_auxv_entry(entry: &[u8; AUXV_ENTRY_SIZE]) -> (usize, usize) {
let mut key = [0_u8; core::mem::size_of::<usize>()];
let mut value = [0_u8; core::mem::size_of::<usize>()];
key.copy_from_slice(&entry[..core::mem::size_of::<usize>()]);
value.copy_from_slice(&entry[core::mem::size_of::<usize>()..]);
(usize::from_ne_bytes(key), usize::from_ne_bytes(value))
}
fn valid_page_granule(value: usize) -> bool {
(MIN_PAGE_GRANULE..=CONSERVATIVE_PAGE_GRANULE).contains(&value) && value.is_power_of_two()
}
fn syscall_failed(ret: isize) -> bool {
(-4095..=-1).contains(&ret)
}
fn raw_syscall1(number: usize, arg1: usize) -> isize {
raw_syscall6(number, arg1, 0, 0, 0, 0, 0)
}
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, 0, 0, 0)
}
fn raw_syscall4(number: usize, arg1: usize, arg2: usize, arg3: usize, arg4: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, arg4, 0, 0)
}
fn raw_syscall6(
number: usize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
) -> isize {
let ret: isize;
unsafe {
asm!(
"svc 0",
inlateout("x0") arg1 as isize => ret,
in("x1") arg2,
in("x2") arg3,
in("x3") arg4,
in("x4") arg5,
in("x5") arg6,
in("x8") number,
options(nostack)
);
}
ret
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LengthError {
pub expected: usize,
pub actual: usize,
}
impl fmt::Display for LengthError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"length mismatch: expected {} bytes, got {} bytes",
self.expected, self.actual
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for LengthError {}
pub trait SecureSanitize {
fn secure_sanitize(&mut self);
}
#[inline(never)]
fn sanitize_plain_value<T>(value: &mut T) {
wipe::volatile_wipe((value as *mut T).cast::<u8>(), mem::size_of::<T>());
}
macro_rules! impl_secure_sanitize_scalar {
($($ty:ty),+ $(,)?) => {
$(
impl SecureSanitize for $ty {
#[inline(never)]
fn secure_sanitize(&mut self) {
sanitize_plain_value(self);
}
}
)+
};
}
impl_secure_sanitize_scalar!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, char, f32, f64,
);
#[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);
}
}
};
}
#[allow(unsafe_code)]
mod wipe {
#[cfg(target_arch = "wasm32")]
use core::hint::black_box;
use core::{
ptr,
sync::atomic::{compiler_fence, fence, Ordering},
};
#[cfg(not(target_arch = "wasm32"))]
#[inline(never)]
pub(crate) fn volatile_wipe(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(target_arch = "wasm32")]
#[inline(never)]
pub(crate) fn volatile_wipe(ptr: *mut u8, len: usize) {
compiler_fence(Ordering::SeqCst);
let wipe: fn(*mut u8, usize) = wasm_volatile_wipe_impl;
black_box(wipe)(ptr, len);
compiler_fence(Ordering::SeqCst);
fence(Ordering::SeqCst);
}
#[cfg(target_arch = "wasm32")]
#[inline(never)]
fn wasm_volatile_wipe_impl(ptr: *mut u8, len: usize) {
let mut offset = 0;
while offset < len {
unsafe {
ptr::write_volatile(ptr.add(offset), 0);
}
offset += 1;
}
}
#[cfg(feature = "multi-pass-clear")]
#[cfg(not(target_arch = "wasm32"))]
#[inline(never)]
pub(crate) fn volatile_fill(ptr: *mut u8, len: usize, value: u8) {
compiler_fence(Ordering::SeqCst);
let mut offset = 0;
while offset < len {
unsafe {
ptr::write_volatile(ptr.add(offset), value);
}
offset += 1;
}
compiler_fence(Ordering::SeqCst);
fence(Ordering::SeqCst);
}
#[cfg(all(feature = "multi-pass-clear", target_arch = "wasm32"))]
#[inline(never)]
pub(crate) fn volatile_fill(ptr: *mut u8, len: usize, value: u8) {
compiler_fence(Ordering::SeqCst);
let fill: fn(*mut u8, usize, u8) = wasm_volatile_fill_impl;
black_box(fill)(ptr, len, value);
compiler_fence(Ordering::SeqCst);
fence(Ordering::SeqCst);
}
#[cfg(all(feature = "multi-pass-clear", target_arch = "wasm32"))]
#[inline(never)]
fn wasm_volatile_fill_impl(ptr: *mut u8, len: usize, value: u8) {
let mut offset = 0;
while offset < len {
unsafe {
ptr::write_volatile(ptr.add(offset), value);
}
offset += 1;
}
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub(crate) fn volatile_multi_pass_clear(ptr: *mut u8, len: usize) {
volatile_wipe(ptr, len);
volatile_fill(ptr, len, 0xFF);
volatile_wipe(ptr, len);
}
}
#[inline(never)]
pub fn sanitize_bytes(bytes: &mut [u8]) {
wipe::volatile_wipe(bytes.as_mut_ptr(), bytes.len());
}
#[inline(never)]
pub fn sanitize_bytes_best_effort(bytes: &mut [u8]) {
sanitize_bytes(bytes);
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub fn sanitize_bytes_multi_pass(bytes: &mut [u8]) {
wipe::volatile_multi_pass_clear(bytes.as_mut_ptr(), bytes.len());
}
#[cfg(feature = "alloc")]
#[inline(never)]
fn sanitize_vec_capacity(bytes: &mut Vec<u8>) {
wipe::volatile_wipe(bytes.as_mut_ptr(), bytes.capacity());
bytes.clear();
}
#[cfg(all(feature = "alloc", feature = "multi-pass-clear"))]
#[inline(never)]
fn sanitize_vec_capacity_multi_pass(bytes: &mut Vec<u8>) {
wipe::volatile_multi_pass_clear(bytes.as_mut_ptr(), bytes.capacity());
bytes.clear();
}
#[cfg(feature = "alloc")]
#[inline]
fn next_secret_capacity(current: usize, required: usize) -> usize {
current.saturating_mul(2).max(required).max(8)
}
#[cfg(feature = "alloc")]
#[inline]
fn max_utf8_capacity(char_count: usize) -> usize {
char_count.saturating_mul(4)
}
#[cfg(all(
feature = "memory-lock",
feature = "wasm-compat",
target_arch = "wasm32"
))]
#[allow(unsafe_code)]
mod memory_lock {
use core::{
cell::UnsafeCell,
fmt,
sync::atomic::{compiler_fence, AtomicBool, Ordering},
};
#[cfg(feature = "canary-check")]
const CANARY_SIZE: usize = 8;
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
const CANARY_MASK: u64 = 0xDEAD_BEEF_CAFE_BABE;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MemoryLockOperation {
Length,
Map,
DontDump,
DontFork,
Lock,
Unlock,
Unmap,
Random,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MemoryLockError {
pub operation: MemoryLockOperation,
pub errno: i32,
}
impl fmt::Display for MemoryLockError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"memory lock operation {:?} failed with errno {}",
self.operation, self.errno
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for MemoryLockError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesError {
Length(crate::LengthError),
Memory(MemoryLockError),
}
impl fmt::Display for LockedSecretBytesError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length(error) => error.fmt(formatter),
Self::Memory(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LockedSecretBytesError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Length(error) => Some(error),
Self::Memory(error) => Some(error),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesGenerateError<E> {
Memory(MemoryLockError),
Generate(E),
}
impl<E: fmt::Display> fmt::Display for LockedSecretBytesGenerateError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory(error) => error.fmt(formatter),
Self::Generate(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl<E> std::error::Error for LockedSecretBytesGenerateError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Memory(error) => Some(error),
Self::Generate(error) => Some(error),
}
}
}
#[cfg(feature = "canary-check")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CanaryCorruptedError;
#[cfg(feature = "canary-check")]
impl fmt::Display for CanaryCorruptedError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("locked secret canary corrupted")
}
}
#[cfg(all(feature = "canary-check", feature = "std"))]
impl std::error::Error for CanaryCorruptedError {}
#[cfg(feature = "canary-check")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesCheckedCopyError {
Length(crate::LengthError),
Canary(CanaryCorruptedError),
}
#[cfg(feature = "canary-check")]
impl fmt::Display for LockedSecretBytesCheckedCopyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length(error) => error.fmt(formatter),
Self::Canary(error) => error.fmt(formatter),
}
}
}
#[cfg(all(feature = "canary-check", feature = "std"))]
impl std::error::Error for LockedSecretBytesCheckedCopyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Length(error) => Some(error),
Self::Canary(error) => Some(error),
}
}
}
#[cfg(feature = "canary-check")]
impl From<crate::LengthError> for LockedSecretBytesCheckedCopyError {
#[inline]
fn from(error: crate::LengthError) -> Self {
Self::Length(error)
}
}
#[cfg(feature = "canary-check")]
impl From<CanaryCorruptedError> for LockedSecretBytesCheckedCopyError {
#[inline]
fn from(error: CanaryCorruptedError) -> Self {
Self::Canary(error)
}
}
impl From<crate::LengthError> for LockedSecretBytesError {
#[inline]
fn from(error: crate::LengthError) -> Self {
Self::Length(error)
}
}
impl From<MemoryLockError> for LockedSecretBytesError {
#[inline]
fn from(error: MemoryLockError) -> Self {
Self::Memory(error)
}
}
impl<E> From<MemoryLockError> for LockedSecretBytesGenerateError<E> {
#[inline]
fn from(error: MemoryLockError) -> Self {
Self::Memory(error)
}
}
struct WasmLockedStorage<const N: usize> {
#[cfg(feature = "canary-check")]
prefix: [u8; CANARY_SIZE],
bytes: [u8; N],
#[cfg(feature = "canary-check")]
suffix: [u8; CANARY_SIZE],
}
impl<const N: usize> WasmLockedStorage<N> {
#[inline]
fn zeroed() -> Self {
Self {
#[cfg(feature = "canary-check")]
prefix: [0; CANARY_SIZE],
bytes: [0; N],
#[cfg(feature = "canary-check")]
suffix: [0; CANARY_SIZE],
}
}
#[inline(never)]
fn clear_all(&mut self) {
#[cfg(feature = "canary-check")]
crate::wipe::volatile_wipe(self.prefix.as_mut_ptr(), CANARY_SIZE);
crate::wipe::volatile_wipe(self.bytes.as_mut_ptr(), N);
#[cfg(feature = "canary-check")]
crate::wipe::volatile_wipe(self.suffix.as_mut_ptr(), CANARY_SIZE);
}
}
pub struct LockedSecretBytes<const N: usize> {
storage: UnsafeCell<WasmLockedStorage<N>>,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl<const N: usize> Send for LockedSecretBytes<N> {}
impl<const N: usize> LockedSecretBytes<N> {
#[inline]
pub fn zeroed() -> Result<Self, MemoryLockError> {
let mut secret = Self {
storage: UnsafeCell::new(WasmLockedStorage::zeroed()),
#[cfg(feature = "random-canary")]
canary: random_canary_value()?,
};
secret.write_canaries();
Ok(secret)
}
#[must_use]
#[inline]
pub const fn is_memory_locked(&self) -> bool {
false
}
#[inline]
pub fn from_array(mut bytes: [u8; N]) -> Result<Self, MemoryLockError> {
let mut secret = match Self::zeroed() {
Ok(secret) => secret,
Err(error) => {
crate::sanitize_bytes(&mut bytes);
return Err(error);
}
};
let _ = secret.copy_from_slice(&bytes);
crate::sanitize_bytes(&mut bytes);
Ok(secret)
}
#[inline]
pub fn from_slice(source: &[u8]) -> Result<Self, LockedSecretBytesError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
}
.into());
}
let mut secret = Self::zeroed()?;
secret.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(secret)
}
#[inline]
pub fn from_fn(mut make_byte: impl FnMut(usize) -> u8) -> Result<Self, MemoryLockError> {
let mut secret = Self::zeroed()?;
let mut index = 0;
while index < N {
secret.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(secret)
}
#[inline]
pub fn try_from_fn<E>(
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, LockedSecretBytesGenerateError<E>> {
let mut secret = Self::zeroed()?;
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => secret.as_mut_slice()[index] = byte,
Err(error) => return Err(LockedSecretBytesGenerateError::Generate(error)),
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(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<(), crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), LockedSecretBytesError> {
let mut replacement = Self::from_slice(source)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn replace_from_array(&mut self, bytes: [u8; N]) -> Result<(), MemoryLockError> {
let mut replacement = Self::from_array(bytes)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn replace_from_fn(
&mut self,
make_byte: impl FnMut(usize) -> u8,
) -> Result<(), MemoryLockError> {
let mut replacement = Self::from_fn(make_byte)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), LockedSecretBytesGenerateError<E>> {
let mut replacement = Self::try_from_fn(make_byte)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn copy_to_slice(&self, destination: &mut [u8]) -> Result<(), crate::LengthError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
});
}
self.assert_canaries_intact();
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_array()))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn copy_to_slice_checked(
&self,
destination: &mut [u8],
) -> Result<(), LockedSecretBytesCheckedCopyError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
}
.into());
}
self.verify_integrity()?;
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(&self, other: &[u8]) -> Result<bool, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(CanaryCorruptedError)
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[inline(never)]
pub fn secure_clear(&mut self) {
self.storage.get_mut().clear_all();
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[inline]
fn storage(&self) -> &WasmLockedStorage<N> {
unsafe { &*self.storage.get() }
}
#[inline]
fn as_slice(&self) -> &[u8] {
&self.storage().bytes
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.storage.get_mut().bytes
}
#[inline]
fn as_array(&self) -> &[u8; N] {
&self.storage().bytes
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
CANARY_MASK.to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
if N == 0 {
return true;
}
let expected = self.canary_value();
crate::constant_time_eq_slices(&self.storage().prefix, &expected)
& crate::constant_time_eq_slices(&self.storage().suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
if N == 0 {
return;
}
let canary = self.canary_value();
self.storage.get_mut().prefix.copy_from_slice(&canary);
self.storage.get_mut().suffix.copy_from_slice(&canary);
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("locked secret canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
unsafe { (&mut *self.storage.get()).clear_all() };
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_prefix_canary_for_test(&mut self) {
if N != 0 {
self.storage.get_mut().prefix[0] ^= 0xFF;
}
}
}
impl<const N: usize> Drop for LockedSecretBytes<N> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> crate::SecureSanitize for LockedSecretBytes<N> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> fmt::Debug for LockedSecretBytes<N> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LockedSecretBytes")
.field("len", &N)
.field("memory_locked", &false)
.field("contents", &"<redacted>")
.finish()
}
}
struct WasmPoolSlotStorage<const N: usize> {
#[cfg(feature = "canary-check")]
prefix: [u8; CANARY_SIZE],
bytes: [u8; N],
#[cfg(feature = "canary-check")]
suffix: [u8; CANARY_SIZE],
}
impl<const N: usize> WasmPoolSlotStorage<N> {
#[inline]
fn zeroed() -> Self {
Self {
#[cfg(feature = "canary-check")]
prefix: [0; CANARY_SIZE],
bytes: [0; N],
#[cfg(feature = "canary-check")]
suffix: [0; CANARY_SIZE],
}
}
#[inline(never)]
fn clear_all(&mut self) {
#[cfg(feature = "canary-check")]
crate::wipe::volatile_wipe(self.prefix.as_mut_ptr(), CANARY_SIZE);
crate::wipe::volatile_wipe(self.bytes.as_mut_ptr(), N);
#[cfg(feature = "canary-check")]
crate::wipe::volatile_wipe(self.suffix.as_mut_ptr(), CANARY_SIZE);
}
}
pub struct SecretPool<const N: usize, const SLOTS: usize> {
slots: [UnsafeCell<WasmPoolSlotStorage<N>>; SLOTS],
used: [AtomicBool; SLOTS],
}
pub struct SecretPoolSlot<'pool, const N: usize, const SLOTS: usize> {
slot_index: usize,
pool: &'pool SecretPool<N, SLOTS>,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl<const N: usize, const SLOTS: usize> Send for SecretPool<N, SLOTS> {}
unsafe impl<const N: usize, const SLOTS: usize> Sync for SecretPool<N, SLOTS> {}
unsafe impl<'pool, const N: usize, const SLOTS: usize> Send for SecretPoolSlot<'pool, N, SLOTS> {}
impl<const N: usize, const SLOTS: usize> SecretPool<N, SLOTS> {
#[inline]
pub fn new() -> Result<Self, MemoryLockError> {
Ok(Self {
slots: core::array::from_fn(|_| UnsafeCell::new(WasmPoolSlotStorage::zeroed())),
used: core::array::from_fn(|_| AtomicBool::new(false)),
})
}
#[must_use]
#[inline]
pub const fn slot_size(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn capacity_slots(&self) -> usize {
SLOTS
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0 || SLOTS == 0
}
#[must_use]
#[inline]
pub const fn locked_len(&self) -> usize {
0
}
#[must_use]
#[inline]
pub fn available_slots(&self) -> usize {
self.used
.iter()
.filter(|used| !used.load(Ordering::Acquire))
.count()
}
#[inline]
pub fn allocate(&self) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
match self.try_allocate() {
Ok(slot) => slot,
Err(_) => panic!("random canary generation failed"),
}
}
#[inline]
pub fn try_allocate(
&self,
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, MemoryLockError> {
for (slot_index, flag) in self.used.iter().enumerate() {
if flag
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
let mut slot = SecretPoolSlot {
slot_index,
pool: self,
#[cfg(feature = "random-canary")]
canary: [0; CANARY_SIZE],
};
if let Err(error) = slot.initialize_canaries() {
drop(slot);
return Err(error);
}
return Ok(Some(slot));
}
}
Ok(None)
}
#[inline]
pub fn allocate_from_slice(
&self,
source: &[u8],
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
let Some(mut slot) = self.allocate() else {
return Ok(None);
};
let _ = slot.copy_from_slice(source);
Ok(Some(slot))
}
#[inline]
pub fn allocate_from_array(
&self,
mut bytes: [u8; N],
) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
let slot = match self.allocate() {
Some(mut slot) => {
let _ = slot.copy_from_slice(&bytes);
Some(slot)
}
None => None,
};
crate::sanitize_bytes(&mut bytes);
slot
}
#[inline]
pub fn allocate_from_fn(
&self,
mut make_byte: impl FnMut(usize) -> u8,
) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
let mut slot = self.allocate()?;
let mut index = 0;
while index < N {
slot.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
Some(slot)
}
#[inline]
pub fn try_allocate_from_fn<E>(
&self,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, E> {
let Some(mut slot) = self.allocate() else {
return Ok(None);
};
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => slot.as_mut_slice()[index] = byte,
Err(error) => return Err(error),
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(Some(slot))
}
#[inline(never)]
pub fn secure_clear(&mut self) {
for slot in self.slots.iter_mut() {
slot.get_mut().clear_all();
}
for flag in self.used.iter() {
flag.store(false, Ordering::Release);
}
compiler_fence(Ordering::SeqCst);
}
}
impl<'pool, const N: usize, const SLOTS: usize> SecretPoolSlot<'pool, N, SLOTS> {
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[must_use]
#[inline]
pub const fn slot_index(&self) -> usize {
self.slot_index
}
#[inline]
pub fn copy_from_slice(&mut self, source: &[u8]) -> Result<(), crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), crate::LengthError> {
self.copy_from_slice(source)
}
#[inline]
pub fn replace_from_array(&mut self, mut bytes: [u8; N]) {
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(&bytes);
compiler_fence(Ordering::SeqCst);
crate::sanitize_bytes(&mut bytes);
}
#[inline]
pub fn replace_from_fn(&mut self, mut make_byte: impl FnMut(usize) -> u8) {
self.assert_canaries_intact();
let mut index = 0;
while index < N {
self.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
self.assert_canaries_intact();
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => self.as_mut_slice()[index] = byte,
Err(error) => {
self.secure_clear();
return Err(error);
}
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn copy_to_slice(&self, destination: &mut [u8]) -> Result<(), crate::LengthError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
});
}
self.assert_canaries_intact();
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, inspect: impl FnOnce(&mut [u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array_mut())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_array()))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(&self, other: &[u8]) -> Result<bool, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(CanaryCorruptedError)
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[inline(never)]
pub fn secure_clear(&mut self) {
self.storage_mut().clear_all();
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[inline]
fn storage(&self) -> &WasmPoolSlotStorage<N> {
unsafe { &*self.pool.slots[self.slot_index].get() }
}
#[inline]
fn storage_mut(&mut self) -> &mut WasmPoolSlotStorage<N> {
unsafe { &mut *self.pool.slots[self.slot_index].get() }
}
#[inline]
fn as_slice(&self) -> &[u8] {
&self.storage().bytes
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.storage_mut().bytes
}
#[inline]
fn as_array(&self) -> &[u8; N] {
&self.storage().bytes
}
#[inline]
fn as_array_mut(&mut self) -> &mut [u8; N] {
&mut self.storage_mut().bytes
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
((self.slot_index as u64) ^ CANARY_MASK).to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "random-canary")]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
if N == 0 {
return Ok(());
}
self.canary = random_canary_value()?;
self.write_canaries();
Ok(())
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
self.write_canaries();
Ok(())
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
Ok(())
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
if N == 0 {
return true;
}
let expected = self.canary_value();
crate::constant_time_eq_slices(&self.storage().prefix, &expected)
& crate::constant_time_eq_slices(&self.storage().suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
if N == 0 {
return;
}
let canary = self.canary_value();
self.storage_mut().prefix.copy_from_slice(&canary);
self.storage_mut().suffix.copy_from_slice(&canary);
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("pooled secret slot canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
unsafe { (&mut *self.pool.slots[self.slot_index].get()).clear_all() };
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_prefix_canary_for_test(&mut self) {
if N != 0 {
self.storage_mut().prefix[0] ^= 0xFF;
}
}
}
impl<const N: usize, const SLOTS: usize> Drop for SecretPool<N, SLOTS> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
}
}
impl<'pool, const N: usize, const SLOTS: usize> Drop for SecretPoolSlot<'pool, N, SLOTS> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
self.pool.used[self.slot_index].store(false, Ordering::Release);
}
}
impl<const N: usize, const SLOTS: usize> crate::SecureSanitize for SecretPool<N, SLOTS> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<'pool, const N: usize, const SLOTS: usize> crate::SecureSanitize
for SecretPoolSlot<'pool, N, SLOTS>
{
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize, const SLOTS: usize> fmt::Debug for SecretPool<N, SLOTS> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretPool")
.field("slot_size", &N)
.field("capacity_slots", &SLOTS)
.field("locked_len", &0)
.field("memory_locked", &false)
.field("contents", &"<redacted>")
.finish()
}
}
impl<'pool, const N: usize, const SLOTS: usize> fmt::Debug for SecretPoolSlot<'pool, N, SLOTS> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretPoolSlot")
.field("len", &N)
.field("slot_index", &self.slot_index)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "random-canary")]
fn random_canary_value() -> Result<[u8; CANARY_SIZE], MemoryLockError> {
let mut canary = [0; CANARY_SIZE];
crate::canary_random::fill(&mut canary).map_err(|errno| MemoryLockError {
operation: MemoryLockOperation::Random,
errno,
})?;
Ok(canary)
}
}
#[cfg(all(
feature = "memory-lock",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
)
))]
#[allow(unsafe_code)]
mod memory_lock {
use core::{
fmt,
ptr::NonNull,
sync::atomic::{compiler_fence, AtomicBool, Ordering},
};
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
use core::arch::asm;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
use core::ffi::c_int;
#[cfg(not(target_os = "linux"))]
use core::ffi::c_void;
#[cfg(target_os = "windows")]
use core::mem::MaybeUninit;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PAGE_GRANULE: usize = 4096;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const UNIX_FALLBACK_PAGE_GRANULE: usize = 4096;
#[cfg(target_os = "windows")]
const WINDOWS_FALLBACK_PAGE_GRANULE: usize = 4096;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const PROT_READ: usize = 0x1;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const PROT_WRITE: usize = 0x2;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const MAP_PRIVATE: usize = 0x02;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const MAP_ANONYMOUS: usize = 0x1000;
#[cfg(target_os = "android")]
const MAP_ANONYMOUS: usize = 0x20;
#[cfg(target_os = "freebsd")]
const MADV_NOCORE: i32 = 8;
#[cfg(target_os = "linux")]
const PROT_READ: usize = 0x1;
#[cfg(target_os = "linux")]
const PROT_WRITE: usize = 0x2;
#[cfg(target_os = "linux")]
const MAP_PRIVATE: usize = 0x02;
#[cfg(target_os = "linux")]
const MAP_ANONYMOUS: usize = 0x20;
#[cfg(target_os = "linux")]
const MADV_DONTFORK: usize = 10;
#[cfg(target_os = "linux")]
const MADV_DONTDUMP: usize = 16;
#[cfg(target_os = "windows")]
const MEM_COMMIT: u32 = 0x1000;
#[cfg(target_os = "windows")]
const MEM_RESERVE: u32 = 0x2000;
#[cfg(target_os = "windows")]
const MEM_RELEASE: u32 = 0x8000;
#[cfg(target_os = "windows")]
const PAGE_READWRITE: u32 = 0x04;
#[cfg(feature = "canary-check")]
const CANARY_SIZE: usize = 8;
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
const CANARY_MASK: u64 = 0xDEAD_BEEF_CAFE_BABE;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MMAP: usize = 9;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MUNMAP: usize = 11;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MADVISE: usize = 28;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MLOCK: usize = 149;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MUNLOCK: usize = 150;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MMAP: usize = 222;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MUNMAP: usize = 215;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MADVISE: usize = 233;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MLOCK: usize = 228;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MUNLOCK: usize = 229;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
unsafe extern "C" {
fn getpagesize() -> i32;
fn mmap(
addr: *mut c_void,
len: usize,
prot: i32,
flags: i32,
fd: i32,
offset: isize,
) -> *mut c_void;
fn munmap(addr: *mut c_void, len: usize) -> i32;
fn mlock(addr: *const c_void, len: usize) -> i32;
fn munlock(addr: *const c_void, len: usize) -> i32;
#[cfg(target_os = "freebsd")]
fn madvise(addr: *mut c_void, len: usize, advice: i32) -> i32;
#[cfg_attr(
any(target_os = "macos", target_os = "ios", target_os = "freebsd"),
link_name = "__error"
)]
#[cfg_attr(
any(target_os = "android", target_os = "openbsd", target_os = "netbsd"),
link_name = "__errno"
)]
#[cfg_attr(target_os = "dragonfly", link_name = "__errno_location")]
fn errno_location() -> *mut c_int;
}
#[cfg(target_os = "windows")]
#[repr(C)]
struct SystemInfo {
processor_architecture: u16,
reserved: u16,
page_size: u32,
minimum_application_address: *mut c_void,
maximum_application_address: *mut c_void,
active_processor_mask: usize,
number_of_processors: u32,
processor_type: u32,
allocation_granularity: u32,
processor_level: u16,
processor_revision: u16,
}
#[cfg(target_os = "windows")]
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetLastError() -> u32;
fn GetSystemInfo(system_info: *mut SystemInfo);
fn VirtualAlloc(
address: *mut c_void,
size: usize,
allocation_type: u32,
protect: u32,
) -> *mut c_void;
fn VirtualFree(address: *mut c_void, size: usize, free_type: u32) -> i32;
fn VirtualLock(address: *mut c_void, size: usize) -> i32;
fn VirtualUnlock(address: *mut c_void, size: usize) -> i32;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MemoryLockOperation {
Length,
Map,
DontDump,
DontFork,
Lock,
Unlock,
Unmap,
Random,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MemoryLockError {
pub operation: MemoryLockOperation,
pub errno: i32,
}
impl fmt::Display for MemoryLockError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"memory lock operation {:?} failed with errno {}",
self.operation, self.errno
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for MemoryLockError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesError {
Length(crate::LengthError),
Memory(MemoryLockError),
}
impl fmt::Display for LockedSecretBytesError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length(error) => error.fmt(formatter),
Self::Memory(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LockedSecretBytesError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Length(error) => Some(error),
Self::Memory(error) => Some(error),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesGenerateError<E> {
Memory(MemoryLockError),
Generate(E),
}
impl<E: fmt::Display> fmt::Display for LockedSecretBytesGenerateError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory(error) => error.fmt(formatter),
Self::Generate(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl<E> std::error::Error for LockedSecretBytesGenerateError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Memory(error) => Some(error),
Self::Generate(error) => Some(error),
}
}
}
#[cfg(feature = "canary-check")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CanaryCorruptedError;
#[cfg(feature = "canary-check")]
impl fmt::Display for CanaryCorruptedError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("locked secret canary corrupted")
}
}
#[cfg(all(feature = "canary-check", feature = "std"))]
impl std::error::Error for CanaryCorruptedError {}
#[cfg(feature = "canary-check")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretBytesCheckedCopyError {
Length(crate::LengthError),
Canary(CanaryCorruptedError),
}
#[cfg(feature = "canary-check")]
impl fmt::Display for LockedSecretBytesCheckedCopyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length(error) => error.fmt(formatter),
Self::Canary(error) => error.fmt(formatter),
}
}
}
#[cfg(all(feature = "canary-check", feature = "std"))]
impl std::error::Error for LockedSecretBytesCheckedCopyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Length(error) => Some(error),
Self::Canary(error) => Some(error),
}
}
}
#[cfg(feature = "canary-check")]
impl From<crate::LengthError> for LockedSecretBytesCheckedCopyError {
#[inline]
fn from(error: crate::LengthError) -> Self {
Self::Length(error)
}
}
#[cfg(feature = "canary-check")]
impl From<CanaryCorruptedError> for LockedSecretBytesCheckedCopyError {
#[inline]
fn from(error: CanaryCorruptedError) -> Self {
Self::Canary(error)
}
}
impl From<crate::LengthError> for LockedSecretBytesError {
#[inline]
fn from(error: crate::LengthError) -> Self {
Self::Length(error)
}
}
impl From<MemoryLockError> for LockedSecretBytesError {
#[inline]
fn from(error: MemoryLockError) -> Self {
Self::Memory(error)
}
}
impl<E> From<MemoryLockError> for LockedSecretBytesGenerateError<E> {
#[inline]
fn from(error: MemoryLockError) -> Self {
Self::Memory(error)
}
}
pub struct LockedSecretBytes<const N: usize> {
ptr: NonNull<u8>,
map_len: usize,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl<const N: usize> Send for LockedSecretBytes<N> {}
impl<const N: usize> LockedSecretBytes<N> {
#[inline]
pub fn zeroed() -> Result<Self, MemoryLockError> {
if N == 0 {
return Ok(Self {
ptr: NonNull::dangling(),
map_len: 0,
#[cfg(feature = "random-canary")]
canary: [0; CANARY_SIZE],
});
}
#[cfg(feature = "random-canary")]
let canary = random_canary_value()?;
let map_len = rounded_mapping_len(Self::mapping_payload_len()?)?;
let ptr = map_private(map_len)?;
if let Err(error) = mark_dontdump(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
if let Err(error) = mark_dontfork(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
if let Err(error) = lock_mapping(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
let mut secret = Self {
ptr,
map_len,
#[cfg(feature = "random-canary")]
canary,
};
secret.write_canaries();
Ok(secret)
}
#[inline]
pub fn from_array(mut bytes: [u8; N]) -> Result<Self, MemoryLockError> {
let mut secret = match Self::zeroed() {
Ok(secret) => secret,
Err(error) => {
crate::sanitize_bytes(&mut bytes);
return Err(error);
}
};
let _ = secret.copy_from_slice(&bytes);
crate::sanitize_bytes(&mut bytes);
Ok(secret)
}
#[inline]
pub fn from_slice(source: &[u8]) -> Result<Self, LockedSecretBytesError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
}
.into());
}
let mut secret = Self::zeroed()?;
secret.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(secret)
}
#[inline]
pub fn from_fn(mut make_byte: impl FnMut(usize) -> u8) -> Result<Self, MemoryLockError> {
let mut secret = Self::zeroed()?;
let mut index = 0;
while index < N {
secret.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(secret)
}
#[inline]
pub fn try_from_fn<E>(
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, LockedSecretBytesGenerateError<E>> {
let mut secret = Self::zeroed()?;
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => secret.as_mut_slice()[index] = byte,
Err(error) => return Err(LockedSecretBytesGenerateError::Generate(error)),
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(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<(), crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), LockedSecretBytesError> {
let mut replacement = Self::from_slice(source)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn replace_from_array(&mut self, bytes: [u8; N]) -> Result<(), MemoryLockError> {
let mut replacement = Self::from_array(bytes)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn replace_from_fn(
&mut self,
make_byte: impl FnMut(usize) -> u8,
) -> Result<(), MemoryLockError> {
let mut replacement = Self::from_fn(make_byte)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), LockedSecretBytesGenerateError<E>> {
let mut replacement = Self::try_from_fn(make_byte)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn copy_to_slice(&self, destination: &mut [u8]) -> Result<(), crate::LengthError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
});
}
self.assert_canaries_intact();
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_array()))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn copy_to_slice_checked(
&self,
destination: &mut [u8],
) -> Result<(), LockedSecretBytesCheckedCopyError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
}
.into());
}
self.verify_integrity()?;
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(&self, other: &[u8]) -> Result<bool, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(CanaryCorruptedError)
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[inline(never)]
pub fn secure_clear(&mut self) {
if self.map_len != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.map_len);
}
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn secure_clear_and_flush(&mut self) {
self.secure_clear();
crate::cache_flush::flush_cache_lines(self.as_mapping_slice());
}
#[inline]
fn as_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.data_ptr(), N) }
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline]
fn as_mapping_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.map_len) }
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.data_ptr(), N) }
}
#[inline]
fn as_array(&self) -> &[u8; N] {
unsafe { &*(self.data_ptr() as *const [u8; N]) }
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
((self.ptr.as_ptr() as u64) ^ CANARY_MASK).to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
if self.map_len == 0 {
return true;
}
let Some(suffix_offset) = Self::suffix_offset() else {
return false;
};
let expected = self.canary_value();
let prefix = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), CANARY_SIZE) };
let suffix = unsafe {
core::slice::from_raw_parts(self.ptr.as_ptr().add(suffix_offset), CANARY_SIZE)
};
crate::constant_time_eq_slices(prefix, &expected)
& crate::constant_time_eq_slices(suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
if self.map_len == 0 {
return;
}
let Some(suffix_offset) = Self::suffix_offset() else {
return;
};
let canary = self.canary_value();
unsafe {
core::ptr::copy_nonoverlapping(canary.as_ptr(), self.ptr.as_ptr(), CANARY_SIZE);
core::ptr::copy_nonoverlapping(
canary.as_ptr(),
self.ptr.as_ptr().add(suffix_offset),
CANARY_SIZE,
);
}
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("locked secret canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
if self.map_len != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.map_len);
}
}
#[inline]
fn data_ptr(&self) -> *mut u8 {
unsafe { self.ptr.as_ptr().add(Self::data_offset()) }
}
#[cfg(feature = "canary-check")]
#[inline]
const fn data_offset() -> usize {
if N == 0 {
0
} else {
CANARY_SIZE
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
const fn data_offset() -> usize {
0
}
#[cfg(feature = "canary-check")]
#[inline]
const fn suffix_offset() -> Option<usize> {
match CANARY_SIZE.checked_add(N) {
Some(offset) if N != 0 => Some(offset),
_ => None,
}
}
#[cfg(feature = "canary-check")]
#[inline]
fn mapping_payload_len() -> Result<usize, MemoryLockError> {
if N == 0 {
return Ok(0);
}
N.checked_add(CANARY_SIZE.saturating_mul(2))
.ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn mapping_payload_len() -> Result<usize, MemoryLockError> {
Ok(N)
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_prefix_canary_for_test(&mut self) {
if self.map_len == 0 {
return;
}
unsafe {
let byte = self.ptr.as_ptr();
core::ptr::write(byte, core::ptr::read(byte) ^ 0xFF);
}
}
}
impl<const N: usize> Drop for LockedSecretBytes<N> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
if self.map_len != 0 {
let _ = unlock_mapping(self.ptr, self.map_len);
let _ = unmap_private(self.ptr, self.map_len);
}
}
}
impl<const N: usize> crate::SecureSanitize for LockedSecretBytes<N> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> fmt::Debug for LockedSecretBytes<N> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LockedSecretBytes")
.field("len", &N)
.field("contents", &"<redacted>")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockedSecretVecGenerateError<E> {
Memory(MemoryLockError),
Generate(E),
}
impl<E: fmt::Display> fmt::Display for LockedSecretVecGenerateError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory(error) => error.fmt(formatter),
Self::Generate(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl<E> std::error::Error for LockedSecretVecGenerateError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Memory(error) => Some(error),
Self::Generate(error) => Some(error),
}
}
}
impl<E> From<MemoryLockError> for LockedSecretVecGenerateError<E> {
#[inline]
fn from(error: MemoryLockError) -> Self {
Self::Memory(error)
}
}
pub struct LockedSecretVec {
ptr: NonNull<u8>,
map_len: usize,
data_capacity: usize,
len: usize,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl Send for LockedSecretVec {}
impl LockedSecretVec {
pub fn with_capacity(capacity: usize) -> Result<Self, MemoryLockError> {
if capacity == 0 {
return Ok(Self {
ptr: NonNull::dangling(),
map_len: 0,
data_capacity: 0,
len: 0,
#[cfg(feature = "random-canary")]
canary: [0; CANARY_SIZE],
});
}
#[cfg(feature = "random-canary")]
let canary = random_canary_value()?;
let map_len = rounded_mapping_len(Self::mapping_payload_len(capacity)?)?;
let ptr = map_private(map_len)?;
if let Err(error) = mark_dontdump(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
if let Err(error) = mark_dontfork(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
if let Err(error) = lock_mapping(ptr, map_len) {
let _ = unmap_private(ptr, map_len);
return Err(error);
}
let mut secret = Self {
ptr,
map_len,
data_capacity: capacity,
len: 0,
#[cfg(feature = "random-canary")]
canary,
};
secret.write_canaries();
Ok(secret)
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, MemoryLockError> {
let mut secret = Self::with_capacity(bytes.len())?;
secret.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
secret.finish_initialization(bytes.len());
Ok(secret)
}
pub fn from_fn(
len: usize,
mut make_byte: impl FnMut(usize) -> u8,
) -> Result<Self, MemoryLockError> {
let mut secret = Self::with_capacity(len)?;
secret.fill_from_fn(len, &mut make_byte);
Ok(secret)
}
pub fn try_from_fn<E>(
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, LockedSecretVecGenerateError<E>> {
let mut secret = Self::with_capacity(len)?;
secret
.fill_from_try_fn(len, &mut make_byte)
.map_err(LockedSecretVecGenerateError::Generate)?;
Ok(secret)
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
#[inline]
pub const fn capacity(&self) -> usize {
self.data_capacity
}
#[must_use]
#[inline]
pub const fn locked_len(&self) -> usize {
self.map_len
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_slice())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [u8]) -> R) -> R {
self.assert_canaries_intact();
edit(self.as_mut_slice())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8]) -> R,
) -> Result<R, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_slice()))
}
pub fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), MemoryLockError> {
self.assert_canaries_intact();
let required = self.len.checked_add(bytes.len()).ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})?;
if required > self.data_capacity {
self.grow_to(required)?;
}
let start = self.len;
self.as_mut_capacity_slice()[start..required].copy_from_slice(bytes);
self.finish_initialization(required);
Ok(())
}
pub fn replace_from_slice(&mut self, bytes: &[u8]) -> Result<(), MemoryLockError> {
self.assert_canaries_intact();
if bytes.len() > self.data_capacity {
let mut replacement = Self::with_capacity(bytes.len())?;
replacement.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
replacement.finish_initialization(bytes.len());
self.clear_secret();
core::mem::swap(self, &mut replacement);
return Ok(());
}
self.clear_secret();
self.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
self.finish_initialization(bytes.len());
Ok(())
}
pub fn replace_from_fn(
&mut self,
len: usize,
mut make_byte: impl FnMut(usize) -> u8,
) -> Result<(), MemoryLockError> {
self.assert_canaries_intact();
let mut replacement = Self::with_capacity(len)?;
replacement.fill_from_fn(len, &mut make_byte);
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
pub fn try_replace_from_fn<E>(
&mut self,
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), LockedSecretVecGenerateError<E>> {
self.assert_canaries_intact();
let mut replacement = Self::with_capacity(len)?;
replacement
.fill_from_try_fn(len, &mut make_byte)
.map_err(LockedSecretVecGenerateError::Generate)?;
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline(never)]
pub fn clear_secret(&mut self) {
if self.map_len != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.map_len);
}
self.len = 0;
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.clear_secret();
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn clear_secret_and_flush(&mut self) {
self.clear_secret();
crate::cache_flush::flush_cache_lines(self.as_mapping_slice());
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(&self, other: &[u8]) -> Result<bool, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(CanaryCorruptedError)
}
}
fn grow_to(&mut self, required: usize) -> Result<(), MemoryLockError> {
self.assert_canaries_intact();
let next_capacity = self.data_capacity.saturating_mul(2).max(required).max(1);
let mut replacement = Self::with_capacity(next_capacity)?;
replacement.as_mut_capacity_slice()[..self.len].copy_from_slice(self.as_slice());
replacement.finish_initialization(self.len);
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
fn fill_from_fn(&mut self, len: usize, make_byte: &mut impl FnMut(usize) -> u8) {
debug_assert!(len <= self.data_capacity);
let capacity = self.as_mut_capacity_slice();
let mut index = 0;
while index < len {
capacity[index] = make_byte(index);
index += 1;
}
self.finish_initialization(len);
}
fn fill_from_try_fn<E>(
&mut self,
len: usize,
make_byte: &mut impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
debug_assert!(len <= self.data_capacity);
let capacity = self.as_mut_capacity_slice();
let mut index = 0;
while index < len {
capacity[index] = make_byte(index)?;
index += 1;
}
self.finish_initialization(len);
Ok(())
}
#[inline]
fn finish_initialization(&mut self, len: usize) {
debug_assert!(len <= self.data_capacity);
self.len = len;
self.write_canaries();
compiler_fence(Ordering::SeqCst);
}
#[inline]
fn as_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.payload_ptr(), self.len) }
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.payload_ptr(), self.len) }
}
#[inline]
fn as_mut_capacity_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.payload_ptr(), self.data_capacity) }
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline]
fn as_mapping_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.map_len) }
}
#[inline]
fn payload_ptr(&self) -> *mut u8 {
if self.map_len == 0 {
return self.ptr.as_ptr();
}
unsafe { self.ptr.as_ptr().add(Self::payload_offset()) }
}
#[cfg(feature = "canary-check")]
#[inline]
const fn payload_offset() -> usize {
CANARY_SIZE
}
#[cfg(not(feature = "canary-check"))]
#[inline]
const fn payload_offset() -> usize {
0
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
((self.ptr.as_ptr() as u64) ^ CANARY_MASK).to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
if self.map_len == 0 {
return true;
}
let expected = self.canary_value();
let prefix = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), CANARY_SIZE) };
let suffix = unsafe {
core::slice::from_raw_parts(
self.ptr.as_ptr().add(CANARY_SIZE + self.len),
CANARY_SIZE,
)
};
crate::constant_time_eq_slices(prefix, &expected)
& crate::constant_time_eq_slices(suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
if self.map_len == 0 {
return;
}
let canary = self.canary_value();
unsafe {
core::ptr::copy_nonoverlapping(canary.as_ptr(), self.ptr.as_ptr(), CANARY_SIZE);
core::ptr::copy_nonoverlapping(
canary.as_ptr(),
self.ptr.as_ptr().add(CANARY_SIZE + self.len),
CANARY_SIZE,
);
}
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("locked dynamic secret canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
if self.map_len != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.map_len);
}
}
#[cfg(feature = "canary-check")]
#[inline]
fn mapping_payload_len(capacity: usize) -> Result<usize, MemoryLockError> {
capacity
.checked_add(CANARY_SIZE.saturating_mul(2))
.ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn mapping_payload_len(capacity: usize) -> Result<usize, MemoryLockError> {
Ok(capacity)
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_prefix_canary_for_test(&mut self) {
if self.map_len == 0 {
return;
}
unsafe {
let byte = self.ptr.as_ptr();
core::ptr::write(byte, core::ptr::read(byte) ^ 0xFF);
}
}
}
impl Drop for LockedSecretVec {
#[inline]
fn drop(&mut self) {
self.clear_secret();
if self.map_len != 0 {
let _ = unlock_mapping(self.ptr, self.map_len);
let _ = unmap_private(self.ptr, self.map_len);
}
}
}
impl crate::SecureSanitize for LockedSecretVec {
#[inline]
fn secure_sanitize(&mut self) {
self.clear_secret();
}
}
impl fmt::Debug for LockedSecretVec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LockedSecretVec")
.field("len", &self.len)
.field("capacity", &self.data_capacity)
.field("locked_len", &self.map_len)
.field("contents", &"<redacted>")
.finish()
}
}
pub struct SecretPool<const N: usize, const SLOTS: usize> {
base: NonNull<u8>,
map_len: usize,
used: [AtomicBool; SLOTS],
}
pub struct SecretPoolSlot<'pool, const N: usize, const SLOTS: usize> {
ptr: NonNull<u8>,
slot_index: usize,
pool: &'pool SecretPool<N, SLOTS>,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl<const N: usize, const SLOTS: usize> Send for SecretPool<N, SLOTS> {}
unsafe impl<const N: usize, const SLOTS: usize> Sync for SecretPool<N, SLOTS> {}
unsafe impl<'pool, const N: usize, const SLOTS: usize> Send for SecretPoolSlot<'pool, N, SLOTS> {}
impl<const N: usize, const SLOTS: usize> SecretPool<N, SLOTS> {
#[inline]
pub fn new() -> Result<Self, MemoryLockError> {
let used = core::array::from_fn(|_| AtomicBool::new(false));
let slot_stride = Self::slot_stride()?;
let total_bytes = slot_stride.checked_mul(SLOTS).ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})?;
if total_bytes == 0 {
return Ok(Self {
base: NonNull::dangling(),
map_len: 0,
used,
});
}
let map_len = rounded_mapping_len(total_bytes)?;
let base = map_private(map_len)?;
if let Err(error) = mark_dontdump(base, map_len) {
let _ = unmap_private(base, map_len);
return Err(error);
}
if let Err(error) = mark_dontfork(base, map_len) {
let _ = unmap_private(base, map_len);
return Err(error);
}
if let Err(error) = lock_mapping(base, map_len) {
let _ = unmap_private(base, map_len);
return Err(error);
}
Ok(Self {
base,
map_len,
used,
})
}
#[must_use]
#[inline]
pub const fn slot_size(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn capacity_slots(&self) -> usize {
SLOTS
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0 || SLOTS == 0
}
#[must_use]
#[inline]
pub const fn locked_len(&self) -> usize {
self.map_len
}
#[must_use]
#[inline]
pub fn available_slots(&self) -> usize {
self.used
.iter()
.filter(|used| !used.load(Ordering::Acquire))
.count()
}
#[inline]
pub fn allocate(&self) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
match self.try_allocate() {
Ok(slot) => slot,
Err(_) => panic!("random canary generation failed"),
}
}
#[inline]
pub fn try_allocate(
&self,
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, MemoryLockError> {
for (slot_index, flag) in self.used.iter().enumerate() {
if flag
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
let ptr = match self.slot_ptr(slot_index) {
Some(ptr) => ptr,
None => {
flag.store(false, Ordering::Release);
continue;
}
};
let mut slot = SecretPoolSlot {
ptr,
slot_index,
pool: self,
#[cfg(feature = "random-canary")]
canary: [0; CANARY_SIZE],
};
if let Err(error) = slot.initialize_canaries() {
drop(slot);
return Err(error);
}
return Ok(Some(slot));
}
}
Ok(None)
}
#[inline]
pub fn allocate_from_slice(
&self,
source: &[u8],
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
let Some(mut slot) = self.allocate() else {
return Ok(None);
};
let _ = slot.copy_from_slice(source);
Ok(Some(slot))
}
#[inline]
pub fn allocate_from_array(
&self,
mut bytes: [u8; N],
) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
let slot = match self.allocate() {
Some(mut slot) => {
let _ = slot.copy_from_slice(&bytes);
Some(slot)
}
None => None,
};
crate::sanitize_bytes(&mut bytes);
slot
}
#[inline]
pub fn allocate_from_fn(
&self,
mut make_byte: impl FnMut(usize) -> u8,
) -> Option<SecretPoolSlot<'_, N, SLOTS>> {
let mut slot = self.allocate()?;
let mut index = 0;
while index < N {
slot.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
Some(slot)
}
#[inline]
pub fn try_allocate_from_fn<E>(
&self,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Option<SecretPoolSlot<'_, N, SLOTS>>, E> {
let Some(mut slot) = self.allocate() else {
return Ok(None);
};
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => slot.as_mut_slice()[index] = byte,
Err(error) => return Err(error),
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(Some(slot))
}
#[inline(never)]
pub fn secure_clear(&mut self) {
if self.map_len != 0 {
crate::wipe::volatile_wipe(self.base.as_ptr(), self.map_len);
}
for flag in self.used.iter() {
flag.store(false, Ordering::Release);
}
compiler_fence(Ordering::SeqCst);
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn secure_clear_and_flush(&mut self) {
self.secure_clear();
crate::cache_flush::flush_cache_lines(self.as_mapping_slice());
}
#[inline]
fn slot_ptr(&self, slot_index: usize) -> Option<NonNull<u8>> {
if N == 0 {
return Some(NonNull::dangling());
}
let offset = slot_index.checked_mul(Self::slot_stride().ok()?)?;
NonNull::new(unsafe { self.base.as_ptr().add(offset) })
}
#[cfg(feature = "canary-check")]
#[inline]
fn slot_stride() -> Result<usize, MemoryLockError> {
if N == 0 {
return Ok(0);
}
N.checked_add(CANARY_SIZE.saturating_mul(2))
.ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn slot_stride() -> Result<usize, MemoryLockError> {
Ok(N)
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline]
fn as_mapping_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.base.as_ptr(), self.map_len) }
}
}
impl<'pool, const N: usize, const SLOTS: usize> SecretPoolSlot<'pool, N, SLOTS> {
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[must_use]
#[inline]
pub const fn slot_index(&self) -> usize {
self.slot_index
}
#[inline]
pub fn copy_from_slice(&mut self, source: &[u8]) -> Result<(), crate::LengthError> {
if source.len() != N {
return Err(crate::LengthError {
expected: N,
actual: source.len(),
});
}
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(source);
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), crate::LengthError> {
self.copy_from_slice(source)
}
#[inline]
pub fn replace_from_array(&mut self, mut bytes: [u8; N]) {
self.assert_canaries_intact();
self.as_mut_slice().copy_from_slice(&bytes);
compiler_fence(Ordering::SeqCst);
crate::sanitize_bytes(&mut bytes);
}
#[inline]
pub fn replace_from_fn(&mut self, mut make_byte: impl FnMut(usize) -> u8) {
self.assert_canaries_intact();
let mut index = 0;
while index < N {
self.as_mut_slice()[index] = make_byte(index);
index += 1;
}
compiler_fence(Ordering::SeqCst);
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
self.assert_canaries_intact();
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => self.as_mut_slice()[index] = byte,
Err(error) => {
self.secure_clear();
return Err(error);
}
}
index += 1;
}
compiler_fence(Ordering::SeqCst);
Ok(())
}
#[inline]
pub fn copy_to_slice(&self, destination: &mut [u8]) -> Result<(), crate::LengthError> {
if destination.len() != N {
return Err(crate::LengthError {
expected: N,
actual: destination.len(),
});
}
self.assert_canaries_intact();
destination.copy_from_slice(self.as_slice());
compiler_fence(Ordering::SeqCst);
core::hint::black_box(destination);
Ok(())
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, inspect: impl FnOnce(&mut [u8; N]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_array_mut())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_array()))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(&self, other: &[u8]) -> Result<bool, CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(CanaryCorruptedError)
}
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[inline(never)]
pub fn secure_clear(&mut self) {
if N != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.slot_stride());
}
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn secure_clear_and_flush(&mut self) {
self.secure_clear();
crate::cache_flush::flush_cache_lines(self.as_slot_slice());
}
#[inline]
fn as_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.data_ptr(), N) }
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.data_ptr(), N) }
}
#[inline]
fn as_array(&self) -> &[u8; N] {
unsafe { &*(self.data_ptr() as *const [u8; N]) }
}
#[inline]
fn as_array_mut(&mut self) -> &mut [u8; N] {
unsafe { &mut *(self.data_ptr() as *mut [u8; N]) }
}
#[inline]
fn data_ptr(&self) -> *mut u8 {
unsafe { self.ptr.as_ptr().add(Self::data_offset()) }
}
#[cfg(feature = "canary-check")]
#[inline]
const fn data_offset() -> usize {
if N == 0 {
0
} else {
CANARY_SIZE
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
const fn data_offset() -> usize {
0
}
#[inline]
fn slot_stride(&self) -> usize {
Self::slot_stride_static().unwrap_or(0)
}
#[cfg(feature = "canary-check")]
#[inline]
fn slot_stride_static() -> Result<usize, MemoryLockError> {
if N == 0 {
return Ok(0);
}
N.checked_add(CANARY_SIZE.saturating_mul(2))
.ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn slot_stride_static() -> Result<usize, MemoryLockError> {
Ok(N)
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline]
fn as_slot_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.slot_stride()) }
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
((self.ptr.as_ptr() as u64) ^ CANARY_MASK).to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "random-canary")]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
if N == 0 {
return Ok(());
}
self.canary = random_canary_value()?;
self.write_canaries();
Ok(())
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
self.write_canaries();
Ok(())
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn initialize_canaries(&mut self) -> Result<(), MemoryLockError> {
Ok(())
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
if N == 0 {
return true;
}
let expected = self.canary_value();
let prefix = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), CANARY_SIZE) };
let suffix = unsafe {
core::slice::from_raw_parts(self.ptr.as_ptr().add(CANARY_SIZE + N), CANARY_SIZE)
};
crate::constant_time_eq_slices(prefix, &expected)
& crate::constant_time_eq_slices(suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
if N == 0 {
return;
}
let canary = self.canary_value();
unsafe {
core::ptr::copy_nonoverlapping(canary.as_ptr(), self.ptr.as_ptr(), CANARY_SIZE);
core::ptr::copy_nonoverlapping(
canary.as_ptr(),
self.ptr.as_ptr().add(CANARY_SIZE + N),
CANARY_SIZE,
);
}
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("pooled secret slot canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
if N != 0 {
crate::wipe::volatile_wipe(self.ptr.as_ptr(), self.slot_stride());
}
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_prefix_canary_for_test(&mut self) {
if N == 0 {
return;
}
unsafe {
let byte = self.ptr.as_ptr();
core::ptr::write(byte, core::ptr::read(byte) ^ 0xFF);
}
}
}
impl<const N: usize, const SLOTS: usize> Drop for SecretPool<N, SLOTS> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
if self.map_len != 0 {
let _ = unlock_mapping(self.base, self.map_len);
let _ = unmap_private(self.base, self.map_len);
}
}
}
impl<'pool, const N: usize, const SLOTS: usize> Drop for SecretPoolSlot<'pool, N, SLOTS> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
self.pool.used[self.slot_index].store(false, Ordering::Release);
}
}
impl<const N: usize, const SLOTS: usize> crate::SecureSanitize for SecretPool<N, SLOTS> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<'pool, const N: usize, const SLOTS: usize> crate::SecureSanitize
for SecretPoolSlot<'pool, N, SLOTS>
{
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize, const SLOTS: usize> fmt::Debug for SecretPool<N, SLOTS> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretPool")
.field("slot_size", &N)
.field("capacity_slots", &SLOTS)
.field("locked_len", &self.map_len)
.field("contents", &"<redacted>")
.finish()
}
}
impl<'pool, const N: usize, const SLOTS: usize> fmt::Debug for SecretPoolSlot<'pool, N, SLOTS> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecretPoolSlot")
.field("len", &N)
.field("slot_index", &self.slot_index)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "random-canary")]
fn random_canary_value() -> Result<[u8; CANARY_SIZE], MemoryLockError> {
let mut canary = [0; CANARY_SIZE];
crate::canary_random::fill(&mut canary).map_err(|errno| MemoryLockError {
operation: MemoryLockOperation::Random,
errno,
})?;
Ok(canary)
}
fn rounded_mapping_len(len: usize) -> Result<usize, MemoryLockError> {
let page_granule = platform_page_granule();
len.checked_add(page_granule - 1)
.map(|value| value & !(page_granule - 1))
.ok_or(MemoryLockError {
operation: MemoryLockOperation::Length,
errno: 0,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[inline]
const fn platform_page_granule() -> usize {
LINUX_PAGE_GRANULE
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
#[inline]
fn platform_page_granule() -> usize {
crate::linux_aarch64_page_size::detect_page_granule()
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
#[inline]
fn platform_page_granule() -> usize {
let page_size = unsafe { getpagesize() };
if page_size > 0 && (page_size as usize).is_power_of_two() {
page_size as usize
} else {
UNIX_FALLBACK_PAGE_GRANULE
}
}
#[cfg(target_os = "windows")]
#[inline]
fn platform_page_granule() -> usize {
let mut info = MaybeUninit::<SystemInfo>::zeroed();
unsafe {
GetSystemInfo(info.as_mut_ptr());
let page_size = info.assume_init().page_size as usize;
if page_size != 0 && page_size.is_power_of_two() {
page_size
} else {
WINDOWS_FALLBACK_PAGE_GRANULE
}
}
}
#[cfg(target_os = "linux")]
fn syscall_failed(ret: isize) -> bool {
(-4095..=-1).contains(&ret)
}
#[cfg(target_os = "linux")]
fn syscall_error(operation: MemoryLockOperation, ret: isize) -> MemoryLockError {
MemoryLockError {
operation,
errno: (-ret) as i32,
}
}
#[cfg(target_os = "windows")]
fn windows_error(operation: MemoryLockOperation) -> MemoryLockError {
let errno = unsafe { GetLastError() } as i32;
MemoryLockError { operation, errno }
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unix_error(operation: MemoryLockOperation) -> MemoryLockError {
MemoryLockError {
operation,
errno: unix_errno(),
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unix_errno() -> i32 {
unsafe { *errno_location() as i32 }
}
#[cfg(target_os = "linux")]
fn map_private(len: usize) -> Result<NonNull<u8>, MemoryLockError> {
let ret = raw_syscall6(
SYS_MMAP,
0,
len,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
usize::MAX,
0,
);
if syscall_failed(ret) {
return Err(syscall_error(MemoryLockOperation::Map, ret));
}
NonNull::new(ret as *mut u8).ok_or(MemoryLockError {
operation: MemoryLockOperation::Map,
errno: 0,
})
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn map_private(len: usize) -> Result<NonNull<u8>, MemoryLockError> {
let ptr = unsafe {
mmap(
core::ptr::null_mut(),
len,
(PROT_READ | PROT_WRITE) as i32,
(MAP_PRIVATE | MAP_ANONYMOUS) as i32,
-1,
0,
)
};
if ptr as isize == -1 {
return Err(unix_error(MemoryLockOperation::Map));
}
NonNull::new(ptr.cast::<u8>()).ok_or(MemoryLockError {
operation: MemoryLockOperation::Map,
errno: 0,
})
}
#[cfg(target_os = "windows")]
fn map_private(len: usize) -> Result<NonNull<u8>, MemoryLockError> {
let ptr = unsafe {
VirtualAlloc(
core::ptr::null_mut(),
len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
NonNull::new(ptr.cast::<u8>()).ok_or_else(|| windows_error(MemoryLockOperation::Map))
}
#[cfg(target_os = "linux")]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = raw_syscall2(SYS_MLOCK, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(MemoryLockOperation::Lock, ret))
} else {
Ok(())
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { mlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(MemoryLockOperation::Lock))
} else {
Ok(())
}
}
#[cfg(target_os = "windows")]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { VirtualLock(ptr.as_ptr().cast::<c_void>(), len) };
if ret == 0 {
Err(windows_error(MemoryLockOperation::Lock))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn mark_dontdump(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = raw_syscall3(SYS_MADVISE, ptr.as_ptr() as usize, len, MADV_DONTDUMP);
if syscall_failed(ret) {
Err(syscall_error(MemoryLockOperation::DontDump, ret))
} else {
Ok(())
}
}
#[cfg(all(not(target_os = "linux"), not(target_os = "freebsd")))]
#[inline]
fn mark_dontdump(_ptr: NonNull<u8>, _len: usize) -> Result<(), MemoryLockError> {
Ok(())
}
#[cfg(target_os = "freebsd")]
fn mark_dontdump(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { madvise(ptr.as_ptr().cast::<c_void>(), len, MADV_NOCORE) };
if ret != 0 {
Err(unix_error(MemoryLockOperation::DontDump))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn mark_dontfork(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = raw_syscall3(SYS_MADVISE, ptr.as_ptr() as usize, len, MADV_DONTFORK);
if syscall_failed(ret) {
Err(syscall_error(MemoryLockOperation::DontFork, ret))
} else {
Ok(())
}
}
#[cfg(not(target_os = "linux"))]
#[inline]
fn mark_dontfork(_ptr: NonNull<u8>, _len: usize) -> Result<(), MemoryLockError> {
Ok(())
}
#[cfg(target_os = "linux")]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = raw_syscall2(SYS_MUNLOCK, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(MemoryLockOperation::Unlock, ret))
} else {
Ok(())
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { munlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(MemoryLockOperation::Unlock))
} else {
Ok(())
}
}
#[cfg(target_os = "windows")]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { VirtualUnlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret == 0 {
Err(windows_error(MemoryLockOperation::Unlock))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn unmap_private(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = raw_syscall2(SYS_MUNMAP, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(MemoryLockOperation::Unmap, ret))
} else {
Ok(())
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unmap_private(ptr: NonNull<u8>, len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { munmap(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(MemoryLockOperation::Unmap))
} else {
Ok(())
}
}
#[cfg(target_os = "windows")]
fn unmap_private(ptr: NonNull<u8>, _len: usize) -> Result<(), MemoryLockError> {
let ret = unsafe { VirtualFree(ptr.as_ptr().cast::<c_void>(), 0, MEM_RELEASE) };
if ret == 0 {
Err(windows_error(MemoryLockOperation::Unmap))
} else {
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall2(number: usize, arg1: usize, arg2: usize) -> isize {
raw_syscall6(number, arg1, arg2, 0, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall6(
number: usize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
) -> isize {
let ret: isize;
unsafe {
asm!(
"syscall",
inlateout("rax") number as isize => ret,
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
in("r10") arg4,
in("r8") arg5,
in("r9") arg6,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
}
ret
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall2(number: usize, arg1: usize, arg2: usize) -> isize {
raw_syscall6(number, arg1, arg2, 0, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall6(
number: usize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
) -> isize {
let ret: isize;
unsafe {
asm!(
"svc 0",
inlateout("x0") arg1 as isize => ret,
in("x1") arg2,
in("x2") arg3,
in("x3") arg4,
in("x4") arg5,
in("x5") arg6,
in("x8") number,
options(nostack)
);
}
ret
}
}
#[cfg(all(
feature = "memory-lock",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
all(target_arch = "wasm32", feature = "wasm-compat"),
)
))]
pub use memory_lock::{
LockedSecretBytes, LockedSecretBytesError, LockedSecretBytesGenerateError, MemoryLockError,
MemoryLockOperation, SecretPool, SecretPoolSlot,
};
#[cfg(all(
feature = "memory-lock",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
),
not(miri)
))]
pub use memory_lock::{LockedSecretVec, LockedSecretVecGenerateError};
#[cfg(all(
feature = "canary-check",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
all(target_arch = "wasm32", feature = "wasm-compat"),
)
))]
pub use memory_lock::{CanaryCorruptedError, LockedSecretBytesCheckedCopyError};
#[cfg(all(feature = "asm-compare", target_arch = "x86_64", not(miri)))]
#[allow(unsafe_code)]
mod compare_asm {
use core::arch::asm;
#[inline(never)]
pub(crate) fn constant_time_eq_equal_len(left: &[u8], right: &[u8]) -> bool {
debug_assert_eq!(left.len(), right.len());
let mut left_ptr = left.as_ptr();
let mut right_ptr = right.as_ptr();
let mut remaining = left.len();
let diff: usize;
let tmp: usize;
unsafe {
asm!(
"xor {diff:e}, {diff:e}",
"xor {tmp:e}, {tmp:e}",
"test {remaining}, {remaining}",
"je 3f",
"2:",
"movzx {tmp:e}, byte ptr [{left_ptr}]",
"xor {tmp:l}, byte ptr [{right_ptr}]",
"or {diff:l}, {tmp:l}",
"inc {left_ptr}",
"inc {right_ptr}",
"dec {remaining}",
"jne 2b",
"3:",
left_ptr = inout(reg) left_ptr,
right_ptr = inout(reg) right_ptr,
remaining = inout(reg) remaining,
diff = lateout(reg) diff,
tmp = lateout(reg) tmp,
options(nostack, readonly)
);
}
let _ = (left_ptr, right_ptr, remaining, tmp);
core::hint::black_box(diff & 0xFF) == 0
}
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[allow(unsafe_code)]
pub mod cache_flush {
#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
use core::{
arch::asm,
sync::atomic::{compiler_fence, Ordering},
};
const CACHE_LINE_SIZE: usize = 64;
pub trait CacheFlushSanitize {
fn cache_flush_sanitize(&mut self);
}
#[inline(never)]
pub fn flush_cache_lines(bytes: &[u8]) {
flush_raw(bytes.as_ptr(), bytes.len());
}
#[inline(never)]
pub fn cache_flush_sanitize_bytes(bytes: &mut [u8]) {
crate::sanitize_bytes(bytes);
flush_raw(bytes.as_ptr(), bytes.len());
}
#[inline(never)]
pub fn cache_flush_sanitize_array<const N: usize>(bytes: &mut [u8; N]) {
cache_flush_sanitize_bytes(bytes);
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn cache_flush_sanitize_vec(bytes: &mut Vec<u8>) {
let ptr = bytes.as_ptr();
let len = bytes.capacity();
crate::unsafe_wipe::volatile_sanitize_vec(bytes);
flush_raw(ptr, len);
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn cache_flush_sanitize_string(text: &mut String) {
let ptr = text.as_ptr();
let len = text.capacity();
crate::unsafe_wipe::volatile_sanitize_string(text);
flush_raw(ptr, len);
}
impl CacheFlushSanitize for [u8] {
#[inline(never)]
fn cache_flush_sanitize(&mut self) {
cache_flush_sanitize_bytes(self);
}
}
impl<const N: usize> CacheFlushSanitize for [u8; N] {
#[inline(never)]
fn cache_flush_sanitize(&mut self) {
cache_flush_sanitize_array(self);
}
}
#[cfg(feature = "alloc")]
impl CacheFlushSanitize for Vec<u8> {
#[inline(never)]
fn cache_flush_sanitize(&mut self) {
cache_flush_sanitize_vec(self);
}
}
#[cfg(feature = "alloc")]
impl CacheFlushSanitize for String {
#[inline(never)]
fn cache_flush_sanitize(&mut self) {
cache_flush_sanitize_string(self);
}
}
pub struct CacheFlushOnDrop<T: CacheFlushSanitize> {
inner: T,
}
impl<T: CacheFlushSanitize> CacheFlushOnDrop<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.cache_flush_sanitize();
}
}
impl<T: CacheFlushSanitize> Drop for CacheFlushOnDrop<T> {
#[inline]
fn drop(&mut self) {
self.inner.cache_flush_sanitize();
}
}
impl<T: CacheFlushSanitize> core::fmt::Debug for CacheFlushOnDrop<T> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("CacheFlushOnDrop")
.field("contents", &"<redacted>")
.finish()
}
}
#[inline(never)]
fn flush_raw(ptr: *const u8, len: usize) {
if len == 0 {
return;
}
compiler_fence(Ordering::SeqCst);
let start = ptr as usize;
let end = start.saturating_add(len.saturating_sub(1));
let mut current = start & !(CACHE_LINE_SIZE - 1);
let end_line = end & !(CACHE_LINE_SIZE - 1);
while current <= end_line {
unsafe {
asm!(
"clflush [{address}]",
address = in(reg) current as *const u8,
options(nostack, preserves_flags)
);
}
match current.checked_add(CACHE_LINE_SIZE) {
Some(next) => current = next,
None => break,
}
}
unsafe {
asm!("mfence", options(nostack, preserves_flags));
}
compiler_fence(Ordering::SeqCst);
}
}
#[cfg(feature = "register-scrub")]
#[allow(unsafe_code)]
pub mod register_scrub {
use core::sync::atomic::{compiler_fence, Ordering};
#[inline(never)]
pub fn scrub_simd_registers() {
compiler_fence(Ordering::SeqCst);
#[cfg(all(target_arch = "x86_64", not(miri)))]
scrub_x86_64_simd_registers();
#[cfg(all(target_arch = "aarch64", not(miri)))]
scrub_aarch64_neon_registers();
compiler_fence(Ordering::SeqCst);
}
#[cfg(all(target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn scrub_x86_64_simd_registers() {
if avx_os_supported() {
scrub_x86_64_avx_registers();
} else {
scrub_x86_64_sse_registers();
}
}
#[cfg(all(target_arch = "x86_64", not(miri)))]
#[inline(never)]
fn scrub_x86_64_sse_registers() {
unsafe {
core::arch::asm!(
"pxor xmm0, xmm0",
"pxor xmm1, xmm1",
"pxor xmm2, xmm2",
"pxor xmm3, xmm3",
"pxor xmm4, xmm4",
"pxor xmm5, xmm5",
out("xmm0") _,
out("xmm1") _,
out("xmm2") _,
out("xmm3") _,
out("xmm4") _,
out("xmm5") _,
options(nostack, nomem, preserves_flags)
);
}
}
#[cfg(all(target_arch = "x86_64", not(target_os = "windows"), not(miri)))]
#[inline(never)]
fn scrub_x86_64_avx_registers() {
unsafe {
core::arch::asm!(
"vzeroall",
out("xmm0") _,
out("xmm1") _,
out("xmm2") _,
out("xmm3") _,
out("xmm4") _,
out("xmm5") _,
out("xmm6") _,
out("xmm7") _,
out("xmm8") _,
out("xmm9") _,
out("xmm10") _,
out("xmm11") _,
out("xmm12") _,
out("xmm13") _,
out("xmm14") _,
out("xmm15") _,
options(nostack, nomem, preserves_flags)
);
}
}
#[cfg(all(target_arch = "x86_64", target_os = "windows", not(miri)))]
#[inline(never)]
fn scrub_x86_64_avx_registers() {
scrub_x86_64_sse_registers();
unsafe {
core::arch::asm!("vzeroupper", options(nostack, nomem, preserves_flags));
}
}
#[cfg(all(target_arch = "x86_64", not(miri)))]
#[inline]
fn avx_os_supported() -> bool {
const CPUID_1_ECX_OSXSAVE: u32 = 1 << 27;
const CPUID_1_ECX_AVX: u32 = 1 << 28;
const XCR0_XMM: u64 = 1 << 1;
const XCR0_YMM: u64 = 1 << 2;
unsafe {
let cpuid = core::arch::x86_64::__cpuid_count(1, 0);
if (cpuid.ecx & (CPUID_1_ECX_OSXSAVE | CPUID_1_ECX_AVX))
!= (CPUID_1_ECX_OSXSAVE | CPUID_1_ECX_AVX)
{
return false;
}
let xcr0 = core::arch::x86_64::_xgetbv(0);
(xcr0 & (XCR0_XMM | XCR0_YMM)) == (XCR0_XMM | XCR0_YMM)
}
}
#[cfg(all(target_arch = "aarch64", not(miri)))]
#[inline(never)]
pub fn scrub_aarch64_neon_registers() {
unsafe {
core::arch::asm!(
"eor v0.16b, v0.16b, v0.16b",
"eor v1.16b, v1.16b, v1.16b",
"eor v2.16b, v2.16b, v2.16b",
"eor v3.16b, v3.16b, v3.16b",
"eor v4.16b, v4.16b, v4.16b",
"eor v5.16b, v5.16b, v5.16b",
"eor v6.16b, v6.16b, v6.16b",
"eor v7.16b, v7.16b, v7.16b",
"eor v16.16b, v16.16b, v16.16b",
"eor v17.16b, v17.16b, v17.16b",
"eor v18.16b, v18.16b, v18.16b",
"eor v19.16b, v19.16b, v19.16b",
"eor v20.16b, v20.16b, v20.16b",
"eor v21.16b, v21.16b, v21.16b",
"eor v22.16b, v22.16b, v22.16b",
"eor v23.16b, v23.16b, v23.16b",
"eor v24.16b, v24.16b, v24.16b",
"eor v25.16b, v25.16b, v25.16b",
"eor v26.16b, v26.16b, v26.16b",
"eor v27.16b, v27.16b, v27.16b",
"eor v28.16b, v28.16b, v28.16b",
"eor v29.16b, v29.16b, v29.16b",
"eor v30.16b, v30.16b, v30.16b",
"eor v31.16b, v31.16b, v31.16b",
out("v0") _,
out("v1") _,
out("v2") _,
out("v3") _,
out("v4") _,
out("v5") _,
out("v6") _,
out("v7") _,
out("v16") _,
out("v17") _,
out("v18") _,
out("v19") _,
out("v20") _,
out("v21") _,
out("v22") _,
out("v23") _,
out("v24") _,
out("v25") _,
out("v26") _,
out("v27") _,
out("v28") _,
out("v29") _,
out("v30") _,
out("v31") _,
options(nostack, nomem)
);
}
}
}
#[cfg(feature = "hardware-secrets")]
pub mod hardware {
use core::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HardwareSecretErrorKind {
Unavailable,
AccessDenied,
InvalidHandle,
OutputTooSmall,
Backend,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HardwareSecretError {
pub kind: HardwareSecretErrorKind,
pub code: i32,
}
impl fmt::Display for HardwareSecretError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"hardware secret operation {:?} failed with code {}",
self.kind, self.code
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for HardwareSecretError {}
pub trait HardwareSecretHandle {}
pub trait HardwareSecretProvider {
type Handle: HardwareSecretHandle;
type Error;
fn seal_from_slice(&self, secret: &[u8]) -> Result<Self::Handle, Self::Error>;
fn expose_secret<R, F: FnOnce(&[u8]) -> R>(
&self,
handle: &Self::Handle,
inspect: F,
) -> Result<R, Self::Error>;
fn rotate_from_slice(
&self,
handle: &mut Self::Handle,
secret: &[u8],
) -> Result<(), Self::Error>;
fn destroy(&self, handle: Self::Handle) -> Result<(), Self::Error>;
}
}
#[cfg(all(
feature = "guard-pages",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
),
not(miri)
))]
#[allow(unsafe_code)]
mod guard_pages {
use core::{
fmt,
ptr::NonNull,
sync::atomic::{compiler_fence, Ordering},
};
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
use core::arch::asm;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
use core::ffi::c_int;
#[cfg(not(target_os = "linux"))]
use core::ffi::c_void;
#[cfg(target_os = "windows")]
use core::mem::MaybeUninit;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PAGE_GRANULE: usize = 4096;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const UNIX_FALLBACK_PAGE_GRANULE: usize = 4096;
#[cfg(target_os = "windows")]
const WINDOWS_FALLBACK_PAGE_GRANULE: usize = 4096;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const PROT_NONE: usize = 0x0;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const PROT_READ: usize = 0x1;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const PROT_WRITE: usize = 0x2;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const MAP_PRIVATE: usize = 0x02;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
const MAP_ANONYMOUS: usize = 0x1000;
#[cfg(target_os = "android")]
const MAP_ANONYMOUS: usize = 0x20;
#[cfg(all(feature = "memory-lock", target_os = "freebsd"))]
const MADV_NOCORE: i32 = 8;
#[cfg(target_os = "linux")]
const PROT_NONE: usize = 0x0;
#[cfg(target_os = "linux")]
const PROT_READ: usize = 0x1;
#[cfg(target_os = "linux")]
const PROT_WRITE: usize = 0x2;
#[cfg(target_os = "linux")]
const MAP_PRIVATE: usize = 0x02;
#[cfg(target_os = "linux")]
const MAP_ANONYMOUS: usize = 0x20;
#[cfg(feature = "memory-lock")]
#[cfg(target_os = "linux")]
const MADV_DONTFORK: usize = 10;
#[cfg(feature = "memory-lock")]
#[cfg(target_os = "linux")]
const MADV_DONTDUMP: usize = 16;
#[cfg(target_os = "windows")]
const MEM_COMMIT: u32 = 0x1000;
#[cfg(target_os = "windows")]
const MEM_RESERVE: u32 = 0x2000;
#[cfg(target_os = "windows")]
const MEM_RELEASE: u32 = 0x8000;
#[cfg(target_os = "windows")]
const PAGE_NOACCESS: u32 = 0x01;
#[cfg(target_os = "windows")]
const PAGE_READWRITE: u32 = 0x04;
#[cfg(feature = "canary-check")]
const CANARY_SIZE: usize = 8;
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
const CANARY_MASK: u64 = 0xA11C_E5AF_EC0D_EC0D;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MMAP: usize = 9;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MPROTECT: usize = 10;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYS_MUNMAP: usize = 11;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "x86_64"))]
const SYS_MADVISE: usize = 28;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "x86_64"))]
const SYS_MLOCK: usize = 149;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "x86_64"))]
const SYS_MUNLOCK: usize = 150;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MMAP: usize = 222;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MPROTECT: usize = 226;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYS_MUNMAP: usize = 215;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "aarch64"))]
const SYS_MADVISE: usize = 233;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "aarch64"))]
const SYS_MLOCK: usize = 228;
#[cfg(all(feature = "memory-lock", target_os = "linux", target_arch = "aarch64"))]
const SYS_MUNLOCK: usize = 229;
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
unsafe extern "C" {
fn getpagesize() -> i32;
fn mmap(
addr: *mut c_void,
len: usize,
prot: i32,
flags: i32,
fd: i32,
offset: isize,
) -> *mut c_void;
fn mprotect(addr: *mut c_void, len: usize, prot: i32) -> i32;
fn munmap(addr: *mut c_void, len: usize) -> i32;
#[cfg(all(feature = "memory-lock", target_os = "freebsd"))]
fn madvise(addr: *mut c_void, len: usize, advice: i32) -> i32;
#[cfg(feature = "memory-lock")]
fn mlock(addr: *const c_void, len: usize) -> i32;
#[cfg(feature = "memory-lock")]
fn munlock(addr: *const c_void, len: usize) -> i32;
#[cfg_attr(
any(target_os = "macos", target_os = "ios", target_os = "freebsd"),
link_name = "__error"
)]
#[cfg_attr(
any(target_os = "android", target_os = "openbsd", target_os = "netbsd"),
link_name = "__errno"
)]
#[cfg_attr(target_os = "dragonfly", link_name = "__errno_location")]
fn errno_location() -> *mut c_int;
}
#[cfg(target_os = "windows")]
#[repr(C)]
struct SystemInfo {
processor_architecture: u16,
reserved: u16,
page_size: u32,
minimum_application_address: *mut c_void,
maximum_application_address: *mut c_void,
active_processor_mask: usize,
number_of_processors: u32,
processor_type: u32,
allocation_granularity: u32,
processor_level: u16,
processor_revision: u16,
}
#[cfg(target_os = "windows")]
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetLastError() -> u32;
fn GetSystemInfo(system_info: *mut SystemInfo);
fn VirtualAlloc(
address: *mut c_void,
size: usize,
allocation_type: u32,
protect: u32,
) -> *mut c_void;
fn VirtualFree(address: *mut c_void, size: usize, free_type: u32) -> i32;
fn VirtualProtect(
address: *mut c_void,
size: usize,
new_protect: u32,
old_protect: *mut u32,
) -> i32;
#[cfg(feature = "memory-lock")]
fn VirtualLock(address: *mut c_void, size: usize) -> i32;
#[cfg(feature = "memory-lock")]
fn VirtualUnlock(address: *mut c_void, size: usize) -> i32;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GuardPageOperation {
Length,
Map,
Protect,
DontDump,
DontFork,
Lock,
Unlock,
Unmap,
Random,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GuardPageError {
pub operation: GuardPageOperation,
pub errno: i32,
}
impl fmt::Display for GuardPageError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"guard page operation {:?} failed with errno {}",
self.operation, self.errno
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for GuardPageError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GuardedSecretVecGenerateError<E> {
Guard(GuardPageError),
Generate(E),
}
impl<E: fmt::Display> fmt::Display for GuardedSecretVecGenerateError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Guard(error) => error.fmt(formatter),
Self::Generate(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl<E> std::error::Error for GuardedSecretVecGenerateError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Guard(error) => Some(error),
Self::Generate(error) => Some(error),
}
}
}
impl<E> From<GuardPageError> for GuardedSecretVecGenerateError<E> {
#[inline]
fn from(error: GuardPageError) -> Self {
Self::Guard(error)
}
}
pub struct GuardedSecretVec {
base: NonNull<u8>,
data: NonNull<u8>,
map_len: usize,
writable_len: usize,
data_capacity: usize,
len: usize,
locked: bool,
#[cfg(feature = "random-canary")]
canary: [u8; CANARY_SIZE],
}
unsafe impl Send for GuardedSecretVec {}
impl GuardedSecretVec {
pub fn with_capacity(capacity: usize) -> Result<Self, GuardPageError> {
Self::with_capacity_locked_state(capacity, false)
}
#[cfg(feature = "memory-lock")]
pub fn locked_with_capacity(capacity: usize) -> Result<Self, GuardPageError> {
Self::with_capacity_locked_state(capacity, true)
}
fn with_capacity_locked_state(
capacity: usize,
locked: bool,
) -> Result<Self, GuardPageError> {
#[cfg(feature = "random-canary")]
let canary = random_canary_value().map_err(|error| GuardPageError {
operation: error.operation,
errno: error.errno,
})?;
let page_granule = platform_page_granule();
let data_capacity = guarded_payload_capacity(capacity)?;
let writable_len = guarded_writable_len(data_capacity)?;
let total_len = writable_len
.checked_add(page_granule)
.and_then(|value| value.checked_add(page_granule))
.ok_or(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
})?;
let base = map_guarded(total_len)?;
let data_addr = match (base.as_ptr() as usize).checked_add(page_granule) {
Some(address) => address,
None => {
let _ = unmap_guarded(base, total_len);
return Err(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
});
}
};
let data = match NonNull::new(data_addr as *mut u8) {
Some(data) => data,
None => {
let _ = unmap_guarded(base, total_len);
return Err(GuardPageError {
operation: GuardPageOperation::Map,
errno: 0,
});
}
};
if let Err(error) = protect_data(data, writable_len) {
let _ = unmap_guarded(base, total_len);
return Err(error);
}
#[cfg(feature = "memory-lock")]
if locked {
if let Err(error) = mark_dontdump(data, writable_len) {
let _ = unmap_guarded(base, total_len);
return Err(error);
}
if let Err(error) = mark_dontfork(data, writable_len) {
let _ = unmap_guarded(base, total_len);
return Err(error);
}
if let Err(error) = lock_mapping(data, writable_len) {
let _ = unmap_guarded(base, total_len);
return Err(error);
}
}
let mut secret = Self {
base,
data,
map_len: total_len,
writable_len,
data_capacity,
len: 0,
locked,
#[cfg(feature = "random-canary")]
canary,
};
secret.write_canaries();
Ok(secret)
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, GuardPageError> {
let mut secret = Self::with_capacity(bytes.len())?;
secret.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
secret.finish_initialization(bytes.len());
Ok(secret)
}
pub fn from_fn(
len: usize,
mut make_byte: impl FnMut(usize) -> u8,
) -> Result<Self, GuardPageError> {
let mut secret = Self::with_capacity(len)?;
secret.fill_from_fn(len, &mut make_byte);
Ok(secret)
}
pub fn try_from_fn<E>(
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, GuardedSecretVecGenerateError<E>> {
let mut secret = Self::with_capacity(len)?;
secret
.fill_from_try_fn(len, &mut make_byte)
.map_err(GuardedSecretVecGenerateError::Generate)?;
Ok(secret)
}
#[cfg(feature = "memory-lock")]
pub fn locked_from_slice(bytes: &[u8]) -> Result<Self, GuardPageError> {
let mut secret = Self::locked_with_capacity(bytes.len())?;
secret.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
secret.finish_initialization(bytes.len());
Ok(secret)
}
#[cfg(feature = "memory-lock")]
pub fn locked_from_fn(
len: usize,
mut make_byte: impl FnMut(usize) -> u8,
) -> Result<Self, GuardPageError> {
let mut secret = Self::locked_with_capacity(len)?;
secret.fill_from_fn(len, &mut make_byte);
Ok(secret)
}
#[cfg(feature = "memory-lock")]
pub fn locked_try_from_fn<E>(
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, GuardedSecretVecGenerateError<E>> {
let mut secret = Self::locked_with_capacity(len)?;
secret
.fill_from_try_fn(len, &mut make_byte)
.map_err(GuardedSecretVecGenerateError::Generate)?;
Ok(secret)
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
#[inline]
pub const fn capacity(&self) -> usize {
self.data_capacity
}
#[must_use]
#[inline]
pub const fn is_memory_locked(&self) -> bool {
self.locked
}
#[inline]
pub fn with_secret<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
self.assert_canaries_intact();
inspect(self.as_slice())
}
#[inline]
pub fn with_secret_mut<R>(&mut self, edit: impl FnOnce(&mut [u8]) -> R) -> R {
self.assert_canaries_intact();
edit(self.as_mut_slice())
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn expose_secret_checked<R>(
&self,
inspect: impl FnOnce(&[u8]) -> R,
) -> Result<R, crate::CanaryCorruptedError> {
self.verify_integrity()?;
Ok(inspect(self.as_slice()))
}
pub fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), GuardPageError> {
self.assert_canaries_intact();
let required = self.len.checked_add(bytes.len()).ok_or(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
})?;
if required > self.data_capacity {
self.grow_to(required)?;
}
let start = self.len;
let end = required;
self.as_mut_capacity_slice()[start..end].copy_from_slice(bytes);
self.finish_initialization(required);
Ok(())
}
pub fn replace_from_slice(&mut self, bytes: &[u8]) -> Result<(), GuardPageError> {
self.assert_canaries_intact();
if bytes.len() > self.data_capacity {
let mut replacement = Self::with_capacity_locked_state(bytes.len(), self.locked)?;
replacement.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
replacement.finish_initialization(bytes.len());
self.clear_secret();
core::mem::swap(self, &mut replacement);
return Ok(());
}
self.clear_secret();
self.as_mut_capacity_slice()[..bytes.len()].copy_from_slice(bytes);
self.finish_initialization(bytes.len());
Ok(())
}
pub fn replace_from_fn(
&mut self,
len: usize,
mut make_byte: impl FnMut(usize) -> u8,
) -> Result<(), GuardPageError> {
self.assert_canaries_intact();
let mut replacement = Self::with_capacity_locked_state(len, self.locked)?;
replacement.fill_from_fn(len, &mut make_byte);
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
pub fn try_replace_from_fn<E>(
&mut self,
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), GuardedSecretVecGenerateError<E>> {
self.assert_canaries_intact();
let mut replacement = Self::with_capacity_locked_state(len, self.locked)?;
replacement
.fill_from_try_fn(len, &mut make_byte)
.map_err(GuardedSecretVecGenerateError::Generate)?;
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline(never)]
pub fn clear_secret(&mut self) {
crate::wipe::volatile_wipe(self.data.as_ptr(), self.writable_len);
self.len = 0;
self.write_canaries();
}
#[inline]
pub fn into_cleared(mut self) {
self.clear_secret();
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn clear_secret_and_flush(&mut self) {
self.clear_secret();
crate::cache_flush::flush_cache_lines(self.as_capacity_slice());
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
self.assert_canaries_intact();
crate::constant_time_eq_slices(self.as_slice(), other)
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn constant_time_eq_checked(
&self,
other: &[u8],
) -> Result<bool, crate::CanaryCorruptedError> {
self.verify_integrity()?;
Ok(crate::constant_time_eq_slices(self.as_slice(), other))
}
#[cfg(feature = "canary-check")]
#[inline]
pub fn verify_integrity(&self) -> Result<(), crate::CanaryCorruptedError> {
if self.canaries_intact() {
Ok(())
} else {
self.clear_after_canary_failure();
Err(crate::CanaryCorruptedError)
}
}
fn grow_to(&mut self, required: usize) -> Result<(), GuardPageError> {
self.assert_canaries_intact();
let page_granule = platform_page_granule();
let next_capacity = self
.data_capacity
.saturating_mul(2)
.max(required)
.max(page_granule);
let mut replacement = Self::with_capacity_locked_state(next_capacity, self.locked)?;
replacement.as_mut_capacity_slice()[..self.len].copy_from_slice(self.as_slice());
replacement.finish_initialization(self.len);
self.clear_secret();
core::mem::swap(self, &mut replacement);
Ok(())
}
fn fill_from_fn(&mut self, len: usize, make_byte: &mut impl FnMut(usize) -> u8) {
debug_assert!(len <= self.data_capacity);
let capacity = self.as_mut_capacity_slice();
let mut index = 0;
while index < len {
capacity[index] = make_byte(index);
index += 1;
}
self.finish_initialization(len);
}
fn fill_from_try_fn<E>(
&mut self,
len: usize,
make_byte: &mut impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
debug_assert!(len <= self.data_capacity);
let capacity = self.as_mut_capacity_slice();
let mut index = 0;
while index < len {
capacity[index] = make_byte(index)?;
index += 1;
}
self.finish_initialization(len);
Ok(())
}
#[inline]
fn finish_initialization(&mut self, len: usize) {
debug_assert!(len <= self.data_capacity);
self.len = len;
self.write_canaries();
compiler_fence(Ordering::SeqCst);
}
#[inline]
fn as_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.payload_ptr(), self.len) }
}
#[inline]
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.payload_ptr(), self.len) }
}
#[inline]
fn as_mut_capacity_slice(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.payload_ptr(), self.data_capacity) }
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline]
fn as_capacity_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.data.as_ptr(), self.writable_len) }
}
#[inline]
fn payload_ptr(&self) -> *mut u8 {
unsafe { self.data.as_ptr().add(Self::payload_offset()) }
}
#[cfg(feature = "canary-check")]
#[inline]
const fn payload_offset() -> usize {
CANARY_SIZE
}
#[cfg(not(feature = "canary-check"))]
#[inline]
const fn payload_offset() -> usize {
0
}
#[cfg(all(feature = "canary-check", not(feature = "random-canary")))]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
((self.data.as_ptr() as u64) ^ CANARY_MASK).to_ne_bytes()
}
#[cfg(feature = "random-canary")]
#[inline]
fn canary_value(&self) -> [u8; CANARY_SIZE] {
self.canary
}
#[cfg(feature = "canary-check")]
#[inline]
fn canaries_intact(&self) -> bool {
let expected = self.canary_value();
let prefix = unsafe { core::slice::from_raw_parts(self.data.as_ptr(), CANARY_SIZE) };
let suffix = unsafe {
core::slice::from_raw_parts(
self.data.as_ptr().add(CANARY_SIZE + self.len),
CANARY_SIZE,
)
};
crate::constant_time_eq_slices(prefix, &expected)
& crate::constant_time_eq_slices(suffix, &expected)
}
#[cfg(feature = "canary-check")]
#[inline]
fn write_canaries(&mut self) {
let canary = self.canary_value();
unsafe {
core::ptr::copy_nonoverlapping(canary.as_ptr(), self.data.as_ptr(), CANARY_SIZE);
core::ptr::copy_nonoverlapping(
canary.as_ptr(),
self.data.as_ptr().add(CANARY_SIZE + self.len),
CANARY_SIZE,
);
}
compiler_fence(Ordering::SeqCst);
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn write_canaries(&mut self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn assert_canaries_intact(&self) {
if self.verify_integrity().is_err() {
panic!("guarded secret canary corrupted");
}
}
#[cfg(not(feature = "canary-check"))]
#[inline]
fn assert_canaries_intact(&self) {}
#[cfg(feature = "canary-check")]
#[inline]
fn clear_after_canary_failure(&self) {
crate::wipe::volatile_wipe(self.data.as_ptr(), self.writable_len);
}
#[cfg(all(test, feature = "canary-check", feature = "std"))]
#[inline]
pub(crate) fn corrupt_suffix_canary_for_test(&mut self) {
unsafe {
let byte = self.data.as_ptr().add(CANARY_SIZE + self.len);
core::ptr::write(byte, core::ptr::read(byte) ^ 0xFF);
}
}
}
impl Drop for GuardedSecretVec {
#[inline]
fn drop(&mut self) {
self.clear_secret();
#[cfg(feature = "memory-lock")]
if self.locked {
let _ = unlock_mapping(self.data, self.writable_len);
}
let _ = unmap_guarded(self.base, self.map_len);
}
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
impl crate::cache_flush::CacheFlushSanitize for GuardedSecretVec {
#[inline(never)]
fn cache_flush_sanitize(&mut self) {
self.clear_secret_and_flush();
}
}
impl crate::SecureSanitize for GuardedSecretVec {
#[inline]
fn secure_sanitize(&mut self) {
self.clear_secret();
}
}
impl fmt::Debug for GuardedSecretVec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("GuardedSecretVec")
.field("len", &self.len)
.field("capacity", &self.data_capacity)
.field("writable_len", &self.writable_len)
.field("memory_locked", &self.locked)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "random-canary")]
fn random_canary_value() -> Result<[u8; CANARY_SIZE], GuardPageError> {
let mut canary = [0; CANARY_SIZE];
crate::canary_random::fill(&mut canary).map_err(|errno| GuardPageError {
operation: GuardPageOperation::Random,
errno,
})?;
Ok(canary)
}
fn rounded_data_len(len: usize) -> Result<usize, GuardPageError> {
let page_granule = platform_page_granule();
len.max(1)
.checked_add(page_granule - 1)
.map(|value| value & !(page_granule - 1))
.ok_or(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
})
}
#[cfg(feature = "canary-check")]
fn guarded_payload_capacity(requested: usize) -> Result<usize, GuardPageError> {
let requested_with_canaries =
requested
.checked_add(guarded_extra_len())
.ok_or(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
})?;
rounded_data_len(requested_with_canaries).map(|writable_len| {
writable_len
.saturating_sub(guarded_extra_len())
.max(requested)
})
}
#[cfg(not(feature = "canary-check"))]
fn guarded_payload_capacity(requested: usize) -> Result<usize, GuardPageError> {
rounded_data_len(requested)
}
#[cfg(feature = "canary-check")]
fn guarded_writable_len(payload_capacity: usize) -> Result<usize, GuardPageError> {
payload_capacity
.checked_add(guarded_extra_len())
.ok_or(GuardPageError {
operation: GuardPageOperation::Length,
errno: 0,
})
}
#[cfg(not(feature = "canary-check"))]
fn guarded_writable_len(payload_capacity: usize) -> Result<usize, GuardPageError> {
Ok(payload_capacity)
}
#[cfg(feature = "canary-check")]
#[inline]
const fn guarded_extra_len() -> usize {
CANARY_SIZE * 2
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[inline]
const fn platform_page_granule() -> usize {
LINUX_PAGE_GRANULE
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
#[inline]
fn platform_page_granule() -> usize {
crate::linux_aarch64_page_size::detect_page_granule()
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
#[inline]
fn platform_page_granule() -> usize {
let page_size = unsafe { getpagesize() };
if page_size > 0 && (page_size as usize).is_power_of_two() {
page_size as usize
} else {
UNIX_FALLBACK_PAGE_GRANULE
}
}
#[cfg(target_os = "windows")]
#[inline]
fn platform_page_granule() -> usize {
let mut info = MaybeUninit::<SystemInfo>::zeroed();
unsafe {
GetSystemInfo(info.as_mut_ptr());
let page_size = info.assume_init().page_size as usize;
if page_size != 0 && page_size.is_power_of_two() {
page_size
} else {
WINDOWS_FALLBACK_PAGE_GRANULE
}
}
}
#[cfg(target_os = "linux")]
fn syscall_failed(ret: isize) -> bool {
(-4095..=-1).contains(&ret)
}
#[cfg(target_os = "linux")]
fn syscall_error(operation: GuardPageOperation, ret: isize) -> GuardPageError {
GuardPageError {
operation,
errno: (-ret) as i32,
}
}
#[cfg(target_os = "windows")]
fn windows_error(operation: GuardPageOperation) -> GuardPageError {
let errno = unsafe { GetLastError() } as i32;
GuardPageError { operation, errno }
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unix_error(operation: GuardPageOperation) -> GuardPageError {
GuardPageError {
operation,
errno: unix_errno(),
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unix_errno() -> i32 {
unsafe { *errno_location() as i32 }
}
#[cfg(target_os = "linux")]
fn map_guarded(len: usize) -> Result<NonNull<u8>, GuardPageError> {
let ret = raw_syscall6(
SYS_MMAP,
0,
len,
PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS,
usize::MAX,
0,
);
if syscall_failed(ret) {
return Err(syscall_error(GuardPageOperation::Map, ret));
}
NonNull::new(ret as *mut u8).ok_or(GuardPageError {
operation: GuardPageOperation::Map,
errno: 0,
})
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn map_guarded(len: usize) -> Result<NonNull<u8>, GuardPageError> {
let ptr = unsafe {
mmap(
core::ptr::null_mut(),
len,
PROT_NONE as i32,
(MAP_PRIVATE | MAP_ANONYMOUS) as i32,
-1,
0,
)
};
if ptr as isize == -1 {
return Err(unix_error(GuardPageOperation::Map));
}
NonNull::new(ptr.cast::<u8>()).ok_or(GuardPageError {
operation: GuardPageOperation::Map,
errno: 0,
})
}
#[cfg(target_os = "windows")]
fn map_guarded(len: usize) -> Result<NonNull<u8>, GuardPageError> {
let ptr = unsafe {
VirtualAlloc(
core::ptr::null_mut(),
len,
MEM_COMMIT | MEM_RESERVE,
PAGE_NOACCESS,
)
};
NonNull::new(ptr.cast::<u8>()).ok_or_else(|| windows_error(GuardPageOperation::Map))
}
#[cfg(target_os = "linux")]
fn protect_data(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall3(
SYS_MPROTECT,
ptr.as_ptr() as usize,
len,
PROT_READ | PROT_WRITE,
);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::Protect, ret))
} else {
Ok(())
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn protect_data(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe {
mprotect(
ptr.as_ptr().cast::<c_void>(),
len,
(PROT_READ | PROT_WRITE) as i32,
)
};
if ret != 0 {
Err(unix_error(GuardPageOperation::Protect))
} else {
Ok(())
}
}
#[cfg(target_os = "windows")]
fn protect_data(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let mut old_protect = 0_u32;
let ret = unsafe {
VirtualProtect(
ptr.as_ptr().cast::<c_void>(),
len,
PAGE_READWRITE,
&mut old_protect,
)
};
if ret == 0 {
Err(windows_error(GuardPageOperation::Protect))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", target_os = "linux"))]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall2(SYS_MLOCK, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::Lock, ret))
} else {
Ok(())
}
}
#[cfg(all(
feature = "memory-lock",
any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
)
))]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { mlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(GuardPageOperation::Lock))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", target_os = "windows"))]
fn lock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { VirtualLock(ptr.as_ptr().cast::<c_void>(), len) };
if ret == 0 {
Err(windows_error(GuardPageOperation::Lock))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", target_os = "linux"))]
fn mark_dontdump(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall3(SYS_MADVISE, ptr.as_ptr() as usize, len, MADV_DONTDUMP);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::DontDump, ret))
} else {
Ok(())
}
}
#[cfg(all(
feature = "memory-lock",
not(target_os = "linux"),
not(target_os = "freebsd")
))]
#[inline]
fn mark_dontdump(_ptr: NonNull<u8>, _len: usize) -> Result<(), GuardPageError> {
Ok(())
}
#[cfg(all(feature = "memory-lock", target_os = "freebsd"))]
fn mark_dontdump(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { madvise(ptr.as_ptr().cast::<c_void>(), len, MADV_NOCORE) };
if ret != 0 {
Err(unix_error(GuardPageOperation::DontDump))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", target_os = "linux"))]
fn mark_dontfork(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall3(SYS_MADVISE, ptr.as_ptr() as usize, len, MADV_DONTFORK);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::DontFork, ret))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", not(target_os = "linux")))]
#[inline]
fn mark_dontfork(_ptr: NonNull<u8>, _len: usize) -> Result<(), GuardPageError> {
Ok(())
}
#[cfg(all(feature = "memory-lock", target_os = "linux"))]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall2(SYS_MUNLOCK, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::Unlock, ret))
} else {
Ok(())
}
}
#[cfg(all(
feature = "memory-lock",
any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
)
))]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { munlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(GuardPageOperation::Unlock))
} else {
Ok(())
}
}
#[cfg(all(feature = "memory-lock", target_os = "windows"))]
fn unlock_mapping(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { VirtualUnlock(ptr.as_ptr().cast::<c_void>(), len) };
if ret == 0 {
Err(windows_error(GuardPageOperation::Unlock))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn unmap_guarded(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = raw_syscall2(SYS_MUNMAP, ptr.as_ptr() as usize, len);
if syscall_failed(ret) {
Err(syscall_error(GuardPageOperation::Unmap, ret))
} else {
Ok(())
}
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
))]
fn unmap_guarded(ptr: NonNull<u8>, len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { munmap(ptr.as_ptr().cast::<c_void>(), len) };
if ret != 0 {
Err(unix_error(GuardPageOperation::Unmap))
} else {
Ok(())
}
}
#[cfg(target_os = "windows")]
fn unmap_guarded(ptr: NonNull<u8>, _len: usize) -> Result<(), GuardPageError> {
let ret = unsafe { VirtualFree(ptr.as_ptr().cast::<c_void>(), 0, MEM_RELEASE) };
if ret == 0 {
Err(windows_error(GuardPageOperation::Unmap))
} else {
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall2(number: usize, arg1: usize, arg2: usize) -> isize {
raw_syscall6(number, arg1, arg2, 0, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_syscall6(
number: usize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
) -> isize {
let ret: isize;
unsafe {
asm!(
"syscall",
inlateout("rax") number as isize => ret,
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
in("r10") arg4,
in("r8") arg5,
in("r9") arg6,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
}
ret
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall2(number: usize, arg1: usize, arg2: usize) -> isize {
raw_syscall6(number, arg1, arg2, 0, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> isize {
raw_syscall6(number, arg1, arg2, arg3, 0, 0, 0)
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
fn raw_syscall6(
number: usize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
) -> isize {
let ret: isize;
unsafe {
asm!(
"svc 0",
inlateout("x0") arg1 as isize => ret,
in("x1") arg2,
in("x2") arg3,
in("x3") arg4,
in("x4") arg5,
in("x5") arg6,
in("x8") number,
options(nostack)
);
}
ret
}
}
#[cfg(all(
feature = "guard-pages",
any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos",
target_os = "ios",
target_os = "android",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
),
not(miri)
))]
pub use guard_pages::{
GuardPageError, GuardPageOperation, GuardedSecretVec, GuardedSecretVecGenerateError,
};
impl<T: SecureSanitize> SecureSanitize for [T] {
#[inline(never)]
fn secure_sanitize(&mut self) {
for item in self.iter_mut() {
item.secure_sanitize();
}
compiler_fence(Ordering::SeqCst);
}
}
impl<T: SecureSanitize, const N: usize> SecureSanitize for [T; N] {
#[inline(never)]
fn secure_sanitize(&mut self) {
self.as_mut_slice().secure_sanitize();
}
}
impl<T: SecureSanitize> SecureSanitize for Option<T> {
#[inline]
fn secure_sanitize(&mut self) {
if let Some(value) = self.as_mut() {
value.secure_sanitize();
}
*self = None;
compiler_fence(Ordering::SeqCst);
}
}
impl<T: SecureSanitize, E: SecureSanitize> SecureSanitize for Result<T, E> {
#[inline]
fn secure_sanitize(&mut self) {
match self {
Ok(value) => value.secure_sanitize(),
Err(error) => error.secure_sanitize(),
}
compiler_fence(Ordering::SeqCst);
}
}
impl<T> SecureSanitize for PhantomData<T> {
#[inline]
fn secure_sanitize(&mut self) {}
}
#[cfg(feature = "alloc")]
impl<T: SecureSanitize + ?Sized> SecureSanitize for Box<T> {
#[inline]
fn secure_sanitize(&mut self) {
self.as_mut().secure_sanitize();
}
}
#[cfg(feature = "alloc")]
impl<T: SecureSanitize> SecureSanitize for Vec<T> {
#[inline]
fn secure_sanitize(&mut self) {
for item in self.iter_mut() {
item.secure_sanitize();
}
self.clear();
wipe::volatile_wipe(
self.as_mut_ptr().cast::<u8>(),
self.capacity().saturating_mul(core::mem::size_of::<T>()),
);
compiler_fence(Ordering::SeqCst);
}
}
#[cfg(feature = "alloc")]
impl SecureSanitize for String {
#[inline(never)]
fn secure_sanitize(&mut self) {
wipe::volatile_wipe(self.as_mut_ptr(), self.capacity());
self.clear();
}
}
#[inline]
fn constant_time_eq_slices(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
constant_time_eq_equal_len(left, right)
}
#[inline]
fn constant_time_eq_equal_len(left: &[u8], right: &[u8]) -> bool {
debug_assert_eq!(left.len(), right.len());
#[cfg(all(feature = "asm-compare", target_arch = "x86_64", not(miri)))]
{
compare_asm::constant_time_eq_equal_len(left, right)
}
#[cfg(not(all(feature = "asm-compare", target_arch = "x86_64", not(miri))))]
{
portable_constant_time_eq_equal_len(left, right)
}
}
#[inline]
#[cfg_attr(
all(feature = "asm-compare", target_arch = "x86_64", not(miri)),
allow(dead_code)
)]
fn portable_constant_time_eq_equal_len(left: &[u8], right: &[u8]) -> bool {
debug_assert_eq!(left.len(), right.len());
let mut diff = 0usize;
let mut index = 0;
while index < left.len() {
diff = black_box(diff | usize::from(left[index] ^ right[index]));
index += 1;
}
black_box(diff) == 0
}
#[cfg(kani)]
mod kani_verification {
use super::*;
#[kani::proof]
fn prove_sanitize_bytes_clears_fixed_buffer() {
let mut bytes: [u8; 4] = kani::any();
sanitize_bytes(&mut bytes);
assert_eq!(bytes, [0; 4]);
}
#[kani::proof]
fn prove_secret_bytes_clear_erases_visible_contents() {
let source: [u8; 4] = kani::any();
let mut secret = SecretBytes::<4>::from_array(source);
let mut output = [0xA5; 4];
secret.secure_clear();
assert!(secret.copy_to_slice(&mut output).is_ok());
assert_eq!(output, [0; 4]);
}
#[kani::proof]
fn prove_secret_bytes_constant_time_eq_matches_byte_equality() {
let left: [u8; 4] = kani::any();
let right: [u8; 4] = kani::any();
let secret = SecretBytes::<4>::from_array(left);
let mut expected = true;
let mut index = 0;
while index < 4 {
expected &= left[index] == right[index];
index += 1;
}
assert_eq!(secret.constant_time_eq(&right), expected);
}
#[kani::proof]
fn prove_constant_time_eq_rejects_length_mismatch() {
let left: [u8; 4] = kani::any();
let right: [u8; 3] = kani::any();
assert!(!constant_time_eq_slices(&left, &right));
}
#[kani::proof]
#[cfg(feature = "alloc")]
fn prove_next_secret_capacity_never_under_allocates() {
let current: usize = kani::any();
let required: usize = kani::any();
let capacity = next_secret_capacity(current, required);
assert!(capacity >= required);
assert!(capacity >= 8);
}
}
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(self.bytes);
}
}
struct VolatileTemporaryBytes<'a, const N: usize> {
bytes: &'a mut [u8; N],
}
impl<const N: usize> Drop for VolatileTemporaryBytes<'_, N> {
#[inline]
fn drop(&mut self) {
crate::unsafe_wipe::volatile_sanitize_array(self.bytes);
}
}
pub struct SecretBytes<const N: usize> {
bytes: [u8; N],
}
impl<const N: usize> SecretBytes<N> {
#[must_use]
#[inline]
pub const fn zeroed() -> Self {
Self { bytes: [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(&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
}
#[inline]
pub fn try_from_fn<E>(mut make_byte: impl FnMut(usize) -> Result<u8, E>) -> Result<Self, E> {
let mut secret = Self::zeroed();
let mut index = 0;
while index < N {
match make_byte(index) {
Ok(byte) => secret.store(index, byte),
Err(error) => return Err(error),
}
index += 1;
}
secret.after_secret_write();
Ok(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 replace_from_array(&mut self, mut bytes: [u8; N]) {
for (index, byte) in bytes.iter().copied().enumerate() {
self.store(index, byte);
}
self.after_secret_write();
sanitize_bytes(&mut bytes);
}
#[inline]
pub fn replace_from_fn(&mut self, make_byte: impl FnMut(usize) -> u8) {
let mut replacement = Self::from_fn(make_byte);
self.secure_clear();
core::mem::swap(self, &mut replacement);
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
let mut replacement = Self::try_from_fn(make_byte)?;
self.secure_clear();
core::mem::swap(self, &mut replacement);
Ok(())
}
#[inline]
pub fn transform(&mut self, edit: impl FnOnce(&mut [u8; N])) {
edit(&mut self.bytes);
self.after_secret_write();
}
#[inline]
pub fn try_transform<E>(
&mut self,
edit: impl FnOnce(&mut [u8; N]) -> Result<(), E>,
) -> Result<(), E> {
edit(&mut self.bytes)?;
self.after_secret_write();
Ok(())
}
#[must_use]
#[inline]
pub fn derive<const M: usize>(
&self,
derive: impl FnOnce(&[u8; N], &mut [u8; M]),
) -> SecretBytes<M> {
let mut output = SecretBytes::<M>::zeroed();
derive(&self.bytes, &mut output.bytes);
output.after_secret_write();
output
}
#[inline]
pub fn try_derive<const M: usize, E>(
&self,
derive: impl FnOnce(&[u8; N], &mut [u8; M]) -> Result<(), E>,
) -> Result<SecretBytes<M>, E> {
let mut output = SecretBytes::<M>::zeroed();
derive(&self.bytes, &mut output.bytes)?;
output.after_secret_write();
Ok(output)
}
#[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);
black_box(destination);
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
}
#[inline]
pub fn expose_secret_volatile<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 = VolatileTemporaryBytes {
bytes: &mut temporary,
};
let result = inspect(guard.bytes);
crate::unsafe_wipe::volatile_sanitize_array(guard.bytes);
result
}
#[must_use]
#[inline]
pub fn constant_time_eq(&self, other: &[u8]) -> bool {
constant_time_eq_slices(self.bytes.as_slice(), other)
}
#[must_use]
#[inline]
pub fn constant_time_eq_secret(&self, other: &Self) -> bool {
constant_time_eq_equal_len(self.bytes.as_slice(), other.bytes.as_slice())
}
#[inline(never)]
pub fn secure_clear(&mut self) {
wipe::volatile_wipe(self.bytes.as_mut_ptr(), N);
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub fn secure_clear_multi_pass(&mut self) {
wipe::volatile_multi_pass_clear(self.bytes.as_mut_ptr(), N);
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn secure_clear_and_flush(&mut self) {
crate::cache_flush::cache_flush_sanitize_bytes(self.bytes.as_mut_slice());
}
#[inline]
fn load(&self, index: usize) -> u8 {
self.bytes[index]
}
#[inline]
fn store(&mut self, index: usize, value: u8) {
self.bytes[index] = value;
}
#[inline]
fn after_secret_write(&self) {
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 = "split-secret")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SplitSecretError {
TooFewShares,
}
#[cfg(feature = "split-secret")]
impl fmt::Display for SplitSecretError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFewShares => formatter.write_str("split secrets require at least two shares"),
}
}
}
#[cfg(all(feature = "split-secret", feature = "std"))]
impl std::error::Error for SplitSecretError {}
#[cfg(feature = "split-secret")]
pub struct SplitSecretBytes<const N: usize, const SHARES: usize> {
shares: [SecretBytes<N>; SHARES],
}
#[cfg(feature = "split-secret")]
impl<const N: usize, const SHARES: usize> SplitSecretBytes<N, SHARES> {
pub fn from_array_with_generator(
mut secret: [u8; N],
mut make_mask_byte: impl FnMut(usize, usize) -> u8,
) -> Result<Self, SplitSecretError> {
let guard = TemporaryBytes { bytes: &mut secret };
if SHARES < 2 {
return Err(SplitSecretError::TooFewShares);
}
let split = Self::from_secret_bytes_with_generator(guard.bytes, &mut make_mask_byte);
sanitize_bytes(guard.bytes);
Ok(split)
}
pub fn from_secret_with_generator(
secret: &SecretBytes<N>,
mut make_mask_byte: impl FnMut(usize, usize) -> u8,
) -> Result<Self, SplitSecretError> {
if SHARES < 2 {
return Err(SplitSecretError::TooFewShares);
}
Ok(Self::from_secret_bytes_with_generator(
&secret.bytes,
&mut make_mask_byte,
))
}
#[must_use]
pub fn reconstruct(&self) -> SecretBytes<N> {
let mut output = SecretBytes::<N>::zeroed();
let mut byte_index = 0;
while byte_index < N {
let mut value = 0;
let mut share_index = 0;
while share_index < SHARES {
value ^= self.shares[share_index].load(byte_index);
share_index += 1;
}
output.store(byte_index, value);
byte_index += 1;
}
output.after_secret_write();
output
}
#[must_use]
#[inline]
pub const fn shares(&self) -> &[SecretBytes<N>; SHARES] {
&self.shares
}
#[must_use]
#[inline]
pub fn share(&self, index: usize) -> Option<&SecretBytes<N>> {
self.shares.get(index)
}
#[must_use]
#[inline]
pub fn into_shares(self) -> [SecretBytes<N>; SHARES] {
self.shares
}
fn from_secret_bytes_with_generator(
secret: &[u8; N],
make_mask_byte: &mut impl FnMut(usize, usize) -> u8,
) -> Self {
let mut shares = core::array::from_fn(|_| SecretBytes::<N>::zeroed());
let mut byte_index = 0;
while byte_index < N {
let mut accumulator = 0;
let mut share_index = 0;
while share_index + 1 < SHARES {
let mask = make_mask_byte(share_index, byte_index);
shares[share_index].store(byte_index, mask);
accumulator ^= mask;
share_index += 1;
}
shares[SHARES - 1].store(byte_index, secret[byte_index] ^ accumulator);
byte_index += 1;
}
debug_assert!(
!Self::mask_shares_are_trivially_constant(&shares),
"split-secret mask shares are constant; use cryptographically random mask bytes"
);
for share in shares.iter() {
share.after_secret_write();
}
Self { shares }
}
#[inline]
fn mask_shares_are_trivially_constant(shares: &[SecretBytes<N>; SHARES]) -> bool {
if N < 2 {
return false;
}
let mut share_index = 0;
while share_index + 1 < SHARES {
let first = shares[share_index].load(0);
let mut byte_index = 1;
let mut all_same = true;
while byte_index < N {
all_same &= shares[share_index].load(byte_index) == first;
byte_index += 1;
}
if all_same {
return true;
}
share_index += 1;
}
false
}
}
#[cfg(feature = "split-secret")]
impl<const N: usize, const SHARES: usize> SecureSanitize for SplitSecretBytes<N, SHARES> {
#[inline]
fn secure_sanitize(&mut self) {
self.shares.secure_sanitize();
}
}
#[cfg(feature = "split-secret")]
impl<const N: usize, const SHARES: usize> fmt::Debug for SplitSecretBytes<N, SHARES> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SplitSecretBytes")
.field("len", &N)
.field("shares", &SHARES)
.field("contents", &"<redacted>")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SecretExpiredError;
impl fmt::Display for SecretExpiredError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("secret has expired")
}
}
#[cfg(feature = "std")]
impl std::error::Error for SecretExpiredError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExpiringSecretError {
Expired(SecretExpiredError),
Length(LengthError),
}
impl fmt::Display for ExpiringSecretError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Expired(error) => error.fmt(formatter),
Self::Length(error) => error.fmt(formatter),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ExpiringSecretError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Expired(error) => Some(error),
Self::Length(error) => Some(error),
}
}
}
impl From<SecretExpiredError> for ExpiringSecretError {
#[inline]
fn from(error: SecretExpiredError) -> Self {
Self::Expired(error)
}
}
impl From<LengthError> for ExpiringSecretError {
#[inline]
fn from(error: LengthError) -> Self {
Self::Length(error)
}
}
pub trait MonotonicClock {
fn now(&self) -> u64;
}
impl<C: MonotonicClock + ?Sized> MonotonicClock for &C {
#[inline]
fn now(&self) -> u64 {
(**self).now()
}
}
pub struct MonotonicExpiringSecretBytes<const N: usize, C: MonotonicClock> {
inner: SecretBytes<N>,
clock: C,
created_at: u64,
max_age: u64,
}
impl<const N: usize, C: MonotonicClock> MonotonicExpiringSecretBytes<N, C> {
#[must_use]
#[inline]
pub fn zeroed(clock: C, max_age: u64) -> Self {
let created_at = clock.now();
Self {
inner: SecretBytes::zeroed(),
clock,
created_at,
max_age,
}
}
#[must_use]
#[inline]
pub fn from_array(bytes: [u8; N], clock: C, max_age: u64) -> Self {
let created_at = clock.now();
Self {
inner: SecretBytes::from_array(bytes),
clock,
created_at,
max_age,
}
}
#[must_use]
#[inline]
pub fn from_fn(clock: C, max_age: u64, make_byte: impl FnMut(usize) -> u8) -> Self {
let created_at = clock.now();
Self {
inner: SecretBytes::from_fn(make_byte),
clock,
created_at,
max_age,
}
}
#[inline]
pub fn try_from_fn<E>(
clock: C,
max_age: u64,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, E> {
let created_at = clock.now();
Ok(Self {
inner: SecretBytes::try_from_fn(make_byte)?,
clock,
created_at,
max_age,
})
}
#[must_use]
#[inline]
pub fn from_secret(secret: SecretBytes<N>, clock: C, max_age: u64) -> Self {
let created_at = clock.now();
Self {
inner: secret,
clock,
created_at,
max_age,
}
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[must_use]
#[inline]
pub const fn max_age_ticks(&self) -> u64 {
self.max_age
}
#[must_use]
#[inline]
pub fn age_ticks(&self) -> u64 {
self.clock.now().saturating_sub(self.created_at)
}
#[must_use]
#[inline]
pub fn is_expired(&self) -> bool {
self.age_ticks() >= self.max_age
}
#[must_use]
#[inline]
pub const fn clock(&self) -> &C {
&self.clock
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), LengthError> {
if self.is_expired() {
self.inner.secure_clear();
}
self.inner.copy_from_slice(source)?;
self.created_at = self.clock.now();
Ok(())
}
#[inline]
pub fn replace_from_array(&mut self, bytes: [u8; N]) {
if self.is_expired() {
self.inner.secure_clear();
}
self.inner.replace_from_array(bytes);
self.created_at = self.clock.now();
}
#[inline]
pub fn replace_from_fn(&mut self, make_byte: impl FnMut(usize) -> u8) {
let expired = self.is_expired();
if expired {
self.inner.secure_clear();
}
let replacement = SecretBytes::from_fn(make_byte);
if !expired {
self.inner.secure_clear();
}
self.inner = replacement;
self.created_at = self.clock.now();
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
let expired = self.is_expired();
if expired {
self.inner.secure_clear();
}
let replacement = SecretBytes::try_from_fn(make_byte)?;
if !expired {
self.inner.secure_clear();
}
self.inner = replacement;
self.created_at = self.clock.now();
Ok(())
}
#[inline]
pub fn try_copy_to_slice(&mut self, destination: &mut [u8]) -> Result<(), ExpiringSecretError> {
self.enforce_live()?;
self.inner.copy_to_slice(destination).map_err(Into::into)
}
#[inline]
pub fn try_expose_secret<R>(
&mut self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.expose_secret(inspect))
}
#[inline]
pub fn try_expose_secret_volatile<R>(
&mut self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.expose_secret_volatile(inspect))
}
#[inline]
pub fn try_constant_time_eq(&mut self, other: &[u8]) -> Result<bool, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.constant_time_eq(other))
}
#[inline(never)]
pub fn secure_clear(&mut self) {
self.inner.secure_clear();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[inline]
fn enforce_live(&mut self) -> Result<(), SecretExpiredError> {
if self.is_expired() {
self.inner.secure_clear();
Err(SecretExpiredError)
} else {
Ok(())
}
}
}
impl<const N: usize, C: MonotonicClock> Drop for MonotonicExpiringSecretBytes<N, C> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
}
}
impl<const N: usize, C: MonotonicClock> SecureSanitize for MonotonicExpiringSecretBytes<N, C> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize, C: MonotonicClock> fmt::Debug for MonotonicExpiringSecretBytes<N, C> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("MonotonicExpiringSecretBytes")
.field("len", &N)
.field("age_ticks", &self.age_ticks())
.field("max_age_ticks", &self.max_age)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "std")]
pub struct ExpiringSecretBytes<const N: usize> {
inner: SecretBytes<N>,
created_at: std::time::Instant,
max_age: std::time::Duration,
}
#[cfg(feature = "std")]
impl<const N: usize> ExpiringSecretBytes<N> {
#[must_use]
#[inline]
pub fn zeroed(max_age: std::time::Duration) -> Self {
Self {
inner: SecretBytes::zeroed(),
created_at: std::time::Instant::now(),
max_age,
}
}
#[must_use]
#[inline]
pub fn from_array(bytes: [u8; N], max_age: std::time::Duration) -> Self {
Self {
inner: SecretBytes::from_array(bytes),
created_at: std::time::Instant::now(),
max_age,
}
}
#[must_use]
#[inline]
pub fn from_fn(max_age: std::time::Duration, make_byte: impl FnMut(usize) -> u8) -> Self {
Self {
inner: SecretBytes::from_fn(make_byte),
created_at: std::time::Instant::now(),
max_age,
}
}
#[inline]
pub fn try_from_fn<E>(
max_age: std::time::Duration,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, E> {
Ok(Self {
inner: SecretBytes::try_from_fn(make_byte)?,
created_at: std::time::Instant::now(),
max_age,
})
}
#[must_use]
#[inline]
pub fn from_secret(secret: SecretBytes<N>, max_age: std::time::Duration) -> Self {
Self {
inner: secret,
created_at: std::time::Instant::now(),
max_age,
}
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[must_use]
#[inline]
pub const fn max_age(&self) -> std::time::Duration {
self.max_age
}
#[must_use]
#[inline]
pub fn age(&self) -> std::time::Duration {
self.created_at.elapsed()
}
#[must_use]
#[inline]
pub fn is_expired(&self) -> bool {
self.age() >= self.max_age
}
#[inline]
pub fn replace_from_slice(&mut self, source: &[u8]) -> Result<(), LengthError> {
if self.is_expired() {
self.inner.secure_clear();
}
self.inner.copy_from_slice(source)?;
self.created_at = std::time::Instant::now();
Ok(())
}
#[inline]
pub fn replace_from_array(&mut self, bytes: [u8; N]) {
if self.is_expired() {
self.inner.secure_clear();
}
self.inner.replace_from_array(bytes);
self.created_at = std::time::Instant::now();
}
#[inline]
pub fn replace_from_fn(&mut self, make_byte: impl FnMut(usize) -> u8) {
let expired = self.is_expired();
if expired {
self.inner.secure_clear();
}
let replacement = SecretBytes::from_fn(make_byte);
if !expired {
self.inner.secure_clear();
}
self.inner = replacement;
self.created_at = std::time::Instant::now();
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
let expired = self.is_expired();
if expired {
self.inner.secure_clear();
}
let replacement = SecretBytes::try_from_fn(make_byte)?;
if !expired {
self.inner.secure_clear();
}
self.inner = replacement;
self.created_at = std::time::Instant::now();
Ok(())
}
#[inline]
pub fn try_copy_to_slice(&mut self, destination: &mut [u8]) -> Result<(), ExpiringSecretError> {
self.enforce_live()?;
self.inner.copy_to_slice(destination).map_err(Into::into)
}
#[inline]
pub fn try_expose_secret<R>(
&mut self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.expose_secret(inspect))
}
#[inline]
pub fn try_expose_secret_volatile<R>(
&mut self,
inspect: impl FnOnce(&[u8; N]) -> R,
) -> Result<R, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.expose_secret_volatile(inspect))
}
#[inline]
pub fn try_constant_time_eq(&mut self, other: &[u8]) -> Result<bool, SecretExpiredError> {
self.enforce_live()?;
Ok(self.inner.constant_time_eq(other))
}
#[inline(never)]
pub fn secure_clear(&mut self) {
self.inner.secure_clear();
}
#[inline]
pub fn into_cleared(mut self) {
self.secure_clear();
}
#[inline]
fn enforce_live(&mut self) -> Result<(), SecretExpiredError> {
if self.is_expired() {
self.inner.secure_clear();
Err(SecretExpiredError)
} else {
Ok(())
}
}
}
#[cfg(feature = "std")]
impl<const N: usize> Drop for ExpiringSecretBytes<N> {
#[inline]
fn drop(&mut self) {
self.secure_clear();
}
}
#[cfg(feature = "std")]
impl<const N: usize> SecureSanitize for ExpiringSecretBytes<N> {
#[inline]
fn secure_sanitize(&mut self) {
self.secure_clear();
}
}
#[cfg(feature = "std")]
impl<const N: usize> fmt::Debug for ExpiringSecretBytes<N> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ExpiringSecretBytes")
.field("len", &N)
.field("max_age", &self.max_age)
.field("contents", &"<redacted>")
.finish()
}
}
#[cfg(feature = "alloc")]
pub struct SecretVec {
inner: Vec<u8>,
}
#[cfg(feature = "alloc")]
impl SecretVec {
#[must_use]
#[inline]
pub const fn new(inner: Vec<u8>) -> Self {
Self { inner }
}
#[must_use]
#[inline]
pub const fn from_vec(bytes: Vec<u8>) -> Self {
Self::new(bytes)
}
#[must_use]
#[inline]
pub const fn new_volatile(inner: Vec<u8>) -> Self {
Self::new(inner)
}
#[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))
}
#[must_use]
#[inline]
pub fn with_capacity_volatile(capacity: usize) -> Self {
Self::with_capacity(capacity)
}
#[must_use]
#[inline]
pub fn from_slice(bytes: &[u8]) -> Self {
Self::new(Vec::from(bytes))
}
#[must_use]
#[inline]
pub fn from_fn(len: usize, mut make_byte: impl FnMut(usize) -> u8) -> Self {
let mut secret = Self::with_capacity(len);
let mut index = 0;
while index < len {
secret.inner.push(make_byte(index));
index += 1;
}
secret
}
#[inline]
pub fn try_from_fn<E>(
len: usize,
mut make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<Self, E> {
let mut secret = Self::with_capacity(len);
let mut index = 0;
while index < len {
match make_byte(index) {
Ok(byte) => secret.inner.push(byte),
Err(error) => return Err(error),
}
index += 1;
}
Ok(secret)
}
#[must_use]
#[inline]
pub fn from_slice_volatile(bytes: &[u8]) -> Self {
Self::from_slice(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()
}
#[must_use]
#[inline]
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
#[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]
pub fn replace_from_slice(&mut self, bytes: &[u8]) {
if bytes.len() > self.inner.capacity() {
let new_capacity = next_secret_capacity(self.inner.capacity(), bytes.len());
let mut replacement = Vec::with_capacity(new_capacity);
replacement.extend_from_slice(bytes);
self.clear_secret();
self.inner = replacement;
return;
}
self.clear_secret();
self.inner.extend_from_slice(bytes);
}
#[inline]
pub fn replace_from_vec(&mut self, bytes: Vec<u8>) {
self.clear_secret();
self.inner = bytes;
}
#[inline]
pub fn replace_from_fn(&mut self, len: usize, make_byte: impl FnMut(usize) -> u8) {
let mut replacement = Self::from_fn(len, make_byte);
self.clear_secret();
core::mem::swap(&mut self.inner, &mut replacement.inner);
}
#[inline]
pub fn try_replace_from_fn<E>(
&mut self,
len: usize,
make_byte: impl FnMut(usize) -> Result<u8, E>,
) -> Result<(), E> {
let mut replacement = Self::try_from_fn(len, make_byte)?;
self.clear_secret();
core::mem::swap(&mut self.inner, &mut replacement.inner);
Ok(())
}
#[inline(never)]
pub fn clear_secret(&mut self) {
sanitize_vec_capacity(&mut self.inner);
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub fn clear_secret_multi_pass(&mut self) {
sanitize_vec_capacity_multi_pass(&mut self.inner);
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn clear_secret_and_flush(&mut self) {
crate::cache_flush::cache_flush_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 new_capacity = next_secret_capacity(self.inner.capacity(), required);
let mut replacement = Vec::with_capacity(new_capacity);
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 Default for SecretVec {
#[inline]
fn default() -> Self {
Self::empty()
}
}
#[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>,
}
#[cfg(feature = "alloc")]
impl SecretString {
#[must_use]
#[inline]
pub fn new(inner: String) -> Self {
Self {
inner: inner.into_bytes(),
}
}
#[must_use]
#[inline]
pub fn from_string(text: String) -> Self {
Self::new(text)
}
#[must_use]
#[inline]
pub fn new_volatile(inner: String) -> Self {
Self::new(inner)
}
#[must_use]
#[inline]
pub const fn empty() -> Self {
Self { inner: Vec::new() }
}
#[must_use]
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
}
}
#[must_use]
#[inline]
pub fn with_capacity_volatile(capacity: usize) -> Self {
Self::with_capacity(capacity)
}
#[must_use]
#[inline]
pub fn from_secret_str(text: &str) -> Self {
Self {
inner: Vec::from(text.as_bytes()),
}
}
#[must_use]
#[inline]
pub fn from_chars(char_count: usize, mut make_char: impl FnMut(usize) -> char) -> Self {
let mut secret = Self::with_capacity(max_utf8_capacity(char_count));
let mut index = 0;
while index < char_count {
secret.push_secret_char(make_char(index));
index += 1;
}
secret
}
#[inline]
pub fn try_from_chars<E>(
char_count: usize,
mut make_char: impl FnMut(usize) -> Result<char, E>,
) -> Result<Self, E> {
let mut secret = Self::with_capacity(max_utf8_capacity(char_count));
let mut index = 0;
while index < char_count {
match make_char(index) {
Ok(character) => secret.push_secret_char(character),
Err(error) => return Err(error),
}
index += 1;
}
Ok(secret)
}
#[must_use]
#[inline]
pub fn from_secret_str_volatile(text: &str) -> Self {
Self::from_secret_str(text)
}
#[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 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 try_with_secret_mut<R>(
&mut self,
edit: impl FnOnce(&mut str) -> R,
) -> Result<R, Utf8Error> {
core::str::from_utf8_mut(self.inner.as_mut_slice()).map(edit)
}
#[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]
pub fn replace_from_secret_str(&mut self, text: &str) {
if text.len() > self.inner.capacity() {
let new_capacity = next_secret_capacity(self.inner.capacity(), text.len());
let mut replacement = Vec::with_capacity(new_capacity);
replacement.extend_from_slice(text.as_bytes());
self.clear_secret();
self.inner = replacement;
return;
}
self.clear_secret();
self.inner.extend_from_slice(text.as_bytes());
}
#[inline]
pub fn replace_from_string(&mut self, text: String) {
let replacement = text.into_bytes();
self.clear_secret();
self.inner = replacement;
}
#[inline]
pub fn replace_from_chars(&mut self, char_count: usize, make_char: impl FnMut(usize) -> char) {
let mut replacement = Self::from_chars(char_count, make_char);
self.clear_secret();
core::mem::swap(&mut self.inner, &mut replacement.inner);
}
#[inline]
pub fn try_replace_from_chars<E>(
&mut self,
char_count: usize,
make_char: impl FnMut(usize) -> Result<char, E>,
) -> Result<(), E> {
let mut replacement = Self::try_from_chars(char_count, make_char)?;
self.clear_secret();
core::mem::swap(&mut self.inner, &mut replacement.inner);
Ok(())
}
#[inline(never)]
pub fn clear_secret(&mut self) {
sanitize_vec_capacity(&mut self.inner);
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub fn clear_secret_multi_pass(&mut self) {
sanitize_vec_capacity_multi_pass(&mut self.inner);
}
#[cfg(all(feature = "cache-flush", target_arch = "x86_64", not(miri)))]
#[inline(never)]
pub fn clear_secret_and_flush(&mut self) {
crate::cache_flush::cache_flush_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 new_capacity = next_secret_capacity(self.inner.capacity(), required);
let mut replacement = Vec::with_capacity(new_capacity);
replacement.extend_from_slice(self.inner.as_slice());
self.clear_secret();
self.inner = replacement;
}
fn push_secret_char(&mut self, character: char) {
let mut encoded = [0; 4];
let text = character.encode_utf8(&mut encoded);
self.inner.extend_from_slice(text.as_bytes());
sanitize_bytes(&mut encoded);
}
}
#[cfg(feature = "alloc")]
impl Drop for SecretString {
#[inline]
fn drop(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl Default for SecretString {
#[inline]
fn default() -> Self {
Self::empty()
}
}
#[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 + Default> Default for Secret<T> {
#[inline]
fn default() -> Self {
Self::new(T::default())
}
}
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()
}
}
#[allow(unsafe_code)]
mod read_once {
use super::{fmt, SecureSanitize};
use core::{
cell::UnsafeCell,
sync::atomic::{AtomicBool, Ordering},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AlreadyConsumedError;
impl fmt::Display for AlreadyConsumedError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("read-once secret already consumed")
}
}
#[cfg(feature = "std")]
impl std::error::Error for AlreadyConsumedError {}
pub struct ReadOnceSecret<T: SecureSanitize> {
inner: UnsafeCell<T>,
consumed: AtomicBool,
}
unsafe impl<T: SecureSanitize + Send> Send for ReadOnceSecret<T> {}
unsafe impl<T: SecureSanitize + Send> Sync for ReadOnceSecret<T> {}
impl<T: SecureSanitize> ReadOnceSecret<T> {
#[must_use]
#[inline]
pub const fn new(inner: T) -> Self {
Self {
inner: UnsafeCell::new(inner),
consumed: AtomicBool::new(false),
}
}
#[inline]
pub fn consume<R>(&self, inspect: impl FnOnce(&T) -> R) -> Result<R, AlreadyConsumedError> {
self.claim()?;
let result = inspect(unsafe { &*self.inner.get() });
self.clear_inner();
Ok(result)
}
#[inline]
pub fn consume_mut<R>(
&self,
edit: impl FnOnce(&mut T) -> R,
) -> Result<R, AlreadyConsumedError> {
self.claim()?;
let result = edit(unsafe { &mut *self.inner.get() });
self.clear_inner();
Ok(result)
}
#[inline]
pub fn into_cleared(mut self) {
self.consumed.store(true, Ordering::Release);
self.inner.get_mut().secure_sanitize();
}
#[must_use]
#[inline]
pub fn is_consumed(&self) -> bool {
self.consumed.load(Ordering::Acquire)
}
#[inline]
fn claim(&self) -> Result<(), AlreadyConsumedError> {
if self.consumed.swap(true, Ordering::AcqRel) {
Err(AlreadyConsumedError)
} else {
Ok(())
}
}
#[inline]
fn clear_inner(&self) {
unsafe { (&mut *self.inner.get()).secure_sanitize() };
}
}
impl<T: SecureSanitize> Drop for ReadOnceSecret<T> {
#[inline]
fn drop(&mut self) {
self.inner.get_mut().secure_sanitize();
}
}
impl<T: SecureSanitize + Default> Default for ReadOnceSecret<T> {
#[inline]
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: SecureSanitize> SecureSanitize for ReadOnceSecret<T> {
#[inline]
fn secure_sanitize(&mut self) {
self.consumed.store(true, Ordering::Release);
self.inner.get_mut().secure_sanitize();
}
}
impl<T: SecureSanitize> fmt::Debug for ReadOnceSecret<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ReadOnceSecret")
.field("consumed", &self.is_consumed())
.field("contents", &"<redacted>")
.finish()
}
}
}
pub use read_once::{AlreadyConsumedError, ReadOnceSecret};
#[cfg(feature = "zeroize-interop")]
mod zeroize_interop {
use super::*;
impl<const N: usize> zeroize::Zeroize for SecretBytes<N> {
#[inline]
fn zeroize(&mut self) {
self.secure_clear();
}
}
impl<const N: usize> zeroize::ZeroizeOnDrop for SecretBytes<N> {}
#[cfg(feature = "alloc")]
impl zeroize::Zeroize for SecretVec {
#[inline]
fn zeroize(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl zeroize::ZeroizeOnDrop for SecretVec {}
#[cfg(feature = "alloc")]
impl zeroize::Zeroize for SecretString {
#[inline]
fn zeroize(&mut self) {
self.clear_secret();
}
}
#[cfg(feature = "alloc")]
impl zeroize::ZeroizeOnDrop for SecretString {}
impl<T: SecureSanitize> zeroize::Zeroize for Secret<T> {
#[inline]
fn zeroize(&mut self) {
self.inner.secure_sanitize();
}
}
impl<T: SecureSanitize> zeroize::ZeroizeOnDrop for Secret<T> {}
impl<T: SecureSanitize> zeroize::Zeroize for ReadOnceSecret<T> {
#[inline]
fn zeroize(&mut self) {
self.secure_sanitize();
}
}
impl<T: SecureSanitize> zeroize::ZeroizeOnDrop for ReadOnceSecret<T> {}
#[cfg(feature = "split-secret")]
impl<const N: usize, const SHARES: usize> zeroize::Zeroize for SplitSecretBytes<N, SHARES> {
#[inline]
fn zeroize(&mut self) {
self.secure_sanitize();
}
}
#[cfg(feature = "split-secret")]
impl<const N: usize, const SHARES: usize> zeroize::ZeroizeOnDrop for SplitSecretBytes<N, SHARES> {}
#[cfg(feature = "memory-lock")]
impl<const N: usize> zeroize::Zeroize for LockedSecretBytes<N> {
#[inline]
fn zeroize(&mut self) {
self.secure_clear();
}
}
#[cfg(feature = "memory-lock")]
impl<const N: usize> zeroize::ZeroizeOnDrop for LockedSecretBytes<N> {}
#[cfg(all(feature = "memory-lock", not(target_arch = "wasm32"), not(miri)))]
impl zeroize::Zeroize for LockedSecretVec {
#[inline]
fn zeroize(&mut self) {
self.clear_secret();
}
}
#[cfg(all(feature = "memory-lock", not(target_arch = "wasm32"), not(miri)))]
impl zeroize::ZeroizeOnDrop for LockedSecretVec {}
#[cfg(all(feature = "guard-pages", not(miri)))]
impl zeroize::Zeroize for GuardedSecretVec {
#[inline]
fn zeroize(&mut self) {
self.clear_secret();
}
}
#[cfg(all(feature = "guard-pages", not(miri)))]
impl zeroize::ZeroizeOnDrop for GuardedSecretVec {}
}
#[cfg(feature = "subtle-interop")]
mod subtle_interop {
use super::*;
use subtle::{Choice, ConstantTimeEq};
impl<const N: usize> ConstantTimeEq for SecretBytes<N> {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(self.constant_time_eq_secret(other) as u8)
}
}
#[cfg(feature = "alloc")]
impl ConstantTimeEq for SecretVec {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(
self.with_secret(|left| {
other.with_secret(|right| constant_time_eq_slices(left, right))
}) as u8,
)
}
}
#[cfg(feature = "alloc")]
impl ConstantTimeEq for SecretString {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(constant_time_eq_slices(&self.inner, &other.inner) as u8)
}
}
#[cfg(feature = "memory-lock")]
impl<const N: usize> ConstantTimeEq for LockedSecretBytes<N> {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(other.with_secret(|bytes| self.constant_time_eq(bytes)) as u8)
}
}
#[cfg(all(feature = "memory-lock", not(target_arch = "wasm32"), not(miri)))]
impl ConstantTimeEq for LockedSecretVec {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(other.with_secret(|bytes| self.constant_time_eq(bytes)) as u8)
}
}
#[cfg(all(feature = "guard-pages", not(miri)))]
impl ConstantTimeEq for GuardedSecretVec {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Choice::from(other.with_secret(|bytes| self.constant_time_eq(bytes)) as u8)
}
}
}
#[cfg(feature = "serde")]
mod serde_impls {
use super::*;
use serde::{
de::{Error as DeError, IgnoredAny, SeqAccess, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
const REDACTED: &str = "<redacted>";
impl<const N: usize> Serialize for SecretBytes<N> {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(REDACTED)
}
}
impl<'de, const N: usize> Deserialize<'de> for SecretBytes<N> {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_bytes(SecretBytesVisitor::<N>)
}
}
struct SecretBytesVisitor<const N: usize>;
impl<'de, const N: usize> Visitor<'de> for SecretBytesVisitor<N> {
type Value = SecretBytes<N>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "exactly {N} secret bytes")
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: DeError,
{
if bytes.len() != N {
return Err(E::invalid_length(bytes.len(), &self));
}
let mut secret = SecretBytes::<N>::zeroed();
secret.copy_from_slice(bytes).map_err(E::custom)?;
Ok(secret)
}
fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut secret = SecretBytes::<N>::zeroed();
let mut index = 0;
while index < N {
let Some(byte) = sequence.next_element::<u8>()? else {
return Err(A::Error::invalid_length(index, &self));
};
secret.store(index, byte);
index += 1;
}
if sequence.next_element::<IgnoredAny>()?.is_some() {
return Err(A::Error::invalid_length(N.saturating_add(1), &self));
}
secret.after_secret_write();
Ok(secret)
}
}
#[cfg(feature = "alloc")]
impl Serialize for SecretVec {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(REDACTED)
}
}
#[cfg(feature = "alloc")]
impl<'de> Deserialize<'de> for SecretVec {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_bytes(SecretVecVisitor)
}
}
#[cfg(feature = "alloc")]
struct SecretVecVisitor;
#[cfg(feature = "alloc")]
impl<'de> Visitor<'de> for SecretVecVisitor {
type Value = SecretVec;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("secret bytes")
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(SecretVec::from_slice(bytes))
}
fn visit_byte_buf<E>(self, bytes: Vec<u8>) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(SecretVec::from_vec(bytes))
}
fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let capacity = sequence.size_hint().unwrap_or(0);
let mut secret = SecretVec::with_capacity(capacity);
while let Some(byte) = sequence.next_element::<u8>()? {
secret.extend_from_slice(&[byte]);
}
Ok(secret)
}
}
#[cfg(feature = "alloc")]
impl Serialize for SecretString {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(REDACTED)
}
}
#[cfg(feature = "alloc")]
impl<'de> Deserialize<'de> for SecretString {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(SecretStringVisitor)
}
}
#[cfg(feature = "alloc")]
struct SecretStringVisitor;
#[cfg(feature = "alloc")]
impl<'de> Visitor<'de> for SecretStringVisitor {
type Value = SecretString;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("secret UTF-8 text")
}
fn visit_str<E>(self, text: &str) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(SecretString::from_secret_str(text))
}
fn visit_string<E>(self, text: String) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(SecretString::from_string(text))
}
}
impl<T> Serialize for Secret<T>
where
T: SecureSanitize,
{
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(REDACTED)
}
}
impl<'de, T> Deserialize<'de> for Secret<T>
where
T: SecureSanitize + Deserialize<'de>,
{
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(Secret::new)
}
}
impl<T> Serialize for ReadOnceSecret<T>
where
T: SecureSanitize,
{
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(REDACTED)
}
}
impl<'de, T> Deserialize<'de> for ReadOnceSecret<T>
where
T: SecureSanitize + Deserialize<'de>,
{
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(ReadOnceSecret::new)
}
}
}
pub mod unsafe_wipe {
#[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]) {
crate::wipe::volatile_wipe(bytes.as_mut_ptr(), bytes.len());
}
#[cfg(feature = "multi-pass-clear")]
#[inline(never)]
pub fn volatile_sanitize_bytes_multi_pass(bytes: &mut [u8]) {
crate::wipe::volatile_multi_pass_clear(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 = "multi-pass-clear")]
#[inline(never)]
pub fn volatile_sanitize_array_multi_pass<const N: usize>(bytes: &mut [u8; N]) {
volatile_sanitize_bytes_multi_pass(bytes);
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn volatile_sanitize_vec(bytes: &mut Vec<u8>) {
crate::wipe::volatile_wipe(bytes.as_mut_ptr(), bytes.capacity());
bytes.clear();
}
#[cfg(all(feature = "alloc", feature = "multi-pass-clear"))]
#[inline(never)]
pub fn volatile_sanitize_vec_multi_pass(bytes: &mut Vec<u8>) {
crate::wipe::volatile_multi_pass_clear(bytes.as_mut_ptr(), bytes.capacity());
bytes.clear();
}
#[cfg(feature = "alloc")]
#[inline(never)]
pub fn volatile_sanitize_string(text: &mut String) {
crate::wipe::volatile_wipe(text.as_mut_ptr(), text.capacity());
text.clear();
}
#[cfg(all(feature = "alloc", feature = "multi-pass-clear"))]
#[inline(never)]
pub fn volatile_sanitize_string_multi_pass(text: &mut String) {
crate::wipe::volatile_multi_pass_clear(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()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestClock<'a>(&'a core::cell::Cell<u64>);
impl MonotonicClock for TestClock<'_> {
#[inline]
fn now(&self) -> u64 {
self.0.get()
}
}
#[test]
fn length_error_formats_clearly() {
let error = LengthError {
expected: 4,
actual: 2,
};
assert_eq!(
std::format!("{error}"),
"length mismatch: expected 4 bytes, got 2 bytes"
);
}
#[cfg(feature = "zeroize-interop")]
#[test]
fn zeroize_interop_clears_secret_bytes() {
use zeroize::Zeroize;
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
secret.zeroize();
assert_eq!(secret.expose_secret(|bytes| *bytes), [0; 4]);
}
#[cfg(feature = "subtle-interop")]
#[test]
fn subtle_interop_compares_secret_bytes() {
use subtle::ConstantTimeEq;
let left = SecretBytes::<4>::from_array([1, 2, 3, 4]);
let same = SecretBytes::<4>::from_array([1, 2, 3, 4]);
let different = SecretBytes::<4>::from_array([1, 2, 3, 0]);
assert_eq!(left.ct_eq(&same).unwrap_u8(), 1);
assert_eq!(left.ct_eq(&different).unwrap_u8(), 0);
}
#[cfg(feature = "serde")]
#[test]
fn serde_interop_loads_fixed_secret_bytes_and_redacts_output() {
let secret: SecretBytes<4> = serde_json::from_str("[1,2,3,4]").unwrap();
assert_eq!(secret.expose_secret(|bytes| *bytes), [1, 2, 3, 4]);
assert_eq!(serde_json::to_string(&secret).unwrap(), "\"<redacted>\"");
}
#[cfg(all(feature = "serde", feature = "alloc"))]
#[test]
fn serde_interop_loads_alloc_secrets_and_redacts_output() {
let bytes: SecretVec = serde_json::from_str("[1,2,3,4]").unwrap();
let text: SecretString = serde_json::from_str("\"token\"").unwrap();
assert_eq!(bytes.with_secret(|secret| secret.len()), 4);
assert_eq!(text.try_with_secret(str::len), Ok(5));
assert_eq!(serde_json::to_string(&bytes).unwrap(), "\"<redacted>\"");
assert_eq!(serde_json::to_string(&text).unwrap(), "\"<redacted>\"");
}
#[cfg(feature = "std")]
#[test]
fn expiring_error_exposes_source() {
let error = ExpiringSecretError::Length(LengthError {
expected: 4,
actual: 2,
});
assert!(std::error::Error::source(&error).is_some());
}
#[cfg(all(
feature = "std",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
#[test]
fn locked_secret_errors_expose_sources() {
let length = LockedSecretBytesError::Length(LengthError {
expected: 4,
actual: 2,
});
let memory = LockedSecretBytesError::Memory(MemoryLockError {
operation: MemoryLockOperation::Lock,
errno: 12,
});
let generated: LockedSecretBytesGenerateError<std::io::Error> =
LockedSecretBytesGenerateError::Generate(std::io::Error::other("generation failed"));
assert!(std::error::Error::source(&length).is_some());
assert!(std::error::Error::source(&memory).is_some());
assert!(std::error::Error::source(&generated).is_some());
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
#[test]
fn locked_secret_bytes_is_send() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<LockedSecretBytes<4>>();
assert_send::<SecretPool<4, 2>>();
assert_sync::<SecretPool<4, 2>>();
}
#[cfg(all(
feature = "std",
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_errors_expose_sources() {
let guarded: GuardedSecretVecGenerateError<std::io::Error> =
GuardedSecretVecGenerateError::Guard(GuardPageError {
operation: GuardPageOperation::Protect,
errno: 13,
});
let generated: GuardedSecretVecGenerateError<std::io::Error> =
GuardedSecretVecGenerateError::Generate(std::io::Error::other("generation failed"));
assert!(std::error::Error::source(&guarded).is_some());
assert!(std::error::Error::source(&generated).is_some());
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_is_send() {
fn assert_send<T: Send>() {}
assert_send::<GuardedSecretVec>();
}
#[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]);
secret.into_cleared();
}
#[cfg(feature = "multi-pass-clear")]
#[test]
fn secret_bytes_can_clear_multi_pass() {
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
secret.secure_clear_multi_pass();
assert!(secret.constant_time_eq(&[0, 0, 0, 0]));
}
#[test]
fn secret_bytes_can_initialize_from_fallible_fn() {
let mut secret =
SecretBytes::<4>::try_from_fn(|index| Ok::<u8, &'static str>((index as u8) + 1))
.unwrap();
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
SecretBytes::<4>::try_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some("generation failed")
);
secret.secure_clear();
assert!(secret.constant_time_eq(&[0, 0, 0, 0]));
}
#[test]
fn secret_bytes_can_replace_from_fn() {
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
secret.replace_from_array([4, 3, 2, 1]);
assert!(secret.constant_time_eq(&[4, 3, 2, 1]));
secret.replace_from_fn(|index| (index as u8) + 7);
assert!(secret.constant_time_eq(&[7, 8, 9, 10]));
assert_eq!(
secret.try_replace_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}),
Err("generation failed")
);
assert!(secret.constant_time_eq(&[7, 8, 9, 10]));
secret
.try_replace_from_fn(|index| Ok::<u8, &'static str>((index as u8) + 1))
.unwrap();
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.secure_clear();
}
#[test]
fn secret_bytes_can_transform_in_place() {
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
secret.transform(|bytes| {
for byte in bytes.iter_mut() {
*byte ^= 0xFF;
}
});
assert!(secret.constant_time_eq(&[254, 253, 252, 251]));
assert_eq!(
secret.try_transform(|bytes| {
bytes[0] = 7;
Ok::<(), &'static str>(())
}),
Ok(())
);
assert!(secret.constant_time_eq(&[7, 253, 252, 251]));
}
#[test]
fn secret_bytes_can_derive_new_secret() {
let secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
let derived = secret.derive::<8>(|input, output| {
output[..4].copy_from_slice(input);
output[4..].copy_from_slice(input);
});
assert!(derived.constant_time_eq(&[1, 2, 3, 4, 1, 2, 3, 4]));
let fallible = secret
.try_derive::<2, _>(|input, output| {
output.copy_from_slice(&input[..2]);
Ok::<(), &'static str>(())
})
.unwrap();
assert!(fallible.constant_time_eq(&[1, 2]));
assert_eq!(
secret
.try_derive::<2, _>(|_input, output| {
output[0] = 9;
Err::<(), &'static str>("derive failed")
})
.err(),
Some("derive failed")
);
}
#[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));
}
#[cfg(all(feature = "asm-compare", target_arch = "x86_64", not(miri)))]
#[test]
fn assembly_comparison_matches_portable_path() {
let left = [1_u8, 2, 3, 4, 5, 6, 7, 8];
let same = [1_u8, 2, 3, 4, 5, 6, 7, 8];
let different = [1_u8, 2, 3, 4, 5, 6, 7, 0];
let empty: [u8; 0] = [];
assert_eq!(
compare_asm::constant_time_eq_equal_len(&left, &same),
portable_constant_time_eq_equal_len(&left, &same)
);
assert_eq!(
compare_asm::constant_time_eq_equal_len(&left, &different),
portable_constant_time_eq_equal_len(&left, &different)
);
assert_eq!(
compare_asm::constant_time_eq_equal_len(&empty, &empty),
portable_constant_time_eq_equal_len(&empty, &empty)
);
}
#[test]
fn volatile_exposure_returns_closure_result() {
let secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
let sum = secret.expose_secret_volatile(|bytes| {
bytes
.iter()
.copied()
.fold(0_u8, |total, byte| total.wrapping_add(byte))
});
assert_eq!(sum, 10);
}
#[cfg(feature = "std")]
#[test]
fn expiring_secret_allows_access_before_expiration() {
let mut secret =
ExpiringSecretBytes::<4>::from_array([1, 2, 3, 4], std::time::Duration::from_secs(60));
let mut out = [0; 4];
assert!(secret.try_copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
assert_eq!(
secret.try_expose_secret(|bytes| bytes[0].wrapping_add(bytes[3])),
Ok(5)
);
assert_eq!(
secret.try_expose_secret_volatile(|bytes| bytes[1].wrapping_add(bytes[2])),
Ok(5)
);
assert_eq!(secret.try_constant_time_eq(&[1, 2, 3, 4]), Ok(true));
secret.into_cleared();
}
#[cfg(feature = "std")]
#[test]
fn expiring_secret_clears_and_rejects_after_expiration() {
let mut secret =
ExpiringSecretBytes::<4>::from_array([1, 2, 3, 4], std::time::Duration::ZERO);
let mut out = [9; 4];
assert_eq!(
secret.try_expose_secret(|bytes| bytes[0]),
Err(SecretExpiredError)
);
assert_eq!(
secret.try_copy_to_slice(&mut out),
Err(ExpiringSecretError::Expired(SecretExpiredError))
);
}
#[cfg(feature = "std")]
#[test]
fn expiring_secret_replacement_restarts_lifetime() {
let mut secret =
ExpiringSecretBytes::<4>::from_array([1, 2, 3, 4], std::time::Duration::from_secs(60));
let mut out = [0; 4];
secret.replace_from_slice(&[5, 6, 7, 8]).unwrap();
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [5, 6, 7, 8]);
secret.replace_from_array([8, 7, 6, 5]);
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [8, 7, 6, 5]);
}
#[cfg(feature = "std")]
#[test]
fn expiring_secret_can_initialize_from_fallible_fn() {
let mut secret =
ExpiringSecretBytes::<4>::try_from_fn(std::time::Duration::from_secs(60), |index| {
Ok::<u8, &'static str>((index as u8) + 1)
})
.unwrap();
let mut out = [0; 4];
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [1, 2, 3, 4]);
assert_eq!(
ExpiringSecretBytes::<4>::try_from_fn(std::time::Duration::from_secs(60), |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some("generation failed")
);
}
#[cfg(feature = "std")]
#[test]
fn expiring_secret_can_replace_from_fn() {
let mut secret =
ExpiringSecretBytes::<4>::from_array([1, 2, 3, 4], std::time::Duration::from_secs(60));
let mut out = [0; 4];
secret.replace_from_fn(|index| (index as u8) + 7);
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [7, 8, 9, 10]);
assert_eq!(
secret.try_replace_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}),
Err("generation failed")
);
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [7, 8, 9, 10]);
secret
.try_replace_from_fn(|index| Ok::<u8, &'static str>((index as u8) + 1))
.unwrap();
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [1, 2, 3, 4]);
}
#[test]
fn monotonic_expiring_secret_allows_access_before_expiration() {
let ticks = core::cell::Cell::new(10);
let mut secret =
MonotonicExpiringSecretBytes::<4, _>::from_array([1, 2, 3, 4], TestClock(&ticks), 5);
let mut out = [0; 4];
ticks.set(14);
assert_eq!(secret.age_ticks(), 4);
assert!(!secret.is_expired());
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [1, 2, 3, 4]);
assert_eq!(secret.try_constant_time_eq(&[1, 2, 3, 4]), Ok(true));
}
#[test]
fn monotonic_expiring_secret_clears_and_rejects_after_expiration() {
let ticks = core::cell::Cell::new(10);
let mut secret =
MonotonicExpiringSecretBytes::<4, _>::from_array([1, 2, 3, 4], TestClock(&ticks), 5);
let mut out = [9; 4];
ticks.set(15);
assert!(secret.is_expired());
assert_eq!(
secret.try_copy_to_slice(&mut out),
Err(ExpiringSecretError::Expired(SecretExpiredError))
);
assert_eq!(
secret.try_expose_secret(|bytes| bytes[0]),
Err(SecretExpiredError)
);
}
#[test]
fn monotonic_expiring_secret_zero_max_age_expires_immediately() {
let ticks = core::cell::Cell::new(10);
let mut secret =
MonotonicExpiringSecretBytes::<4, _>::from_array([1, 2, 3, 4], TestClock(&ticks), 0);
assert_eq!(secret.age_ticks(), 0);
assert!(secret.is_expired());
assert_eq!(
secret.try_expose_secret(|bytes| bytes[0]),
Err(SecretExpiredError)
);
}
#[test]
fn monotonic_expiring_secret_replacement_restarts_lifetime() {
let ticks = core::cell::Cell::new(10);
let mut secret =
MonotonicExpiringSecretBytes::<4, _>::from_array([1, 2, 3, 4], TestClock(&ticks), 5);
let mut out = [0; 4];
ticks.set(15);
secret.replace_from_array([5, 6, 7, 8]);
assert_eq!(secret.age_ticks(), 0);
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [5, 6, 7, 8]);
ticks.set(17);
secret.replace_from_slice(&[8, 7, 6, 5]).unwrap();
assert_eq!(secret.age_ticks(), 0);
assert_eq!(secret.try_copy_to_slice(&mut out), Ok(()));
assert_eq!(out, [8, 7, 6, 5]);
}
#[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 generic_secret_default_wraps_default_value() {
let mut secret = Secret::<[u8; 4]>::default();
assert_eq!(secret.with_secret(|bytes| *bytes), [0; 4]);
secret.with_secret_mut(|bytes| bytes[0] = 7);
assert_eq!(secret.with_secret(|bytes| bytes[0]), 7);
}
#[test]
fn read_once_secret_consumes_once_by_shared_reference() {
let secret = ReadOnceSecret::new(SecretBytes::<4>::from_array([1, 2, 3, 4]));
let sum = secret.consume(|bytes| {
let mut out = [0; 4];
bytes.copy_to_slice(&mut out).unwrap();
out.iter().copied().fold(0_u8, u8::wrapping_add)
});
assert_eq!(sum, Ok(10));
assert_eq!(
secret.consume(|_| unreachable!()),
Err(AlreadyConsumedError)
);
assert!(secret.is_consumed());
}
#[test]
fn read_once_secret_allows_mutable_finalization() {
let secret = ReadOnceSecret::new([1_u8, 2, 3, 4]);
let first = secret.consume_mut(|bytes| {
bytes[0] = 9;
bytes[0]
});
assert_eq!(first, Ok(9));
assert_eq!(
secret.consume_mut(|_| unreachable!()),
Err(AlreadyConsumedError)
);
}
#[test]
fn read_once_secret_allows_only_one_shared_consumer() {
let secret = std::sync::Arc::new(ReadOnceSecret::new([1_u8, 2, 3, 4]));
let worker_secret = std::sync::Arc::clone(&secret);
let start = std::sync::Arc::new(std::sync::Barrier::new(2));
let worker_start = std::sync::Arc::clone(&start);
let worker = std::thread::spawn(move || {
worker_start.wait();
worker_secret.consume(|bytes| bytes[0])
});
start.wait();
let main_result = secret.consume(|bytes| bytes[0]);
let worker_result = worker.join().unwrap();
let successes = usize::from(main_result.is_ok()) + usize::from(worker_result.is_ok());
let failures = usize::from(main_result == Err(AlreadyConsumedError))
+ usize::from(worker_result == Err(AlreadyConsumedError));
assert_eq!(successes, 1);
assert_eq!(failures, 1);
}
#[test]
fn read_once_secret_default_and_debug_are_safe() {
let secret = ReadOnceSecret::<[u8; 4]>::default();
let rendered = std::format!("{secret:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("[0, 0, 0, 0]"));
}
#[cfg(feature = "split-secret")]
#[test]
fn split_secret_reconstructs_with_all_shares() {
let split =
SplitSecretBytes::<4, 3>::from_array_with_generator([9, 8, 7, 6], |share, index| {
((share as u8) << 4) ^ (index as u8)
})
.unwrap();
assert_eq!(split.shares().len(), 3);
assert!(split
.reconstruct()
.constant_time_eq_secret(&SecretBytes::from_array([9, 8, 7, 6])));
assert!(std::format!("{split:?}").contains("redacted"));
}
#[cfg(feature = "split-secret")]
#[test]
fn split_secret_requires_multiple_shares() {
assert!(matches!(
SplitSecretBytes::<4, 1>::from_array_with_generator([1, 2, 3, 4], |_, _| 0),
Err(SplitSecretError::TooFewShares)
));
}
#[cfg(feature = "hardware-secrets")]
#[test]
fn hardware_secret_error_is_displayable() {
let error = hardware::HardwareSecretError {
kind: hardware::HardwareSecretErrorKind::Unavailable,
code: 0,
};
assert!(std::format!("{error}").contains("Unavailable"));
}
#[cfg(feature = "register-scrub")]
#[test]
fn register_scrub_api_is_callable() {
register_scrub::scrub_simd_registers();
}
#[test]
fn scalar_values_implement_secure_sanitize() {
let mut unsigned = Secret::new(0xDEAD_BEEF_u64);
let mut signed = Secret::new(-42_i32);
let mut flag = Secret::new(true);
let mut float = Secret::new(12.5_f64);
unsigned.with_secret_mut(SecureSanitize::secure_sanitize);
signed.with_secret_mut(SecureSanitize::secure_sanitize);
flag.with_secret_mut(SecureSanitize::secure_sanitize);
float.with_secret_mut(SecureSanitize::secure_sanitize);
assert_eq!(unsigned.with_secret(|value| *value), 0);
assert_eq!(signed.with_secret(|value| *value), 0);
assert!(!flag.with_secret(|value| *value));
assert_eq!(float.with_secret(|value| value.to_bits()), 0);
}
#[test]
fn compound_standard_types_implement_secure_sanitize() {
let mut array = [1_u64, 2, 3, 4];
let mut optional = Some([9_u8, 8, 7, 6]);
let mut result = Ok::<[u8; 2], [u8; 2]>([5, 4]);
array.secure_sanitize();
optional.secure_sanitize();
result.secure_sanitize();
assert_eq!(array, [0; 4]);
assert_eq!(optional, None);
assert_eq!(result, Ok([0, 0]));
}
#[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_vec(std::vec![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());
secret.into_cleared();
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_default_is_empty() {
let mut secret = SecretVec::default();
assert!(secret.is_empty());
secret.extend_from_slice(&[1, 2, 3]);
assert!(secret.constant_time_eq(&[1, 2, 3]));
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_can_initialize_from_fn() {
let mut secret = SecretVec::from_fn(4, |index| (index as u8) + 1);
assert_eq!(secret.len(), 4);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.clear_secret();
assert!(secret.is_empty());
secret.into_cleared();
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_can_initialize_from_fallible_fn() {
let mut secret =
SecretVec::try_from_fn(4, |index| Ok::<u8, &'static str>((index as u8) + 1)).unwrap();
assert_eq!(secret.len(), 4);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
SecretVec::try_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some("generation failed")
);
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_can_replace_secret() {
let mut secret = SecretVec::with_capacity(8);
secret.extend_from_slice(&[1, 2, 3, 4]);
assert!(secret.capacity() >= 8);
secret.replace_from_slice(&[9, 8]);
assert_eq!(secret.len(), 2);
assert!(secret.constant_time_eq(&[9, 8]));
let larger = [7_u8; 64];
secret.replace_from_slice(&larger);
assert_eq!(secret.len(), larger.len());
assert_eq!(secret.with_secret(|bytes| (bytes[0], bytes[63])), (7, 7));
secret.replace_from_vec(std::vec![4, 5, 6]);
assert_eq!(secret.len(), 3);
assert!(secret.constant_time_eq(&[4, 5, 6]));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_can_replace_from_fn() {
let mut secret = SecretVec::from_slice(&[1, 2, 3, 4]);
secret.replace_from_fn(3, |index| (index as u8) + 7);
assert_eq!(secret.len(), 3);
assert!(secret.constant_time_eq(&[7, 8, 9]));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
secret.replace_from_fn(4, |index| {
if index == 2 {
panic!("intentional generator panic");
}
index as u8
});
}));
assert!(result.is_err());
assert!(secret.constant_time_eq(&[7, 8, 9]));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_try_replace_from_fn_preserves_old_secret_on_error() {
let mut secret = SecretVec::from_slice(&[1, 2, 3, 4]);
secret
.try_replace_from_fn(3, |index| Ok::<u8, &'static str>((index as u8) + 7))
.unwrap();
assert!(secret.constant_time_eq(&[7, 8, 9]));
assert_eq!(
secret
.try_replace_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some("generation failed")
);
assert!(secret.constant_time_eq(&[7, 8, 9]));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_vec_grows_exponentially() {
let mut secret = SecretVec::from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
let initial_capacity = secret.inner.capacity();
secret.extend_from_slice(&[9]);
assert!(secret.inner.capacity() >= initial_capacity.saturating_mul(2));
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_round_trip_and_clear() {
let mut secret = SecretString::from_string(std::string::String::from("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_eq!(
secret.try_with_secret_mut(|text| text.make_ascii_uppercase()),
Ok(())
);
assert!(secret.constant_time_eq("SECRET-TOKEN"));
assert!(!secret.constant_time_eq("secret-token"));
assert_eq!(
secret.try_with_secret_mut(|text| text.make_ascii_lowercase()),
Ok(())
);
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 = "alloc")]
#[test]
fn secret_string_default_is_empty() {
let mut secret = SecretString::default();
assert!(secret.is_empty());
secret.push_str("secret");
assert!(secret.constant_time_eq("secret"));
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_can_replace_secret() {
let mut secret = SecretString::with_capacity(8);
secret.push_str("secret");
assert!(secret.capacity() >= 8);
secret.replace_from_secret_str("rotated");
assert_eq!(secret.len(), 7);
assert!(secret.constant_time_eq("rotated"));
let larger = "larger-rotated-secret";
secret.replace_from_secret_str(larger);
assert_eq!(secret.len(), larger.len());
assert_eq!(secret.try_with_secret(|text| text == larger), Ok(true));
secret.replace_from_string(std::string::String::from("owned-token"));
assert_eq!(
secret.try_with_secret(|text| text == "owned-token"),
Ok(true)
);
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_can_initialize_from_chars() {
let mut secret = SecretString::from_chars(4, |index| match index {
0 => 's',
1 => 'e',
2 => 'c',
_ => '\u{1F512}',
});
assert_eq!(
secret.try_with_secret(|text| text == "sec\u{1F512}"),
Ok(true)
);
assert_eq!(secret.len(), "sec\u{1F512}".len());
assert_eq!(
SecretString::try_from_chars(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok('x')
}
})
.err(),
Some("generation failed")
);
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_can_replace_from_chars() {
let mut secret = SecretString::from_secret_str("secret");
secret.replace_from_chars(3, |index| match index {
0 => 'k',
1 => 'e',
_ => 'y',
});
assert!(secret.constant_time_eq("key"));
assert_eq!(
secret
.try_replace_from_chars(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok('z')
}
})
.err(),
Some("generation failed")
);
assert!(secret.constant_time_eq("key"));
secret
.try_replace_from_chars(2, |index| {
Ok::<char, &'static str>(if index == 0 { '\u{00F8}' } else { 'k' })
})
.unwrap();
assert_eq!(secret.try_with_secret(|text| text == "\u{00F8}k"), Ok(true));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn secret_string_grows_exponentially() {
let mut secret = SecretString::from_secret_str("abcdefgh");
let initial_capacity = secret.inner.capacity();
secret.push_str("i");
assert!(secret.inner.capacity() >= initial_capacity.saturating_mul(2));
}
#[test]
fn volatile_wipe_clears_slice() {
let mut bytes = [0xA5; 16];
crate::unsafe_wipe::volatile_sanitize_bytes(&mut bytes);
assert_eq!(bytes, [0; 16]);
}
#[cfg(feature = "multi-pass-clear")]
#[test]
fn multi_pass_wipe_clears_slice() {
let mut bytes = [0xA5; 16];
sanitize_bytes_multi_pass(&mut bytes);
assert_eq!(bytes, [0; 16]);
}
#[cfg(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(all(feature = "alloc", feature = "multi-pass-clear"))]
#[test]
fn multi_pass_wipe_clears_alloc_types_when_enabled() {
let mut bytes = SecretVec::from_slice(&[1, 2, 3]);
let mut text = SecretString::from_secret_str("secret");
let mut ordinary = std::vec![0xBB; 8];
let mut ordinary_text = std::string::String::from("secret");
bytes.clear_secret_multi_pass();
text.clear_secret_multi_pass();
crate::unsafe_wipe::volatile_sanitize_vec_multi_pass(&mut ordinary);
crate::unsafe_wipe::volatile_sanitize_string_multi_pass(&mut ordinary_text);
assert!(bytes.is_empty());
assert!(text.is_empty());
assert!(ordinary.is_empty());
assert!(ordinary_text.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn alloc_standard_types_implement_secure_sanitize() {
let mut boxed = std::boxed::Box::new([1_u64, 2, 3]);
let mut values = std::vec![7_u32, 8, 9];
let mut text = std::string::String::from("secret");
boxed.secure_sanitize();
values.secure_sanitize();
text.secure_sanitize();
assert_eq!(*boxed, [0; 3]);
assert!(values.is_empty());
assert!(text.is_empty());
}
#[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 = "cache-flush", target_arch = "x86_64", not(miri)))]
#[test]
fn cache_flush_sanitize_clears_slice_and_secret_bytes() {
let mut bytes = [0xA5; 16];
crate::cache_flush::cache_flush_sanitize_array(&mut bytes);
assert_eq!(bytes, [0; 16]);
let mut secret = SecretBytes::<4>::from_array([1, 2, 3, 4]);
secret.secure_clear_and_flush();
assert!(secret.constant_time_eq(&[0, 0, 0, 0]));
let mut wrapped = crate::cache_flush::CacheFlushOnDrop::new([1, 2, 3, 4]);
wrapped.with_secret_mut(|value| value[0] = 9);
assert_eq!(wrapped.with_secret(|value| value[0]), 9);
wrapped.into_cleared();
}
#[cfg(feature = "alloc")]
#[test]
fn volatile_constructor_aliases_still_work() {
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());
}
#[cfg(all(
feature = "cache-flush",
feature = "alloc",
target_arch = "x86_64",
not(miri)
))]
#[test]
fn cache_flush_sanitize_clears_alloc_types() {
let mut bytes = SecretVec::from_slice(&[1, 2, 3]);
let mut text = SecretString::from_secret_str("secret");
bytes.clear_secret_and_flush();
text.clear_secret_and_flush();
assert!(bytes.is_empty());
assert!(text.is_empty());
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_bytes_round_trip_and_clear() {
let mut secret = LockedSecretBytes::<4>::from_array([1, 2, 3, 4]).unwrap();
let mut out = [0; 4];
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert!(!secret.constant_time_eq(&[1, 2, 3]));
secret.secure_clear();
#[cfg(feature = "canary-check")]
{
assert_eq!(secret.verify_integrity(), Ok(()));
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [0, 0, 0, 0]);
assert!(secret.copy_from_slice(&[9, 8, 7, 6]).is_ok());
assert!(secret.constant_time_eq(&[9, 8, 7, 6]));
}
#[cfg(not(feature = "canary-check"))]
{
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [0, 0, 0, 0]);
}
secret.into_cleared();
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_bytes_can_load_from_slice() {
let mut secret = LockedSecretBytes::<4>::from_slice(&[1, 2, 3, 4]).unwrap();
let mut out = [0; 4];
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
LockedSecretBytes::<4>::from_slice(&[1, 2]).err(),
Some(LockedSecretBytesError::Length(LengthError {
expected: 4,
actual: 2,
}))
);
secret.secure_clear();
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_bytes_can_initialize_from_fn() {
let mut secret = LockedSecretBytes::<4>::from_fn(|index| (index as u8) + 1).unwrap();
let mut out = [0; 4];
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.secure_clear();
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_bytes_can_initialize_from_fallible_fn() {
let mut secret = match LockedSecretBytes::<4>::try_from_fn(|index| {
Ok::<u8, &'static str>((index as u8) + 1)
}) {
Ok(secret) => secret,
Err(LockedSecretBytesGenerateError::Memory(_)) => return,
Err(LockedSecretBytesGenerateError::Generate(error)) => {
panic!("unexpected generator error: {error}")
}
};
let mut out = [0; 4];
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [1, 2, 3, 4]);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
match LockedSecretBytes::<4>::try_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}) {
Ok(_) => panic!("generation should have failed"),
Err(LockedSecretBytesGenerateError::Memory(_)) => return,
Err(LockedSecretBytesGenerateError::Generate(error)) => {
assert_eq!(error, "generation failed");
}
}
secret.secure_clear();
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_bytes_can_replace_secret() {
let mut secret = match LockedSecretBytes::<4>::from_array([1, 2, 3, 4]) {
Ok(secret) => secret,
Err(_) => return,
};
let mut out = [0; 4];
if let Err(LockedSecretBytesError::Memory(_)) = secret.replace_from_slice(&[9, 8, 7, 6]) {
return;
}
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [9, 8, 7, 6]);
if secret.replace_from_array([6, 7, 8, 9]).is_err() {
return;
}
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [6, 7, 8, 9]);
assert_eq!(
secret.replace_from_slice(&[1, 2]).err(),
Some(LockedSecretBytesError::Length(LengthError {
expected: 4,
actual: 2,
}))
);
assert!(secret.constant_time_eq(&[6, 7, 8, 9]));
if secret.replace_from_fn(|index| (index as u8) + 1).is_err() {
return;
}
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
match secret.try_replace_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}) {
Ok(_) => panic!("generation should have failed"),
Err(LockedSecretBytesGenerateError::Memory(_)) => return,
Err(LockedSecretBytesGenerateError::Generate(error)) => {
assert_eq!(error, "generation failed");
}
}
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
match secret.try_replace_from_fn(|index| Ok::<u8, &'static str>((index as u8) + 7)) {
Ok(()) => {}
Err(LockedSecretBytesGenerateError::Memory(_)) => return,
Err(LockedSecretBytesGenerateError::Generate(error)) => {
panic!("unexpected generator error: {error}")
}
}
assert!(secret.constant_time_eq(&[7, 8, 9, 10]));
secret.secure_clear();
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_vec_round_trip_grow_replace_and_clear() {
let mut secret = match LockedSecretVec::from_slice(b"key") {
Ok(secret) => secret,
Err(_) => return,
};
assert_eq!(secret.len(), 3);
assert!(secret.capacity() >= 3);
assert!(secret.locked_len() >= 3);
assert_eq!(secret.with_secret(|bytes| bytes[0]), b'k');
assert!(secret.constant_time_eq(b"key"));
assert!(!secret.constant_time_eq(b"ke"));
secret.extend_from_slice(b"-material").unwrap();
assert!(secret.constant_time_eq(b"key-material"));
secret
.replace_from_fn(4, |index| (index as u8) + 1)
.unwrap();
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
secret.try_replace_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}),
Err(LockedSecretVecGenerateError::Generate("generation failed"))
);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.with_secret_mut(|bytes| bytes[0] = 9);
assert!(secret.constant_time_eq(&[9, 2, 3, 4]));
secret.clear_secret();
assert!(secret.is_empty());
#[cfg(feature = "canary-check")]
assert_eq!(secret.verify_integrity(), Ok(()));
secret.extend_from_slice(b"next").unwrap();
assert!(secret.constant_time_eq(b"next"));
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_vec_zero_capacity_is_reusable() {
let mut secret = LockedSecretVec::with_capacity(0).unwrap();
assert!(secret.is_empty());
assert_eq!(secret.capacity(), 0);
assert_eq!(secret.locked_len(), 0);
secret.clear_secret();
#[cfg(feature = "canary-check")]
assert_eq!(secret.verify_integrity(), Ok(()));
if secret.extend_from_slice(b"x").is_err() {
return;
}
assert!(secret.constant_time_eq(b"x"));
}
#[cfg(all(
feature = "std",
feature = "canary-check",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_vec_canaries_detect_corruption() {
let mut secret = match LockedSecretVec::from_slice(b"secret") {
Ok(secret) => secret,
Err(_) => return,
};
assert_eq!(secret.verify_integrity(), Ok(()));
assert_eq!(secret.expose_secret_checked(|bytes| bytes[0]), Ok(b's'));
assert_eq!(secret.constant_time_eq_checked(b"secret"), Ok(true));
secret.corrupt_prefix_canary_for_test();
assert_eq!(
secret.expose_secret_checked(|bytes| bytes[0]),
Err(CanaryCorruptedError)
);
}
#[cfg(all(
feature = "memory-lock",
feature = "cache-flush",
target_os = "linux",
target_arch = "x86_64",
not(miri)
))]
#[test]
fn locked_secret_bytes_can_clear_and_flush() {
let mut secret = LockedSecretBytes::<4>::from_array([1, 2, 3, 4]).unwrap();
#[cfg(not(feature = "canary-check"))]
let mut out = [0; 4];
secret.secure_clear_and_flush();
#[cfg(feature = "canary-check")]
assert_eq!(secret.verify_integrity(), Ok(()));
#[cfg(not(feature = "canary-check"))]
{
assert!(secret.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [0, 0, 0, 0]);
}
}
#[cfg(all(
feature = "std",
feature = "canary-check",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_canary_checked_apis_detect_corruption() {
let mut secret = match LockedSecretBytes::<4>::from_array([1, 2, 3, 4]) {
Ok(secret) => secret,
Err(_) => return,
};
let mut out = [0; 4];
assert_eq!(secret.verify_integrity(), Ok(()));
assert_eq!(secret.expose_secret_checked(|bytes| bytes[0]), Ok(1));
assert_eq!(secret.copy_to_slice_checked(&mut out), Ok(()));
assert_eq!(out, [1, 2, 3, 4]);
assert_eq!(secret.constant_time_eq_checked(&[1, 2, 3, 4]), Ok(true));
assert_eq!(
secret.copy_to_slice_checked(&mut [0; 2]),
Err(LockedSecretBytesCheckedCopyError::Length(LengthError {
expected: 4,
actual: 2,
}))
);
secret.corrupt_prefix_canary_for_test();
assert_eq!(
secret.expose_secret_checked(|bytes| bytes[0]),
Err(CanaryCorruptedError)
);
}
#[cfg(all(
feature = "std",
feature = "canary-check",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn locked_secret_canary_legacy_exposure_fails_closed() {
let mut secret = match LockedSecretBytes::<4>::from_array([1, 2, 3, 4]) {
Ok(secret) => secret,
Err(_) => return,
};
secret.corrupt_prefix_canary_for_test();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = secret.with_secret(|bytes| bytes[0]);
}));
assert!(result.is_err());
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn secret_pool_allocates_reuses_and_clears_slots() {
let pool = match SecretPool::<4, 2>::new() {
Ok(pool) => pool,
Err(_) => return,
};
assert_eq!(pool.slot_size(), 4);
assert_eq!(pool.capacity_slots(), 2);
assert!(pool.locked_len() >= 8);
assert_eq!(pool.available_slots(), 2);
let mut first = pool.allocate_from_array([1, 2, 3, 4]).unwrap();
let mut second = pool.allocate_from_fn(|index| (index as u8) + 5).unwrap();
let mut out = [0; 4];
assert_eq!(pool.available_slots(), 0);
assert!(pool.allocate().is_none());
assert!(pool.try_allocate().unwrap().is_none());
assert!(first.constant_time_eq(&[1, 2, 3, 4]));
assert!(second.copy_to_slice(&mut out).is_ok());
assert_eq!(out, [5, 6, 7, 8]);
first.with_secret_mut(|bytes| bytes[0] = 9);
assert!(first.constant_time_eq(&[9, 2, 3, 4]));
first.secure_clear();
#[cfg(feature = "canary-check")]
assert_eq!(first.verify_integrity(), Ok(()));
assert!(first.constant_time_eq(&[0, 0, 0, 0]));
first.copy_from_slice(&[4, 3, 2, 1]).unwrap();
assert!(first.constant_time_eq(&[4, 3, 2, 1]));
first.secure_clear();
assert!(first.constant_time_eq(&[0, 0, 0, 0]));
let freed_index = first.slot_index();
drop(first);
assert_eq!(pool.available_slots(), 1);
let reused = pool.allocate_from_slice(&[7, 7, 7, 7]).unwrap().unwrap();
assert_eq!(reused.slot_index(), freed_index);
assert!(reused.constant_time_eq(&[7, 7, 7, 7]));
second.replace_from_array([8, 8, 8, 8]);
assert!(second.constant_time_eq(&[8, 8, 8, 8]));
}
#[cfg(all(
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn secret_pool_handles_generation_and_zero_slot_cases() {
let pool = match SecretPool::<4, 1>::new() {
Ok(pool) => pool,
Err(_) => return,
};
let mut slot = match pool
.try_allocate_from_fn(|index| Ok::<u8, &'static str>((index as u8).wrapping_add(1)))
{
Ok(Some(slot)) => slot,
Ok(None) => panic!("pool should have one available slot"),
Err(error) => panic!("unexpected generator error: {error}"),
};
assert!(slot.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
slot.try_replace_from_fn(|index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}),
Err("generation failed")
);
#[cfg(feature = "canary-check")]
assert_eq!(slot.verify_integrity(), Ok(()));
assert!(slot.constant_time_eq(&[0, 0, 0, 0]));
slot.copy_from_slice(&[9, 9, 9, 9]).unwrap();
assert!(slot.constant_time_eq(&[9, 9, 9, 9]));
drop(slot);
match pool.try_allocate_from_fn(|index| {
if index == 1 {
Err("generation failed")
} else {
Ok(index as u8)
}
}) {
Ok(_) => panic!("generation should have failed"),
Err(error) => assert_eq!(error, "generation failed"),
}
assert_eq!(pool.available_slots(), 1);
let empty = SecretPool::<0, 2>::new().unwrap();
assert!(empty.is_empty());
assert_eq!(empty.locked_len(), 0);
let slot = empty.allocate().unwrap();
assert!(slot.is_empty());
}
#[cfg(all(
feature = "std",
feature = "canary-check",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn secret_pool_slot_canaries_detect_corruption() {
let pool = match SecretPool::<4, 1>::new() {
Ok(pool) => pool,
Err(_) => return,
};
let mut slot = pool.allocate_from_array([1, 2, 3, 4]).unwrap();
assert_eq!(slot.verify_integrity(), Ok(()));
assert_eq!(slot.expose_secret_checked(|bytes| bytes[0]), Ok(1));
assert_eq!(slot.constant_time_eq_checked(&[1, 2, 3, 4]), Ok(true));
slot.corrupt_prefix_canary_for_test();
assert_eq!(
slot.expose_secret_checked(|bytes| bytes[0]),
Err(CanaryCorruptedError)
);
}
#[cfg(all(
feature = "std",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn secret_pool_concurrent_allocation_gets_distinct_slots() {
let pool = match SecretPool::<4, 2>::new() {
Ok(pool) => std::sync::Arc::new(pool),
Err(_) => return,
};
let worker_pool = std::sync::Arc::clone(&pool);
let start = std::sync::Arc::new(std::sync::Barrier::new(2));
let finish = std::sync::Arc::new(std::sync::Barrier::new(2));
let worker_start = std::sync::Arc::clone(&start);
let worker_finish = std::sync::Arc::clone(&finish);
let worker = std::thread::spawn(move || {
worker_start.wait();
let slot = worker_pool.allocate();
let index = slot.as_ref().map(|slot| slot.slot_index());
worker_finish.wait();
index
});
start.wait();
let slot = pool.allocate();
let main_index = slot.as_ref().map(|slot| slot.slot_index());
finish.wait();
let worker_index = worker.join().unwrap();
if let (Some(left), Some(right)) = (main_index, worker_index) {
assert_ne!(left, right);
}
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_round_trip_grow_and_clear() {
let mut secret = GuardedSecretVec::from_slice(&[1, 2, 3]).unwrap();
assert_eq!(secret.len(), 3);
assert!(secret.capacity() >= 3);
assert!(!secret.is_memory_locked());
assert_eq!(secret.with_secret(|bytes| bytes[0]), 1);
assert!(secret.constant_time_eq(&[1, 2, 3]));
assert!(!secret.constant_time_eq(&[1, 2]));
secret.with_secret_mut(|bytes| bytes[0] = 9);
let original_capacity = secret.capacity();
let extra = [4_u8; 5000];
secret.extend_from_slice(&extra).unwrap();
assert!(secret.capacity() > original_capacity);
assert_eq!(secret.len(), 5003);
assert_eq!(
secret.with_secret(|bytes| (bytes[0], bytes[2], bytes[5002])),
(9, 3, 4)
);
secret.clear_secret();
assert!(secret.is_empty());
#[cfg(feature = "canary-check")]
assert_eq!(secret.verify_integrity(), Ok(()));
secret.extend_from_slice(b"world").unwrap();
assert!(secret.constant_time_eq(b"world"));
secret.into_cleared();
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_replace_secret() {
let mut secret = GuardedSecretVec::from_slice(&[1, 2, 3, 4]).unwrap();
let original_capacity = secret.capacity();
secret.replace_from_slice(&[9, 8]).unwrap();
assert_eq!(secret.len(), 2);
assert_eq!(secret.capacity(), original_capacity);
assert!(secret.constant_time_eq(&[9, 8]));
let larger = [7_u8; 70_000];
secret.replace_from_slice(&larger).unwrap();
assert_eq!(secret.len(), larger.len());
assert!(secret.capacity() >= larger.len());
assert_eq!(
secret.with_secret(|bytes| (bytes[0], bytes[69_999])),
(7, 7)
);
secret.clear_secret();
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_replace_from_fn() {
let mut secret = GuardedSecretVec::from_slice(&[1, 2, 3, 4]).unwrap();
secret
.replace_from_fn(3, |index| (index as u8) + 7)
.unwrap();
assert_eq!(secret.len(), 3);
assert!(secret.constant_time_eq(&[7, 8, 9]));
assert_eq!(
secret
.try_replace_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some(GuardedSecretVecGenerateError::Generate("generation failed"))
);
assert!(secret.constant_time_eq(&[7, 8, 9]));
secret
.try_replace_from_fn(4, |index| Ok::<u8, &'static str>((index as u8) + 1))
.unwrap();
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
feature = "cache-flush",
target_os = "linux",
target_arch = "x86_64",
not(miri)
))]
#[test]
fn guarded_secret_vec_can_clear_and_flush() {
let mut secret = GuardedSecretVec::from_slice(&[1, 2, 3, 4]).unwrap();
secret.clear_secret_and_flush();
assert!(secret.is_empty());
#[cfg(feature = "canary-check")]
assert_eq!(secret.verify_integrity(), Ok(()));
#[cfg(not(feature = "canary-check"))]
assert_eq!(secret.with_secret(|bytes| bytes.len()), 0);
let wrapped = crate::cache_flush::CacheFlushOnDrop::new(
GuardedSecretVec::from_slice(&[5, 6, 7, 8]).unwrap(),
);
assert_eq!(wrapped.with_secret(|secret| secret.len()), 4);
wrapped.into_cleared();
}
#[cfg(all(
feature = "std",
feature = "guard-pages",
feature = "canary-check",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_canaries_detect_corruption() {
let mut secret = GuardedSecretVec::from_slice(&[1, 2, 3, 4]).unwrap();
assert_eq!(secret.verify_integrity(), Ok(()));
assert_eq!(secret.expose_secret_checked(|bytes| bytes[0]), Ok(1));
assert_eq!(secret.constant_time_eq_checked(&[1, 2, 3, 4]), Ok(true));
secret.extend_from_slice(&[5, 6]).unwrap();
assert_eq!(secret.expose_secret_checked(|bytes| bytes[5]), Ok(6));
secret.corrupt_suffix_canary_for_test();
assert_eq!(
secret.expose_secret_checked(|bytes| bytes[0]),
Err(CanaryCorruptedError)
);
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_initialize_from_fn() {
let mut secret = GuardedSecretVec::from_fn(4, |index| (index as u8) + 1).unwrap();
assert_eq!(secret.len(), 4);
assert!(!secret.is_memory_locked());
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(secret.with_secret(|bytes| (bytes[0], bytes[3])), (1, 4));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_initialize_from_fallible_fn() {
let mut secret =
GuardedSecretVec::try_from_fn(4, |index| Ok::<u8, &'static str>((index as u8) + 1))
.unwrap();
assert_eq!(secret.len(), 4);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(
GuardedSecretVec::try_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
})
.err(),
Some(GuardedSecretVecGenerateError::Generate("generation failed"))
);
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_be_memory_locked() {
let mut secret = match GuardedSecretVec::locked_from_slice(&[1, 2, 3]) {
Ok(secret) => secret,
Err(GuardPageError {
operation:
GuardPageOperation::DontDump
| GuardPageOperation::DontFork
| GuardPageOperation::Lock,
..
}) => return,
Err(error) => panic!("unexpected guarded lock error: {error:?}"),
};
assert!(secret.is_memory_locked());
assert!(secret.constant_time_eq(&[1, 2, 3]));
secret.extend_from_slice(&[4]).unwrap();
assert!(secret.is_memory_locked());
assert_eq!(secret.with_secret(|bytes| (bytes[0], bytes[3])), (1, 4));
let larger = [9_u8; 5000];
secret.replace_from_slice(&larger).unwrap();
assert!(secret.is_memory_locked());
assert_eq!(secret.len(), larger.len());
assert_eq!(secret.with_secret(|bytes| (bytes[0], bytes[4999])), (9, 9));
secret
.replace_from_fn(4, |index| (index as u8) + 1)
.unwrap();
assert!(secret.is_memory_locked());
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
match secret.try_replace_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}) {
Ok(_) => panic!("generation should have failed"),
Err(GuardedSecretVecGenerateError::Generate(error)) => {
assert_eq!(error, "generation failed");
}
Err(GuardedSecretVecGenerateError::Guard(error)) => {
panic!("unexpected guarded setup error: {error:?}")
}
}
assert!(secret.is_memory_locked());
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_initialize_locked_from_fn() {
let mut secret = match GuardedSecretVec::locked_from_fn(4, |index| (index as u8) + 1) {
Ok(secret) => secret,
Err(GuardPageError {
operation:
GuardPageOperation::DontDump
| GuardPageOperation::DontFork
| GuardPageOperation::Lock,
..
}) => return,
Err(error) => panic!("unexpected guarded lock error: {error:?}"),
};
assert!(secret.is_memory_locked());
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
assert_eq!(secret.with_secret(|bytes| (bytes[0], bytes[3])), (1, 4));
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
feature = "memory-lock",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_can_initialize_locked_from_fallible_fn() {
let mut secret = match GuardedSecretVec::locked_try_from_fn(4, |index| {
Ok::<u8, &'static str>((index as u8) + 1)
}) {
Ok(secret) => secret,
Err(GuardedSecretVecGenerateError::Guard(GuardPageError {
operation:
GuardPageOperation::DontDump
| GuardPageOperation::DontFork
| GuardPageOperation::Lock,
..
})) => return,
Err(error) => panic!("unexpected guarded generation error: {error:?}"),
};
assert!(secret.is_memory_locked());
assert_eq!(secret.len(), 4);
assert!(secret.constant_time_eq(&[1, 2, 3, 4]));
match GuardedSecretVec::locked_try_from_fn(4, |index| {
if index == 2 {
Err("generation failed")
} else {
Ok(index as u8)
}
}) {
Ok(_) => panic!("generation should have failed"),
Err(GuardedSecretVecGenerateError::Guard(GuardPageError {
operation:
GuardPageOperation::DontDump
| GuardPageOperation::DontFork
| GuardPageOperation::Lock,
..
})) => return,
Err(GuardedSecretVecGenerateError::Guard(error)) => {
panic!("unexpected guarded setup error: {error:?}")
}
Err(GuardedSecretVecGenerateError::Generate(error)) => {
assert_eq!(error, "generation failed");
}
}
secret.clear_secret();
assert!(secret.is_empty());
}
#[cfg(all(
feature = "guard-pages",
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64"),
not(miri)
))]
#[test]
fn guarded_secret_vec_debug_is_redacted_and_sanitizable() {
let mut secret = GuardedSecretVec::from_slice(b"secret").unwrap();
let rendered = std::format!("{secret:?}");
assert!(rendered.contains("redacted"));
assert!(!rendered.contains("secret"));
secret.secure_sanitize();
assert!(secret.is_empty());
}
}