use std::fmt;
use std::ops::Range;
use std::ptr::NonNull;
use zeroize::Zeroize;
pub struct Secret {
ptr: NonNull<u8>,
capacity: usize,
len: usize,
}
unsafe impl Send for Secret {}
unsafe impl Sync for Secret {}
impl Secret {
pub const fn new() -> Self {
Self {
ptr: NonNull::dangling(),
capacity: 0,
len: 0,
}
}
pub fn expose(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.bytes()) }
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn duplicate(&self) -> Secret {
let mut copy = Secret::new();
copy.replace_range(0..0, self.expose());
copy
}
fn bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
pub(crate) fn replace_range(&mut self, range: Range<usize>, with: &str) {
let text = self.expose();
assert!(
range.start <= range.end
&& range.end <= text.len()
&& text.is_char_boundary(range.start)
&& text.is_char_boundary(range.end),
"replace_range out of bounds or off a character boundary"
);
let old_len = self.len;
let new_len = old_len - range.len() + with.len();
if new_len > self.capacity {
let mut grown = Self::with_capacity(new_len.max(self.capacity * 2));
let dst = grown.ptr.as_ptr();
let src = self.ptr.as_ptr();
unsafe {
std::ptr::copy_nonoverlapping(src, dst, range.start);
std::ptr::copy_nonoverlapping(with.as_ptr(), dst.add(range.start), with.len());
std::ptr::copy_nonoverlapping(
src.add(range.end),
dst.add(range.start + with.len()),
old_len - range.end,
);
}
grown.len = new_len;
*self = grown;
return;
}
let base = self.ptr.as_ptr();
unsafe {
std::ptr::copy(
base.add(range.end),
base.add(range.start + with.len()),
old_len - range.end,
);
std::ptr::copy_nonoverlapping(with.as_ptr(), base.add(range.start), with.len());
}
if new_len < old_len {
self.slice_mut(new_len..old_len).zeroize();
}
self.len = new_len;
}
pub(crate) fn clear(&mut self) {
self.slice_mut(0..self.len).zeroize();
self.len = 0;
}
fn slice_mut(&mut self, range: Range<usize>) -> &mut [u8] {
debug_assert!(range.end <= self.capacity || range.is_empty());
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr().add(range.start), range.len()) }
}
fn with_capacity(bytes: usize) -> Self {
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let capacity = bytes.div_ceil(page).max(1) * page;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
capacity,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert!(
ptr != libc::MAP_FAILED,
"could not map {capacity} bytes for a secret: {}",
std::io::Error::last_os_error()
);
unsafe {
if libc::mlock(ptr, capacity) != 0 {
log::warn!(
"could not lock a secret in memory, so it may be swapped: {}",
std::io::Error::last_os_error()
);
}
libc::madvise(ptr, capacity, libc::MADV_DONTDUMP);
}
Self {
ptr: NonNull::new(ptr.cast()).expect("mmap returned null"),
capacity,
len: 0,
}
}
}
impl Default for Secret {
fn default() -> Self {
Self::new()
}
}
impl Secret {
fn wipe(&mut self) {
self.slice_mut(0..self.capacity).zeroize();
}
}
impl Drop for Secret {
fn drop(&mut self) {
if self.capacity == 0 {
return;
}
self.wipe();
unsafe {
libc::munlock(self.ptr.as_ptr().cast(), self.capacity);
libc::munmap(self.ptr.as_ptr().cast(), self.capacity);
}
}
}
impl From<String> for Secret {
fn from(mut text: String) -> Self {
let mut secret = Self::new();
secret.replace_range(0..0, &text);
text.zeroize();
secret
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(..)")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn raw(secret: &Secret, range: Range<usize>) -> Vec<u8> {
assert!(range.end <= secret.capacity);
unsafe { std::slice::from_raw_parts(secret.ptr.as_ptr().add(range.start), range.len()) }
.to_vec()
}
fn smaps_entry(addr: usize) -> (String, usize) {
let smaps = std::fs::read_to_string("/proc/self/smaps").expect("Linux has smaps");
let mut inside = false;
let mut locked = 0;
for line in smaps.lines() {
if let Some((range, _)) = line.split_once(' ')
&& let Some((start, end)) = range.split_once('-')
&& let (Ok(start), Ok(end)) = (
usize::from_str_radix(start, 16),
usize::from_str_radix(end, 16),
)
{
inside = (start..end).contains(&addr);
continue;
}
if !inside {
continue;
}
if let Some(kb) = line.strip_prefix("Locked:") {
locked = kb
.trim()
.trim_end_matches(" kB")
.trim()
.parse()
.unwrap_or(0);
}
if let Some(flags) = line.strip_prefix("VmFlags:") {
return (flags.to_owned(), locked);
}
}
panic!("no mapping holds {addr:#x}");
}
#[test]
fn its_pages_are_left_out_of_core_dumps_and_locked_when_the_kernel_allows() {
let secret = Secret::from(String::from("hunter2"));
let (flags, locked) = smaps_entry(secret.ptr.as_ptr() as usize);
assert!(
flags.split_whitespace().any(|f| f == "dd"),
"the pages are not marked MADV_DONTDUMP: {flags}"
);
if flags.split_whitespace().any(|f| f == "lo") {
assert!(locked > 0, "locked flag without locked pages");
}
}
#[test]
fn it_edits_in_place_and_keeps_its_text_across_a_regrowth() {
let mut secret = Secret::new();
assert!(secret.is_empty());
assert_eq!(secret.capacity, 0, "an empty secret maps nothing");
secret.replace_range(0..0, "held");
secret.replace_range(2..2, "-lo-");
assert_eq!(secret.expose(), "he-lo-ld");
secret.replace_range(2..6, "");
assert_eq!(secret.expose(), "held");
let first = secret.capacity;
let long = "x".repeat(first);
secret.replace_range(4..4, &long);
assert!(
secret.capacity > first,
"it did not move to a larger mapping"
);
assert!(secret.expose().starts_with("held"));
assert_eq!(secret.expose().len(), 4 + first);
secret.clear();
assert!(secret.is_empty());
assert!(
secret.slice_mut(0..8).iter().all(|&b| b == 0),
"clear left bytes behind"
);
}
#[test]
fn a_shrink_zeroes_what_it_leaves_past_the_end() {
let mut secret = Secret::from(String::from("abcdefgh"));
secret.replace_range(2..8, "");
assert_eq!(secret.expose(), "ab");
assert_eq!(raw(&secret, 2..8), [0; 6]);
}
#[test]
fn its_edits_match_a_string_making_the_same_ones() {
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let long = "é".repeat(page); let edits: [(Range<usize>, &str); 7] = [
(0..0, "hunter2"),
(3..3, "--"), (1..5, "X"), (2..3, "abcdef"), (4..4, &long), (6..6 + long.len() - 2, ""), (0..2, ""), ];
let mut secret = Secret::new();
let mut reference = String::new();
for (range, with) in edits {
secret.replace_range(range.clone(), with);
reference.replace_range(range, with);
assert_eq!(secret.expose(), reference);
let len = secret.expose().len();
assert!(
raw(&secret, len..secret.capacity).iter().all(|&b| b == 0),
"bytes left past the end of the text"
);
}
}
#[test]
fn growing_at_the_end_of_a_full_mapping_copies_only_the_text() {
let mut secret = Secret::from(String::from("a"));
let capacity = secret.capacity;
secret.replace_range(1..1, &"a".repeat(capacity - 2));
let end = secret.expose().len();
secret.replace_range(end..end, "bcd");
let mut expected = "a".repeat(capacity - 1);
expected.push_str("bcd");
assert_eq!(secret.expose(), expected);
}
#[test]
fn text_that_fills_the_pages_exactly_does_not_move_them() {
let mut secret = Secret::from(String::from("a"));
let (ptr, capacity) = (secret.ptr, secret.capacity);
secret.replace_range(1..1, &"a".repeat(capacity - 1));
assert_eq!(secret.ptr, ptr, "a secret that fit was moved");
assert_eq!(secret.expose().len(), capacity);
}
#[test]
fn clearing_zeroes_the_text_and_keeps_the_pages() {
let mut secret = Secret::from(String::from("hunter2"));
let ptr = secret.ptr;
secret.clear();
assert!(secret.is_empty());
assert_eq!(secret.ptr, ptr);
assert_eq!(raw(&secret, 0..7), [0; 7]);
}
#[test]
fn wiping_zeroes_every_byte_of_the_mapping() {
let mut secret = Secret::from("x".repeat(100));
secret.wipe();
assert!(raw(&secret, 0..secret.capacity).iter().all(|&b| b == 0));
}
#[test]
fn a_duplicate_has_pages_of_its_own_and_outlives_the_original() {
let original = Secret::from(String::from("hunter2"));
let copy = original.duplicate();
assert_eq!(copy.expose(), "hunter2");
assert_ne!(
copy.ptr, original.ptr,
"the copy shares the original's pages"
);
let (flags, _) = smaps_entry(copy.ptr.as_ptr() as usize);
assert!(
flags.split_whitespace().any(|f| f == "dd"),
"the copy is not left out of core dumps"
);
drop(original);
assert_eq!(copy.expose(), "hunter2");
}
#[test]
fn debug_does_not_print_it() {
let secret = Secret::from(String::from("hunter2"));
assert_eq!(format!("{secret:?}"), "Secret(..)");
}
}