1#![doc = include_str!("../README.md")]
2
3use std::marker::PhantomData;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6mod protected;
7pub use protected::{ProtectedRegion, ProtectionError};
8
9pub struct Siegel<State> {
21 region: ProtectedRegion,
22 _state: PhantomData<State>,
23}
24
25pub struct Empty;
27
28pub struct Loaded;
30
31impl Siegel<Empty> {
32 pub fn new(len: usize) -> Result<Self, SiegelError> {
40 let region = ProtectedRegion::new(len)?;
41 Ok(Self {
42 region,
43 _state: PhantomData,
44 })
45 }
46
47 pub fn write(mut self, bytes: &[u8]) -> Result<Siegel<Loaded>, SiegelError> {
55 let expected = self.region.len();
56 if bytes.len() != expected {
57 return Err(SiegelError::LengthMismatch {
58 expected,
59 got: bytes.len(),
60 });
61 }
62 self.region.with_write(|buf| buf.copy_from_slice(bytes))?;
63 Ok(Siegel {
64 region: self.region,
65 _state: PhantomData,
66 })
67 }
68}
69
70impl Siegel<Loaded> {
71 pub fn read_once<T, F>(mut self, f: F) -> Result<T, SiegelError>
83 where
84 F: FnOnce(&[u8]) -> T,
85 {
86 let result = self.region.with_read(f)?;
87 Ok(result)
88 }
90}
91
92impl<State> Siegel<State> {
93 pub fn obliviate(self) {
95 drop(self);
96 }
97
98 #[must_use]
100 #[expect(clippy::len_without_is_empty, reason = "always non-empty")]
101 pub fn len(&self) -> usize {
102 self.region.len()
103 }
104}
105
106impl<State> Zeroize for Siegel<State> {
107 fn zeroize(&mut self) {
108 self.region.zeroize();
109 }
110}
111
112impl<State> ZeroizeOnDrop for Siegel<State> {}
113
114#[derive(Debug, thiserror::Error)]
116pub enum SiegelError {
117 #[error("requested size must be 1..=1Mb")]
118 InvalidLength,
119 #[error("input length {got} doesn't match siegel capacity {expected}")]
120 LengthMismatch { expected: usize, got: usize },
121 #[error("memory allocation failed: {reason}")]
122 AllocationFailed { reason: String },
123 #[error("memory protection failed: {reason}")]
124 ProtectionFailed { reason: String },
125 #[error("memory lock failed: {reason}")]
126 LockFailed { reason: String },
127 #[error("canary check failed — possible memory corruption")]
128 CanaryCorrupted,
129}
130
131impl From<ProtectionError> for SiegelError {
132 fn from(e: ProtectionError) -> Self {
133 match e {
134 ProtectionError::InvalidSize => Self::InvalidLength,
135 ProtectionError::Mmap(e) => Self::AllocationFailed {
136 reason: e.to_string(),
137 },
138 ProtectionError::Mprotect(e) => Self::ProtectionFailed {
139 reason: e.to_string(),
140 },
141 ProtectionError::Mlock(e) => Self::LockFailed {
142 reason: e.to_string(),
143 },
144 ProtectionError::CanaryCorrupted => Self::CanaryCorrupted,
145 }
146 }
147}
148
149#[cfg(test)]
150#[expect(clippy::unwrap_used, reason = "tests")]
151mod tests {
152 use sha2::{Digest, Sha256};
153
154 use super::*;
155
156 #[test]
157 fn new_creates_empty_of_given_size() {
158 let s: Siegel<Empty> = Siegel::new(32).unwrap();
159 assert_eq!(s.len(), 32);
160 }
161
162 #[test]
163 fn new_rejects_zero_length() {
164 assert!(Siegel::<Empty>::new(0).is_err());
165 }
166
167 #[test]
168 fn test_completew_flow() {
169 let secret = vec![0x42; 32];
170 let empty: Siegel<Empty> = Siegel::new(32).unwrap();
171 let loaded: Siegel<Loaded> = empty.write(&secret).unwrap();
172 let copy = loaded.read_once(<[u8]>::to_vec).unwrap();
173 assert_eq!(copy, secret);
174 }
175
176 #[test]
177 #[expect(clippy::panic, reason = "looking for specific failure")]
178 fn write_rejects_length_mismatch() {
179 let empty: Siegel<Empty> = Siegel::new(16).unwrap();
180 match empty.write(&[0u8; 8]) {
181 Ok(_) => panic!("expected LengthMismatch"),
182 Err(SiegelError::LengthMismatch {
183 expected: 16,
184 got: 8,
185 }) => {}
186 Err(other) => panic!("unexpected error: {other:?}"),
187 }
188 }
189
190 #[test]
191 fn closure_can_perform_arbitrary_operation() {
192 let secret = vec![0x42; 16];
193 let loaded = Siegel::<Empty>::new(16).unwrap().write(&secret).unwrap();
194 let digest = loaded
195 .read_once(|bytes| {
196 let mut h = Sha256::new();
197 h.update(bytes);
198 h.update(b"context");
199 h.finalize().to_vec()
200 })
201 .unwrap();
202 let mut expected = Sha256::new();
203 expected.update(&secret);
204 expected.update(b"context");
205 assert_eq!(digest, expected.finalize().to_vec());
206 }
207
208 #[test]
209 fn closure_return_type_is_generic() {
210 let loaded = Siegel::<Empty>::new(8).unwrap().write(&[1u8; 8]).unwrap();
211 let len: usize = loaded.read_once(<[u8]>::len).unwrap();
212 assert_eq!(len, 8);
213 }
214
215 #[test]
216 fn obliviate_on_empty() {
217 let empty: Siegel<Empty> = Siegel::new(16).unwrap();
218 empty.obliviate();
219 }
220
221 #[test]
222 fn obliviate_on_loaded() {
223 let loaded = Siegel::<Empty>::new(16).unwrap().write(&[7u8; 16]).unwrap();
224 loaded.obliviate();
225 }
226
227 #[test]
228 #[expect(clippy::panic, reason = "deliberately panicking inside the closure")]
229 fn closure_panic_still_drops_loaded() {
230 let loaded = Siegel::<Empty>::new(8).unwrap().write(&[6u8; 8]).unwrap();
231 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
232 loaded.read_once(|_| panic!("operation failed"))
233 }));
234 assert!(result.is_err());
235 }
238}