1#![allow(deprecated)]
7
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use aes_gcm::aead::Aead;
11use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
12use rand::TryRngCore;
13
14use super::pool::{hot_cache_evict, hot_cache_get, hot_cache_insert, pool_acquire, PoolSlot};
15use super::secure_buffer::SecureBuffer;
16use crate::error::{Error, Result};
17
18const NONCE_LEN: usize = 12;
19const TAG_LEN: usize = 16;
20
21fn fresh_nonce() -> Result<[u8; NONCE_LEN]> {
30 let mut nonce = [0_u8; NONCE_LEN];
31 rand::rngs::OsRng
32 .try_fill_bytes(&mut nonce)
33 .map_err(|e| Error::Memory(format!("MemoryEnclave: OsRng nonce failure: {e}")))?;
34 Ok(nonce)
35}
36
37pub struct MemoryEnclave {
54 id: u64,
55 ciphertext: Vec<u8>,
57 plaintext_len: usize,
58}
59
60static SEAL_ID: AtomicU64 = AtomicU64::new(1);
61
62impl std::fmt::Debug for MemoryEnclave {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("MemoryEnclave")
65 .field("id", &self.id)
66 .field("plaintext_len", &self.plaintext_len)
67 .finish()
68 }
69}
70
71impl MemoryEnclave {
72 fn do_seal(plaintext: &[u8]) -> Result<Self> {
73 let key_slot = super::pool::global_pool().coffer_view()?;
75 let nonce_bytes = fresh_nonce()?;
76 let nonce = Nonce::from_slice(&nonce_bytes);
77
78 let cipher = Aes256Gcm::new_from_slice(key_slot.as_slice())
79 .map_err(|e| Error::Memory(format!("MemoryEnclave::seal cipher init: {e}")))?;
80
81 let ct = cipher
82 .encrypt(nonce, plaintext)
83 .map_err(|e| Error::Memory(format!("MemoryEnclave::seal encrypt: {e}")))?;
84
85 drop(cipher); drop(key_slot); let mut blob = Vec::with_capacity(NONCE_LEN + ct.len());
93 blob.extend_from_slice(&nonce_bytes);
94 blob.extend_from_slice(&ct);
95
96 let id = SEAL_ID.fetch_add(1, Ordering::Relaxed);
97 Ok(Self {
98 id,
99 ciphertext: blob,
100 plaintext_len: plaintext.len(),
101 })
102 }
103
104 pub fn seal(plaintext: &[u8]) -> Result<Self> {
106 Self::do_seal(plaintext)
107 }
108
109 pub fn seal_buffer(buf: &mut SecureBuffer) -> Result<Self> {
111 buf.melt()?;
112 let result = Self::do_seal(buf.as_slice());
113 drop(buf.freeze());
114 result
115 }
116
117 pub fn seal_slot(slot: &PoolSlot) -> Result<Self> {
120 Self::do_seal(slot.as_slice())
121 }
122
123 pub fn open(&self) -> Result<PoolSlot> {
129 if let Some(cached) = hot_cache_get(self.id) {
131 return Ok(cached);
132 }
133
134 if self.ciphertext.len() < NONCE_LEN + TAG_LEN {
136 return Err(Error::Memory(
137 "MemoryEnclave::open: ciphertext too short".into(),
138 ));
139 }
140
141 let key_slot = super::pool::global_pool().coffer_view()?;
142 let nonce = Nonce::from_slice(&self.ciphertext[..NONCE_LEN]);
143
144 let cipher = Aes256Gcm::new_from_slice(key_slot.as_slice())
145 .map_err(|e| Error::Memory(format!("MemoryEnclave::open cipher init: {e}")))?;
146
147 let plaintext = zeroize::Zeroizing::new(
150 cipher
151 .decrypt(nonce, &self.ciphertext[NONCE_LEN..])
152 .map_err(|_| Error::DecryptFailed {
153 detail: "MemoryEnclave::open: authentication failed".into(),
154 })?,
155 );
156
157 drop(cipher); drop(key_slot); hot_cache_insert(self.id, &plaintext);
167
168 let mut out_slot = pool_acquire(plaintext.len()).map_err(|e| {
170 hot_cache_evict(self.id);
171 e
172 })?;
173 let copy_len = plaintext.len().min(out_slot.size());
174 out_slot.bytes()[..copy_len].copy_from_slice(&plaintext[..copy_len]);
175 Ok(out_slot)
176 }
177
178 pub fn plaintext_len(&self) -> usize {
179 self.plaintext_len
180 }
181
182 pub fn id(&self) -> u64 {
183 self.id
184 }
185}
186
187impl Drop for MemoryEnclave {
188 fn drop(&mut self) {
189 let id = self.id;
191 drop(std::panic::catch_unwind(move || {
193 hot_cache_evict(id);
194 }));
195 }
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used, clippy::panic)]
200mod tests {
201 use std::sync::Mutex;
202
203 use super::*;
204 use crate::memory::pool::{coffer_view, hot_cache_get};
205
206 static TEST_LOCK: Mutex<()> = Mutex::new(());
208
209 #[test]
210 fn seal_and_open_roundtrip() {
211 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
212 let secret = b"my secret data 1234";
213 let enc = MemoryEnclave::seal(secret).unwrap();
214 let slot = enc.open().unwrap();
215 assert_eq!(&slot.as_slice()[..secret.len()], secret.as_ref());
216 }
217
218 #[test]
219 fn open_twice_uses_hot_cache() {
220 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
221 let secret = b"cached secret";
222 let enc = MemoryEnclave::seal(secret).unwrap();
223 let s1 = enc.open().unwrap();
224 let bytes1 = s1.as_slice()[..secret.len()].to_vec();
226 drop(s1);
227 let s2 = enc.open().unwrap();
228 assert_eq!(bytes1, secret.as_ref());
229 assert_eq!(&s2.as_slice()[..secret.len()], secret.as_ref());
230 }
231
232 #[test]
233 fn drop_evicts_hot_cache() {
234 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
235 let secret = b"evicted secret";
236 let id = {
237 let enc = MemoryEnclave::seal(secret).unwrap();
238 let slot = enc.open().unwrap(); drop(slot);
240 enc.id()
241 }; assert!(hot_cache_get(id).is_none());
243 }
244
245 #[test]
246 fn different_enclaves_are_independent() {
247 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
248 let enc1 = MemoryEnclave::seal(b"secret one").unwrap();
249 let enc2 = MemoryEnclave::seal(b"secret two").unwrap();
250 assert_ne!(enc1.id(), enc2.id());
251 let s1 = enc1.open().unwrap();
252 let s2 = enc2.open().unwrap();
253 assert_eq!(&s1.as_slice()[..10], b"secret one");
254 assert_eq!(&s2.as_slice()[..10], b"secret two");
255 drop(s1);
256 drop(s2);
257 }
258
259 #[test]
260 fn seal_empty_slice() {
261 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
262 let enc = MemoryEnclave::seal(b"").unwrap();
263 assert_eq!(enc.plaintext_len(), 0);
264 let slot = enc.open().unwrap();
265 drop(slot);
266 }
267
268 #[test]
269 fn coffer_view_returns_key_sized_slot() {
270 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
271 let slot = coffer_view().unwrap();
272 assert_eq!(slot.size(), 32);
273 }
274
275 #[test]
276 fn pool_acquire_small_uses_slab() {
277 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
278 use crate::memory::pool::pool_acquire;
279 let slot = pool_acquire(16).unwrap();
280 assert!(slot.slab_index().is_some());
281 }
282
283 #[test]
284 fn pool_acquire_large_uses_standalone() {
285 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
286 use crate::memory::pool::pool_acquire;
287 let slot = pool_acquire(8192).unwrap();
288 assert!(slot.slab_index().is_none());
289 }
290
291 #[test]
292 fn seal_open_large() {
293 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
294 let plaintext = vec![0xAB_u8; 4096];
295 let enc = MemoryEnclave::seal(&plaintext).unwrap();
296 let slot = enc.open().unwrap();
297 assert_eq!(&slot.as_slice()[..4096], plaintext.as_slice());
298 }
299
300 #[test]
301 fn tampered_ciphertext_fails() {
302 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
303 let plaintext = b"tamper test";
304 let mut enc = MemoryEnclave::seal(plaintext).unwrap();
305 enc.ciphertext[NONCE_LEN] ^= 0xFF;
306 let result = enc.open();
307 assert!(
308 matches!(result, Err(Error::DecryptFailed { .. })),
309 "expected DecryptFailed, got {result:?}"
310 );
311 }
312
313 #[test]
314 fn truncated_ciphertext_fails() {
315 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
316 let enc = MemoryEnclave::seal(b"short").unwrap();
317 let truncated = MemoryEnclave {
318 id: enc.id,
319 ciphertext: vec![0_u8; NONCE_LEN + TAG_LEN - 1],
320 plaintext_len: 5,
321 };
322 let result = truncated.open();
323 assert!(
324 matches!(result, Err(Error::Memory(_))),
325 "expected Memory error, got {result:?}"
326 );
327 }
328
329 #[test]
330 fn unique_ids() {
331 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
332 let a = MemoryEnclave::seal(b"a").unwrap();
333 let b = MemoryEnclave::seal(b"b").unwrap();
334 assert_ne!(a.id(), b.id());
335 }
336
337 #[test]
338 fn seal_buffer_roundtrip() {
339 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
340 let secret = b"buffered secret";
341 let mut sbuf = SecureBuffer::new(secret.len()).unwrap();
342 sbuf.bytes().copy_from_slice(secret);
343 let enc = MemoryEnclave::seal_buffer(&mut sbuf).unwrap();
344 assert!(sbuf.is_alive());
345 let slot = enc.open().unwrap();
346 assert_eq!(&slot.as_slice()[..secret.len()], secret);
347 }
348
349 #[test]
350 fn seal_slot_roundtrip() {
351 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
352 let secret = b"slot secret data";
353 let mut slot = pool_acquire(secret.len()).unwrap();
354 slot.bytes()[..secret.len()].copy_from_slice(secret);
355 let enc = MemoryEnclave::seal_slot(&slot).unwrap();
356 drop(slot);
357 let out = enc.open().unwrap();
358 assert_eq!(&out.as_slice()[..secret.len()], secret);
359 }
360
361 #[test]
362 fn debug_does_not_leak_plaintext() {
363 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
364 let enc = MemoryEnclave::seal(b"top secret").unwrap();
365 let debug = format!("{enc:?}");
366 assert!(!debug.contains("top secret"));
367 assert!(debug.contains("MemoryEnclave"));
368 }
369
370 #[test]
371 fn same_plaintext_different_nonces() {
372 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
373 let a = MemoryEnclave::seal(b"same").unwrap();
374 let b = MemoryEnclave::seal(b"same").unwrap();
375 assert_ne!(a.ciphertext, b.ciphertext);
376 }
377
378 #[test]
381 fn plaintext_is_zeroized_after_open() {
382 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
386 let secret = b"zeroize test secret";
387 let enc = MemoryEnclave::seal(secret).unwrap();
388 let slot = enc.open().unwrap();
389 assert_eq!(&slot.as_slice()[..secret.len()], secret.as_ref());
390 }
391
392 #[test]
393 fn open_cache_evicted_on_drop() {
394 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
397 let enc = MemoryEnclave::seal(b"test").unwrap();
398 let id = enc.id();
399 let slot = enc.open().unwrap(); drop(slot);
401 drop(enc); assert!(hot_cache_get(id).is_none());
403 }
404
405 #[test]
406 fn nonce_prefix_is_nonzero() {
407 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
410 let enc1 = MemoryEnclave::seal(b"a").unwrap();
411 let enc2 = MemoryEnclave::seal(b"b").unwrap();
412 assert_ne!(enc1.ciphertext, enc2.ciphertext);
414 }
415
416 #[test]
417 fn fresh_nonce_never_returns_all_zeros() {
418 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
419 let enc1 = MemoryEnclave::seal(b"probe1").unwrap();
422 let enc2 = MemoryEnclave::seal(b"probe2").unwrap();
423 assert_ne!(enc1.ciphertext, enc2.ciphertext);
425 assert!(
427 enc1.ciphertext[..NONCE_LEN].iter().any(|&b| b != 0),
428 "nonce is all zeros — OsRng may be broken"
429 );
430 }
431
432 #[test]
433 fn seal_and_open_boundary_sizes() {
434 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
435 for size in [
436 0_usize, 1, 15, 16, 31, 32, 33, 63, 64, 1023, 1024, 4095, 4096, 65535,
437 ] {
438 let plaintext = vec![0x5A_u8; size];
439 let enc = MemoryEnclave::seal(&plaintext).unwrap();
440 assert_eq!(
441 enc.plaintext_len(),
442 size,
443 "size {size}: plaintext_len mismatch"
444 );
445 let slot = enc.open().unwrap();
446 assert_eq!(
447 &slot.as_slice()[..size],
448 &plaintext[..],
449 "size {size}: roundtrip mismatch"
450 );
451 }
452 }
453
454 #[test]
455 fn nonce_tampering_fails_authentication() {
456 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
457 let mut enc = MemoryEnclave::seal(b"tamper nonce test").unwrap();
458 enc.ciphertext[0] ^= 0x01;
460 let result = enc.open();
461 assert!(
462 matches!(result, Err(Error::DecryptFailed { .. })),
463 "nonce tampering must fail authentication: {result:?}"
464 );
465 }
466
467 #[test]
468 fn tag_tampering_fails_authentication() {
469 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
470 let mut enc = MemoryEnclave::seal(b"tamper tag test").unwrap();
471 let len = enc.ciphertext.len();
473 enc.ciphertext[len - 1] ^= 0x01;
474 let result = enc.open();
475 assert!(
476 matches!(result, Err(Error::DecryptFailed { .. })),
477 "tag tampering must fail authentication: {result:?}"
478 );
479 }
480
481 #[test]
482 fn seal_ids_are_unique_and_increasing() {
483 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
484 let encs: Vec<_> = (0..10)
485 .map(|_| MemoryEnclave::seal(b"x").unwrap())
486 .collect();
487 let ids: Vec<u64> = encs.iter().map(|e| e.id()).collect();
488 let mut sorted = ids.clone();
490 sorted.dedup();
491 assert_eq!(sorted.len(), ids.len(), "all IDs must be unique");
492 for w in ids.windows(2) {
494 assert!(
495 w[0] < w[1],
496 "IDs must be strictly increasing: {} < {}",
497 w[0],
498 w[1]
499 );
500 }
501 }
502
503 #[test]
504 fn different_plaintexts_produce_different_ciphertexts() {
505 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
506 let enc1 = MemoryEnclave::seal(b"first plaintext").unwrap();
507 let enc2 = MemoryEnclave::seal(b"second plaintext").unwrap();
508 assert_ne!(enc1.ciphertext, enc2.ciphertext);
509 }
510
511 #[test]
512 fn open_after_drop_uses_cold_path() {
513 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
516 let secret = b"cold path test";
517 let enc1 = MemoryEnclave::seal(secret).unwrap();
518 let id1 = enc1.id();
519 let slot1 = enc1.open().unwrap();
520 drop(slot1);
521 drop(enc1); assert!(hot_cache_get(id1).is_none());
524 let enc2 = MemoryEnclave::seal(secret).unwrap();
526 let slot2 = enc2.open().unwrap();
527 assert_eq!(&slot2.as_slice()[..secret.len()], secret);
528 }
529
530 #[test]
531 fn seal_slot_does_not_consume_slot() {
532 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
533 let secret = b"slot test value";
534 let mut slot = pool_acquire(secret.len()).unwrap();
535 slot.bytes()[..secret.len()].copy_from_slice(secret);
536 let enc = MemoryEnclave::seal_slot(&slot).unwrap();
537 assert_eq!(&slot.as_slice()[..secret.len()], secret);
539 drop(slot); let recovered = enc.open().unwrap();
542 assert_eq!(&recovered.as_slice()[..secret.len()], secret);
543 }
544
545 #[test]
546 fn ciphertext_nonce_is_twelve_bytes() {
547 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
548 let enc = MemoryEnclave::seal(b"nonce len test").unwrap();
549 assert!(
551 enc.ciphertext[..12].iter().any(|&b| b != 0),
552 "nonce (first 12 bytes) must not be all zeros"
553 );
554 assert_eq!(enc.ciphertext.len(), 12 + enc.plaintext_len() + 16);
556 }
557
558 #[test]
559 fn coffer_slot_released_after_seal() {
560 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
563 for _ in 0..20 {
564 MemoryEnclave::seal(b"coffer slot release test").unwrap();
565 }
566 }
568
569 #[test]
570 fn concurrent_seal_and_open() {
571 use std::sync::Arc;
572 use std::thread;
573 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
574 let barrier = Arc::new(std::sync::Barrier::new(4));
576 let handles: Vec<_> = (0..4_u8)
577 .map(|i| {
578 let b = Arc::clone(&barrier);
579 thread::spawn(move || {
580 let secret = vec![i; 16];
581 let enc = MemoryEnclave::seal(&secret).unwrap();
582 b.wait();
583 let slot = enc.open().unwrap();
584 assert_eq!(
585 &slot.as_slice()[..16],
586 &secret[..],
587 "thread {i}: roundtrip mismatch"
588 );
589 })
590 })
591 .collect();
592 for h in handles {
593 h.join().expect("thread panicked");
594 }
595 }
596}