#![no_std]
#![deny(unsafe_code)]
#![deny(unsafe_op_in_unsafe_fn)]
#[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,
mem,
sync::atomic::{compiler_fence, Ordering},
};
#[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", 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() {
flag.store(false, Ordering::Release);
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();
}
#[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(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],
});
}
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: random_canary_value()?,
};
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()
}
}
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() {
flag.store(false, Ordering::Release);
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());
}
}
#[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(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",
target_arch = "wasm32",
)
))]
pub use memory_lock::{
LockedSecretBytes, LockedSecretBytesError, LockedSecretBytesGenerateError, MemoryLockError,
MemoryLockOperation, SecretPool, SecretPoolSlot,
};
#[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",
target_arch = "wasm32",
)
))]
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(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> {
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: random_canary_value()?,
};
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);
}
}
#[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()
}
}
#[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};
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 = "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]"));
}
#[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",
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(), Err(CanaryCorruptedError));
#[cfg(not(feature = "canary-check"))]
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(), Err(CanaryCorruptedError));
#[cfg(not(feature = "canary-check"))]
assert!(slot.constant_time_eq(&[0, 0, 0, 0]));
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());
}
}