use alloc::sync::Arc;
use chacha20poly1305::aead::AeadInOut;
use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce};
use rand::{TryRng, rngs::SysRng};
use sha2::{Digest, Sha256};
use zeroize::Zeroize;
use crate::{Fixed, RevealSecret};
pub(crate) const PREKEY_LEN: usize = 16 * 1024;
pub(crate) const TAG_LEN: usize = 16;
pub(crate) const SALT_LEN: usize = 32;
#[repr(C, align(16384))]
struct Block([u8; PREKEY_LEN]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LockError {
os_code: Option<i32>,
page_size: Option<usize>,
}
impl LockError {
fn from_region(err: ®ion::Error) -> Self {
let os_code = match err {
region::Error::SystemCall(code) | region::Error::MachCall(code) => Some(*code),
_ => None,
};
Self {
os_code,
page_size: None,
}
}
pub fn raw_os_error(&self) -> Option<i32> {
self.os_code
}
pub fn page_too_large(&self) -> Option<usize> {
self.page_size
}
}
impl core::fmt::Display for LockError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match (self.os_code, self.page_size) {
(_, Some(size)) => write!(
f,
"the sealed-memory pre-key was not locked: the {size}-byte page would be \
shared with other memory"
),
(Some(code), None) => write!(
f,
"the sealed-memory pre-key could not be locked in RAM (OS error {code})"
),
(None, None) => f.write_str("the sealed-memory pre-key could not be locked in RAM"),
}
}
}
impl std::error::Error for LockError {}
pub struct Prekey {
block: alloc::boxed::Box<Block>,
lock: Option<region::LockGuard>,
lock_error: Option<LockError>,
}
impl Prekey {
pub fn new() -> Arc<Self> {
let mut block = alloc::boxed::Box::new(Block([0u8; PREKEY_LEN]));
let lock = lock_block(&block, region::page::size());
SysRng
.try_fill_bytes(&mut block.0)
.expect("SysRng failure is a program error");
Arc::new(Self::from_parts(block, lock))
}
fn from_parts(
block: alloc::boxed::Box<Block>,
lock: Result<region::LockGuard, LockError>,
) -> Self {
let (lock, lock_error) = match lock {
Ok(guard) => (Some(guard), None),
Err(err) => (None, Some(err)),
};
Self {
block,
lock,
lock_error,
}
}
pub fn lock_status(&self) -> Result<(), LockError> {
match self.lock_error {
None => Ok(()),
Some(err) => Err(err),
}
}
#[inline]
fn sealing_key(&self, salt: &[u8; SALT_LEN]) -> Fixed<[u8; 32]> {
Fixed::new_with(|key: &mut [u8; 32]| {
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(&self.block.0);
hasher.finalize_into(key.into());
})
}
#[inline]
fn cipher(&self, salt: &[u8; SALT_LEN]) -> ChaCha20Poly1305 {
self.sealing_key(salt)
.with_secret(|key| ChaCha20Poly1305::new(key.into()))
}
pub(crate) fn fresh_salt() -> [u8; SALT_LEN] {
let mut rng = SysRng;
let words: [u64; SALT_LEN / 8] = core::array::from_fn(|_| {
rng.try_next_u64()
.expect("SysRng failure is a program error")
});
core::array::from_fn(|i| words[i / 8].to_le_bytes()[i % 8])
}
pub(crate) fn seal_in_place(&self, salt: &[u8; SALT_LEN], buf: &mut [u8]) -> [u8; TAG_LEN] {
self.cipher(salt)
.encrypt_inout_detached(&Nonce::default(), &[], buf.into())
.expect("ChaCha20-Poly1305 rejects only inputs over 256 GiB")
.into()
}
pub(crate) fn open_in_place(&self, salt: &[u8; SALT_LEN], tag: &[u8; TAG_LEN], buf: &mut [u8]) {
let opened = self.cipher(salt).decrypt_inout_detached(
&Nonce::default(),
&[],
buf.into(),
tag.into(),
);
if opened.is_err() {
buf.zeroize();
panic!("sealed memory was modified or corrupted");
}
}
}
fn lock_block(block: &Block, page_size: usize) -> Result<region::LockGuard, LockError> {
if page_size > PREKEY_LEN {
return Err(LockError {
os_code: None,
page_size: Some(page_size),
});
}
region::lock(block.0.as_ptr(), PREKEY_LEN).map_err(|err| LockError::from_region(&err))
}
const SCRUB_LEN: usize = 32 * 1024;
#[inline(never)]
pub(crate) fn scrub_stack() {
let mut area = [0u64; SCRUB_LEN / 8];
area.zeroize();
core::hint::black_box(&area);
}
#[inline(always)]
pub(crate) fn scrubbed<R>(f: impl FnOnce() -> R) -> R {
let result = std::panic::catch_unwind(core::panic::AssertUnwindSafe(f));
scrub_stack();
match result {
Ok(value) => value,
Err(payload) => std::panic::resume_unwind(payload),
}
}
impl Prekey {
fn wipe_and_unlock(&mut self) {
self.block.0.zeroize();
drop(self.lock.take());
}
}
impl Drop for Prekey {
fn drop(&mut self) {
self.wipe_and_unlock();
}
}
impl core::fmt::Debug for Prekey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("[REDACTED]")
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) fn quota() -> std::sync::MutexGuard<'static, ()> {
static QUOTA: std::sync::Mutex<()> = std::sync::Mutex::new(());
QUOTA
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn random_block() -> alloc::boxed::Box<Block> {
let mut block = alloc::boxed::Box::new(Block([0u8; PREKEY_LEN]));
SysRng.try_fill_bytes(&mut block.0).unwrap();
block
}
#[test]
fn block_is_page_aligned_and_fills_whole_pages() {
let block = random_block();
let addr = block.0.as_ptr() as usize;
assert_eq!(addr % 16384, 0);
assert_eq!(core::mem::size_of::<Block>(), PREKEY_LEN);
let page = region::page::size();
if page <= PREKEY_LEN {
assert_eq!(PREKEY_LEN % page, 0);
} else {
let _q = quota();
assert_eq!(
Prekey::new().lock_status().unwrap_err().page_too_large(),
Some(page)
);
}
}
#[test]
fn a_failed_lock_is_reported_and_sealing_still_works() {
let _q = quota();
let prekey = Arc::new(Prekey::from_parts(
random_block(),
Err(LockError::from_region(®ion::Error::SystemCall(1453))),
));
let err = prekey.lock_status().unwrap_err();
assert_eq!(err.raw_os_error(), Some(1453));
assert_eq!(err.page_too_large(), None);
assert_eq!(
format!("{err:?}"),
"LockError { os_code: Some(1453), page_size: None }"
);
assert_eq!(
err.to_string(),
"the sealed-memory pre-key could not be locked in RAM (OS error 1453)"
);
let salt = Prekey::fresh_salt();
let mut buf = *b"still sealed";
let tag = prekey.seal_in_place(&salt, &mut buf);
assert_ne!(&buf, b"still sealed");
prekey.open_in_place(&salt, &tag, &mut buf);
assert_eq!(&buf, b"still sealed");
}
#[test]
fn a_non_os_lock_failure_has_no_code() {
let prekey = Prekey::from_parts(
random_block(),
Err(LockError::from_region(®ion::Error::InvalidParameter(
"size",
))),
);
let err = prekey.lock_status().unwrap_err();
assert_eq!(err.raw_os_error(), None);
assert_eq!(
format!("{err:?}"),
"LockError { os_code: None, page_size: None }"
);
}
#[test]
fn wipe_and_unlock_zeroes_the_prekey_and_releases_the_lock() {
let _q = quota();
let lock = region::lock(core::ptr::null::<u8>(), 0).map_err(|_| ());
assert!(lock.is_err(), "a zero-length lock is rejected");
let block = random_block();
let guard = region::lock(block.0.as_ptr(), PREKEY_LEN);
let locked = guard.is_ok();
let mut prekey = Prekey::from_parts(block, guard.map_err(|e| LockError::from_region(&e)));
assert!(prekey.block.0.iter().any(|&b| b != 0));
assert_eq!(prekey.lock.is_some(), locked);
prekey.wipe_and_unlock();
assert!(prekey.block.0.iter().all(|&b| b == 0));
assert!(prekey.lock.is_none());
}
#[test]
fn wipe_and_unlock_returns_the_quota() {
let _q = quota();
let mut held = Vec::new();
while held.len() < 1024 {
let block = random_block();
let lock = region::lock(block.0.as_ptr(), PREKEY_LEN);
let refused = lock.is_err();
held.push(Prekey::from_parts(
block,
lock.map_err(|e| LockError::from_region(&e)),
));
if refused {
break;
}
}
let locked = held.iter().filter(|p| p.lock.is_some()).count();
if locked == held.len() {
return; }
for p in &mut held {
p.wipe_and_unlock();
}
let relocked: Vec<_> = (0..locked)
.map(|_| {
let block = random_block();
let lock = region::lock(block.0.as_ptr(), PREKEY_LEN);
Prekey::from_parts(block, lock.map_err(|e| LockError::from_region(&e)))
})
.collect();
let ok = relocked.iter().filter(|p| p.lock.is_some()).count();
assert!(
ok * 2 >= locked,
"only {ok} of {locked} relocked after unlocking"
);
drop(held);
}
#[test]
fn the_sealing_key_depends_on_every_prekey_bit() {
let _q = quota();
let mut prekey = Arc::try_unwrap(Prekey::new()).unwrap();
let salt = Prekey::fresh_salt();
let before = prekey.sealing_key(&salt);
prekey.block.0[PREKEY_LEN - 1] ^= 1;
let after = prekey.sealing_key(&salt);
assert_ne!(before.with_secret(|k| *k), after.with_secret(|k| *k));
}
#[test]
fn the_sealing_key_is_sha256_of_salt_then_prekey() {
let _q = quota();
let prekey = Prekey::new();
let salt = Prekey::fresh_salt();
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(prekey.block.0);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(prekey.sealing_key(&salt).with_secret(|k| *k), expected);
}
#[test]
fn every_salt_is_fresh() {
assert_ne!(Prekey::fresh_salt(), Prekey::fresh_salt());
}
#[test]
#[should_panic(expected = "sealed memory was modified or corrupted")]
fn a_wrong_tag_panics() {
let _q = quota();
let prekey = Prekey::new();
let salt = Prekey::fresh_salt();
let mut buf = [1u8; 8];
let mut tag = prekey.seal_in_place(&salt, &mut buf);
tag[0] ^= 1;
prekey.open_in_place(&salt, &tag, &mut buf);
}
#[test]
fn a_failed_open_wipes_the_buffer_before_panicking() {
let _q = quota();
let prekey = Prekey::new();
let salt = Prekey::fresh_salt();
let mut buf = [1u8; 8];
let tag = prekey.seal_in_place(&salt, &mut buf);
buf[0] ^= 1;
let result = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
prekey.open_in_place(&salt, &tag, &mut buf);
}));
assert!(result.is_err());
assert_eq!(buf, [0u8; 8]);
}
#[test]
fn a_page_larger_than_the_prekey_is_refused_without_locking() {
let block = random_block();
let Err(refused) = lock_block(&block, 65536) else {
panic!("a page larger than the pre-key must be refused");
};
assert_eq!(refused.page_too_large(), Some(65536));
assert_eq!(refused.raw_os_error(), None);
assert_eq!(
refused.to_string(),
"the sealed-memory pre-key was not locked: the 65536-byte page would be shared \
with other memory"
);
let prekey = Prekey::from_parts(block, Err(refused));
assert!(prekey.lock.is_none());
assert_eq!(
prekey.lock_status().unwrap_err().page_too_large(),
Some(65536)
);
}
#[test]
fn a_page_no_larger_than_the_prekey_is_locked() {
let _q = quota();
let block = random_block();
let page = region::page::size().min(PREKEY_LEN);
assert!(lock_block(&block, page).is_ok());
}
#[test]
fn debug_is_redacted() {
let _q = quota();
assert_eq!(format!("{:?}", Prekey::new()), "[REDACTED]");
}
}