1use rayon::prelude::*;
42use zeroize::Zeroize;
43
44use std::time::Duration;
45
46use crate::entropy::{self, EntropySnapshot};
47use crate::error::{KkError, Result};
48use crate::kdf;
49use crate::kk_mix::kk_hash;
50use crate::temporal::{self, TemporalCommitment, TemporalProof};
51
52const CHUNK_SIZE: usize = 4096;
56
57#[derive(Clone)]
64pub struct KkPacket {
65 pub ciphertext: Vec<u8>,
67 pub entropy_snapshot: EntropySnapshot,
69 pub commitment: TemporalCommitment,
71}
72
73impl KkPacket {
74 pub fn to_bytes(&self) -> Vec<u8> {
78 let ct_len = self.ciphertext.len() as u32;
79 let snap_bytes = self.entropy_snapshot.to_bytes();
80 let commit_bytes = self.commitment.to_bytes();
81
82 let mut out =
83 Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len());
84 out.extend_from_slice(&ct_len.to_le_bytes());
85 out.extend_from_slice(&self.ciphertext);
86 out.extend_from_slice(&snap_bytes);
87 out.extend_from_slice(&commit_bytes);
88 out
89 }
90
91 pub fn from_bytes(data: &[u8]) -> Result<Self> {
93 if data.len() < 4 {
94 return Err(KkError::InvalidPacket("packet too short".into()));
95 }
96
97 let ct_len = u32::from_le_bytes(
98 data[..4]
99 .try_into()
100 .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
101 ) as usize;
102
103 let expected_min = 4 + ct_len + 48 + 32; if data.len() < expected_min {
105 return Err(KkError::InvalidPacket(format!(
106 "packet too short: expected at least {expected_min}, got {}",
107 data.len()
108 )));
109 }
110
111 let ciphertext = data[4..4 + ct_len].to_vec();
112 let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
113 let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len + 48..])?;
114
115 Ok(Self {
116 ciphertext,
117 entropy_snapshot: snapshot,
118 commitment,
119 })
120 }
121}
122
123#[derive(Clone)]
139pub struct KkSealedMessage {
140 pub ciphertext: Vec<u8>,
142 pub commitment: TemporalCommitment,
144}
145
146impl KkSealedMessage {
147 pub fn to_bytes(&self) -> Vec<u8> {
151 let ct_len = self.ciphertext.len() as u32;
152 let commit_bytes = self.commitment.to_bytes();
153
154 let mut out = Vec::with_capacity(4 + self.ciphertext.len() + commit_bytes.len());
155 out.extend_from_slice(&ct_len.to_le_bytes());
156 out.extend_from_slice(&self.ciphertext);
157 out.extend_from_slice(&commit_bytes);
158 out
159 }
160
161 pub fn from_bytes(data: &[u8]) -> Result<Self> {
163 if data.len() < 4 {
164 return Err(KkError::InvalidPacket("sealed message too short".into()));
165 }
166
167 let ct_len = u32::from_le_bytes(
168 data[..4]
169 .try_into()
170 .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
171 ) as usize;
172
173 let expected_min = 4 + ct_len + 32;
174 if data.len() < expected_min {
175 return Err(KkError::InvalidPacket(format!(
176 "sealed message too short: expected at least {expected_min}, got {}",
177 data.len()
178 )));
179 }
180
181 let ciphertext = data[4..4 + ct_len].to_vec();
182 let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len..])?;
183
184 Ok(Self {
185 ciphertext,
186 commitment,
187 })
188 }
189}
190
191pub fn encode(shared_secret: &[u8], plaintext: &[u8]) -> Result<KkPacket> {
202 if plaintext.is_empty() {
203 return Err(KkError::EmptyInput);
204 }
205
206 let snapshot = entropy::gather()?;
208
209 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
211
212 let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
214
215 Ok(KkPacket {
216 ciphertext,
217 entropy_snapshot: snapshot,
218 commitment,
219 })
220}
221
222pub fn decode(shared_secret: &[u8], packet: &KkPacket) -> Result<Vec<u8>> {
231 temporal::verify(
233 shared_secret,
234 &packet.entropy_snapshot,
235 &packet.ciphertext,
236 &packet.commitment,
237 )?;
238
239 xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
242}
243
244pub fn encode_split(
262 shared_secret: &[u8],
263 plaintext: &[u8],
264) -> Result<(KkSealedMessage, EntropySnapshot)> {
265 if plaintext.is_empty() {
266 return Err(KkError::EmptyInput);
267 }
268
269 let snapshot = entropy::gather()?;
271
272 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
274
275 let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
277
278 let sealed = KkSealedMessage {
279 ciphertext,
280 commitment,
281 };
282
283 Ok((sealed, snapshot))
285}
286
287pub fn decode_split(
296 shared_secret: &[u8],
297 sealed: &KkSealedMessage,
298 epsilon: &EntropySnapshot,
299) -> Result<Vec<u8>> {
300 temporal::verify(
302 shared_secret,
303 epsilon,
304 &sealed.ciphertext,
305 &sealed.commitment,
306 )?;
307
308 xor_with_keystream(shared_secret, epsilon, &sealed.ciphertext)
310}
311
312#[derive(Clone)]
329pub struct KkBoundPacket {
330 pub ciphertext: Vec<u8>,
332 pub entropy_snapshot: EntropySnapshot,
334 pub proof: TemporalProof,
336}
337
338impl KkBoundPacket {
339 pub fn to_bytes(&self) -> Vec<u8> {
343 let ct_len = self.ciphertext.len() as u32;
344 let snap_bytes = self.entropy_snapshot.to_bytes();
345 let proof_bytes = self.proof.to_bytes();
346
347 let mut out =
348 Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + proof_bytes.len());
349 out.extend_from_slice(&ct_len.to_le_bytes());
350 out.extend_from_slice(&self.ciphertext);
351 out.extend_from_slice(&snap_bytes);
352 out.extend_from_slice(&proof_bytes);
353 out
354 }
355
356 pub fn from_bytes(data: &[u8]) -> Result<Self> {
358 if data.len() < 4 {
359 return Err(KkError::InvalidPacket("bound packet too short".into()));
360 }
361
362 let ct_len = u32::from_le_bytes(
363 data[..4]
364 .try_into()
365 .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
366 ) as usize;
367
368 let expected_min = 4 + ct_len + 48 + TemporalProof::BYTES;
369 if data.len() < expected_min {
370 return Err(KkError::InvalidPacket(format!(
371 "bound packet too short: expected at least {expected_min}, got {}",
372 data.len()
373 )));
374 }
375
376 let ciphertext = data[4..4 + ct_len].to_vec();
377 let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
378 let proof = TemporalProof::from_bytes(&data[4 + ct_len + 48..])?;
379
380 Ok(Self {
381 ciphertext,
382 entropy_snapshot: snapshot,
383 proof,
384 })
385 }
386}
387
388pub fn encode_bound(
404 shared_secret: &[u8],
405 plaintext: &[u8],
406 verifier_nonce: &[u8; 32],
407 prev_mac: &[u8; 32],
408) -> Result<KkBoundPacket> {
409 if plaintext.is_empty() {
410 return Err(KkError::EmptyInput);
411 }
412
413 let snapshot = entropy::gather()?;
414 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
415 let proof = temporal::commit_bound(
416 shared_secret,
417 &snapshot,
418 &ciphertext,
419 verifier_nonce,
420 prev_mac,
421 )?;
422
423 Ok(KkBoundPacket {
424 ciphertext,
425 entropy_snapshot: snapshot,
426 proof,
427 })
428}
429
430pub fn decode_bound(
448 shared_secret: &[u8],
449 packet: &KkBoundPacket,
450 expected_nonce: &[u8; 32],
451 max_drift: Duration,
452) -> Result<Vec<u8>> {
453 temporal::verify_bound(
454 shared_secret,
455 &packet.entropy_snapshot,
456 &packet.ciphertext,
457 &packet.proof,
458 expected_nonce,
459 max_drift,
460 )?;
461
462 xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
463}
464
465#[derive(Clone)]
480pub struct KkAeadPacket {
481 pub aad: Vec<u8>,
483 pub ciphertext: Vec<u8>,
485 pub entropy_snapshot: EntropySnapshot,
487 pub commitment: TemporalCommitment,
489}
490
491impl KkAeadPacket {
492 pub fn to_bytes(&self) -> Vec<u8> {
496 let aad_len = self.aad.len() as u32;
497 let ct_len = self.ciphertext.len() as u32;
498 let snap_bytes = self.entropy_snapshot.to_bytes();
499 let commit_bytes = self.commitment.to_bytes();
500
501 let mut out = Vec::with_capacity(
502 4 + self.aad.len() + 4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len(),
503 );
504 out.extend_from_slice(&aad_len.to_le_bytes());
505 out.extend_from_slice(&self.aad);
506 out.extend_from_slice(&ct_len.to_le_bytes());
507 out.extend_from_slice(&self.ciphertext);
508 out.extend_from_slice(&snap_bytes);
509 out.extend_from_slice(&commit_bytes);
510 out
511 }
512
513 pub fn from_bytes(data: &[u8]) -> Result<Self> {
515 if data.len() < 8 {
516 return Err(KkError::InvalidPacket("AEAD packet too short".into()));
517 }
518
519 let aad_len = u32::from_le_bytes(
520 data[..4]
521 .try_into()
522 .map_err(|_| KkError::InvalidPacket("bad aad length".into()))?,
523 ) as usize;
524
525 if data.len() < 4 + aad_len + 4 {
526 return Err(KkError::InvalidPacket(
527 "AEAD packet truncated at ct_len".into(),
528 ));
529 }
530
531 let aad = data[4..4 + aad_len].to_vec();
532 let ct_offset = 4 + aad_len;
533
534 let ct_len = u32::from_le_bytes(
535 data[ct_offset..ct_offset + 4]
536 .try_into()
537 .map_err(|_| KkError::InvalidPacket("bad ct length".into()))?,
538 ) as usize;
539
540 let expected_min = ct_offset + 4 + ct_len + 48 + 32;
541 if data.len() < expected_min {
542 return Err(KkError::InvalidPacket(format!(
543 "AEAD packet too short: expected at least {expected_min}, got {}",
544 data.len()
545 )));
546 }
547
548 let ct_start = ct_offset + 4;
549 let ciphertext = data[ct_start..ct_start + ct_len].to_vec();
550 let snap_start = ct_start + ct_len;
551 let snapshot = EntropySnapshot::from_bytes(&data[snap_start..snap_start + 48])?;
552 let commitment =
553 TemporalCommitment::from_bytes(&data[snap_start + 48..snap_start + 48 + 32])?;
554
555 Ok(Self {
556 aad,
557 ciphertext,
558 entropy_snapshot: snapshot,
559 commitment,
560 })
561 }
562}
563
564pub fn encode_aead(shared_secret: &[u8], plaintext: &[u8], aad: &[u8]) -> Result<KkAeadPacket> {
575 if plaintext.is_empty() {
576 return Err(KkError::EmptyInput);
577 }
578
579 let snapshot = entropy::gather()?;
580 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
581 let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
582
583 Ok(KkAeadPacket {
584 aad: aad.to_vec(),
585 ciphertext,
586 entropy_snapshot: snapshot,
587 commitment,
588 })
589}
590
591pub fn decode_aead(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
596 temporal::verify_aead(
597 shared_secret,
598 &packet.entropy_snapshot,
599 &packet.ciphertext,
600 &packet.aad,
601 &packet.commitment,
602 )?;
603
604 xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
605}
606
607#[doc(hidden)]
620pub fn encode_with_snapshot(
621 shared_secret: &[u8],
622 plaintext: &[u8],
623 snapshot: EntropySnapshot,
624) -> Result<KkPacket> {
625 if plaintext.is_empty() {
626 return Err(KkError::EmptyInput);
627 }
628 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
629 let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
630 Ok(KkPacket {
631 ciphertext,
632 entropy_snapshot: snapshot,
633 commitment,
634 })
635}
636
637#[doc(hidden)]
641pub fn encode_aead_with_snapshot(
642 shared_secret: &[u8],
643 plaintext: &[u8],
644 aad: &[u8],
645 snapshot: EntropySnapshot,
646) -> Result<KkAeadPacket> {
647 if plaintext.is_empty() {
648 return Err(KkError::EmptyInput);
649 }
650 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
651 let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
652 Ok(KkAeadPacket {
653 aad: aad.to_vec(),
654 ciphertext,
655 entropy_snapshot: snapshot,
656 commitment,
657 })
658}
659
660pub fn encode_pooled(
670 shared_secret: &[u8],
671 plaintext: &[u8],
672 pool: &crate::entropy_pool::EntropyPool,
673) -> Result<KkPacket> {
674 if plaintext.is_empty() {
675 return Err(KkError::EmptyInput);
676 }
677 let snapshot = pool.draw()?;
678 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
679 let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
680 Ok(KkPacket {
681 ciphertext,
682 entropy_snapshot: snapshot,
683 commitment,
684 })
685}
686
687pub fn encode_aead_pooled(
691 shared_secret: &[u8],
692 plaintext: &[u8],
693 aad: &[u8],
694 pool: &crate::entropy_pool::EntropyPool,
695) -> Result<KkAeadPacket> {
696 if plaintext.is_empty() {
697 return Err(KkError::EmptyInput);
698 }
699 let snapshot = pool.draw()?;
700 let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
701 let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
702 Ok(KkAeadPacket {
703 aad: aad.to_vec(),
704 ciphertext,
705 entropy_snapshot: snapshot,
706 commitment,
707 })
708}
709
710pub fn encode_aead_batch(
723 shared_secret: &[u8],
724 messages: &[(&[u8], &[u8])], pool: Option<&crate::entropy_pool::EntropyPool>,
726) -> Result<Vec<KkAeadPacket>> {
727 let results: Vec<KkAeadPacket> = messages
729 .par_chunks(8)
730 .flat_map_iter(|chunk| {
731 if chunk.len() == 8 {
732 encode_aead_batch_8_inner(shared_secret, chunk, pool).expect("batch encode failed")
734 } else {
735 chunk
737 .iter()
738 .map(|(pt, aad)| match pool {
739 Some(p) => encode_aead_pooled(shared_secret, pt, aad, p),
740 None => encode_aead(shared_secret, pt, aad),
741 })
742 .collect::<Result<Vec<_>>>()
743 .expect("scalar encode failed")
744 }
745 })
746 .collect();
747
748 Ok(results)
749}
750
751fn encode_aead_batch_8_inner(
753 shared_secret: &[u8],
754 chunk: &[(&[u8], &[u8])],
755 pool: Option<&crate::entropy_pool::EntropyPool>,
756) -> Result<Vec<KkAeadPacket>> {
757 debug_assert_eq!(chunk.len(), 8);
758
759 let snapshots: [EntropySnapshot; 8] = core::array::from_fn(|i| {
761 let _ = i;
762 match pool {
763 Some(p) => p.draw().expect("pool draw failed"),
764 None => entropy::gather().expect("entropy gather failed"),
765 }
766 });
767
768 let ciphertexts: [Vec<u8>; 8] = core::array::from_fn(|i| {
771 xor_with_keystream_seq(shared_secret, &snapshots[i], chunk[i].0)
772 .expect("xor_with_keystream failed")
773 });
774
775 let snap_refs: [&EntropySnapshot; 8] = core::array::from_fn(|i| &snapshots[i]);
777 let ct_refs: [&[u8]; 8] = core::array::from_fn(|i| ciphertexts[i].as_slice());
778 let aad_refs: [&[u8]; 8] = core::array::from_fn(|i| chunk[i].1);
779
780 let commitments = temporal::commit_aead_batch_8(shared_secret, snap_refs, ct_refs, aad_refs)?;
781
782 let mut ct_arr = ciphertexts;
784 let packets: Vec<KkAeadPacket> = (0..8)
785 .map(|i| KkAeadPacket {
786 aad: chunk[i].1.to_vec(),
787 ciphertext: std::mem::take(&mut ct_arr[i]),
788 entropy_snapshot: snapshots[i].clone(),
789 commitment: commitments[i].clone(),
790 })
791 .collect();
792
793 Ok(packets)
794}
795
796pub fn decode_aead_batch(shared_secret: &[u8], packets: &[KkAeadPacket]) -> Result<Vec<Vec<u8>>> {
801 packets
802 .par_iter()
803 .map(|pkt| {
804 temporal::verify_aead(
805 shared_secret,
806 &pkt.entropy_snapshot,
807 &pkt.ciphertext,
808 &pkt.aad,
809 &pkt.commitment,
810 )?;
811 xor_with_keystream_seq(shared_secret, &pkt.entropy_snapshot, &pkt.ciphertext)
813 })
814 .collect()
815}
816
817pub const PARALLEL_CHUNK_SIZE: usize = 1 << 20;
823
824#[derive(Clone)]
832pub struct KkParallelPacket {
833 pub chunks: Vec<KkAeadPacket>,
835 pub chunk_size: usize,
837 pub merkle_root: [u8; 32],
839}
840
841fn compute_merkle_root(chunks: &[KkAeadPacket]) -> [u8; 32] {
845 let mut preimage = Vec::with_capacity(chunks.len() * 32);
846 for chunk in chunks {
847 preimage.extend_from_slice(&chunk.commitment.mac);
848 }
849 kk_hash(&preimage)
850}
851
852pub fn encode_parallel(
866 shared_secret: &[u8],
867 plaintext: &[u8],
868 aad: &[u8],
869 chunk_size: usize,
870 pool: Option<&crate::entropy_pool::EntropyPool>,
871) -> Result<KkParallelPacket> {
872 if plaintext.is_empty() {
873 return Err(KkError::EmptyInput);
874 }
875 if chunk_size == 0 {
876 return Err(KkError::InvalidPacket("chunk_size must be > 0".into()));
877 }
878
879 let chunk_pairs: Vec<(usize, &[u8])> = plaintext.chunks(chunk_size).enumerate().collect();
881
882 let chunks: Vec<KkAeadPacket> = chunk_pairs
883 .par_iter()
884 .map(|(_idx, chunk_data)| {
885 let snapshot = match pool {
886 Some(p) => p.draw()?,
887 None => entropy::gather()?,
888 };
889 encode_aead_par_inner(shared_secret, chunk_data, aad, snapshot)
890 })
891 .collect::<Result<Vec<_>>>()?;
892
893 let merkle_root = compute_merkle_root(&chunks);
894
895 Ok(KkParallelPacket {
896 chunks,
897 chunk_size,
898 merkle_root,
899 })
900}
901
902pub fn decode_parallel(shared_secret: &[u8], packet: &KkParallelPacket) -> Result<Vec<u8>> {
910 if packet.chunks.is_empty() {
911 return Err(KkError::InvalidPacket(
912 "parallel packet has no chunks".into(),
913 ));
914 }
915
916 let computed_root = compute_merkle_root(&packet.chunks);
918 if computed_root != packet.merkle_root {
919 return Err(KkError::CommitmentMismatch);
920 }
921
922 let plaintexts: Vec<Vec<u8>> = packet
924 .chunks
925 .par_iter()
926 .map(|chunk| decode_aead_seq(shared_secret, chunk))
927 .collect::<Result<Vec<_>>>()?;
928
929 let total_len: usize = plaintexts.iter().map(|p| p.len()).sum();
931 let mut result = Vec::with_capacity(total_len);
932 for pt in plaintexts {
933 result.extend_from_slice(&pt);
934 }
935 Ok(result)
936}
937
938impl KkParallelPacket {
939 pub fn to_bytes(&self) -> Vec<u8> {
948 let num_chunks = self.chunks.len() as u32;
949 let chunk_bytes: Vec<Vec<u8>> = self.chunks.iter().map(|c| c.to_bytes()).collect();
951 let payload_size: usize = chunk_bytes.iter().map(|cb| 4 + cb.len()).sum();
952 let header_size = 4 + 4 + 32; let mut out = Vec::with_capacity(header_size + payload_size);
955 out.extend_from_slice(&num_chunks.to_le_bytes());
956 out.extend_from_slice(&(self.chunk_size as u32).to_le_bytes());
957 out.extend_from_slice(&self.merkle_root);
958
959 for cb in &chunk_bytes {
960 out.extend_from_slice(&(cb.len() as u32).to_le_bytes());
961 out.extend_from_slice(cb);
962 }
963 out
964 }
965
966 pub fn from_bytes(data: &[u8]) -> Result<Self> {
968 const HEADER: usize = 4 + 4 + 32;
969 if data.len() < HEADER {
970 return Err(KkError::InvalidPacket("parallel packet too short".into()));
971 }
972
973 let num_chunks = u32::from_le_bytes(
974 data[..4]
975 .try_into()
976 .map_err(|_| KkError::InvalidPacket("bad chunk count".into()))?,
977 ) as usize;
978 let chunk_size = u32::from_le_bytes(
979 data[4..8]
980 .try_into()
981 .map_err(|_| KkError::InvalidPacket("bad chunk size".into()))?,
982 ) as usize;
983
984 let mut merkle_root = [0u8; 32];
985 merkle_root.copy_from_slice(&data[8..40]);
986
987 let mut offset = HEADER;
988 let mut chunks = Vec::with_capacity(num_chunks);
989 for _ in 0..num_chunks {
990 if data.len() < offset + 4 {
991 return Err(KkError::InvalidPacket(
992 "parallel packet truncated at chunk length".into(),
993 ));
994 }
995 let cb_len = u32::from_le_bytes(
996 data[offset..offset + 4]
997 .try_into()
998 .map_err(|_| KkError::InvalidPacket("bad chunk byte length".into()))?,
999 ) as usize;
1000 offset += 4;
1001
1002 if data.len() < offset + cb_len {
1003 return Err(KkError::InvalidPacket(
1004 "parallel packet truncated at chunk data".into(),
1005 ));
1006 }
1007 let chunk = KkAeadPacket::from_bytes(&data[offset..offset + cb_len])?;
1008 chunks.push(chunk);
1009 offset += cb_len;
1010 }
1011
1012 Ok(Self {
1013 chunks,
1014 chunk_size,
1015 merkle_root,
1016 })
1017 }
1018}
1019
1020fn xor_with_keystream(
1026 shared_secret: &[u8],
1027 snapshot: &EntropySnapshot,
1028 input: &[u8],
1029) -> Result<Vec<u8>> {
1030 let mut output = vec![0u8; input.len()];
1031 let batch_bytes = CHUNK_SIZE * 8;
1032
1033 let result = output.par_chunks_mut(batch_bytes).enumerate().try_for_each(
1034 |(batch_idx, out_batch)| -> Result<()> {
1035 let base_chunk = batch_idx * 8;
1036 let in_base = base_chunk * CHUNK_SIZE;
1037
1038 if out_batch.len() == batch_bytes {
1039 let mut keys = kdf::derive_symbol_key_batch(
1041 shared_secret,
1042 snapshot,
1043 base_chunk as u64,
1044 CHUNK_SIZE,
1045 )?;
1046
1047 for (c, key) in keys.iter_mut().enumerate() {
1048 let out_off = c * CHUNK_SIZE;
1049 let in_off = in_base + c * CHUNK_SIZE;
1050 for i in 0..CHUNK_SIZE {
1051 out_batch[out_off + i] = input[in_off + i] ^ key[i];
1052 }
1053 key.zeroize();
1054 }
1055 } else {
1056 let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
1058
1059 for c in 0..chunks_in_batch {
1060 let chunk_idx = base_chunk + c;
1061 let out_off = c * CHUNK_SIZE;
1062 let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
1063 let in_off = in_base + c * CHUNK_SIZE;
1064
1065 let mut key_bytes = kdf::derive_symbol_key(
1066 shared_secret,
1067 snapshot,
1068 chunk_idx as u64,
1069 chunk_len,
1070 )?;
1071
1072 for i in 0..chunk_len {
1073 out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
1074 }
1075 key_bytes.zeroize();
1076 }
1077 }
1078
1079 Ok(())
1080 },
1081 );
1082
1083 match result {
1084 Ok(()) => Ok(output),
1085 Err(e) => {
1086 output.zeroize();
1087 Err(e)
1088 }
1089 }
1090}
1091
1092fn xor_with_keystream_seq(
1096 shared_secret: &[u8],
1097 snapshot: &EntropySnapshot,
1098 input: &[u8],
1099) -> Result<Vec<u8>> {
1100 let mut output = vec![0u8; input.len()];
1101 let batch_bytes = CHUNK_SIZE * 8;
1102
1103 for (batch_idx, out_batch) in output.chunks_mut(batch_bytes).enumerate() {
1104 let base_chunk = batch_idx * 8;
1105 let in_base = base_chunk * CHUNK_SIZE;
1106
1107 if out_batch.len() == batch_bytes {
1108 let mut keys = kdf::derive_symbol_key_batch(
1109 shared_secret,
1110 snapshot,
1111 base_chunk as u64,
1112 CHUNK_SIZE,
1113 )?;
1114
1115 for (c, key) in keys.iter_mut().enumerate() {
1116 let out_off = c * CHUNK_SIZE;
1117 let in_off = in_base + c * CHUNK_SIZE;
1118 for i in 0..CHUNK_SIZE {
1119 out_batch[out_off + i] = input[in_off + i] ^ key[i];
1120 }
1121 key.zeroize();
1122 }
1123 } else {
1124 let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
1125
1126 for c in 0..chunks_in_batch {
1127 let chunk_idx = base_chunk + c;
1128 let out_off = c * CHUNK_SIZE;
1129 let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
1130 let in_off = in_base + c * CHUNK_SIZE;
1131
1132 let mut key_bytes =
1133 kdf::derive_symbol_key(shared_secret, snapshot, chunk_idx as u64, chunk_len)?;
1134
1135 for i in 0..chunk_len {
1136 out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
1137 }
1138 key_bytes.zeroize();
1139 }
1140 }
1141 }
1142
1143 Ok(output)
1144}
1145
1146fn encode_aead_par_inner(
1149 shared_secret: &[u8],
1150 plaintext: &[u8],
1151 aad: &[u8],
1152 snapshot: EntropySnapshot,
1153) -> Result<KkAeadPacket> {
1154 let ciphertext = xor_with_keystream_seq(shared_secret, &snapshot, plaintext)?;
1155 let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
1156 Ok(KkAeadPacket {
1157 aad: aad.to_vec(),
1158 ciphertext,
1159 entropy_snapshot: snapshot,
1160 commitment,
1161 })
1162}
1163
1164fn decode_aead_seq(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
1167 temporal::verify_aead(
1168 shared_secret,
1169 &packet.entropy_snapshot,
1170 &packet.ciphertext,
1171 &packet.aad,
1172 &packet.commitment,
1173 )?;
1174 xor_with_keystream_seq(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
1175}
1176
1177pub struct StreamEncoder {
1200 shared_secret: Vec<u8>,
1201 buffer: Vec<u8>,
1202 snapshot: EntropySnapshot,
1203}
1204
1205impl StreamEncoder {
1206 pub fn new(shared_secret: &[u8]) -> Result<Self> {
1211 let snapshot = entropy::gather()?;
1212 Ok(Self {
1213 shared_secret: shared_secret.to_vec(),
1214 buffer: Vec::new(),
1215 snapshot,
1216 })
1217 }
1218
1219 pub fn update(&mut self, data: &[u8]) {
1221 self.buffer.extend_from_slice(data);
1222 }
1223
1224 pub fn finalize(mut self) -> Result<KkPacket> {
1228 if self.buffer.is_empty() {
1229 return Err(KkError::EmptyInput);
1230 }
1231
1232 let ciphertext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
1233 let commitment = temporal::commit(&self.shared_secret, &self.snapshot, &ciphertext)?;
1234
1235 self.shared_secret.zeroize();
1236 self.buffer.zeroize();
1237
1238 Ok(KkPacket {
1239 ciphertext,
1240 entropy_snapshot: self.snapshot.clone(),
1241 commitment,
1242 })
1243 }
1244}
1245
1246impl Drop for StreamEncoder {
1247 fn drop(&mut self) {
1248 self.shared_secret.zeroize();
1249 self.buffer.zeroize();
1250 }
1251}
1252
1253pub struct StreamDecoder {
1281 shared_secret: Vec<u8>,
1282 buffer: Vec<u8>,
1283 snapshot: EntropySnapshot,
1284 commitment: TemporalCommitment,
1285}
1286
1287impl StreamDecoder {
1288 pub fn new(
1293 shared_secret: &[u8],
1294 snapshot: EntropySnapshot,
1295 commitment: TemporalCommitment,
1296 ) -> Self {
1297 Self {
1298 shared_secret: shared_secret.to_vec(),
1299 buffer: Vec::new(),
1300 snapshot,
1301 commitment,
1302 }
1303 }
1304
1305 pub fn update(&mut self, data: &[u8]) {
1307 self.buffer.extend_from_slice(data);
1308 }
1309
1310 pub fn finalize(mut self) -> Result<Vec<u8>> {
1315 if self.buffer.is_empty() {
1316 return Err(KkError::EmptyInput);
1317 }
1318
1319 temporal::verify(
1321 &self.shared_secret,
1322 &self.snapshot,
1323 &self.buffer,
1324 &self.commitment,
1325 )?;
1326
1327 let plaintext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
1328
1329 self.shared_secret.zeroize();
1330 self.buffer.zeroize();
1331
1332 Ok(plaintext)
1333 }
1334}
1335
1336impl Drop for StreamDecoder {
1337 fn drop(&mut self) {
1338 self.shared_secret.zeroize();
1339 self.buffer.zeroize();
1340 }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345 use super::*;
1346
1347 #[test]
1348 fn encode_decode_roundtrip() {
1349 let secret = b"test-shared-secret-2026";
1350 let plaintext = b"Hello from KK! The language only existed for one cosmic instant.";
1351
1352 let packet = encode(secret, plaintext).unwrap();
1353 let decoded = decode(secret, &packet).unwrap();
1354
1355 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1356 }
1357
1358 #[test]
1359 fn same_plaintext_different_ciphertext() {
1360 let secret = b"test-key";
1361 let plaintext = b"A"; let p1 = encode(secret, plaintext).unwrap();
1364 let p2 = encode(secret, plaintext).unwrap();
1365
1366 assert_ne!(
1369 p1.ciphertext, p2.ciphertext,
1370 "Same symbol at different moments MUST produce different ciphertext"
1371 );
1372 }
1373
1374 #[test]
1375 fn wrong_key_fails_decode() {
1376 let plaintext = b"secret message";
1377 let packet = encode(b"correct-key", plaintext).unwrap();
1378
1379 let result = decode(b"wrong-key", &packet);
1380 assert!(
1381 result.is_err(),
1382 "Decoding with wrong shared secret must fail commitment verification"
1383 );
1384 }
1385
1386 #[test]
1387 fn empty_input_rejected() {
1388 let result = encode(b"key", b"");
1389 assert!(result.is_err());
1390 }
1391
1392 #[test]
1393 fn packet_serialization_roundtrip() {
1394 let secret = b"serialize-test";
1395 let plaintext = b"test packet roundtrip";
1396
1397 let packet = encode(secret, plaintext).unwrap();
1398 let bytes = packet.to_bytes();
1399 let restored = KkPacket::from_bytes(&bytes).unwrap();
1400
1401 let decoded = decode(secret, &restored).unwrap();
1402 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1403 }
1404
1405 #[test]
1406 fn tampered_ciphertext_detected() {
1407 let secret = b"tamper-test";
1408 let packet = encode(secret, b"important data").unwrap();
1409
1410 let mut tampered = packet.clone();
1411 tampered.ciphertext[0] ^= 0xFF; let result = decode(secret, &tampered);
1414 assert!(
1415 result.is_err(),
1416 "Tampered ciphertext must fail commitment verification"
1417 );
1418 }
1419
1420 #[test]
1421 fn large_message_works() {
1422 let secret = b"large-msg-test";
1423 let plaintext: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
1424
1425 let packet = encode(secret, &plaintext).unwrap();
1426 let decoded = decode(secret, &packet).unwrap();
1427
1428 assert_eq!(plaintext, decoded);
1429 }
1430
1431 #[test]
1434 fn split_encode_decode_roundtrip() {
1435 let secret = b"split-test-secret";
1436 let plaintext = b"Split-channel KK: ciphertext and epsilon travel separately.";
1437
1438 let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
1439 let decoded = decode_split(secret, &sealed, &epsilon).unwrap();
1440
1441 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1442 }
1443
1444 #[test]
1445 fn split_wrong_key_fails() {
1446 let plaintext = b"split secret";
1447 let (sealed, epsilon) = encode_split(b"right-key", plaintext).unwrap();
1448
1449 let result = decode_split(b"wrong-key", &sealed, &epsilon);
1450 assert!(result.is_err(), "Wrong passphrase must fail");
1451 }
1452
1453 #[test]
1454 fn split_wrong_epsilon_fails() {
1455 let secret = b"epsilon-test";
1456 let plaintext = b"the moment matters";
1457
1458 let (sealed, _real_epsilon) = encode_split(secret, plaintext).unwrap();
1459
1460 let fake_epsilon = entropy::gather().unwrap();
1462
1463 let result = decode_split(secret, &sealed, &fake_epsilon);
1464 assert!(
1465 result.is_err(),
1466 "Wrong epsilon must fail commitment verification"
1467 );
1468 }
1469
1470 #[test]
1471 fn split_sealed_message_serialization() {
1472 let secret = b"serde-split";
1473 let plaintext = b"roundtrip the sealed half";
1474
1475 let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
1476
1477 let wire = sealed.to_bytes();
1479 let restored = KkSealedMessage::from_bytes(&wire).unwrap();
1480
1481 let eps_wire = epsilon.to_bytes();
1483 let restored_eps = EntropySnapshot::from_bytes(&eps_wire).unwrap();
1484
1485 let decoded = decode_split(secret, &restored, &restored_eps).unwrap();
1486 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1487 }
1488
1489 #[test]
1490 fn split_empty_input_rejected() {
1491 let result = encode_split(b"key", b"");
1492 assert!(result.is_err());
1493 }
1494
1495 #[test]
1498 fn bound_encode_decode_roundtrip() {
1499 let secret = b"bound-test-secret";
1500 let plaintext = b"Temporal proof: challenge-response freshness.";
1501
1502 let nonce = temporal::generate_challenge().unwrap();
1503 let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
1504 let decoded = decode_bound(secret, &packet, &nonce, Duration::from_secs(30)).unwrap();
1505
1506 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1507 }
1508
1509 #[test]
1510 fn bound_wrong_nonce_rejected() {
1511 let secret = b"nonce-reject";
1512 let nonce = temporal::generate_challenge().unwrap();
1513 let wrong_nonce = temporal::generate_challenge().unwrap();
1514
1515 let packet = encode_bound(secret, b"test data", &nonce, &temporal::GENESIS_MAC).unwrap();
1516 let result = decode_bound(secret, &packet, &wrong_nonce, Duration::from_secs(30));
1517 assert!(result.is_err(), "Wrong nonce must be rejected");
1518 }
1519
1520 #[test]
1521 fn bound_wrong_key_rejected() {
1522 let nonce = temporal::generate_challenge().unwrap();
1523 let packet = encode_bound(b"right-key", b"secret", &nonce, &temporal::GENESIS_MAC).unwrap();
1524
1525 let result = decode_bound(b"wrong-key", &packet, &nonce, Duration::from_secs(30));
1526 assert!(result.is_err(), "Wrong key must fail");
1527 }
1528
1529 #[test]
1530 fn bound_packet_serialization_roundtrip() {
1531 let secret = b"bound-serde";
1532 let plaintext = b"serialize a bound packet";
1533
1534 let nonce = temporal::generate_challenge().unwrap();
1535 let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
1536
1537 let bytes = packet.to_bytes();
1538 let restored = KkBoundPacket::from_bytes(&bytes).unwrap();
1539
1540 let decoded = decode_bound(secret, &restored, &nonce, Duration::from_secs(30)).unwrap();
1541 assert_eq!(plaintext.as_slice(), decoded.as_slice());
1542 }
1543
1544 #[test]
1545 fn bound_tampered_ciphertext_detected() {
1546 let secret = b"tamper-bound";
1547 let nonce = temporal::generate_challenge().unwrap();
1548 let mut packet =
1549 encode_bound(secret, b"important", &nonce, &temporal::GENESIS_MAC).unwrap();
1550 packet.ciphertext[0] ^= 0xFF;
1551
1552 let result = decode_bound(secret, &packet, &nonce, Duration::from_secs(30));
1553 assert!(result.is_err(), "Tampered ciphertext must fail");
1554 }
1555
1556 #[test]
1557 fn bound_chain_ordering() {
1558 let secret = b"chain-test";
1559 let nonce1 = temporal::generate_challenge().unwrap();
1560 let nonce2 = temporal::generate_challenge().unwrap();
1561
1562 let p1 = encode_bound(secret, b"first", &nonce1, &temporal::GENESIS_MAC).unwrap();
1563 let p2 = encode_bound(secret, b"second", &nonce2, &p1.proof.mac).unwrap();
1564
1565 let d1 = decode_bound(secret, &p1, &nonce1, Duration::from_secs(30)).unwrap();
1567 let d2 = decode_bound(secret, &p2, &nonce2, Duration::from_secs(30)).unwrap();
1568 assert_eq!(d1, b"first");
1569 assert_eq!(d2, b"second");
1570
1571 assert_eq!(p2.proof.prev_mac, p1.proof.mac);
1573 }
1574
1575 #[test]
1576 fn bound_empty_input_rejected() {
1577 let nonce = temporal::generate_challenge().unwrap();
1578 let result = encode_bound(b"key", b"", &nonce, &temporal::GENESIS_MAC);
1579 assert!(result.is_err());
1580 }
1581
1582 #[test]
1585 fn stream_encode_decode_roundtrip() {
1586 let secret = b"stream-secret";
1587 let mut enc = StreamEncoder::new(secret).unwrap();
1588 enc.update(b"Hello ");
1589 enc.update(b"KK ");
1590 enc.update(b"Stream!");
1591 let packet = enc.finalize().unwrap();
1592
1593 let plaintext = decode(secret, &packet).unwrap();
1594 assert_eq!(plaintext, b"Hello KK Stream!");
1595 }
1596
1597 #[test]
1598 fn stream_decoder_roundtrip() {
1599 let secret = b"stream-dec-secret";
1600 let mut enc = StreamEncoder::new(secret).unwrap();
1601 enc.update(b"chunk1");
1602 enc.update(b"chunk2");
1603 let packet = enc.finalize().unwrap();
1604
1605 let mut dec = StreamDecoder::new(
1606 secret,
1607 packet.entropy_snapshot.clone(),
1608 packet.commitment.clone(),
1609 );
1610 dec.update(&packet.ciphertext);
1611 let plaintext = dec.finalize().unwrap();
1612 assert_eq!(plaintext, b"chunk1chunk2");
1613 }
1614
1615 #[test]
1616 fn stream_decoder_incremental_ciphertext() {
1617 let secret = b"stream-incr-secret";
1618 let mut enc = StreamEncoder::new(secret).unwrap();
1619 enc.update(b"ABCDEFGHIJ");
1620 let packet = enc.finalize().unwrap();
1621
1622 let mid = packet.ciphertext.len() / 2;
1624 let mut dec = StreamDecoder::new(
1625 secret,
1626 packet.entropy_snapshot.clone(),
1627 packet.commitment.clone(),
1628 );
1629 dec.update(&packet.ciphertext[..mid]);
1630 dec.update(&packet.ciphertext[mid..]);
1631 let plaintext = dec.finalize().unwrap();
1632 assert_eq!(plaintext, b"ABCDEFGHIJ");
1633 }
1634
1635 #[test]
1636 fn stream_encoder_empty_rejected() {
1637 let enc = StreamEncoder::new(b"key").unwrap();
1638 assert!(enc.finalize().is_err());
1639 }
1640
1641 #[test]
1642 fn stream_decoder_empty_rejected() {
1643 let snapshot = crate::entropy::gather().unwrap();
1644 let commitment = crate::temporal::commit(b"key", &snapshot, b"dummy").unwrap();
1645 let dec = StreamDecoder::new(b"key", snapshot, commitment);
1646 assert!(dec.finalize().is_err());
1647 }
1648
1649 #[test]
1650 fn stream_matches_oneshot() {
1651 let secret = b"stream-vs-oneshot";
1652 let data = b"The quick brown fox jumps over the lazy dog";
1653
1654 let snapshot = crate::entropy::gather().unwrap();
1656 let oneshot = encode_with_snapshot(secret, data, snapshot.clone()).unwrap();
1657
1658 let mut enc = StreamEncoder::new(secret).unwrap();
1661 enc.update(data);
1662 let stream_pkt = enc.finalize().unwrap();
1663
1664 let oneshot_pt = decode(secret, &oneshot).unwrap();
1665 let stream_pt = decode(secret, &stream_pkt).unwrap();
1666 assert_eq!(oneshot_pt, stream_pt);
1667 assert_eq!(&stream_pt[..], &data[..]);
1668 }
1669
1670 #[test]
1673 fn parallel_roundtrip_small() {
1674 let secret = b"parallel-test-secret";
1675 let plaintext = b"Hello parallel world!";
1676 let aad = b"test-aad";
1677
1678 let packet = encode_parallel(secret, plaintext, aad, 8, None).unwrap();
1679 assert!(packet.chunks.len() >= 2); let decoded = decode_parallel(secret, &packet).unwrap();
1681 assert_eq!(&decoded[..], &plaintext[..]);
1682 }
1683
1684 #[test]
1685 fn parallel_roundtrip_exact_chunk() {
1686 let secret = b"exact-chunk-secret";
1687 let plaintext = vec![0xABu8; 1024];
1688 let aad = b"exact";
1689
1690 let packet = encode_parallel(secret, &plaintext, aad, 1024, None).unwrap();
1691 assert_eq!(packet.chunks.len(), 1);
1692 let decoded = decode_parallel(secret, &packet).unwrap();
1693 assert_eq!(decoded, plaintext);
1694 }
1695
1696 #[test]
1697 fn parallel_roundtrip_large() {
1698 let secret = b"large-parallel-secret";
1699 let plaintext = vec![42u8; 1_000_000]; let aad = b"large-aad";
1701 let chunk_size = PARALLEL_CHUNK_SIZE; let packet = encode_parallel(secret, &plaintext, aad, chunk_size, None).unwrap();
1704 assert_eq!(packet.chunks.len(), 1);
1705 let decoded = decode_parallel(secret, &packet).unwrap();
1706 assert_eq!(decoded, plaintext);
1707 }
1708
1709 #[test]
1710 fn parallel_merkle_detects_reorder() {
1711 let secret = b"merkle-reorder-test";
1712 let plaintext = vec![0u8; 2048];
1713 let aad = b"reorder";
1714
1715 let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
1716 assert!(packet.chunks.len() >= 2);
1717
1718 packet.chunks.swap(0, 1);
1720 let result = decode_parallel(secret, &packet);
1721 assert!(result.is_err());
1722 }
1723
1724 #[test]
1725 fn parallel_merkle_detects_removal() {
1726 let secret = b"merkle-removal-test";
1727 let plaintext = vec![0u8; 2048];
1728 let aad = b"removal";
1729
1730 let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
1731 assert!(packet.chunks.len() >= 2);
1732
1733 packet.chunks.pop();
1735 let result = decode_parallel(secret, &packet);
1736 assert!(result.is_err());
1737 }
1738
1739 #[test]
1740 fn parallel_serde_roundtrip() {
1741 let secret = b"serde-parallel-secret";
1742 let plaintext = b"serialize me in parallel chunks";
1743 let aad = b"serde-aad";
1744
1745 let packet = encode_parallel(secret, plaintext, aad, 10, None).unwrap();
1746 let bytes = packet.to_bytes();
1747 let restored = KkParallelPacket::from_bytes(&bytes).unwrap();
1748
1749 assert_eq!(restored.chunks.len(), packet.chunks.len());
1750 assert_eq!(restored.chunk_size, packet.chunk_size);
1751 assert_eq!(restored.merkle_root, packet.merkle_root);
1752
1753 let decoded = decode_parallel(secret, &restored).unwrap();
1754 assert_eq!(&decoded[..], &plaintext[..]);
1755 }
1756
1757 #[test]
1758 fn parallel_empty_input_rejected() {
1759 let secret = b"empty-test";
1760 let result = encode_parallel(secret, b"", b"aad", 1024, None);
1761 assert!(result.is_err());
1762 }
1763
1764 #[test]
1765 fn parallel_zero_chunk_size_rejected() {
1766 let secret = b"zero-chunk";
1767 let result = encode_parallel(secret, b"data", b"aad", 0, None);
1768 assert!(result.is_err());
1769 }
1770}