#![doc = include_str!("../README.md")]
use std::marker::PhantomData;
use zeroize::{Zeroize, ZeroizeOnDrop};
mod protected;
pub use protected::{ProtectedRegion, ProtectionError};
pub struct Siegel<State> {
region: ProtectedRegion,
_state: PhantomData<State>,
}
pub struct Empty;
pub struct Loaded;
impl Siegel<Empty> {
pub fn new(len: usize) -> Result<Self, SiegelError> {
let region = ProtectedRegion::new(len)?;
Ok(Self {
region,
_state: PhantomData,
})
}
pub fn write(mut self, bytes: &[u8]) -> Result<Siegel<Loaded>, SiegelError> {
let expected = self.region.len();
if bytes.len() != expected {
return Err(SiegelError::LengthMismatch {
expected,
got: bytes.len(),
});
}
self.region.with_write(|buf| buf.copy_from_slice(bytes))?;
Ok(Siegel {
region: self.region,
_state: PhantomData,
})
}
}
impl Siegel<Loaded> {
pub fn read_once<T, F>(mut self, f: F) -> Result<T, SiegelError>
where
F: FnOnce(&[u8]) -> T,
{
let result = self.region.with_read(f)?;
Ok(result)
}
}
impl<State> Siegel<State> {
pub fn obliviate(self) {
drop(self);
}
#[must_use]
#[expect(clippy::len_without_is_empty, reason = "always non-empty")]
pub fn len(&self) -> usize {
self.region.len()
}
}
impl<State> Zeroize for Siegel<State> {
fn zeroize(&mut self) {
self.region.zeroize();
}
}
impl<State> ZeroizeOnDrop for Siegel<State> {}
#[derive(Debug, thiserror::Error)]
pub enum SiegelError {
#[error("requested size must be 1..=1Mb")]
InvalidLength,
#[error("input length {got} doesn't match siegel capacity {expected}")]
LengthMismatch { expected: usize, got: usize },
#[error("memory allocation failed: {reason}")]
AllocationFailed { reason: String },
#[error("memory protection failed: {reason}")]
ProtectionFailed { reason: String },
#[error("memory lock failed: {reason}")]
LockFailed { reason: String },
#[error("canary check failed — possible memory corruption")]
CanaryCorrupted,
}
impl From<ProtectionError> for SiegelError {
fn from(e: ProtectionError) -> Self {
match e {
ProtectionError::InvalidSize => Self::InvalidLength,
ProtectionError::Mmap(e) => Self::AllocationFailed {
reason: e.to_string(),
},
ProtectionError::Mprotect(e) => Self::ProtectionFailed {
reason: e.to_string(),
},
ProtectionError::Mlock(e) => Self::LockFailed {
reason: e.to_string(),
},
ProtectionError::CanaryCorrupted => Self::CanaryCorrupted,
}
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests")]
mod tests {
use sha2::{Digest, Sha256};
use super::*;
#[test]
fn new_creates_empty_of_given_size() {
let s: Siegel<Empty> = Siegel::new(32).unwrap();
assert_eq!(s.len(), 32);
}
#[test]
fn new_rejects_zero_length() {
assert!(Siegel::<Empty>::new(0).is_err());
}
#[test]
fn test_completew_flow() {
let secret = vec![0x42; 32];
let empty: Siegel<Empty> = Siegel::new(32).unwrap();
let loaded: Siegel<Loaded> = empty.write(&secret).unwrap();
let copy = loaded.read_once(<[u8]>::to_vec).unwrap();
assert_eq!(copy, secret);
}
#[test]
#[expect(clippy::panic, reason = "looking for specific failure")]
fn write_rejects_length_mismatch() {
let empty: Siegel<Empty> = Siegel::new(16).unwrap();
match empty.write(&[0u8; 8]) {
Ok(_) => panic!("expected LengthMismatch"),
Err(SiegelError::LengthMismatch {
expected: 16,
got: 8,
}) => {}
Err(other) => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn closure_can_perform_arbitrary_operation() {
let secret = vec![0x42; 16];
let loaded = Siegel::<Empty>::new(16).unwrap().write(&secret).unwrap();
let digest = loaded
.read_once(|bytes| {
let mut h = Sha256::new();
h.update(bytes);
h.update(b"context");
h.finalize().to_vec()
})
.unwrap();
let mut expected = Sha256::new();
expected.update(&secret);
expected.update(b"context");
assert_eq!(digest, expected.finalize().to_vec());
}
#[test]
fn closure_return_type_is_generic() {
let loaded = Siegel::<Empty>::new(8).unwrap().write(&[1u8; 8]).unwrap();
let len: usize = loaded.read_once(<[u8]>::len).unwrap();
assert_eq!(len, 8);
}
#[test]
fn obliviate_on_empty() {
let empty: Siegel<Empty> = Siegel::new(16).unwrap();
empty.obliviate();
}
#[test]
fn obliviate_on_loaded() {
let loaded = Siegel::<Empty>::new(16).unwrap().write(&[7u8; 16]).unwrap();
loaded.obliviate();
}
#[test]
#[expect(clippy::panic, reason = "deliberately panicking inside the closure")]
fn closure_panic_still_drops_loaded() {
let loaded = Siegel::<Empty>::new(8).unwrap().write(&[6u8; 8]).unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
loaded.read_once(|_| panic!("operation failed"))
}));
assert!(result.is_err());
}
}