1use std::collections::BTreeSet;
55use std::sync::atomic::{AtomicU64, Ordering};
56use std::sync::{Arc, Mutex};
57
58use logicaffeine_proof::ait::{classify_bytes, CompressibilityClass};
59use logicaffeine_system::aead::poly1305;
60use logicaffeine_system::keccak::sha3_256_bytes;
61
62use super::channel::{ActiveSession, CHAN_HEADER_LEN, CHAN_MAGIC, CHAN_VER, SUITE_PNP};
63
64pub const MAC_KEY_LEN: usize = 32;
67
68pub const TAG_LEN: usize = 16;
70
71const KIND_LEN: usize = 1;
73const EPOCH_LEN: usize = 4;
75const OFF_LEN: usize = 8;
77const LEN_LEN: usize = 4;
79const PREAMBLE: usize = KIND_LEN + EPOCH_LEN + OFF_LEN + LEN_LEN;
81
82const KIND_DATA: u8 = 0;
84const KIND_ROLL: u8 = 1;
86
87pub const RECV_WINDOW_BYTES: u64 = 1 << 20;
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Role {
98 Initiator,
100 Responder,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum PadError {
107 TooSmall,
109 Compressible,
112}
113
114pub trait PadLedgerStore: Send + Sync {
119 fn load_cursor(&self) -> u64;
121 fn commit_cursor(&self, cursor: u64);
123}
124
125#[derive(Default)]
128pub struct MemLedger {
129 cursor: AtomicU64,
130}
131
132impl PadLedgerStore for MemLedger {
133 fn load_cursor(&self) -> u64 {
134 self.cursor.load(Ordering::Relaxed)
135 }
136 fn commit_cursor(&self, cursor: u64) {
137 self.cursor.store(cursor, Ordering::Relaxed);
138 }
139}
140
141#[cfg(not(target_arch = "wasm32"))]
146pub struct FileLedger {
147 path: std::path::PathBuf,
148}
149
150#[cfg(not(target_arch = "wasm32"))]
151impl FileLedger {
152 pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
154 Self { path: path.into() }
155 }
156}
157
158#[cfg(not(target_arch = "wasm32"))]
159impl PadLedgerStore for FileLedger {
160 fn load_cursor(&self) -> u64 {
161 match std::fs::read(&self.path) {
162 Ok(bytes) if bytes.len() == 8 => u64::from_le_bytes(bytes.try_into().unwrap()),
163 _ => 0,
164 }
165 }
166 fn commit_cursor(&self, cursor: u64) {
167 use std::io::Write;
168 let tmp = self.path.with_extension("tmp");
169 let mut f = std::fs::File::create(&tmp).expect("pnp ledger: create temp");
170 f.write_all(&cursor.to_le_bytes()).expect("pnp ledger: write");
171 f.sync_all().expect("pnp ledger: fsync");
172 std::fs::rename(&tmp, &self.path).expect("pnp ledger: rename");
173 }
174}
175
176struct RecvWindow {
180 consumed: BTreeSet<u64>,
181 newest_end: u64,
182 window: u64,
183}
184
185impl RecvWindow {
186 fn new(window: u64) -> Self {
187 Self { consumed: BTreeSet::new(), newest_end: 0, window }
188 }
189 fn floor(&self) -> u64 {
190 self.newest_end.saturating_sub(self.window)
191 }
192 fn acceptable(&self, offset: u64) -> bool {
193 !self.consumed.contains(&offset) && offset >= self.floor()
194 }
195 fn accept(&mut self, offset: u64, end: u64) {
196 self.consumed.insert(offset);
197 if end > self.newest_end {
198 self.newest_end = end;
199 }
200 let floor = self.floor();
201 while let Some(&first) = self.consumed.iter().next() {
202 if first < floor {
203 self.consumed.remove(&first);
204 } else {
205 break;
206 }
207 }
208 }
209}
210
211pub struct PnpSuite {
220 pad: Arc<[u8]>,
221 base: usize,
222 len: usize,
223 epoch: u32,
224 cursor: Mutex<u64>,
225 ledger: Arc<dyn PadLedgerStore>,
226 recv: Mutex<RecvWindow>,
227}
228
229impl PnpSuite {
230 fn over(pad: Arc<[u8]>, base: usize, len: usize, epoch: u32) -> Self {
231 Self {
232 pad,
233 base,
234 len,
235 epoch,
236 cursor: Mutex::new(0),
237 ledger: Arc::new(MemLedger::default()),
238 recv: Mutex::new(RecvWindow::new(RECV_WINDOW_BYTES)),
239 }
240 }
241
242 pub fn with_ledger(self, ledger: Arc<dyn PadLedgerStore>) -> Self {
244 let resumed = ledger.load_cursor();
245 *self.cursor.lock().unwrap() = resumed;
246 Self { ledger, ..self }
247 }
248
249 pub fn with_recv_window(self, bytes: u64) -> Self {
251 *self.recv.lock().unwrap() = RecvWindow::new(bytes);
252 self
253 }
254
255 pub fn epoch(&self) -> u32 {
257 self.epoch
258 }
259
260 pub fn send_remaining(&self) -> usize {
263 let cursor = *self.cursor.lock().unwrap();
264 self.len.saturating_sub(cursor as usize)
265 }
266
267 pub fn is_low(&self, threshold: usize) -> bool {
270 self.send_remaining() < threshold
271 }
272
273 pub fn seal(&self, blob: &[u8]) -> Option<Vec<u8>> {
275 self.seal_kind(KIND_DATA, blob)
276 }
277
278 pub fn seal_roll(&self, next_epoch: u32, next_commitment: &[u8; 32]) -> Option<Vec<u8>> {
282 let mut payload = Vec::with_capacity(EPOCH_LEN + 32);
283 payload.extend_from_slice(&next_epoch.to_le_bytes());
284 payload.extend_from_slice(next_commitment);
285 self.seal_kind(KIND_ROLL, &payload)
286 }
287
288 fn seal_kind(&self, kind: u8, blob: &[u8]) -> Option<Vec<u8>> {
289 let len = blob.len();
290 if len > u32::MAX as usize {
291 return None;
292 }
293 let need = (MAC_KEY_LEN + len) as u64;
294
295 let offset = {
296 let mut cursor = self.cursor.lock().unwrap();
297 let offset = *cursor;
298 let end = offset.checked_add(need)?;
299 if end > self.len as u64 {
300 return None; }
302 self.ledger.commit_cursor(end);
304 *cursor = end;
305 offset
306 };
307
308 let seg = self.base + offset as usize;
309 let mac_key: [u8; MAC_KEY_LEN] = self.pad[seg..seg + MAC_KEY_LEN].try_into().ok()?;
310
311 let mut out = Vec::with_capacity(CHAN_HEADER_LEN + PREAMBLE + len + TAG_LEN);
312 out.push(CHAN_MAGIC);
313 out.push(CHAN_VER);
314 out.extend_from_slice(&SUITE_PNP.to_le_bytes());
315 out.push(kind);
316 out.extend_from_slice(&self.epoch.to_le_bytes());
317 out.extend_from_slice(&offset.to_le_bytes());
318 out.extend_from_slice(&(len as u32).to_le_bytes());
319 let payload_start = CHAN_HEADER_LEN + PREAMBLE;
323 out.resize(payload_start + len, 0);
324 let xor = &self.pad[seg + MAC_KEY_LEN..seg + MAC_KEY_LEN + len];
325 for ((d, b), p) in out[payload_start..payload_start + len].iter_mut().zip(blob).zip(xor) {
326 *d = b ^ p;
327 }
328
329 let tag = poly1305(&mac_key, &out[CHAN_HEADER_LEN..CHAN_HEADER_LEN + PREAMBLE + len]);
331 out.extend_from_slice(&tag);
332 Some(out)
333 }
334
335 pub fn open(&self, frame: &[u8]) -> Option<Vec<u8>> {
337 self.open_kind(KIND_DATA, frame)
338 }
339
340 pub fn open_roll(&self, frame: &[u8]) -> Option<(u32, [u8; 32])> {
343 let pt = self.open_kind(KIND_ROLL, frame)?;
344 if pt.len() != EPOCH_LEN + 32 {
345 return None;
346 }
347 let next_epoch = u32::from_le_bytes(pt[0..EPOCH_LEN].try_into().ok()?);
348 let commitment: [u8; 32] = pt[EPOCH_LEN..EPOCH_LEN + 32].try_into().ok()?;
349 Some((next_epoch, commitment))
350 }
351
352 fn open_kind(&self, want_kind: u8, frame: &[u8]) -> Option<Vec<u8>> {
353 if frame.len() < CHAN_HEADER_LEN || frame[0] != CHAN_MAGIC || frame[1] != CHAN_VER {
354 return None;
355 }
356 if u16::from_le_bytes([frame[2], frame[3]]) != SUITE_PNP {
357 return None;
358 }
359 let body = &frame[CHAN_HEADER_LEN..];
360 if body.len() < PREAMBLE + TAG_LEN {
361 return None;
362 }
363 if body[0] != want_kind {
364 return None;
365 }
366 let epoch = u32::from_le_bytes(body[KIND_LEN..KIND_LEN + EPOCH_LEN].try_into().ok()?);
367 if epoch != self.epoch {
368 return None; }
370 let off_at = KIND_LEN + EPOCH_LEN;
371 let offset = u64::from_le_bytes(body[off_at..off_at + OFF_LEN].try_into().ok()?);
372 let len = u32::from_le_bytes(body[off_at + OFF_LEN..PREAMBLE].try_into().ok()?) as usize;
373 if body.len() != PREAMBLE + len + TAG_LEN {
374 return None;
375 }
376 let payload = &body[PREAMBLE..PREAMBLE + len];
377 let tag = &body[PREAMBLE + len..];
378
379 let end = offset.checked_add((MAC_KEY_LEN + len) as u64)?;
380 if end > self.len as u64 {
381 return None;
382 }
383 if !self.recv.lock().unwrap().acceptable(offset) {
384 return None; }
386
387 let seg = self.base + offset as usize;
388 let mac_key: [u8; MAC_KEY_LEN] = self.pad[seg..seg + MAC_KEY_LEN].try_into().ok()?;
389 let expect = poly1305(&mac_key, &body[0..PREAMBLE + len]);
390 if !ct_eq(&expect, tag) {
391 return None; }
393
394 let xor = &self.pad[seg + MAC_KEY_LEN..seg + MAC_KEY_LEN + len];
395 let mut plain = vec![0u8; len];
396 for ((d, c), p) in plain.iter_mut().zip(payload).zip(xor) {
397 *d = c ^ p;
398 }
399 self.recv.lock().unwrap().accept(offset, end);
400 Some(plain)
401 }
402}
403
404pub struct PnpSession {
409 send: PnpSuite,
410 recv: PnpSuite,
411}
412
413impl PnpSession {
414 pub fn new(send: PnpSuite, recv: PnpSuite) -> Self {
416 Self { send, recv }
417 }
418 pub fn send(&self) -> &PnpSuite {
420 &self.send
421 }
422 pub fn recv(&self) -> &PnpSuite {
424 &self.recv
425 }
426}
427
428impl ActiveSession for PnpSession {
429 fn seal(&self, bytes: &[u8]) -> Option<Vec<u8>> {
430 self.send.seal(bytes)
431 }
432 fn open(&self, bytes: &[u8]) -> Option<Vec<u8>> {
433 self.recv.open(bytes)
434 }
435}
436
437pub struct PadPool {
441 pad: Arc<[u8]>,
442 split: usize,
443 epoch: u32,
444}
445
446impl PadPool {
447 pub fn shared(pad: Vec<u8>) -> Result<PadPool, PadError> {
451 if pad.len() < 2 {
452 return Err(PadError::TooSmall);
453 }
454 if classify_bytes(&pad).class != CompressibilityClass::Incompressible {
455 return Err(PadError::Compressible);
456 }
457 Ok(Self::shared_unchecked(pad))
458 }
459
460 pub fn shared_unchecked(pad: Vec<u8>) -> PadPool {
463 let split = pad.len() / 2;
464 PadPool { pad: pad.into(), split, epoch: 0 }
465 }
466
467 pub fn with_epoch(mut self, epoch: u32) -> PadPool {
469 self.epoch = epoch;
470 self
471 }
472
473 pub fn epoch(&self) -> u32 {
475 self.epoch
476 }
477
478 pub fn commitment(&self) -> [u8; 32] {
483 let mut input = Vec::with_capacity(EPOCH_LEN + self.pad.len());
484 input.extend_from_slice(&self.epoch.to_le_bytes());
485 input.extend_from_slice(&self.pad);
486 sha3_256_bytes(&input)
487 }
488
489 pub fn direction_len(&self) -> usize {
491 self.split.min(self.pad.len() - self.split)
492 }
493
494 fn half(&self, first: bool) -> (usize, usize) {
495 if first {
496 (0, self.split)
497 } else {
498 (self.split, self.pad.len() - self.split)
499 }
500 }
501
502 pub fn send_suite(&self, role: Role) -> PnpSuite {
504 let (base, len) = self.half(role == Role::Initiator);
505 PnpSuite::over(self.pad.clone(), base, len, self.epoch)
506 }
507
508 pub fn recv_suite(&self, role: Role) -> PnpSuite {
510 let (base, len) = self.half(role == Role::Responder);
511 PnpSuite::over(self.pad.clone(), base, len, self.epoch)
512 }
513
514 pub fn session(&self, role: Role) -> PnpSession {
518 PnpSession::new(self.send_suite(role), self.recv_suite(role))
519 }
520}
521
522pub fn frame_kind(frame: &[u8]) -> Option<u8> {
525 if frame.len() < CHAN_HEADER_LEN + KIND_LEN
526 || frame[0] != CHAN_MAGIC
527 || frame[1] != CHAN_VER
528 || u16::from_le_bytes([frame[2], frame[3]]) != SUITE_PNP
529 {
530 return None;
531 }
532 Some(frame[CHAN_HEADER_LEN])
533}
534
535pub fn frame_offset(frame: &[u8]) -> Option<u64> {
538 let off_at = CHAN_HEADER_LEN + KIND_LEN + EPOCH_LEN;
539 if frame.len() < off_at + OFF_LEN
540 || frame[0] != CHAN_MAGIC
541 || frame[1] != CHAN_VER
542 || u16::from_le_bytes([frame[2], frame[3]]) != SUITE_PNP
543 {
544 return None;
545 }
546 Some(u64::from_le_bytes(frame[off_at..off_at + OFF_LEN].try_into().ok()?))
547}
548
549fn ct_eq(a: &[u8], b: &[u8]) -> bool {
551 if a.len() != b.len() {
552 return false;
553 }
554 let mut diff = 0u8;
555 for (x, y) in a.iter().zip(b) {
556 diff |= x ^ y;
557 }
558 diff == 0
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use super::super::marshal::{message_to_wire_with, WireCodec, WireIntegrity};
565 use crate::interpreter::RuntimeValue;
566
567 fn splitmix64(state: &mut u64) -> u64 {
570 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
571 let mut z = *state;
572 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
573 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
574 z ^ (z >> 31)
575 }
576
577 fn random_pad(len: usize, seed: u64) -> Vec<u8> {
578 let mut s = seed;
579 (0..len).map(|_| splitmix64(&mut s) as u8).collect()
580 }
581
582 const PAYLOAD_START: usize = CHAN_HEADER_LEN + PREAMBLE;
583
584 fn ciphertext_of(frame: &[u8]) -> Vec<u8> {
586 let off_at = CHAN_HEADER_LEN + KIND_LEN + EPOCH_LEN + OFF_LEN;
587 let len = u32::from_le_bytes(frame[off_at..off_at + LEN_LEN].try_into().unwrap()) as usize;
588 frame[PAYLOAD_START..PAYLOAD_START + len].to_vec()
589 }
590
591 fn cover_range(frame: &[u8]) -> (u64, u64) {
593 let off = frame_offset(frame).unwrap();
594 let len = ciphertext_of(frame).len();
595 (off, off + (MAC_KEY_LEN + len) as u64)
596 }
597
598 fn ranges_disjoint(ranges: &[(u64, u64)]) -> bool {
599 for i in 0..ranges.len() {
600 for j in (i + 1)..ranges.len() {
601 let (a0, a1) = ranges[i];
602 let (b0, b1) = ranges[j];
603 if a0 < b1 && b0 < a1 {
604 return false;
605 }
606 }
607 }
608 true
609 }
610
611 #[test]
612 fn roundtrip_seal_open() {
613 let pool = PadPool::shared(random_pad(4096, 0x1111_2222_3333_4444)).expect("incompressible pad");
614 let sender = pool.send_suite(Role::Initiator);
615 let receiver = pool.recv_suite(Role::Responder);
616 let msg = b"attack at dawn";
617 let frame = sender.seal(msg).expect("pad available");
618 assert_eq!(receiver.open(&frame).as_deref(), Some(&msg[..]), "cover round-trips exactly");
619 }
620
621 #[test]
622 fn ciphertext_is_plaintext_xor_pad() {
623 let keystream = [0xA5u8, 0x5A, 0xFF, 0x00, 0x13, 0x37, 0xBE, 0xEF];
624 let mut pad = vec![7u8; MAC_KEY_LEN];
625 pad.extend_from_slice(&keystream);
626 pad.extend(std::iter::repeat(0u8).take(64));
627 let pool = PadPool::shared_unchecked(pad);
628 let sender = pool.send_suite(Role::Initiator);
629 let plaintext = [1u8, 2, 3, 4, 5, 6, 7, 8];
630 let frame = sender.seal(&plaintext).expect("pad available");
631 let ct = ciphertext_of(&frame);
632 let expect: Vec<u8> = plaintext.iter().zip(&keystream).map(|(p, k)| p ^ k).collect();
633 assert_eq!(ct, expect, "ciphertext is the exact plaintext ⊕ pad");
634 assert_ne!(ct, plaintext.to_vec(), "ciphertext is not the plaintext");
635 }
636
637 #[test]
638 fn seal_matches_independent_reference() {
639 let pad = random_pad(1024, 0xCAFE_F00D_1234_5678);
642 let pool = PadPool::shared_unchecked(pad.clone()).with_epoch(0);
643 let sender = pool.send_suite(Role::Initiator); let msg = b"reference vector";
645 let frame = sender.seal(msg).expect("pad");
646
647 let mac_key: [u8; 32] = pad[0..32].try_into().unwrap();
649 let ks = &pad[32..32 + msg.len()];
650 let ct: Vec<u8> = msg.iter().zip(ks).map(|(m, k)| m ^ k).collect();
651 let mut expect = Vec::new();
652 expect.push(CHAN_MAGIC);
653 expect.push(CHAN_VER);
654 expect.extend_from_slice(&SUITE_PNP.to_le_bytes());
655 expect.push(KIND_DATA);
656 expect.extend_from_slice(&0u32.to_le_bytes()); expect.extend_from_slice(&0u64.to_le_bytes()); expect.extend_from_slice(&(msg.len() as u32).to_le_bytes());
659 expect.extend_from_slice(&ct);
660 let tag = logicaffeine_system::aead::poly1305(&mac_key, &expect[CHAN_HEADER_LEN..]);
661 expect.extend_from_slice(&tag);
662 assert_eq!(frame, expect, "sealed frame matches the independent reference byte-for-byte");
663 }
664
665 #[test]
666 fn never_reuses_pad_bytes() {
667 let pool = PadPool::shared(random_pad(8192, 0xDEAD_BEEF_0000_0001)).expect("pad");
668 let sender = pool.send_suite(Role::Initiator);
669 let mut ranges = Vec::new();
670 let mut s = 0xABCD_1234u64;
671 for _ in 0..200 {
672 let len = (splitmix64(&mut s) % 17) as usize;
673 let msg: Vec<u8> = (0..len).map(|_| splitmix64(&mut s) as u8).collect();
674 if let Some(frame) = sender.seal(&msg) {
675 ranges.push(cover_range(&frame));
676 }
677 }
678 assert!(!ranges.is_empty(), "some covers were sealed");
679 assert!(ranges_disjoint(&ranges), "no two covers share a single pad byte");
680 for w in ranges.windows(2) {
681 assert!(w[0].0 < w[1].0, "cursor is monotonic");
682 assert!(w[0].1 <= w[1].0, "covers are laid down contiguously, never overlapping");
683 }
684 }
685
686 #[test]
687 fn concurrent_seals_never_overlap() {
688 let pool = PadPool::shared(random_pad(1 << 16, 0x0F0F_0F0F_1234_5678)).expect("pad");
689 let sender = Arc::new(pool.send_suite(Role::Initiator));
690 let mut handles = Vec::new();
691 for t in 0..8 {
692 let s = sender.clone();
693 handles.push(std::thread::spawn(move || {
694 let mut out = Vec::new();
695 for i in 0..64 {
696 let msg = [(t as u8), (i as u8), 0xEE];
697 if let Some(frame) = s.seal(&msg) {
698 out.push(cover_range(&frame));
699 }
700 }
701 out
702 }));
703 }
704 let mut ranges = Vec::new();
705 for h in handles {
706 ranges.extend(h.join().unwrap());
707 }
708 assert_eq!(ranges.len(), 8 * 64, "every concurrent seal succeeded");
709 assert!(ranges_disjoint(&ranges), "concurrent covers are pairwise disjoint");
710 }
711
712 #[test]
713 fn directional_split_no_overlap() {
714 let mut pad = random_pad(2048, 0x5555_6666_7777_8888);
715 let mid = pad.len() / 2;
716 for b in &mut pad[..mid] {
717 *b |= 0x01;
718 }
719 for b in &mut pad[mid..] {
720 *b &= 0xFE;
721 }
722 let pool = PadPool::shared_unchecked(pad);
723
724 let init_send = pool.send_suite(Role::Initiator);
725 let resp_send = pool.send_suite(Role::Responder);
726 let msg = b"same plaintext";
727 let f_i = init_send.seal(msg).expect("pad");
728 let f_r = resp_send.seal(msg).expect("pad");
729 assert_ne!(ciphertext_of(&f_i), ciphertext_of(&f_r), "different halves ⇒ different ciphertext");
730
731 assert_eq!(pool.recv_suite(Role::Responder).open(&f_i).as_deref(), Some(&msg[..]), "matching direction opens");
732 assert!(pool.recv_suite(Role::Initiator).open(&f_i).is_none(), "wrong direction cannot open");
733 }
734
735 #[test]
736 fn bitflip_detected() {
737 let pool = PadPool::shared(random_pad(4096, 0x9999_AAAA_BBBB_CCCC)).expect("pad");
738 let sender = pool.send_suite(Role::Initiator);
739 let frame = sender.seal(b"integrity matters").expect("pad");
740
741 let mut ct_tamper = frame.clone();
742 ct_tamper[PAYLOAD_START] ^= 0x01;
743 assert!(pool.recv_suite(Role::Responder).open(&ct_tamper).is_none(), "flipped ciphertext bit caught");
744
745 let mut tag_tamper = frame.clone();
746 let last = tag_tamper.len() - 1;
747 tag_tamper[last] ^= 0x80;
748 assert!(pool.recv_suite(Role::Responder).open(&tag_tamper).is_none(), "flipped tag bit caught");
749
750 assert!(pool.recv_suite(Role::Responder).open(&frame).is_some(), "untampered cover opens");
751 }
752
753 #[test]
754 fn every_single_byte_corruption_is_rejected() {
755 let pool = PadPool::shared(random_pad(1 << 15, 0x0BADF00D_5EED_1234)).expect("pad");
759 let sender = pool.send_suite(Role::Initiator);
760 let mut s = 0x1357_9BDF_2468_ACE0u64;
761 for _ in 0..40 {
762 let len = (splitmix64(&mut s) % 24) as usize;
763 let msg: Vec<u8> = (0..len).map(|_| splitmix64(&mut s) as u8).collect();
764 let frame = sender.seal(&msg).expect("pad");
765 assert!(pool.recv_suite(Role::Responder).open(&frame).is_some());
767 for pos in 0..frame.len() {
768 for bit in [0x01u8, 0x80] {
769 let mut bad = frame.clone();
770 bad[pos] ^= bit;
771 assert!(
772 pool.recv_suite(Role::Responder).open(&bad).is_none(),
773 "corrupting byte {pos} (bit {bit:#x}) must be rejected"
774 );
775 }
776 }
777 }
778 }
779
780 #[test]
781 fn cross_pad_frame_is_rejected() {
782 let a = PadPool::shared(random_pad(4096, 0xAAAA_0000_1111_2222)).expect("pad A");
783 let b = PadPool::shared(random_pad(4096, 0xBBBB_3333_4444_5555)).expect("pad B");
784 let frame = a.send_suite(Role::Initiator).seal(b"for A only").expect("pad");
785 assert!(a.recv_suite(Role::Responder).open(&frame).is_some(), "A's receiver opens it");
786 assert!(b.recv_suite(Role::Responder).open(&frame).is_none(), "a foreign pad cannot open it");
787 }
788
789 #[test]
790 fn fuzz_roundtrip_many_sizes() {
791 let pool = PadPool::shared(random_pad(1 << 20, 0xF1F2_F3F4_F5F6_F7F8)).expect("pad");
792 let sender = pool.send_suite(Role::Initiator);
793 let receiver = pool.recv_suite(Role::Responder);
794 let mut s = 0x2222_4444_6666_8888u64;
795 let mut sealed = 0;
796 for _ in 0..3000 {
797 let len = (splitmix64(&mut s) % 300) as usize;
798 let msg: Vec<u8> = (0..len).map(|_| splitmix64(&mut s) as u8).collect();
799 match sender.seal(&msg) {
800 Some(frame) => {
801 assert_eq!(receiver.open(&frame), Some(msg), "fuzz round-trip, len {len}");
802 sealed += 1;
803 }
804 None => break, }
806 }
807 assert!(sealed > 100, "a healthy number of covers round-tripped ({sealed})");
808 }
809
810 #[test]
811 fn mac_key_is_one_time() {
812 let pool = PadPool::shared(random_pad(8192, 0x1212_3434_5656_7878)).expect("pad");
813 let sender = pool.send_suite(Role::Initiator);
814 let mut mac_ranges = Vec::new();
815 for i in 0..100u8 {
816 let frame = sender.seal(&[i, i, i]).expect("pad");
817 let off = frame_offset(&frame).unwrap();
818 mac_ranges.push((off, off + MAC_KEY_LEN as u64));
819 }
820 assert!(ranges_disjoint(&mac_ranges), "MAC keys are drawn from pairwise-disjoint pad ranges");
821 }
822
823 #[test]
824 fn replay_rejected() {
825 let pool = PadPool::shared(random_pad(4096, 0x2468_ACE0_1357_9BDF)).expect("pad");
826 let sender = pool.send_suite(Role::Initiator);
827 let receiver = pool.recv_suite(Role::Responder);
828 let frame = sender.seal(b"once and only once").expect("pad");
829 assert!(receiver.open(&frame).is_some(), "first delivery accepted");
830 assert!(receiver.open(&frame).is_none(), "exact replay of a consumed cover rejected");
831 }
832
833 #[test]
834 fn reorder_within_window_ok_beyond_window_rejected() {
835 let pool = PadPool::shared(random_pad(4096, 0x3141_5926_5358_9793)).expect("pad");
836 let sender = pool.send_suite(Role::Initiator);
837 let f0 = sender.seal(&[0u8; 8]).unwrap();
838 let f1 = sender.seal(&[1u8; 8]).unwrap();
839 let f2 = sender.seal(&[2u8; 8]).unwrap();
840 assert_eq!(frame_offset(&f0), Some(0));
841 assert_eq!(frame_offset(&f1), Some(40));
842 assert_eq!(frame_offset(&f2), Some(80));
843
844 let receiver = pool.recv_suite(Role::Responder).with_recv_window(100);
845 assert!(receiver.open(&f2).is_some(), "newest cover accepted (end 120)");
846 assert!(receiver.open(&f1).is_some(), "in-window reorder (offset 40 ≥ floor 20) accepted");
847 assert!(receiver.open(&f0).is_none(), "beyond-window cover (offset 0 < floor 20) rejected");
848 }
849
850 #[test]
851 fn drop_then_resync() {
852 let pool = PadPool::shared(random_pad(4096, 0xF00D_BABE_1234_5678)).expect("pad");
853 let sender = pool.send_suite(Role::Initiator);
854 let f0 = sender.seal(b"first").unwrap();
855 let _dropped = sender.seal(b"lost in flight").unwrap();
856 let f2 = sender.seal(b"third").unwrap();
857
858 let receiver = pool.recv_suite(Role::Responder);
859 assert_eq!(receiver.open(&f0).as_deref(), Some(&b"first"[..]), "first arrives");
860 assert_eq!(receiver.open(&f2).as_deref(), Some(&b"third"[..]), "third decodes despite the gap");
861 }
862
863 #[test]
864 fn exhaustion_fails_closed() {
865 let pool = PadPool::shared_unchecked(random_pad(2 * (MAC_KEY_LEN + 8), 0xC0DE_F00D_0BAD_BEEF));
866 let sender = pool.send_suite(Role::Initiator);
867 assert_eq!(sender.send_remaining(), MAC_KEY_LEN + 8);
868 assert!(sender.seal(&[0u8; 8]).is_some(), "the one cover the half holds is sealed");
869 assert_eq!(sender.send_remaining(), 0, "the half is now spent");
870 assert!(sender.is_low(1), "low-water tripped");
871 assert!(sender.seal(&[0u8; 1]).is_none(), "exhausted pad fails closed");
872 assert!(sender.seal(&[]).is_none(), "even an empty message needs a MAC key ⇒ still closed");
873 }
874
875 #[test]
876 #[cfg(not(target_arch = "wasm32"))]
877 fn cursor_persists_across_restart() {
878 let dir = tempfile::tempdir().expect("tempdir");
879 let path = dir.path().join("pnp.cursor");
880 let pad = random_pad(8192, 0x0BEE_F00D_1234_ABCD);
881
882 let end1 = {
883 let pool = PadPool::shared(pad.clone()).expect("pad");
884 let sender = pool.send_suite(Role::Initiator).with_ledger(Arc::new(FileLedger::new(&path)));
885 let f = sender.seal(b"before the crash").expect("pad");
886 cover_range(&f).1
887 };
888
889 let pool = PadPool::shared(pad).expect("pad");
890 let sender = pool.send_suite(Role::Initiator).with_ledger(Arc::new(FileLedger::new(&path)));
891 let f2 = sender.seal(b"after the restart").expect("pad");
892 let (off2, _) = cover_range(&f2);
893 assert!(off2 >= end1, "post-restart cover starts at or beyond the durable high-water ({off2} ≥ {end1})");
894 }
895
896 #[test]
897 fn seal_is_deterministic_no_runtime_randomness() {
898 let pad = random_pad(4096, 0xAAAA_5555_AAAA_5555);
899 let a = PadPool::shared(pad.clone()).unwrap().send_suite(Role::Initiator);
900 let b = PadPool::shared(pad).unwrap().send_suite(Role::Initiator);
901 let m = b"deterministic";
902 assert_eq!(a.seal(m), b.seal(m), "seal is a pure function of (pad, cursor, plaintext)");
903 let f1 = a.seal(m).unwrap();
904 let f2 = a.seal(m).unwrap();
905 assert_ne!(f1, f2, "successive covers draw fresh pad");
906 }
907
908 #[test]
909 fn pad_quality_gate_rejects_compressible() {
910 assert_eq!(PadPool::shared(vec![0u8; 4096]).err(), Some(PadError::Compressible), "zero pad refused");
911 let counter: Vec<u8> = (0..4096).map(|i| i as u8).collect();
912 assert_eq!(PadPool::shared(counter).err(), Some(PadError::Compressible), "counter pad refused");
913 assert_eq!(PadPool::shared(vec![1]).err(), Some(PadError::TooSmall), "tiny pool refused");
914 assert!(PadPool::shared(random_pad(4096, 0x7777_8888_9999_0000)).is_ok(), "true-random pad accepted");
915 }
916
917 #[test]
918 fn perfect_secrecy_leaks_only_length() {
919 let target_ct = [0x11u8, 0x22, 0x33, 0x44];
920 let p1 = [0xDEu8, 0xAD, 0xBE, 0xEF];
921 let p2 = [0x00u8, 0x01, 0x02, 0x03];
922
923 let build = |plaintext: &[u8]| {
924 let mut pad = vec![0x5Au8; MAC_KEY_LEN];
925 pad.extend(plaintext.iter().zip(&target_ct).map(|(p, c)| p ^ c));
926 pad.extend(std::iter::repeat(0u8).take(64));
927 let pool = PadPool::shared_unchecked(pad);
928 let frame = pool.send_suite(Role::Initiator).seal(plaintext).unwrap();
929 ciphertext_of(&frame)
930 };
931 assert_eq!(build(&p1), target_ct.to_vec(), "plaintext 1 seals to the target ciphertext under its pad");
932 assert_eq!(build(&p2), target_ct.to_vec(), "a different plaintext seals to the SAME ciphertext under its pad");
933 assert_ne!(p1, p2, "the two plaintexts are genuinely different");
934 }
935
936 #[test]
937 fn end_to_end_two_peers_real_wire_message() {
938 let material = random_pad(1 << 14, 0x1BAD_C0DE_F00D_FACE);
939 let initiator = PadPool::shared(material.clone()).expect("pad");
940 let responder = PadPool::shared(material).expect("pad");
941
942 let blob = message_to_wire_with("responder", &RuntimeValue::Int(42), WireCodec::Native, WireIntegrity::Checked)
943 .expect("encode");
944
945 let sealed = initiator.send_suite(Role::Initiator).seal(&blob).expect("pad");
946 assert_eq!(&sealed[..2], &[CHAN_MAGIC, CHAN_VER], "channel header present");
947 assert_eq!(&sealed[2..4], &SUITE_PNP.to_le_bytes(), "PNP suite id bound in the envelope");
948 assert_eq!(frame_kind(&sealed), Some(KIND_DATA), "a data cover");
949 assert_ne!(ciphertext_of(&sealed), blob, "body is ciphertext, not the raw blob");
950 assert_eq!(
951 responder.recv_suite(Role::Responder).open(&sealed).as_deref(),
952 Some(blob.as_slice()),
953 "responder opens the initiator's one-time-pad-sealed wire message"
954 );
955
956 let ack = responder.send_suite(Role::Responder).seal(b"ack").expect("pad");
957 assert_eq!(initiator.recv_suite(Role::Initiator).open(&ack).as_deref(), Some(&b"ack"[..]), "reverse direction opens");
958 assert_eq!(frame_offset(&sealed), Some(0));
959 assert_eq!(frame_offset(&ack), Some(0));
960 }
961
962 #[test]
965 fn epoch_mismatch_rejected() {
966 let material = random_pad(4096, 0x0E0E_0E0E_1234_5678);
968 let e1 = PadPool::shared(material.clone()).unwrap().with_epoch(1);
969 let e2 = PadPool::shared(material).unwrap().with_epoch(2);
970 let frame = e2.send_suite(Role::Initiator).seal(b"epoch two").expect("pad");
971 assert_eq!(frame_kind(&frame), Some(KIND_DATA));
972 assert!(e1.recv_suite(Role::Responder).open(&frame).is_none(), "epoch-1 receiver rejects an epoch-2 cover");
973 assert!(e2.recv_suite(Role::Responder).open(&frame).is_some(), "the matching epoch opens it");
974 }
975
976 #[test]
977 fn pad_commitment_binds_bytes_and_epoch() {
978 let m = random_pad(4096, 0x9090_9090_ABAB_ABAB);
979 let a = PadPool::shared(m.clone()).unwrap().with_epoch(7);
980 let a2 = PadPool::shared(m.clone()).unwrap().with_epoch(7);
981 let diff_epoch = PadPool::shared(m).unwrap().with_epoch(8);
982 let diff_bytes = PadPool::shared(random_pad(4096, 0x1111_2222_3333_4444)).unwrap().with_epoch(7);
983 assert_eq!(a.commitment(), a2.commitment(), "same bytes + epoch ⇒ identical commitment");
984 assert_ne!(a.commitment(), diff_epoch.commitment(), "epoch is bound into the commitment");
985 assert_ne!(a.commitment(), diff_bytes.commitment(), "pad bytes are bound into the commitment");
986 }
987
988 #[test]
989 fn roll_handoff_roundtrip_and_authenticated() {
990 let pad0 = random_pad(1 << 13, 0x0000_1111_2222_3333);
994 let next_material = random_pad(1 << 13, 0x4444_5555_6666_7777);
995 let init0 = PadPool::shared(pad0.clone()).unwrap().with_epoch(0);
996 let resp0 = PadPool::shared(pad0).unwrap().with_epoch(0);
997 let next_on_init = PadPool::shared(next_material.clone()).unwrap().with_epoch(1);
998 let next_on_resp = PadPool::shared(next_material).unwrap().with_epoch(1);
999
1000 let roll = init0.send_suite(Role::Initiator).seal_roll(1, &next_on_init.commitment()).expect("pad");
1001 assert_eq!(frame_kind(&roll), Some(KIND_ROLL), "a roll cover");
1002 assert!(resp0.recv_suite(Role::Responder).open(&roll).is_none(), "a roll cover is NOT a data cover");
1003
1004 let (next_epoch, commitment) = resp0.recv_suite(Role::Responder).open_roll(&roll).expect("roll opens");
1005 assert_eq!(next_epoch, 1, "announced the next epoch");
1006 assert_eq!(commitment, next_on_resp.commitment(), "the receiver confirms it holds the identical next pad");
1007
1008 let mut bad = roll.clone();
1010 bad[PAYLOAD_START] ^= 0x01;
1011 assert!(resp0.recv_suite(Role::Responder).open_roll(&bad).is_none(), "a tampered roll is rejected");
1012 }
1013
1014 #[test]
1015 fn chain_rollover_end_to_end() {
1016 let pad0 = random_pad(2 * (MAC_KEY_LEN + 16), 0xEE11_EE22_EE33_EE44); let next_material = random_pad(1 << 12, 0x55AA_55AA_55AA_55AA);
1019 let s_init0 = PadPool::shared_unchecked(pad0.clone()).with_epoch(0);
1020 let s_resp0 = PadPool::shared_unchecked(pad0).with_epoch(0);
1021 let e1_init = PadPool::shared(next_material.clone()).unwrap().with_epoch(1);
1022 let e1_resp = PadPool::shared(next_material).unwrap().with_epoch(1);
1023
1024 let send0 = s_init0.send_suite(Role::Initiator);
1025 let recv0 = s_resp0.recv_suite(Role::Responder);
1026
1027 let f = send0.seal(b"epoch0 payload!!").expect("first cover fits");
1028 assert_eq!(recv0.open(&f).as_deref(), Some(&b"epoch0 payload!!"[..]));
1029 assert!(send0.is_low(MAC_KEY_LEN + 1), "epoch 0 is now low");
1030 assert_eq!(e1_init.commitment(), e1_resp.commitment(), "both peers hold the identical epoch 1");
1033
1034 let send1 = e1_init.send_suite(Role::Initiator);
1036 let recv1 = e1_resp.recv_suite(Role::Responder);
1037 let f1 = send1.seal(b"epoch1 continues after the roll").expect("epoch 1 has pad");
1038 assert_eq!(recv1.open(&f1).as_deref(), Some(&b"epoch1 continues after the roll"[..]));
1039 }
1040
1041 #[test]
1044 fn wired_through_the_send_and_receive_seam() {
1045 use super::super::channel::{active_session, open_active, seal_active_checked, with_session, ActiveSession};
1046 use std::rc::Rc;
1047
1048 let material = random_pad(1 << 14, 0x5EA1_D00D_0FEE_1234);
1049 let initiator = PadPool::shared(material.clone()).expect("pad");
1050 let responder = PadPool::shared(material).expect("pad");
1051 let blob = b"through the very functions the interpreter Send/receive path calls".to_vec();
1052
1053 assert!(active_session().is_none());
1055 assert_eq!(seal_active_checked(blob.clone()), Some(blob.clone()), "passthrough seal");
1056 assert_eq!(open_active(blob.clone()), Some(blob.clone()), "passthrough open");
1057
1058 let send: Rc<dyn ActiveSession> = Rc::new(initiator.session(Role::Initiator));
1060 let frame = with_session(Some(send), || seal_active_checked(blob.clone())).expect("pad available");
1061 assert_eq!(frame_kind(&frame), Some(KIND_DATA), "a PNP data cover");
1062 assert_eq!(&frame[2..4], &SUITE_PNP.to_le_bytes(), "PNP suite id bound in the envelope");
1063 assert_ne!(frame, blob, "sealed under the one-time pad, not plaintext");
1064
1065 let recv: Rc<dyn ActiveSession> = Rc::new(responder.session(Role::Responder));
1067 let opened = with_session(Some(recv), || open_active(frame.clone()));
1068 assert_eq!(opened.as_deref(), Some(blob.as_slice()), "opened through the wired receive seam");
1069 assert!(active_session().is_none(), "session scope restored after with_session");
1070 }
1071
1072 #[test]
1073 fn wired_open_active_drops_foreign_frame_under_session() {
1074 use super::super::channel::{open_active, with_session, ActiveSession};
1075 use std::rc::Rc;
1076 let pool = PadPool::shared(random_pad(4096, 0xF0E1_D2C3_B4A5_9687)).unwrap();
1077 let recv: Rc<dyn ActiveSession> = Rc::new(pool.session(Role::Responder));
1078 with_session(Some(recv), || {
1079 assert!(open_active(b"not a pnp frame at all".to_vec()).is_none(), "foreign frame dropped");
1080 });
1081 }
1082
1083 #[test]
1084 fn wired_seal_active_fails_closed_on_exhaustion() {
1085 use super::super::channel::{seal_active_checked, with_session, ActiveSession};
1086 use std::rc::Rc;
1087 let pool = PadPool::shared_unchecked(random_pad(2 * (MAC_KEY_LEN + 8), 0x0B0E_0A0D_0C0F_0E0D));
1090 let send: Rc<dyn ActiveSession> = Rc::new(pool.session(Role::Initiator));
1091 with_session(Some(send), || {
1092 assert!(seal_active_checked(vec![0u8; 8]).is_some(), "first cover fits");
1093 assert!(seal_active_checked(vec![0u8; 1]).is_none(), "exhausted ⇒ fail-closed None (no plaintext)");
1094 });
1095 }
1096
1097 #[test]
1099 #[ignore = "benchmark: prints seal/open throughput; run in --release"]
1100 fn throughput_seal_open() {
1101 use std::hint::black_box;
1102 use std::time::Instant;
1103 let half: usize = 64 << 20; let pad = random_pad(2 * half, 0x7E57_7E57_7E57_7E57);
1105 let pool = PadPool::shared_unchecked(pad);
1106
1107 {
1109 let n = 1usize << 20;
1110 let x = random_pad(n, 0x1111_1111_1111_1111);
1111 let y = random_pad(n, 0x2222_2222_2222_2222);
1112 let reps = 200;
1113 let mut z = vec![0u8; n];
1114 let t = Instant::now();
1115 for _ in 0..reps {
1116 for ((o, a), b) in z.iter_mut().zip(&x).zip(&y) {
1117 *o = a ^ b;
1118 }
1119 black_box(&z);
1120 }
1121 let xor_gibs = (reps * n) as f64 / t.elapsed().as_secs_f64() / (1u64 << 30) as f64;
1122 let key = [7u8; 32];
1123 let t = Instant::now();
1124 for _ in 0..reps {
1125 black_box(poly1305(&key, &x));
1126 }
1127 let mac_gibs = (reps * n) as f64 / t.elapsed().as_secs_f64() / (1u64 << 30) as f64;
1128 eprintln!("components @1MiB: raw XOR {xor_gibs:.2} GiB/s | Poly1305 one-time MAC {mac_gibs:.2} GiB/s (this is the ceiling for authenticated OTP)");
1129 }
1130
1131 eprintln!("PNP throughput (fused XOR + Poly1305 one-time MAC), 64 MiB pad/direction:");
1132 for &size in &[256usize, 4096, 65536, 1 << 20] {
1133 let msg = vec![0xABu8; size];
1134 let iters = (half / (size + MAC_KEY_LEN)).min(50_000).max(1);
1135
1136 let sender = pool.send_suite(Role::Initiator);
1137 let mut frames: Vec<Vec<u8>> = Vec::with_capacity(iters);
1138 let t = Instant::now();
1139 for _ in 0..iters {
1140 frames.push(sender.seal(&msg).expect("pad"));
1141 }
1142 let seal_s = t.elapsed().as_secs_f64();
1143
1144 let receiver = pool.recv_suite(Role::Responder);
1145 let t = Instant::now();
1146 let mut ok = 0usize;
1147 for f in &frames {
1148 if receiver.open(f).is_some() {
1149 ok += 1;
1150 }
1151 }
1152 let open_s = t.elapsed().as_secs_f64();
1153 assert_eq!(ok, iters, "all opened");
1154
1155 let bytes = (iters * size) as f64;
1156 let seal_mbps = bytes / seal_s / (1 << 20) as f64;
1157 let open_mbps = bytes / open_s / (1 << 20) as f64;
1158 eprintln!(
1159 " size {:>8}B x{:>6} seal {:>8.1} MiB/s ({:>6.0} ns/op) open {:>8.1} MiB/s ({:>6.0} ns/op)",
1160 size,
1161 iters,
1162 seal_mbps,
1163 seal_s / iters as f64 * 1e9,
1164 open_mbps,
1165 open_s / iters as f64 * 1e9,
1166 );
1167 }
1168 }
1169}