use alloc::sync::Arc;
use chacha20poly1305::aead::AeadInOut;
use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce};
use guarded_block::GuardedBlock;
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;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LockError {
os_code: Option<i32>,
idle_noaccess: bool,
forked: bool,
}
impl LockError {
fn from_guarded(err: guarded_block::LockError) -> Self {
Self {
os_code: err.raw_os_error(),
idle_noaccess: err.idle_noaccess(),
forked: err.forked(),
}
}
pub fn raw_os_error(&self) -> Option<i32> {
self.os_code
}
pub fn idle_noaccess(&self) -> bool {
self.idle_noaccess
}
pub fn forked(&self) -> bool {
self.forked
}
}
impl core::fmt::Display for LockError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.forked {
return f.write_str(
"the sealed-memory pre-key is not locked in RAM in this process: it was made \
before a fork, and page locks are not inherited",
);
}
match (self.idle_noaccess, self.os_code) {
(true, _) => f.write_str(
"the sealed-memory pre-key is not locked in RAM: it is no-access while idle \
(Prekey::new_idle_noaccess), and on Windows a no-access page cannot stay locked",
),
(false, Some(code)) => write!(
f,
"the sealed-memory pre-key could not be locked in RAM (OS error {code})"
),
(false, 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: GuardedBlock<PREKEY_LEN>,
}
impl Prekey {
pub fn new() -> Arc<Self> {
Arc::new(Self::create(false))
}
pub fn new_idle_noaccess() -> Arc<Self> {
Arc::new(Self::create(true))
}
fn create(idle: bool) -> Self {
let fill = |block: &mut [u8; PREKEY_LEN]| {
SysRng
.try_fill_bytes(block)
.expect("SysRng failure is a program error");
};
let block = if idle {
GuardedBlock::new_with_idle_noaccess(fill)
} else {
GuardedBlock::new_with(fill)
};
Self {
block: block.unwrap_or_else(|err| {
panic!("the sealed-memory pre-key could not be set up: {err}")
}),
}
}
pub fn lock_status(&self) -> Result<(), LockError> {
self.block.lock_status().map_err(LockError::from_guarded)
}
pub fn is_idle_noaccess(&self) -> bool {
self.block.is_idle_noaccess()
}
#[inline]
fn sealing_key(&self, salt: &[u8; SALT_LEN]) -> Fixed<[u8; 32]> {
#[cfg(test)]
tests::maybe_fail_derivation();
Fixed::new_with(|key: &mut [u8; 32]| {
self.block.with(|prekey| {
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(prekey);
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_into(
&self,
salt: &[u8; SALT_LEN],
plain: &[u8],
out: &mut [u8],
) -> [u8; TAG_LEN] {
let cipher = self.cipher(salt);
out.copy_from_slice(plain);
cipher
.encrypt_inout_detached(&Nonce::default(), &[], out.into())
.expect("ChaCha20-Poly1305 rejects only inputs over 256 GiB")
.into()
}
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");
}
}
}
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 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)
}
std::thread_local! {
static FAIL_NEXT_DERIVATION: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
pub(crate) fn fail_next_derivation() {
FAIL_NEXT_DERIVATION.with(|f| f.set(true));
}
pub(super) fn maybe_fail_derivation() {
if FAIL_NEXT_DERIVATION.with(|f| f.replace(false)) {
panic!("injected: could not make the guarded block readable");
}
}
fn with_bytes(bytes: &[u8; PREKEY_LEN]) -> Prekey {
Prekey {
block: GuardedBlock::new_with(|b| b.copy_from_slice(bytes)).unwrap(),
}
}
fn protection_at(addr: usize) -> region::Protection {
region::query(addr as *const u8).unwrap().protection()
}
#[test]
fn the_prekey_lies_between_noaccess_guard_pages_and_is_read_only() {
let _q = quota();
let prekey = Prekey::new();
let [below, block, above] = prekey.block.layout();
assert!(block.len() >= PREKEY_LEN);
assert_eq!(protection_at(below.start), region::Protection::NONE);
assert_eq!(protection_at(block.start), region::Protection::READ);
assert_eq!(protection_at(block.end - 1), region::Protection::READ);
assert_eq!(protection_at(above.start), region::Protection::NONE);
assert!(!prekey.is_idle_noaccess());
}
#[test]
fn an_idle_noaccess_prekey_is_noaccess_between_accesses() {
let _q = quota();
let prekey = Prekey::new_idle_noaccess();
assert!(prekey.is_idle_noaccess());
let block = prekey.block.layout()[1].clone();
assert_eq!(protection_at(block.start), region::Protection::NONE);
let salt = Prekey::fresh_salt();
let mut buf = *b"idle";
let tag = prekey.seal_in_place(&salt, &mut buf);
prekey.open_in_place(&salt, &tag, &mut buf);
assert_eq!(&buf, b"idle");
assert_eq!(protection_at(block.start), region::Protection::NONE);
if cfg!(windows) {
let err = prekey.lock_status().unwrap_err();
assert!(err.idle_noaccess());
assert_eq!(err.raw_os_error(), None);
}
}
#[test]
fn lock_errors_say_why_and_carry_no_address() {
let quota = LockError {
os_code: Some(1453),
idle_noaccess: false,
forked: false,
};
assert_eq!(quota.raw_os_error(), Some(1453));
assert!(!quota.idle_noaccess());
assert_eq!(
format!("{quota:?}"),
"LockError { os_code: Some(1453), idle_noaccess: false, forked: false }"
);
assert_eq!(
quota.to_string(),
"the sealed-memory pre-key could not be locked in RAM (OS error 1453)"
);
let bare = LockError {
os_code: None,
idle_noaccess: false,
forked: false,
};
assert_eq!(
bare.to_string(),
"the sealed-memory pre-key could not be locked in RAM"
);
let idle = LockError {
os_code: None,
idle_noaccess: true,
forked: false,
};
assert_eq!(
idle.to_string(),
"the sealed-memory pre-key is not locked in RAM: it is no-access while idle \
(Prekey::new_idle_noaccess), and on Windows a no-access page cannot stay locked"
);
let forked = LockError {
os_code: None,
idle_noaccess: false,
forked: true,
};
assert_eq!(
forked.to_string(),
"the sealed-memory pre-key is not locked in RAM in this process: it was made \
before a fork, and page locks are not inherited"
);
}
#[test]
fn the_sealing_key_matches_an_independent_sha256_vector() {
let _q = quota();
let bytes: [u8; PREKEY_LEN] = core::array::from_fn(|i| (i % 251) as u8);
let prekey = with_bytes(&bytes);
let key = prekey.sealing_key(&[7u8; SALT_LEN]).with_secret(|k| *k);
let hex: alloc::string::String = key.iter().map(|b| alloc::format!("{b:02x}")).collect();
assert_eq!(
hex,
"5fdf15d9989af9c0c55f07f7ba974f74ac47ada2c2b21a6e0539eaebd833cd62"
);
}
#[test]
fn the_sealing_key_depends_on_every_prekey_bit() {
let _q = quota();
let bytes: [u8; PREKEY_LEN] = core::array::from_fn(|i| (i % 251) as u8);
let mut flipped = bytes;
flipped[PREKEY_LEN - 1] ^= 1;
let salt = Prekey::fresh_salt();
let a = with_bytes(&bytes).sealing_key(&salt).with_secret(|k| *k);
let b = with_bytes(&flipped).sealing_key(&salt).with_secret(|k| *k);
assert_ne!(a, b);
}
#[test]
fn the_sealing_key_is_sha256_of_salt_then_prekey() {
let _q = quota();
let prekey = Prekey::new();
let salt = Prekey::fresh_salt();
let expected: [u8; 32] = prekey.block.with(|pk| {
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(pk);
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 debug_is_redacted() {
let _q = quota();
assert_eq!(format!("{:?}", Prekey::new()), "[REDACTED]");
assert_eq!(format!("{:?}", Prekey::new_idle_noaccess()), "[REDACTED]");
}
}