1use alloc::{format, string::String, vec, vec::Vec};
4
5use rand::RngCore;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 encryption::{
10 derive_key_hkdf, derive_key_hkdf_raw, Algorithm as EncryptionAlgorithm, EncryptionResult,
11 Key,
12 },
13 hash::{compare_hashes, generate_hmac_sha256, generate_hmac_sha256_parts},
14 Error, Result,
15};
16
17#[cfg(feature = "compression")]
18use crate::compression::{self, CompressionAlgorithm, CompressionOptions};
19
20pub const SHELL_TAG_SIZE: usize = 16;
22
23pub const FUSED_SHELL_MAGIC: [u8; 4] = *b"VFS2";
25
26pub const FUSED_SHELL_VERSION: u8 = 0x02;
28const FUSED_SHELL_V1_VERSION: u8 = 0x01;
29
30pub const PROTECTED_ARTIFACT_MAGIC: [u8; 4] = *b"VOF2";
32
33pub const PROTECTED_ARTIFACT_VERSION: u8 = 0x03;
35const PROTECTED_ARTIFACT_V2_VERSION: u8 = 0x02;
36const PROTECTED_ARTIFACT_V1_VERSION: u8 = 0x01;
37
38pub const SHELL_NONCE_SIZE: usize = 12;
40
41pub const DEFAULT_CHUNK_SIZE: usize = 16 * 1024;
43
44const MAX_CHUNK_SIZE: usize = 1024 * 1024;
45const FUSED_SHELL_HEADER_LEN: usize = 4 + 1 + 1 + 4 + SHELL_NONCE_SIZE;
46const PROTECTED_ARTIFACT_HEADER_LEN: usize = 4 + 1 + 1 + 1 + 1 + 4 + 8 + 8 + SHELL_NONCE_SIZE;
47const FIELD_MONOLITH_STATE_PURPOSE: &str = "field-monolith.state.v0";
48const FIELD_MONOLITH_PLAN_PURPOSE: &str = "field-monolith.plan.v0";
49#[cfg(feature = "compression")]
50const PROTECTED_MONOLITH_STATE_PURPOSE: &str = "protected-monolith.state.v0";
51const FIELD_MONOLITH_AUTO_CHUNK_SIZE: usize = 0;
52const FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT: usize = 4;
53const FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE: usize = 8;
54const FIELD_MONOLITH_MIN_CELL_BYTES: usize = 24;
55const FIELD_MONOLITH_EXPERIMENTAL_MIN_CELL_BYTES: usize = 8;
56const FIELD_MONOLITH_MAX_CELL_BYTES: usize = 512;
57#[cfg(feature = "compression")]
58const PROTECTED_MONOLITH_MAX_CELL_BYTES: usize = 1024;
59#[cfg(feature = "compression")]
60const FIELD_MONOLITH_DENSE_ANCHOR_MAX_CELL_BYTES: usize = PROTECTED_MONOLITH_MAX_CELL_BYTES;
61#[cfg(not(feature = "compression"))]
62const FIELD_MONOLITH_DENSE_ANCHOR_MAX_CELL_BYTES: usize = FIELD_MONOLITH_MAX_CELL_BYTES;
63const FIELD_MONOLITH_AUTO_TINY_CELL_BYTES: usize = 96;
64const FIELD_MONOLITH_CURRENT_AUTO_TINY_MAX: usize = 224;
65const FIELD_MONOLITH_CURRENT_AUTO_DENSE_SMALL_CELL_BYTES: usize = 96;
66const FIELD_MONOLITH_CURRENT_AUTO_DENSE_SMALL_MAX: usize =
67 FIELD_MONOLITH_CURRENT_AUTO_DENSE_SMALL_CELL_BYTES * FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE;
68const FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_CELL_BYTES: usize = 128;
69const FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_MAX: usize = 1120;
70const FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_UPPER_SMALL_CELL_BYTES: usize = 128;
71const FIELD_MONOLITH_AUTO_SMALL_MAX: usize = 1536;
72const FIELD_MONOLITH_AUTO_MID_CELL_BYTES: usize = 256;
73const FIELD_MONOLITH_AUTO_MID_MAX: usize = 16 * 1024;
74#[cfg(feature = "compression")]
75const WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES: usize = 2;
76#[cfg(feature = "compression")]
77const WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN: usize = 34;
78#[cfg(feature = "compression")]
79const WHOLE_MONOLITH_V0_MIN_ARTIFACT_LEN: usize =
80 WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES + WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN + SHELL_TAG_SIZE;
81#[cfg(feature = "compression")]
82const WHOLE_MONOLITH_V0_CLOAK_HEAD_BYTES: usize = 512;
83#[cfg(feature = "compression")]
84const WHOLE_MONOLITH_V0_MATERIAL_DOMAIN: &str = "whole-monolith-v0-material";
85
86fn domain_info(domain_label: &str) -> String {
87 format!("voided:shell:{domain_label}")
88}
89
90#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
92#[repr(u8)]
93pub enum FusedPreset {
94 Compact = 0x01,
96 Balanced = 0x02,
98 Concealed = 0x03,
100}
101
102impl FusedPreset {
103 pub fn from_byte(byte: u8) -> Result<Self> {
105 match byte {
106 0x01 => Ok(Self::Compact),
107 0x02 => Ok(Self::Balanced),
108 0x03 => Ok(Self::Concealed),
109 other => Err(Error::UnsupportedPreset(other)),
110 }
111 }
112
113 pub fn from_name(name: &str) -> Result<Self> {
115 match name {
116 "compact" => Ok(Self::Compact),
117 "balanced" => Ok(Self::Balanced),
118 "concealed" => Ok(Self::Concealed),
119 other => Err(Error::InvalidConfiguration(format!(
120 "unsupported fused preset `{other}`"
121 ))),
122 }
123 }
124
125 pub fn name(self) -> &'static str {
127 match self {
128 Self::Compact => "compact",
129 Self::Balanced => "balanced",
130 Self::Concealed => "concealed",
131 }
132 }
133}
134
135impl Default for FusedPreset {
136 fn default() -> Self {
137 Self::Balanced
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct FusedShellOptions {
144 pub preset: FusedPreset,
146 pub chunk_size: Option<usize>,
148 pub shell_nonce: Option<[u8; SHELL_NONCE_SIZE]>,
150}
151
152impl Default for FusedShellOptions {
153 fn default() -> Self {
154 Self {
155 preset: FusedPreset::Balanced,
156 chunk_size: None,
157 shell_nonce: None,
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct FusedShellInfo {
165 pub version: u8,
167 pub preset: FusedPreset,
169 pub preset_label: String,
171 pub chunk_size: u32,
173 pub chunk_count: usize,
175 pub payload_size: usize,
177 pub shell_size: usize,
179 pub metadata_size: usize,
181 pub tag_size: usize,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct FusedShellEnvelope {
188 pub version: u8,
190 pub preset: FusedPreset,
192 pub chunk_size: u32,
194 pub nonce: [u8; SHELL_NONCE_SIZE],
196 pub payload: Vec<u8>,
198 pub tag: [u8; SHELL_TAG_SIZE],
200}
201
202#[derive(Debug, Clone, Copy)]
203struct ParsedFusedShellHeader {
204 version: u8,
205 preset: FusedPreset,
206 chunk_size: u32,
207 nonce: [u8; SHELL_NONCE_SIZE],
208 payload_end: usize,
209 geometry: Option<FieldMonolithGeometry>,
210}
211
212impl ParsedFusedShellHeader {
213 fn payload_len(self) -> usize {
214 self.payload_end - FUSED_SHELL_HEADER_LEN
215 }
216
217 fn info(self, shell_size: usize) -> FusedShellInfo {
218 let payload_len = self.payload_len();
219 let chunk_count = self
220 .geometry
221 .map(|geometry| geometry.chunk_count)
222 .unwrap_or_else(|| chunk_count(payload_len, self.chunk_size as usize));
223 FusedShellInfo {
224 version: self.version,
225 preset: self.preset,
226 preset_label: self.preset.name().to_string(),
227 chunk_size: self.chunk_size,
228 chunk_count,
229 payload_size: payload_len,
230 shell_size,
231 metadata_size: FUSED_SHELL_HEADER_LEN,
232 tag_size: SHELL_TAG_SIZE,
233 }
234 }
235}
236
237fn parse_fused_shell_header(data: &[u8]) -> Result<ParsedFusedShellHeader> {
238 if data.len() < FUSED_SHELL_HEADER_LEN + SHELL_TAG_SIZE {
239 return Err(Error::TruncatedPayload {
240 expected: FUSED_SHELL_HEADER_LEN + SHELL_TAG_SIZE,
241 actual: data.len(),
242 });
243 }
244 if data[..4] != FUSED_SHELL_MAGIC {
245 return Err(Error::InvalidFormat);
246 }
247
248 let version = data[4];
249 if !is_supported_fused_shell_version(version) {
250 return Err(Error::UnsupportedVersion(version));
251 }
252 let preset = FusedPreset::from_byte(data[5])?;
253 let chunk_size = u32::from_be_bytes([data[6], data[7], data[8], data[9]]);
254 let mut nonce = [0u8; SHELL_NONCE_SIZE];
255 nonce.copy_from_slice(&data[10..10 + SHELL_NONCE_SIZE]);
256 let payload_end = data.len() - SHELL_TAG_SIZE;
257 let payload_len = payload_end - FUSED_SHELL_HEADER_LEN;
258
259 let geometry = if version == FUSED_SHELL_VERSION {
260 Some(validate_current_field_monolith_geometry(
261 payload_len,
262 chunk_size as usize,
263 FIELD_MONOLITH_MAX_CELL_BYTES,
264 "fused shell",
265 )?)
266 } else {
267 validate_legacy_shell_chunk_size(chunk_size as usize, "legacy fused shell")?;
268 None
269 };
270
271 Ok(ParsedFusedShellHeader {
272 version,
273 preset,
274 chunk_size,
275 nonce,
276 payload_end,
277 geometry,
278 })
279}
280
281impl FusedShellEnvelope {
282 pub fn to_bytes(&self) -> Vec<u8> {
284 let mut bytes = self.to_unsigned_bytes();
285 bytes.extend_from_slice(&self.tag);
286 bytes
287 }
288
289 pub fn to_unsigned_bytes(&self) -> Vec<u8> {
291 let mut bytes = Vec::with_capacity(FUSED_SHELL_HEADER_LEN + self.payload.len());
292 bytes.extend_from_slice(&FUSED_SHELL_MAGIC);
293 bytes.push(self.version);
294 bytes.push(self.preset as u8);
295 bytes.extend_from_slice(&self.chunk_size.to_be_bytes());
296 bytes.extend_from_slice(&self.nonce);
297 bytes.extend_from_slice(&self.payload);
298 bytes
299 }
300
301 pub fn from_bytes(data: &[u8]) -> Result<Self> {
303 let parsed = parse_fused_shell_header(data)?;
304 let payload = data[FUSED_SHELL_HEADER_LEN..parsed.payload_end].to_vec();
305 let mut tag = [0u8; SHELL_TAG_SIZE];
306 tag.copy_from_slice(&data[parsed.payload_end..]);
307
308 Ok(Self {
309 version: parsed.version,
310 preset: parsed.preset,
311 chunk_size: parsed.chunk_size,
312 nonce: parsed.nonce,
313 payload,
314 tag,
315 })
316 }
317
318 pub fn info(&self) -> FusedShellInfo {
320 let chunk_size = self.chunk_size.max(1) as usize;
321 let chunk_count = if self.version == FUSED_SHELL_VERSION {
322 field_monolith_infer_geometry(self.payload.len(), chunk_size)
323 .map(|geometry| geometry.chunk_count)
324 .unwrap_or_else(|| chunk_count(self.payload.len(), chunk_size))
325 } else {
326 chunk_count(self.payload.len(), chunk_size)
327 };
328 FusedShellInfo {
329 version: self.version,
330 preset: self.preset,
331 preset_label: self.preset.name().to_string(),
332 chunk_size: self.chunk_size,
333 chunk_count,
334 payload_size: self.payload.len(),
335 shell_size: self.payload.len() + FUSED_SHELL_HEADER_LEN + SHELL_TAG_SIZE,
336 metadata_size: FUSED_SHELL_HEADER_LEN,
337 tag_size: SHELL_TAG_SIZE,
338 }
339 }
340}
341
342#[cfg(feature = "compression")]
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ProtectOptions {
346 pub preset: FusedPreset,
348 pub compression_algorithm: CompressionAlgorithm,
350 pub compression_level: u32,
352 pub compression_min_size_threshold: usize,
354 pub encryption_algorithm: Option<EncryptionAlgorithm>,
356 pub shell_chunk_size: Option<usize>,
358 pub shell_nonce: Option<[u8; SHELL_NONCE_SIZE]>,
360}
361
362#[cfg(feature = "compression")]
363impl Default for ProtectOptions {
364 fn default() -> Self {
365 Self {
366 preset: FusedPreset::Balanced,
367 compression_algorithm: CompressionAlgorithm::Brotli,
368 compression_level: 6,
369 compression_min_size_threshold: 100,
370 encryption_algorithm: None,
371 shell_chunk_size: None,
372 shell_nonce: None,
373 }
374 }
375}
376
377#[cfg(feature = "compression")]
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct ProtectedArtifactInfo {
381 pub version: u8,
383 pub preset: FusedPreset,
385 pub preset_label: String,
387 pub compression_algorithm: CompressionAlgorithm,
389 pub encryption_algorithm: EncryptionAlgorithm,
391 pub original_size: usize,
393 pub compressed_size: usize,
395 pub encrypted_size: usize,
397 pub protected_size: usize,
399 pub shell_chunk_size: u32,
401 pub shell_chunk_count: usize,
403 pub shell_nonce: [u8; SHELL_NONCE_SIZE],
405}
406
407#[cfg(feature = "compression")]
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct ProtectResult {
411 pub artifact: Vec<u8>,
413 pub info: ProtectedArtifactInfo,
415}
416
417#[cfg(feature = "compression")]
418#[derive(Debug, Clone)]
419struct ProtectedArtifactEnvelope {
420 version: u8,
421 preset: FusedPreset,
422 compression_algorithm: CompressionAlgorithm,
423 encryption_algorithm: EncryptionAlgorithm,
424 chunk_size: u32,
425 original_size: u64,
426 compressed_size: u64,
427 nonce: [u8; SHELL_NONCE_SIZE],
428 payload: Vec<u8>,
429 tag: [u8; SHELL_TAG_SIZE],
430}
431
432#[cfg(feature = "compression")]
433#[derive(Debug, Clone, Copy)]
434struct ParsedProtectedArtifactHeader {
435 version: u8,
436 preset: FusedPreset,
437 compression_algorithm: CompressionAlgorithm,
438 encryption_algorithm: EncryptionAlgorithm,
439 chunk_size: u32,
440 original_size: u64,
441 compressed_size: u64,
442 nonce: [u8; SHELL_NONCE_SIZE],
443 payload_end: usize,
444 geometry: Option<FieldMonolithGeometry>,
445}
446
447#[cfg(feature = "compression")]
448impl ParsedProtectedArtifactHeader {
449 fn payload_len(self) -> usize {
450 self.payload_end - PROTECTED_ARTIFACT_HEADER_LEN
451 }
452
453 fn info(self, protected_size: usize) -> Result<ProtectedArtifactInfo> {
454 let payload_len = self.payload_len();
455 let original_size = protected_artifact_size(self.original_size, "original size")?;
456 let compressed_size = protected_artifact_size(self.compressed_size, "compressed size")?;
457 let encrypted_size = self
458 .geometry
459 .map(|geometry| geometry.original_len)
460 .unwrap_or(payload_len);
461 let shell_chunk_count = self
462 .geometry
463 .map(|geometry| geometry.chunk_count)
464 .unwrap_or_else(|| chunk_count(payload_len, self.chunk_size as usize));
465
466 Ok(ProtectedArtifactInfo {
467 version: self.version,
468 preset: self.preset,
469 preset_label: self.preset.name().to_string(),
470 compression_algorithm: self.compression_algorithm,
471 encryption_algorithm: self.encryption_algorithm,
472 original_size,
473 compressed_size,
474 encrypted_size,
475 protected_size,
476 shell_chunk_size: self.chunk_size,
477 shell_chunk_count,
478 shell_nonce: self.nonce,
479 })
480 }
481}
482
483#[cfg(feature = "compression")]
484fn parse_protected_artifact_header(data: &[u8]) -> Result<ParsedProtectedArtifactHeader> {
485 if data.len() < PROTECTED_ARTIFACT_HEADER_LEN + SHELL_TAG_SIZE {
486 return Err(Error::TruncatedPayload {
487 expected: PROTECTED_ARTIFACT_HEADER_LEN + SHELL_TAG_SIZE,
488 actual: data.len(),
489 });
490 }
491 if data[..4] != PROTECTED_ARTIFACT_MAGIC {
492 return Err(Error::InvalidFormat);
493 }
494
495 let version = data[4];
496 if !is_supported_protected_artifact_version(version) {
497 return Err(Error::UnsupportedVersion(version));
498 }
499 let preset = FusedPreset::from_byte(data[5])?;
500 let compression_algorithm = CompressionAlgorithm::from_byte(data[6])?;
501 let encryption_algorithm = EncryptionAlgorithm::from_byte(data[7])?;
502 let chunk_size = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
503 let original_size = u64::from_be_bytes([
504 data[12], data[13], data[14], data[15], data[16], data[17], data[18], data[19],
505 ]);
506 let compressed_size = u64::from_be_bytes([
507 data[20], data[21], data[22], data[23], data[24], data[25], data[26], data[27],
508 ]);
509 let mut nonce = [0u8; SHELL_NONCE_SIZE];
510 nonce.copy_from_slice(&data[28..28 + SHELL_NONCE_SIZE]);
511 let payload_end = data.len() - SHELL_TAG_SIZE;
512 let payload_len = payload_end - PROTECTED_ARTIFACT_HEADER_LEN;
513
514 let geometry = match version {
515 PROTECTED_ARTIFACT_VERSION => Some(validate_current_field_monolith_geometry(
516 payload_len,
517 chunk_size as usize,
518 PROTECTED_MONOLITH_MAX_CELL_BYTES,
519 "legacy protected monolith",
520 )?),
521 PROTECTED_ARTIFACT_V2_VERSION => Some(validate_current_field_monolith_geometry(
522 payload_len,
523 chunk_size as usize,
524 FIELD_MONOLITH_MAX_CELL_BYTES,
525 "legacy protected shell",
526 )?),
527 PROTECTED_ARTIFACT_V1_VERSION => {
528 validate_legacy_shell_chunk_size(chunk_size as usize, "legacy protected artifact")?;
529 None
530 }
531 other => return Err(Error::UnsupportedVersion(other)),
532 };
533
534 Ok(ParsedProtectedArtifactHeader {
535 version,
536 preset,
537 compression_algorithm,
538 encryption_algorithm,
539 chunk_size,
540 original_size,
541 compressed_size,
542 nonce,
543 payload_end,
544 geometry,
545 })
546}
547
548#[cfg(feature = "compression")]
549impl ProtectedArtifactEnvelope {
550 #[cfg(test)]
551 fn to_bytes(&self) -> Vec<u8> {
552 let mut bytes = self.to_unsigned_bytes();
553 bytes.extend_from_slice(&self.tag);
554 bytes
555 }
556
557 fn to_unsigned_bytes(&self) -> Vec<u8> {
558 let mut bytes = Vec::with_capacity(PROTECTED_ARTIFACT_HEADER_LEN + self.payload.len());
559 bytes.extend_from_slice(&PROTECTED_ARTIFACT_MAGIC);
560 bytes.push(self.version);
561 bytes.push(self.preset as u8);
562 bytes.push(self.compression_algorithm as u8);
563 bytes.push(self.encryption_algorithm as u8);
564 bytes.extend_from_slice(&self.chunk_size.to_be_bytes());
565 bytes.extend_from_slice(&self.original_size.to_be_bytes());
566 bytes.extend_from_slice(&self.compressed_size.to_be_bytes());
567 bytes.extend_from_slice(&self.nonce);
568 bytes.extend_from_slice(&self.payload);
569 bytes
570 }
571
572 fn from_bytes(data: &[u8]) -> Result<Self> {
573 let parsed = parse_protected_artifact_header(data)?;
574 let payload = data[PROTECTED_ARTIFACT_HEADER_LEN..parsed.payload_end].to_vec();
575 let mut tag = [0u8; SHELL_TAG_SIZE];
576 tag.copy_from_slice(&data[parsed.payload_end..]);
577
578 Ok(Self {
579 version: parsed.version,
580 preset: parsed.preset,
581 compression_algorithm: parsed.compression_algorithm,
582 encryption_algorithm: parsed.encryption_algorithm,
583 chunk_size: parsed.chunk_size,
584 original_size: parsed.original_size,
585 compressed_size: parsed.compressed_size,
586 nonce: parsed.nonce,
587 payload,
588 tag,
589 })
590 }
591}
592
593fn protected_artifact_size(value: u64, label: &str) -> Result<usize> {
594 usize::try_from(value)
595 .map_err(|_| Error::InvalidConfiguration(format!("artifact {label} exceeds this platform")))
596}
597
598pub fn derive_domain_key(master: &Key, salt: Option<&[u8]>, domain_label: &str) -> Result<Key> {
600 derive_key_hkdf(
601 master.as_bytes(),
602 salt,
603 domain_info(domain_label).as_bytes(),
604 )
605}
606
607pub fn derive_domain_bytes(
609 master: &Key,
610 salt: Option<&[u8]>,
611 domain_label: &str,
612 len: usize,
613) -> Result<Vec<u8>> {
614 derive_key_hkdf_raw(
615 master.as_bytes(),
616 salt,
617 domain_info(domain_label).as_bytes(),
618 len,
619 )
620}
621
622pub fn derive_chunk_seed(
624 shell_key: &Key,
625 nonce: &[u8],
626 chunk_index: u32,
627 purpose: &str,
628 len: usize,
629) -> Result<Vec<u8>> {
630 let mut info = Vec::new();
631 info.extend_from_slice(b"voided:shell:chunk:");
632 info.extend_from_slice(purpose.as_bytes());
633 info.extend_from_slice(b":");
634 info.extend_from_slice(&chunk_index.to_be_bytes());
635 derive_key_hkdf_raw(shell_key.as_bytes(), Some(nonce), &info, len)
636}
637
638pub fn compute_shell_tag(data: &[u8], tag_key: &Key) -> Result<[u8; SHELL_TAG_SIZE]> {
640 let mac = generate_hmac_sha256(data, tag_key.as_bytes())?;
641 let mut tag = [0u8; SHELL_TAG_SIZE];
642 tag.copy_from_slice(&mac[..SHELL_TAG_SIZE]);
643 Ok(tag)
644}
645
646fn compute_shell_tag_parts(parts: &[&[u8]], tag_key: &Key) -> Result<[u8; SHELL_TAG_SIZE]> {
647 let mac = generate_hmac_sha256_parts(parts, tag_key.as_bytes())?;
648 let mut tag = [0u8; SHELL_TAG_SIZE];
649 tag.copy_from_slice(&mac[..SHELL_TAG_SIZE]);
650 Ok(tag)
651}
652
653pub fn verify_shell_tag(data: &[u8], tag: &[u8], tag_key: &Key) -> Result<bool> {
655 if tag.len() != SHELL_TAG_SIZE {
656 return Ok(false);
657 }
658 let expected = compute_shell_tag(data, tag_key)?;
659 Ok(compare_hashes(&expected, tag))
660}
661
662pub fn fuse(
664 data: &[u8],
665 master_key: &Key,
666 options: Option<FusedShellOptions>,
667) -> Result<FusedShellEnvelope> {
668 let opts = options.unwrap_or_default();
669 let nonce = opts.shell_nonce.unwrap_or_else(random_nonce);
670 let (shell_key, tag_key) = derive_shell_keys(master_key, &nonce)?;
671 let (payload, chunk_size) =
672 field_monolith_encode_payload(data, &shell_key, &nonce, opts.chunk_size)?;
673
674 let mut envelope = FusedShellEnvelope {
675 version: FUSED_SHELL_VERSION,
676 preset: opts.preset,
677 chunk_size: chunk_size as u32,
678 nonce,
679 payload,
680 tag: [0u8; SHELL_TAG_SIZE],
681 };
682 envelope.tag = compute_shell_tag(&envelope.to_unsigned_bytes(), &tag_key)?;
683 Ok(envelope)
684}
685
686pub fn unfuse(envelope: &FusedShellEnvelope, master_key: &Key) -> Result<Vec<u8>> {
688 let (shell_key, tag_key) = derive_shell_keys(master_key, &envelope.nonce)?;
689 if !verify_shell_tag(&envelope.to_unsigned_bytes(), &envelope.tag, &tag_key)? {
690 return Err(Error::AuthenticationFailed);
691 }
692
693 match envelope.version {
694 FUSED_SHELL_VERSION => field_monolith_decode_payload(
695 &envelope.payload,
696 &shell_key,
697 &envelope.nonce,
698 envelope.chunk_size.max(1) as usize,
699 ),
700 FUSED_SHELL_V1_VERSION => transform_payload_v1(
701 &envelope.payload,
702 &shell_key,
703 &envelope.nonce,
704 envelope.preset,
705 envelope.chunk_size.max(1) as usize,
706 false,
707 ),
708 version => Err(Error::UnsupportedVersion(version)),
709 }
710}
711
712pub fn fuse_bytes(
714 data: &[u8],
715 master_key: &Key,
716 options: Option<FusedShellOptions>,
717) -> Result<Vec<u8>> {
718 Ok(fuse(data, master_key, options)?.to_bytes())
719}
720
721pub fn unfuse_bytes(data: &[u8], master_key: &Key) -> Result<Vec<u8>> {
723 unfuse(&FusedShellEnvelope::from_bytes(data)?, master_key)
724}
725
726pub fn inspect_fused(data: &[u8]) -> Result<FusedShellInfo> {
728 Ok(parse_fused_shell_header(data)?.info(data.len()))
729}
730
731#[cfg(feature = "compression")]
732pub fn protect(
734 plaintext: &[u8],
735 master_key: &Key,
736 options: Option<ProtectOptions>,
737) -> Result<ProtectResult> {
738 let opts = options.unwrap_or_default();
739 whole_monolith_v0_protect(plaintext, master_key, opts)
740}
741
742#[cfg(feature = "compression")]
743pub fn open(artifact: &[u8], master_key: &Key) -> Result<Vec<u8>> {
745 whole_monolith_v0_open(artifact, master_key)
746}
747
748#[cfg(feature = "compression")]
749pub fn open_rotation_artifact(artifact: &[u8], master_key: &Key) -> Result<Vec<u8>> {
751 if artifact_starts_with_protected_magic(artifact) {
752 open_legacy_protected_artifact(artifact, master_key)
753 } else {
754 open(artifact, master_key)
755 }
756}
757
758#[cfg(feature = "compression")]
759fn open_legacy_protected_artifact(artifact: &[u8], master_key: &Key) -> Result<Vec<u8>> {
760 let envelope = ProtectedArtifactEnvelope::from_bytes(artifact)?;
761 let (shell_key, tag_key) = derive_shell_keys(master_key, &envelope.nonce)?;
762 if !verify_shell_tag(&envelope.to_unsigned_bytes(), &envelope.tag, &tag_key)? {
763 return Err(Error::AuthenticationFailed);
764 }
765
766 let original_size = protected_artifact_size(envelope.original_size, "original size")?;
767 let compressed_size = protected_artifact_size(envelope.compressed_size, "compressed size")?;
768
769 let encrypted_bytes = match envelope.version {
770 PROTECTED_ARTIFACT_VERSION => protected_monolith_decode_payload(
771 &envelope.payload,
772 &shell_key,
773 &envelope.nonce,
774 envelope.chunk_size.max(1) as usize,
775 original_size,
776 compressed_size,
777 envelope.compression_algorithm,
778 envelope.encryption_algorithm,
779 envelope.preset,
780 )?,
781 PROTECTED_ARTIFACT_V2_VERSION => field_monolith_decode_payload(
782 &envelope.payload,
783 &shell_key,
784 &envelope.nonce,
785 envelope.chunk_size.max(1) as usize,
786 )?,
787 PROTECTED_ARTIFACT_V1_VERSION => transform_payload_v1(
788 &envelope.payload,
789 &shell_key,
790 &envelope.nonce,
791 envelope.preset,
792 envelope.chunk_size.max(1) as usize,
793 false,
794 )?,
795 version => return Err(Error::UnsupportedVersion(version)),
796 };
797 let encrypted = EncryptionResult::from_bytes(&encrypted_bytes)?;
798 if encrypted.algorithm != envelope.encryption_algorithm {
799 return Err(Error::InvalidFormat);
800 }
801
802 let compressed = crate::encryption::decrypt(&encrypted, master_key)?;
803 if compressed.len() != compressed_size {
804 return Err(Error::SizeMismatch {
805 expected: compressed_size,
806 actual: compressed.len(),
807 });
808 }
809
810 let plaintext =
811 compression::decompress_exact(&compressed, envelope.compression_algorithm, original_size)?;
812
813 Ok(plaintext)
814}
815
816#[cfg(feature = "compression")]
817pub fn inspect_artifact(artifact: &[u8]) -> Result<ProtectedArtifactInfo> {
819 whole_monolith_v0_inspect_artifact(artifact)
820}
821
822#[cfg(feature = "compression")]
823pub fn inspect_rotation_artifact(artifact: &[u8]) -> Result<ProtectedArtifactInfo> {
825 if artifact_starts_with_protected_magic(artifact) {
826 parse_protected_artifact_header(artifact)?.info(artifact.len())
827 } else {
828 inspect_artifact(artifact)
829 }
830}
831
832#[cfg(feature = "compression")]
833pub fn repack_artifact(
835 artifact: &[u8],
836 master_key: &Key,
837 options: Option<ProtectOptions>,
838) -> Result<ProtectResult> {
839 let plaintext = open(artifact, master_key)?;
840 protect(&plaintext, master_key, options)
841}
842
843fn derive_shell_keys(master_key: &Key, nonce: &[u8; SHELL_NONCE_SIZE]) -> Result<(Key, Key)> {
844 let shell_key = derive_domain_key(master_key, Some(nonce), "fused-shell")?;
845 let tag_key = derive_domain_key(master_key, Some(nonce), "fused-tag")?;
846 Ok((shell_key, tag_key))
847}
848
849fn is_supported_fused_shell_version(version: u8) -> bool {
850 matches!(version, FUSED_SHELL_V1_VERSION | FUSED_SHELL_VERSION)
851}
852
853#[cfg(feature = "compression")]
854fn is_supported_protected_artifact_version(version: u8) -> bool {
855 matches!(
856 version,
857 PROTECTED_ARTIFACT_V1_VERSION | PROTECTED_ARTIFACT_V2_VERSION | PROTECTED_ARTIFACT_VERSION
858 )
859}
860
861#[derive(Debug, Clone, Copy, Eq, PartialEq)]
862enum FieldMonolithMutationMode {
863 Direct = 0,
864 StrideSwap = 1,
865 SplitWeave = 2,
866 MirrorWeave = 3,
867}
868
869impl FieldMonolithMutationMode {
870 fn from_bits(bits: u8) -> Self {
871 match bits & 0x03 {
872 0 => Self::Direct,
873 1 => Self::StrideSwap,
874 2 => Self::SplitWeave,
875 _ => Self::MirrorWeave,
876 }
877 }
878}
879
880#[derive(Debug, Clone, Copy, Eq, PartialEq)]
881enum FieldMonolithWeaveMode {
882 LeadPad = 0,
883 TrailPad = 1,
884 Alternating = 2,
885 Clustered = 3,
886}
887
888impl FieldMonolithWeaveMode {
889 fn from_bits(bits: u8) -> Self {
890 match bits & 0x03 {
891 0 => Self::LeadPad,
892 1 => Self::TrailPad,
893 2 => Self::Alternating,
894 _ => Self::Clustered,
895 }
896 }
897}
898
899#[derive(Debug, Clone, Copy)]
900struct FieldMonolithState {
901 phase: u8,
902 tension: u8,
903 curvature: u8,
904 drift: u8,
905 echo: u8,
906 reserve: u8,
907 stride: u8,
908 parity: u8,
909}
910
911#[derive(Debug, Clone, Copy)]
912struct FieldMonolithCellPlan {
913 mutation: FieldMonolithMutationMode,
914 weave: FieldMonolithWeaveMode,
915 stride: usize,
916 pad_len: usize,
917}
918
919#[derive(Debug, Clone, Copy, Eq, PartialEq)]
920struct FieldMonolithSurfaceSummary {
921 digest: u8,
922 front: u8,
923 back: u8,
924}
925
926#[derive(Debug, Clone, Copy, Eq, PartialEq)]
927struct FieldMonolithGeometry {
928 original_len: usize,
929 chunk_count: usize,
930 block_cells: usize,
931}
932
933fn field_monolith_encode_payload(
934 data: &[u8],
935 shell_key: &Key,
936 nonce: &[u8; SHELL_NONCE_SIZE],
937 requested_chunk_size: Option<usize>,
938) -> Result<(Vec<u8>, usize)> {
939 let target_cell_size =
940 resolve_field_monolith_current_cell_size(requested_chunk_size, data.len())?;
941 let state = field_monolith_initial_state(shell_key, nonce, data.len())?;
942 let rng = field_monolith_plan_rng(shell_key, nonce, data.len())?;
943 let payload = field_monolith_encode_payload_with_state(data, target_cell_size, state, rng);
944
945 Ok((payload, target_cell_size))
946}
947
948fn field_monolith_encode_payload_with_state(
949 data: &[u8],
950 target_cell_size: usize,
951 mut state: FieldMonolithState,
952 mut rng: DeterministicRng,
953) -> Vec<u8> {
954 let chunk_count = chunk_count(data.len(), target_cell_size.max(1));
955 let block_cells = field_monolith_current_anchor_block_cells(target_cell_size, chunk_count);
956 let block_count = field_monolith_block_count_for_cells(chunk_count, block_cells);
957 let mut payload = Vec::with_capacity(data.len().saturating_add(block_count));
958 let mut cursor = 0usize;
959 let mut cell_index = 0u32;
960 let mut block_index = 0u32;
961 let mut surface = Vec::with_capacity(target_cell_size);
962 let mut mutation_scratch = Vec::with_capacity(target_cell_size);
963
964 while cursor < data.len() {
965 let anchor = field_monolith_current_anchor_for_block(
966 &state,
967 block_index,
968 &mut rng,
969 target_cell_size,
970 );
971 payload.push(anchor);
972
973 for ordinal_in_block in 0..block_cells {
974 if cursor >= data.len() {
975 break;
976 }
977 let remaining = data.len() - cursor;
978 let cell_len = remaining.min(target_cell_size.max(1));
979 let next_cursor = cursor + cell_len;
980 let plaintext = &data[cursor..next_cursor];
981 let plan =
982 field_monolith_plan_for_anchor_cell(anchor, cell_index, cell_len, ordinal_in_block);
983 let descriptor = field_monolith_descriptor(plan);
984 let mask_lanes = field_monolith_mask_lanes(&state, cell_index, plan.stride);
985 surface.clear();
986 surface.extend_from_slice(plaintext);
987 field_monolith_xor_masked_bytes_in_place(&mut surface, &mask_lanes);
988 field_monolith_apply_mutation_in_place(
989 &mut surface,
990 plan.mutation,
991 plan.stride,
992 &mut mutation_scratch,
993 );
994 field_monolith_apply_virtual_permutation(&mut surface, &state, plan, cell_index);
995 let summary = field_monolith_apply_virtual_mix_with_summary(
996 &mut surface,
997 &state,
998 plan,
999 cell_index,
1000 descriptor,
1001 );
1002
1003 payload.extend_from_slice(&surface);
1004 field_monolith_update_state_with_summary(
1005 &mut state, descriptor, summary, cell_index, cell_len,
1006 );
1007 cursor = next_cursor;
1008 cell_index = cell_index.wrapping_add(1);
1009 }
1010
1011 block_index = block_index.wrapping_add(1);
1012 }
1013
1014 payload
1015}
1016
1017fn field_monolith_decode_payload(
1018 payload: &[u8],
1019 shell_key: &Key,
1020 nonce: &[u8; SHELL_NONCE_SIZE],
1021 target_cell_size: usize,
1022) -> Result<Vec<u8>> {
1023 let geometry = validate_current_field_monolith_geometry(
1024 payload.len(),
1025 target_cell_size,
1026 FIELD_MONOLITH_MAX_CELL_BYTES,
1027 "field monolith",
1028 )?;
1029 let state = field_monolith_initial_state(shell_key, nonce, geometry.original_len)?;
1030 field_monolith_decode_payload_with_state_and_geometry(
1031 payload,
1032 target_cell_size,
1033 state,
1034 geometry,
1035 )
1036}
1037
1038fn field_monolith_decode_payload_with_state_and_geometry(
1039 payload: &[u8],
1040 target_cell_size: usize,
1041 mut state: FieldMonolithState,
1042 geometry: FieldMonolithGeometry,
1043) -> Result<Vec<u8>> {
1044 let target_cell_size = target_cell_size.max(1);
1045 let mut output = Vec::with_capacity(geometry.original_len);
1046 let mut cursor = 0usize;
1047 let mut cell_index = 0usize;
1048 let mut mutation_scratch = Vec::with_capacity(target_cell_size);
1049
1050 while cell_index < geometry.chunk_count {
1051 if cursor + 1 > payload.len() {
1052 return Err(Error::TruncatedPayload {
1053 expected: cursor + 1,
1054 actual: payload.len(),
1055 });
1056 }
1057 let anchor = payload[cursor];
1058 cursor += 1;
1059
1060 for ordinal_in_block in 0..geometry.block_cells {
1061 if cell_index >= geometry.chunk_count {
1062 break;
1063 }
1064 let original_len = if cell_index + 1 < geometry.chunk_count {
1065 target_cell_size
1066 } else {
1067 let consumed_before_last = target_cell_size
1068 .checked_mul(geometry.chunk_count.saturating_sub(1))
1069 .ok_or_else(|| {
1070 Error::InvalidConfiguration(
1071 "field monolith current consumed length overflowed".to_string(),
1072 )
1073 })?;
1074 geometry
1075 .original_len
1076 .checked_sub(consumed_before_last)
1077 .ok_or_else(|| {
1078 Error::InvalidConfiguration(
1079 "field monolith current last cell underflowed".to_string(),
1080 )
1081 })?
1082 };
1083 let next_cursor = cursor.checked_add(original_len).ok_or_else(|| {
1084 Error::InvalidConfiguration("field monolith current cursor overflowed".to_string())
1085 })?;
1086 if next_cursor > payload.len() {
1087 return Err(Error::TruncatedPayload {
1088 expected: next_cursor,
1089 actual: payload.len(),
1090 });
1091 }
1092 let surface = &payload[cursor..next_cursor];
1093 let plan = field_monolith_plan_for_anchor_cell(
1094 anchor,
1095 cell_index as u32,
1096 original_len,
1097 ordinal_in_block,
1098 );
1099 let descriptor = field_monolith_descriptor(plan);
1100 let summary = field_monolith_decode_surface_with_summary_append(
1101 surface,
1102 &state,
1103 plan,
1104 cell_index as u32,
1105 descriptor,
1106 &mut output,
1107 &mut mutation_scratch,
1108 );
1109 field_monolith_update_state_with_summary(
1110 &mut state,
1111 descriptor,
1112 summary,
1113 cell_index as u32,
1114 original_len,
1115 );
1116 cursor = next_cursor;
1117 cell_index += 1;
1118 }
1119 }
1120
1121 if cursor != payload.len() {
1122 return Err(Error::InvalidConfiguration(
1123 "payload cursor did not end on field monolith current block boundary".to_string(),
1124 ));
1125 }
1126
1127 Ok(output)
1128}
1129
1130#[cfg(feature = "compression")]
1131#[derive(Debug, Clone, Copy)]
1132struct ProtectedMonolithPlan {
1133 preset: FusedPreset,
1134 compression_algorithm: CompressionAlgorithm,
1135 encryption_algorithm: EncryptionAlgorithm,
1136 original_len: usize,
1137 compressed_len: usize,
1138 encrypted_len: usize,
1139 target_cell_size: usize,
1140}
1141
1142#[cfg(feature = "compression")]
1143fn protected_monolith_encode_payload(
1144 encrypted_bytes: &[u8],
1145 shell_key: &Key,
1146 nonce: &[u8; SHELL_NONCE_SIZE],
1147 requested_chunk_size: Option<usize>,
1148 original_len: usize,
1149 compressed_len: usize,
1150 compression_algorithm: CompressionAlgorithm,
1151 encryption_algorithm: EncryptionAlgorithm,
1152 preset: FusedPreset,
1153) -> Result<(Vec<u8>, usize)> {
1154 let target_cell_size =
1155 resolve_protected_monolith_cell_size(requested_chunk_size, encrypted_bytes.len())?;
1156 let plan = ProtectedMonolithPlan {
1157 preset,
1158 compression_algorithm,
1159 encryption_algorithm,
1160 original_len,
1161 compressed_len,
1162 encrypted_len: encrypted_bytes.len(),
1163 target_cell_size,
1164 };
1165 let state = protected_monolith_initial_state(shell_key, nonce, plan)?;
1166 let rng = field_monolith_plan_rng(shell_key, nonce, encrypted_bytes.len())?;
1167 let payload =
1168 field_monolith_encode_payload_with_state(encrypted_bytes, target_cell_size, state, rng);
1169
1170 Ok((payload, target_cell_size))
1171}
1172
1173#[cfg(feature = "compression")]
1174fn protected_monolith_decode_payload(
1175 payload: &[u8],
1176 shell_key: &Key,
1177 nonce: &[u8; SHELL_NONCE_SIZE],
1178 target_cell_size: usize,
1179 original_len: usize,
1180 compressed_len: usize,
1181 compression_algorithm: CompressionAlgorithm,
1182 encryption_algorithm: EncryptionAlgorithm,
1183 preset: FusedPreset,
1184) -> Result<Vec<u8>> {
1185 let geometry = validate_current_field_monolith_geometry(
1186 payload.len(),
1187 target_cell_size,
1188 PROTECTED_MONOLITH_MAX_CELL_BYTES,
1189 "protected monolith",
1190 )?;
1191 let plan = ProtectedMonolithPlan {
1192 preset,
1193 compression_algorithm,
1194 encryption_algorithm,
1195 original_len,
1196 compressed_len,
1197 encrypted_len: geometry.original_len,
1198 target_cell_size,
1199 };
1200 let state = protected_monolith_initial_state(shell_key, nonce, plan)?;
1201 let decoded = field_monolith_decode_payload_with_state_and_geometry(
1202 payload,
1203 target_cell_size,
1204 state,
1205 geometry,
1206 )?;
1207 if decoded.len() != geometry.original_len {
1208 return Err(Error::SizeMismatch {
1209 expected: geometry.original_len,
1210 actual: decoded.len(),
1211 });
1212 }
1213 Ok(decoded)
1214}
1215
1216#[cfg(feature = "compression")]
1217fn resolve_protected_monolith_cell_size(
1218 requested: Option<usize>,
1219 encrypted_len: usize,
1220) -> Result<usize> {
1221 if requested.is_some() {
1222 return resolve_field_monolith_current_cell_size(requested, encrypted_len);
1223 }
1224
1225 let target = if encrypted_len <= FIELD_MONOLITH_CURRENT_AUTO_TINY_MAX {
1226 FIELD_MONOLITH_AUTO_TINY_CELL_BYTES
1227 } else if encrypted_len <= FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_MAX {
1228 FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_CELL_BYTES
1229 } else {
1230 PROTECTED_MONOLITH_MAX_CELL_BYTES
1231 };
1232
1233 Ok(if encrypted_len == 0 {
1234 target
1235 } else {
1236 target.min(encrypted_len)
1237 })
1238}
1239
1240#[cfg(feature = "compression")]
1241fn protected_monolith_ratio_bucket(numerator: usize, denominator: usize) -> u8 {
1242 if denominator == 0 {
1243 return 0;
1244 }
1245 numerator
1246 .saturating_mul(u8::MAX as usize)
1247 .checked_div(denominator)
1248 .unwrap_or(0)
1249 .min(u8::MAX as usize) as u8
1250}
1251
1252#[cfg(feature = "compression")]
1253fn protected_monolith_initial_state(
1254 shell_key: &Key,
1255 nonce: &[u8; SHELL_NONCE_SIZE],
1256 plan: ProtectedMonolithPlan,
1257) -> Result<FieldMonolithState> {
1258 let mut state = field_monolith_initial_state(shell_key, nonce, plan.encrypted_len)?;
1259 let seed = derive_chunk_seed(shell_key, nonce, 0, PROTECTED_MONOLITH_STATE_PURPOSE, 32)?;
1260 let target_cell_size = (plan.target_cell_size as u16).to_le_bytes();
1261 let compressed_ratio = protected_monolith_ratio_bucket(plan.compressed_len, plan.original_len);
1262 let encrypted_overhead_ratio = protected_monolith_ratio_bucket(
1263 plan.encrypted_len.saturating_sub(plan.compressed_len),
1264 plan.compressed_len.max(1),
1265 );
1266
1267 state.phase = (state.phase ^ seed[0] ^ (plan.preset as u8).rotate_left(1)) | 1;
1268 state.tension ^= seed[1].rotate_right(1) ^ plan.compression_algorithm as u8;
1269 state.curvature ^= seed[2].rotate_left(2) ^ plan.encryption_algorithm as u8;
1270 state.drift ^= seed[3].rotate_right(2) ^ target_cell_size[0];
1271 state.echo ^= seed[4].rotate_left(3) ^ compressed_ratio;
1272 state.reserve ^= seed[5].rotate_right(3) ^ encrypted_overhead_ratio;
1273 state.stride = (state.stride ^ ((seed[6] ^ target_cell_size[0]) & 0x0F)).max(1);
1274 state.parity ^= seed[7] ^ target_cell_size[1];
1275
1276 Ok(state)
1277}
1278
1279#[cfg(feature = "compression")]
1280#[derive(Debug, Clone, Copy)]
1281struct WholeMonolithV0Header {
1282 preset: FusedPreset,
1283 compression_algorithm: CompressionAlgorithm,
1284 encryption_algorithm: EncryptionAlgorithm,
1285 shell_cell_size: usize,
1286 original_size: usize,
1287 compressed_size: usize,
1288 shell_nonce: [u8; SHELL_NONCE_SIZE],
1289}
1290
1291#[cfg(feature = "compression")]
1292impl WholeMonolithV0Header {
1293 fn to_bytes(self) -> [u8; WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN] {
1294 let mut bytes = [0u8; WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN];
1295 bytes[0] = PROTECTED_ARTIFACT_VERSION;
1296 bytes[1] = self.preset as u8;
1297 bytes[2] = self.compression_algorithm as u8;
1298 bytes[3] = self.encryption_algorithm as u8;
1299 bytes[4..6].copy_from_slice(&(self.shell_cell_size as u16).to_be_bytes());
1300 bytes[6..14].copy_from_slice(&(self.original_size as u64).to_be_bytes());
1301 bytes[14..22].copy_from_slice(&(self.compressed_size as u64).to_be_bytes());
1302 bytes[22..34].copy_from_slice(&self.shell_nonce);
1303 bytes
1304 }
1305
1306 fn from_bytes(bytes: &[u8]) -> Result<Self> {
1307 if bytes.len() != WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN {
1308 return Err(Error::TruncatedPayload {
1309 expected: WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN,
1310 actual: bytes.len(),
1311 });
1312 }
1313
1314 if bytes[0] != PROTECTED_ARTIFACT_VERSION {
1315 return Err(Error::UnsupportedVersion(bytes[0]));
1316 }
1317
1318 let preset = FusedPreset::from_byte(bytes[1])?;
1319 let compression_algorithm = CompressionAlgorithm::from_byte(bytes[2])?;
1320 let encryption_algorithm = EncryptionAlgorithm::from_byte(bytes[3])?;
1321 let shell_cell_size = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
1322 let original_size = whole_monolith_v0_read_u64_usize(&bytes[6..14], "original size")?;
1323 let compressed_size = whole_monolith_v0_read_u64_usize(&bytes[14..22], "compressed size")?;
1324 let mut shell_nonce = [0u8; SHELL_NONCE_SIZE];
1325 shell_nonce.copy_from_slice(&bytes[22..34]);
1326
1327 Ok(Self {
1328 preset,
1329 compression_algorithm,
1330 encryption_algorithm,
1331 shell_cell_size,
1332 original_size,
1333 compressed_size,
1334 shell_nonce,
1335 })
1336 }
1337
1338 fn info(self, protected_size: usize) -> Result<ProtectedArtifactInfo> {
1339 let geometry = whole_monolith_v0_geometry(&self, protected_size)?;
1340 let encrypted_size = geometry.original_len;
1341 let shell_chunk_count = geometry.chunk_count;
1342 Ok(ProtectedArtifactInfo {
1343 version: PROTECTED_ARTIFACT_VERSION,
1344 preset: self.preset,
1345 preset_label: self.preset.name().to_string(),
1346 compression_algorithm: self.compression_algorithm,
1347 encryption_algorithm: self.encryption_algorithm,
1348 original_size: self.original_size,
1349 compressed_size: self.compressed_size,
1350 encrypted_size,
1351 protected_size,
1352 shell_chunk_size: self.shell_cell_size as u32,
1353 shell_chunk_count,
1354 shell_nonce: self.shell_nonce,
1355 })
1356 }
1357}
1358
1359#[cfg(feature = "compression")]
1360fn whole_monolith_v0_geometry(
1361 header: &WholeMonolithV0Header,
1362 protected_size: usize,
1363) -> Result<FieldMonolithGeometry> {
1364 let payload_len = protected_size
1365 .saturating_sub(WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES)
1366 .saturating_sub(WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN)
1367 .saturating_sub(SHELL_TAG_SIZE);
1368 validate_current_field_monolith_geometry(
1369 payload_len,
1370 header.shell_cell_size,
1371 PROTECTED_MONOLITH_MAX_CELL_BYTES,
1372 "whole monolith",
1373 )
1374}
1375
1376#[cfg(feature = "compression")]
1377fn artifact_starts_with_protected_magic(artifact: &[u8]) -> bool {
1378 artifact.len() >= PROTECTED_ARTIFACT_MAGIC.len()
1379 && artifact[..PROTECTED_ARTIFACT_MAGIC.len()] == PROTECTED_ARTIFACT_MAGIC
1380}
1381
1382#[cfg(feature = "compression")]
1383fn whole_monolith_v0_protect(
1384 plaintext: &[u8],
1385 master_key: &Key,
1386 opts: ProtectOptions,
1387) -> Result<ProtectResult> {
1388 let shell_nonce = opts.shell_nonce.unwrap_or_else(random_nonce);
1389 let public_seed = [shell_nonce[0], shell_nonce[1]];
1390 let (shell_key, tag_key) = derive_shell_keys(master_key, &shell_nonce)?;
1391 let cloak = whole_monolith_v0_initial_material(master_key, &shell_nonce)?;
1392 let encryption_algorithm = opts
1393 .encryption_algorithm
1394 .unwrap_or(EncryptionAlgorithm::XChaCha20Poly1305);
1395 if opts.shell_chunk_size.unwrap_or(0) > MAX_CHUNK_SIZE {
1396 return Err(Error::InvalidConfiguration(format!(
1397 "shell chunk size must be at most {MAX_CHUNK_SIZE} bytes"
1398 )));
1399 }
1400
1401 let compressed = compression::compress(
1402 plaintext,
1403 Some(CompressionOptions {
1404 algorithm: opts.compression_algorithm,
1405 min_size_threshold: opts.compression_min_size_threshold,
1406 level: opts.compression_level,
1407 }),
1408 )?;
1409 let encrypted = crate::encryption::encrypt(
1410 &compressed.compressed,
1411 master_key,
1412 Some(crate::encryption::EncryptOptions {
1413 algorithm: Some(encryption_algorithm),
1414 aad: None,
1415 }),
1416 )?;
1417 let encrypted_bytes = encrypted.to_bytes();
1418 let (mut body, shell_cell_size) = protected_monolith_encode_payload(
1419 &encrypted_bytes,
1420 &shell_key,
1421 &shell_nonce,
1422 opts.shell_chunk_size,
1423 plaintext.len(),
1424 compressed.compressed.len(),
1425 compressed.algorithm,
1426 encrypted.algorithm,
1427 opts.preset,
1428 )?;
1429
1430 let header = WholeMonolithV0Header {
1431 preset: opts.preset,
1432 compression_algorithm: compressed.algorithm,
1433 encryption_algorithm: encrypted.algorithm,
1434 shell_cell_size,
1435 original_size: plaintext.len(),
1436 compressed_size: compressed.compressed.len(),
1437 shell_nonce,
1438 };
1439 let header_bytes = header.to_bytes();
1440 let tag = compute_shell_tag_parts(&[&header_bytes[..], body.as_slice()], &tag_key)?;
1441
1442 body.extend_from_slice(&tag);
1443 whole_monolith_v0_apply_cloak(&mut body, &cloak);
1444
1445 let mut masked_header = header_bytes;
1446 whole_monolith_v0_mask_public_header(&public_seed, &mut masked_header);
1447
1448 let mut artifact =
1449 Vec::with_capacity(WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES + masked_header.len() + body.len());
1450 artifact.extend_from_slice(&public_seed);
1451 artifact.extend_from_slice(&masked_header);
1452 artifact.extend_from_slice(&body);
1453
1454 let info = header.info(artifact.len())?;
1455 Ok(ProtectResult { artifact, info })
1456}
1457
1458#[cfg(feature = "compression")]
1459fn whole_monolith_v0_open(artifact: &[u8], master_key: &Key) -> Result<Vec<u8>> {
1460 let (header, header_bytes, body_and_tag) = whole_monolith_v0_split_artifact(artifact)?;
1461 let (shell_key, tag_key) = derive_shell_keys(master_key, &header.shell_nonce)?;
1462 let cloak = whole_monolith_v0_initial_material(master_key, &header.shell_nonce)?;
1463 let mut uncloaked = body_and_tag.to_vec();
1464 whole_monolith_v0_apply_cloak(&mut uncloaked, &cloak);
1465
1466 if uncloaked.len() < SHELL_TAG_SIZE {
1467 return Err(Error::TruncatedPayload {
1468 expected: SHELL_TAG_SIZE,
1469 actual: uncloaked.len(),
1470 });
1471 }
1472 let tag_start = uncloaked.len() - SHELL_TAG_SIZE;
1473 let (body, tag) = uncloaked.split_at(tag_start);
1474 let expected_tag = compute_shell_tag_parts(&[&header_bytes[..], body], &tag_key)?;
1475 if !compare_hashes(&expected_tag, tag) {
1476 return Err(Error::AuthenticationFailed);
1477 }
1478
1479 let encrypted_bytes = protected_monolith_decode_payload(
1480 body,
1481 &shell_key,
1482 &header.shell_nonce,
1483 header.shell_cell_size.max(1),
1484 header.original_size,
1485 header.compressed_size,
1486 header.compression_algorithm,
1487 header.encryption_algorithm,
1488 header.preset,
1489 )?;
1490 let encrypted = EncryptionResult::from_bytes(&encrypted_bytes)?;
1491 if encrypted.algorithm != header.encryption_algorithm {
1492 return Err(Error::InvalidFormat);
1493 }
1494 let compressed = crate::encryption::decrypt(&encrypted, master_key)?;
1495 if compressed.len() != header.compressed_size {
1496 return Err(Error::SizeMismatch {
1497 expected: header.compressed_size,
1498 actual: compressed.len(),
1499 });
1500 }
1501 let output = compression::decompress_exact(
1502 &compressed,
1503 header.compression_algorithm,
1504 header.original_size,
1505 )?;
1506
1507 Ok(output)
1508}
1509
1510#[cfg(feature = "compression")]
1511fn whole_monolith_v0_inspect_artifact(artifact: &[u8]) -> Result<ProtectedArtifactInfo> {
1512 let (header, _, _) = whole_monolith_v0_split_artifact(artifact)?;
1513 header.info(artifact.len())
1514}
1515
1516#[cfg(feature = "compression")]
1517fn whole_monolith_v0_split_artifact(
1518 artifact: &[u8],
1519) -> Result<(
1520 WholeMonolithV0Header,
1521 [u8; WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN],
1522 &[u8],
1523)> {
1524 if artifact.len() < WHOLE_MONOLITH_V0_MIN_ARTIFACT_LEN {
1525 return Err(Error::TruncatedPayload {
1526 expected: WHOLE_MONOLITH_V0_MIN_ARTIFACT_LEN,
1527 actual: artifact.len(),
1528 });
1529 }
1530
1531 let mut public_seed = [0u8; WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES];
1532 public_seed.copy_from_slice(&artifact[..WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES]);
1533 let header_start = WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES;
1534 let header_end = header_start + WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN;
1535 let mut header_bytes = [0u8; WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN];
1536 header_bytes.copy_from_slice(&artifact[header_start..header_end]);
1537 whole_monolith_v0_mask_public_header(&public_seed, &mut header_bytes);
1538 let header = WholeMonolithV0Header::from_bytes(&header_bytes)?;
1539 whole_monolith_v0_geometry(&header, artifact.len())?;
1542
1543 Ok((header, header_bytes, &artifact[header_end..]))
1544}
1545
1546#[cfg(feature = "compression")]
1547fn whole_monolith_v0_read_u64_usize(bytes: &[u8], label: &str) -> Result<usize> {
1548 let value = u64::from_be_bytes([
1549 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
1550 ]);
1551 if value > usize::MAX as u64 {
1552 return Err(Error::InvalidConfiguration(format!(
1553 "whole monolith {label} exceeds usize range"
1554 )));
1555 }
1556 Ok(value as usize)
1557}
1558
1559#[cfg(feature = "compression")]
1560fn whole_monolith_v0_mask_public_header(
1561 seed: &[u8; WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES],
1562 header: &mut [u8],
1563) {
1564 for (index, byte) in header.iter_mut().enumerate() {
1565 *byte ^= whole_monolith_v0_public_mask_byte(seed, index);
1566 }
1567}
1568
1569#[cfg(feature = "compression")]
1570fn whole_monolith_v0_public_mask_byte(
1571 seed: &[u8; WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES],
1572 index: usize,
1573) -> u8 {
1574 let mut value = 0x9E37_79B9_7F4A_7C15u64
1575 ^ u16::from_le_bytes(*seed) as u64
1576 ^ ((index as u64) << 17)
1577 ^ ((index as u64).rotate_left(23));
1578 value ^= value >> 30;
1579 value = value.wrapping_mul(0xBF58_476D_1CE4_E5B9);
1580 value ^= value >> 27;
1581 value = value.wrapping_mul(0x94D0_49BB_1331_11EB);
1582 (value ^ (value >> 31)) as u8
1583}
1584
1585#[cfg(feature = "compression")]
1586fn whole_monolith_v0_initial_material(
1587 master_key: &Key,
1588 artifact_nonce: &[u8; SHELL_NONCE_SIZE],
1589) -> Result<[u8; 32]> {
1590 let bytes = derive_domain_bytes(
1591 master_key,
1592 Some(artifact_nonce),
1593 WHOLE_MONOLITH_V0_MATERIAL_DOMAIN,
1594 64,
1595 )?;
1596 let mut cloak = [0u8; 32];
1597 cloak.copy_from_slice(&bytes[32..]);
1598 Ok(cloak)
1599}
1600
1601#[cfg(feature = "compression")]
1602fn whole_monolith_v0_apply_cloak(bytes: &mut [u8], cloak: &[u8; 32]) {
1603 let mut lanes = [0u64; 4];
1604 for (lane, chunk) in lanes.iter_mut().zip(cloak.chunks_exact(8)) {
1605 let mut word = [0u8; 8];
1606 word.copy_from_slice(chunk);
1607 *lane = u64::from_le_bytes(word);
1608 }
1609 if lanes.iter().all(|&lane| lane == 0) {
1610 lanes = [
1611 0x6A09_E667_F3BC_C909,
1612 0xBB67_AE85_84CA_A73B,
1613 0x3C6E_F372_FE94_F82B,
1614 0xA54F_F53A_5F1D_36F1,
1615 ];
1616 }
1617 let mut mask_word = 0u64;
1618 let mut mask_left = 0usize;
1619 let cloak_len = bytes.len().min(WHOLE_MONOLITH_V0_CLOAK_HEAD_BYTES);
1620 for byte in &mut bytes[..cloak_len] {
1621 if mask_left == 0 {
1622 mask_word = whole_monolith_v0_cloak_next(&mut lanes);
1623 mask_left = 8;
1624 }
1625 *byte ^= mask_word as u8;
1626 mask_word >>= 8;
1627 mask_left -= 1;
1628 }
1629}
1630
1631#[cfg(feature = "compression")]
1632fn whole_monolith_v0_cloak_next(state: &mut [u64; 4]) -> u64 {
1633 let result = state[0].wrapping_add(state[3]);
1634 let t = state[1] << 17;
1635 state[2] ^= state[0];
1636 state[3] ^= state[1];
1637 state[1] ^= state[2];
1638 state[0] ^= state[3];
1639 state[2] ^= t;
1640 state[3] = state[3].rotate_left(45);
1641 result
1642}
1643
1644fn resolve_field_monolith_current_cell_size(
1645 requested: Option<usize>,
1646 payload_len: usize,
1647) -> Result<usize> {
1648 let requested = requested.unwrap_or(FIELD_MONOLITH_AUTO_CHUNK_SIZE);
1649 let target = if requested == FIELD_MONOLITH_AUTO_CHUNK_SIZE {
1650 field_monolith_current_auto_target_cell_size(payload_len)
1651 } else {
1652 if requested > MAX_CHUNK_SIZE {
1653 return Err(Error::InvalidConfiguration(format!(
1654 "shell chunk size must be at most {MAX_CHUNK_SIZE} bytes"
1655 )));
1656 }
1657 let min_cell_size = if requested < FIELD_MONOLITH_MIN_CELL_BYTES {
1658 FIELD_MONOLITH_EXPERIMENTAL_MIN_CELL_BYTES
1659 } else {
1660 FIELD_MONOLITH_MIN_CELL_BYTES
1661 };
1662 requested.clamp(min_cell_size, FIELD_MONOLITH_MAX_CELL_BYTES)
1663 };
1664
1665 Ok(if payload_len == 0 {
1666 target
1667 } else {
1668 target.min(payload_len)
1669 })
1670}
1671
1672fn field_monolith_current_auto_target_cell_size(payload_len: usize) -> usize {
1673 if payload_len <= FIELD_MONOLITH_CURRENT_AUTO_TINY_MAX {
1674 FIELD_MONOLITH_AUTO_TINY_CELL_BYTES
1675 } else if payload_len <= FIELD_MONOLITH_CURRENT_AUTO_DENSE_SMALL_MAX {
1676 FIELD_MONOLITH_CURRENT_AUTO_DENSE_SMALL_CELL_BYTES
1677 } else if payload_len <= FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_MAX {
1678 FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_CELL_BYTES
1679 } else if payload_len <= FIELD_MONOLITH_AUTO_SMALL_MAX {
1680 FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_UPPER_SMALL_CELL_BYTES
1681 } else if payload_len <= FIELD_MONOLITH_AUTO_MID_MAX {
1682 FIELD_MONOLITH_AUTO_MID_CELL_BYTES
1683 } else {
1684 FIELD_MONOLITH_MAX_CELL_BYTES
1685 }
1686}
1687
1688fn field_monolith_infer_geometry(
1689 payload_len: usize,
1690 target_cell_size: usize,
1691) -> Option<FieldMonolithGeometry> {
1692 if target_cell_size == 0 {
1693 return None;
1694 }
1695 if payload_len == 0 {
1696 return Some(FieldMonolithGeometry {
1697 original_len: 0,
1698 chunk_count: 0,
1699 block_cells: field_monolith_current_anchor_block_cells(target_cell_size, 0),
1700 });
1701 }
1702
1703 [
1710 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE,
1711 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT,
1712 ]
1713 .into_iter()
1714 .find_map(|block_cells| {
1715 field_monolith_geometry_for_block_cells(payload_len, target_cell_size, block_cells)
1716 })
1717}
1718
1719fn field_monolith_geometry_for_block_cells(
1720 payload_len: usize,
1721 target_cell_size: usize,
1722 block_cells: usize,
1723) -> Option<FieldMonolithGeometry> {
1724 let encoded_block_span = target_cell_size.checked_mul(block_cells)?.checked_add(1)?;
1725 let block_count = payload_len.div_ceil(encoded_block_span);
1726 let original_len = payload_len.checked_sub(block_count)?;
1727 if original_len == 0 {
1728 return None;
1729 }
1730
1731 let chunk_count = original_len.div_ceil(target_cell_size);
1732 if field_monolith_current_anchor_block_cells(target_cell_size, chunk_count) != block_cells
1733 || field_monolith_block_count_for_cells(chunk_count, block_cells) != block_count
1734 {
1735 return None;
1736 }
1737
1738 Some(FieldMonolithGeometry {
1739 original_len,
1740 chunk_count,
1741 block_cells,
1742 })
1743}
1744
1745fn validate_current_field_monolith_geometry(
1746 payload_len: usize,
1747 target_cell_size: usize,
1748 max_cell_size: usize,
1749 label: &str,
1750) -> Result<FieldMonolithGeometry> {
1751 let geometry =
1752 field_monolith_infer_geometry(payload_len, target_cell_size).ok_or_else(|| {
1753 Error::InvalidConfiguration(format!("{label} payload geometry is invalid"))
1754 })?;
1755
1756 let canonical = if geometry.original_len == 0 {
1762 (FIELD_MONOLITH_EXPERIMENTAL_MIN_CELL_BYTES..=max_cell_size).contains(&target_cell_size)
1763 } else if geometry.original_len < FIELD_MONOLITH_EXPERIMENTAL_MIN_CELL_BYTES {
1764 target_cell_size == geometry.original_len
1765 } else {
1766 (FIELD_MONOLITH_EXPERIMENTAL_MIN_CELL_BYTES..=max_cell_size).contains(&target_cell_size)
1767 && target_cell_size <= geometry.original_len
1768 };
1769 if !canonical {
1770 return Err(Error::InvalidConfiguration(format!(
1771 "{label} cell size {target_cell_size} is noncanonical for {} decoded bytes",
1772 geometry.original_len
1773 )));
1774 }
1775
1776 Ok(geometry)
1777}
1778
1779fn validate_legacy_shell_chunk_size(chunk_size: usize, label: &str) -> Result<()> {
1780 if !(1..=MAX_CHUNK_SIZE).contains(&chunk_size) {
1781 return Err(Error::InvalidConfiguration(format!(
1782 "{label} chunk size must be between 1 and {MAX_CHUNK_SIZE} bytes"
1783 )));
1784 }
1785 Ok(())
1786}
1787
1788fn field_monolith_block_count_for_cells(chunk_count: usize, block_cells: usize) -> usize {
1789 if chunk_count == 0 {
1790 0
1791 } else {
1792 1 + (chunk_count - 1) / block_cells.max(1)
1793 }
1794}
1795
1796fn field_monolith_anchor_block_cells(target_cell_size: usize) -> usize {
1797 if target_cell_size.max(1) <= FIELD_MONOLITH_DENSE_ANCHOR_MAX_CELL_BYTES {
1798 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE
1799 } else {
1800 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT
1801 }
1802}
1803
1804fn field_monolith_current_anchor_block_cells(target_cell_size: usize, chunk_count: usize) -> usize {
1805 if target_cell_size == FIELD_MONOLITH_AUTO_MID_CELL_BYTES
1806 && chunk_count > FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT
1807 {
1808 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT
1809 } else {
1810 field_monolith_anchor_block_cells(target_cell_size)
1811 }
1812}
1813
1814fn field_monolith_initial_state(
1815 shell_key: &Key,
1816 nonce: &[u8; SHELL_NONCE_SIZE],
1817 total_len: usize,
1818) -> Result<FieldMonolithState> {
1819 let seed = derive_chunk_seed(shell_key, nonce, 0, FIELD_MONOLITH_STATE_PURPOSE, 32)?;
1820 let len_mix = (total_len as u64).to_le_bytes();
1821 Ok(FieldMonolithState {
1822 phase: (seed[0] ^ seed[8].rotate_left(1) ^ len_mix[0]) | 1,
1823 tension: seed[1] ^ seed[9].rotate_right(1) ^ len_mix[1],
1824 curvature: seed[2] ^ seed[10].rotate_left(2) ^ len_mix[2],
1825 drift: seed[3] ^ seed[11].rotate_right(2) ^ len_mix[3],
1826 echo: seed[4] ^ seed[12].rotate_left(3) ^ len_mix[4],
1827 reserve: seed[5] ^ seed[13].rotate_right(3) ^ len_mix[5],
1828 stride: ((seed[6] ^ seed[14] ^ len_mix[6]) & 0x0F).max(1),
1829 parity: seed[7] ^ seed[15] ^ len_mix[7],
1830 })
1831}
1832
1833fn field_monolith_plan_rng(
1834 shell_key: &Key,
1835 nonce: &[u8; SHELL_NONCE_SIZE],
1836 total_len: usize,
1837) -> Result<DeterministicRng> {
1838 let seed = derive_chunk_seed(shell_key, nonce, 0, FIELD_MONOLITH_PLAN_PURPOSE, 32)?;
1839 let mut seed_bytes = [0u8; 32];
1840 seed_bytes.copy_from_slice(&seed[..32]);
1841 for (index, byte) in (total_len as u64).to_le_bytes().iter().enumerate() {
1842 seed_bytes[24 + index] ^= *byte;
1843 }
1844 Ok(DeterministicRng::from_seed(&seed_bytes))
1845}
1846
1847fn field_monolith_cell_band(cell_len: usize) -> usize {
1848 match cell_len {
1849 0..=64 => 0,
1850 65..=96 => 1,
1851 97..=160 => 2,
1852 _ => 3,
1853 }
1854}
1855
1856fn field_monolith_anchor_stride_limit(cell_len: usize) -> usize {
1857 match field_monolith_cell_band(cell_len) {
1858 0 => 2,
1859 1 => 3,
1860 _ => 4,
1861 }
1862}
1863
1864fn field_monolith_anchor_intensity_limit(cell_len: usize) -> usize {
1865 match field_monolith_cell_band(cell_len) {
1866 0 => 2,
1867 1 => 3,
1868 2 => 4,
1869 _ => 4,
1870 }
1871}
1872
1873fn field_monolith_anchor_diffused_pad_len(
1874 anchor: u8,
1875 cell_index: u32,
1876 cell_len: usize,
1877 ordinal_in_block: usize,
1878) -> usize {
1879 let regime = (anchor & 0x03) as usize;
1880 let cadence = ((anchor >> 2) & 0x03) as usize;
1881 let intensity = ((anchor >> 4) & 0x03) as usize;
1882 let stride_seed = ((anchor >> 6) & 0x03) as usize;
1883 let band = field_monolith_cell_band(cell_len);
1884 let block_cells = field_monolith_anchor_block_cells(cell_len);
1885 let block_phase = (cell_index as usize / block_cells) + band;
1886 let selector = regime * 3 + cadence * 5 + intensity * 7 + stride_seed * 11 + block_phase;
1887 let phase_ordinal = ordinal_in_block % block_cells;
1888
1889 match field_monolith_anchor_intensity_limit(cell_len) {
1890 0 | 1 => 0,
1891 2 => {
1892 if block_cells > FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT {
1893 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE]; 4] = [
1894 [0, 1, 0, 1, 1, 0, 1, 0],
1895 [1, 0, 1, 0, 0, 1, 0, 1],
1896 [0, 1, 1, 0, 1, 0, 0, 1],
1897 [1, 0, 0, 1, 0, 1, 1, 0],
1898 ];
1899 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1900 } else {
1901 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT]; 2] =
1902 [[0, 1, 0, 1], [1, 0, 1, 0]];
1903 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1904 }
1905 }
1906 3 => {
1907 if block_cells > FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT {
1908 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE]; 4] = [
1909 [0, 1, 2, 1, 2, 1, 0, 1],
1910 [1, 2, 1, 0, 1, 0, 1, 2],
1911 [2, 1, 0, 1, 0, 1, 2, 1],
1912 [1, 0, 1, 2, 1, 2, 1, 0],
1913 ];
1914 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1915 } else {
1916 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT]; 4] =
1917 [[0, 1, 2, 1], [1, 2, 1, 0], [2, 1, 0, 1], [1, 0, 1, 2]];
1918 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1919 }
1920 }
1921 _ => {
1922 if block_cells > FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT {
1923 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE]; 8] = [
1924 [0, 1, 2, 3, 3, 2, 1, 0],
1925 [1, 2, 3, 0, 0, 3, 2, 1],
1926 [2, 3, 0, 1, 1, 0, 3, 2],
1927 [3, 0, 1, 2, 2, 1, 0, 3],
1928 [3, 2, 1, 0, 0, 1, 2, 3],
1929 [2, 1, 0, 3, 3, 0, 1, 2],
1930 [1, 0, 3, 2, 2, 3, 0, 1],
1931 [0, 3, 2, 1, 1, 2, 3, 0],
1932 ];
1933 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1934 } else {
1935 const PATTERNS: [[usize; FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_COMPACT]; 8] = [
1936 [0, 1, 2, 3],
1937 [1, 2, 3, 0],
1938 [2, 3, 0, 1],
1939 [3, 0, 1, 2],
1940 [3, 2, 1, 0],
1941 [2, 1, 0, 3],
1942 [1, 0, 3, 2],
1943 [0, 3, 2, 1],
1944 ];
1945 PATTERNS[selector % PATTERNS.len()][phase_ordinal]
1946 }
1947 }
1948 }
1949}
1950
1951fn field_monolith_current_anchor_for_block(
1952 state: &FieldMonolithState,
1953 block_index: u32,
1954 rng: &mut DeterministicRng,
1955 base_cell_size: usize,
1956) -> u8 {
1957 let baseline = base_cell_size >= FIELD_MONOLITH_AUTO_MID_CELL_BYTES;
1958 let block0 = block_index as u8;
1959 let block1 = (block_index >> 8) as u8;
1960 let block2 = (block_index >> 16) as u8;
1961 let block3 = (block_index >> 24) as u8;
1962 let size0 = base_cell_size as u8;
1963 let size1 = (base_cell_size >> 8) as u8;
1964 let regime = (rng.next_u32() as u8 ^ state.phase ^ state.curvature ^ block0 ^ size0) & 0x03;
1965 let mut cadence = (rng.next_u32() as u8 ^ state.echo ^ state.reserve ^ block1 ^ size1) & 0x03;
1966 let mut intensity =
1967 (rng.next_u32() as u8 ^ state.parity ^ state.tension ^ block2 ^ size0.rotate_left(1))
1968 & 0x03;
1969 let mut stride_seed =
1970 (rng.next_u32() as u8 ^ state.stride ^ state.drift ^ block3 ^ size1.rotate_right(1)) & 0x03;
1971
1972 if !baseline {
1973 let counter_orbit = block0
1974 ^ state.phase.rotate_left(1)
1975 ^ state.echo.rotate_right(1)
1976 ^ size0.rotate_left(2)
1977 ^ size1.rotate_right(1);
1978 cadence = (cadence.wrapping_add(counter_orbit) ^ intensity.rotate_left(1)) & 0x03;
1979 intensity = (intensity.wrapping_add(counter_orbit.rotate_right(1))
1980 ^ stride_seed.rotate_left(1))
1981 & 0x03;
1982 stride_seed = (stride_seed.wrapping_add(counter_orbit.rotate_left(1))
1983 ^ regime.rotate_right(1))
1984 & 0x03;
1985 }
1986
1987 regime | (cadence << 2) | (intensity << 4) | (stride_seed << 6)
1988}
1989
1990fn field_monolith_plan_for_anchor_cell(
1991 anchor: u8,
1992 cell_index: u32,
1993 cell_len: usize,
1994 ordinal_in_block: usize,
1995) -> FieldMonolithCellPlan {
1996 let baseline = cell_len >= FIELD_MONOLITH_CURRENT_AUTO_DIFFUSED_SMALL_CELL_BYTES;
1997 let regime = anchor & 0x03;
1998 let cadence = (anchor >> 2) & 0x03;
1999 let intensity = (anchor >> 4) & 0x03;
2000 let stride_seed = (anchor >> 6) & 0x03;
2001 let band = field_monolith_cell_band(cell_len) as u8;
2002 let ordinal = ordinal_in_block as u8;
2003 let cell_phase = (cell_index as u8)
2004 .wrapping_add(ordinal.wrapping_mul(3))
2005 .wrapping_add(band.rotate_left(1));
2006 let lane_orbit = if !baseline && band >= 2 {
2007 intensity.wrapping_add(regime.rotate_left((ordinal_in_block % 2) as u32))
2008 ^ cadence.rotate_right(((cell_index as usize + ordinal_in_block) % 2) as u32)
2009 ^ (cell_index as u8).rotate_left(((ordinal_in_block + band as usize) % 3) as u32)
2010 } else {
2011 0
2012 };
2013 let mutation = FieldMonolithMutationMode::from_bits(
2014 regime.wrapping_add(cell_phase)
2015 ^ cadence.rotate_left((ordinal_in_block % 2) as u32)
2016 ^ lane_orbit,
2017 );
2018 let weave = FieldMonolithWeaveMode::from_bits(
2019 cadence
2020 .wrapping_add(ordinal)
2021 .wrapping_add((cell_index as u8).rotate_left(1))
2022 .wrapping_add(band)
2023 .wrapping_add(lane_orbit.rotate_left(1)),
2024 );
2025 let stride_limit = field_monolith_anchor_stride_limit(cell_len);
2026 let stride = ((stride_seed as usize
2027 + ordinal_in_block
2028 + ((cell_index as usize) & 0x01)
2029 + band as usize
2030 + if !baseline && band >= 2 {
2031 intensity as usize + (((cell_index as usize) >> 1) & 0x01)
2032 } else {
2033 0
2034 })
2035 % stride_limit)
2036 + 1;
2037 let pad_len = if baseline {
2038 let intensity_limit = match field_monolith_cell_band(cell_len) {
2039 0 => 2,
2040 1 | 2 => 3,
2041 _ => 4,
2042 };
2043 (intensity as usize
2044 + cadence as usize
2045 + ordinal_in_block
2046 + (((cell_index as usize) >> 1) & 0x01)
2047 + band as usize)
2048 % intensity_limit
2049 } else {
2050 field_monolith_anchor_diffused_pad_len(anchor, cell_index, cell_len, ordinal_in_block)
2051 };
2052
2053 FieldMonolithCellPlan {
2054 mutation,
2055 weave,
2056 stride,
2057 pad_len,
2058 }
2059}
2060
2061fn field_monolith_descriptor(plan: FieldMonolithCellPlan) -> u8 {
2062 (plan.mutation as u8)
2063 | ((plan.weave as u8) << 2)
2064 | (((plan.stride - 1) as u8) << 4)
2065 | ((plan.pad_len as u8) << 6)
2066}
2067
2068fn field_monolith_surface_checksum_seed(
2069 surface_len: usize,
2070 state: &FieldMonolithState,
2071 cell_index: u32,
2072 descriptor: u8,
2073) -> u8 {
2074 descriptor.wrapping_add(state.phase.rotate_left(1))
2075 ^ state.echo
2076 ^ (cell_index as u8).wrapping_mul(17)
2077 ^ surface_len as u8
2078}
2079
2080fn field_monolith_surface_checksum_step(
2081 acc: u8,
2082 state: &FieldMonolithState,
2083 offset: usize,
2084 byte: u8,
2085) -> u8 {
2086 let mix = byte.wrapping_add((offset as u8).wrapping_mul(19))
2087 ^ state.curvature.rotate_left((offset % 8) as u32);
2088 acc.rotate_left(1) ^ mix
2089}
2090
2091#[derive(Clone, Copy)]
2092struct FieldMonolithMaskLanes {
2093 phase_lanes: [u8; 8],
2094 curvature_lanes: [u8; 8],
2095 reserve_lanes: [u8; 8],
2096 drift_mix: u8,
2097 echo_mix: u8,
2098 tension_base: u8,
2099 parity_base: u8,
2100}
2101
2102fn field_monolith_left_rotation_lanes(byte: u8) -> [u8; 8] {
2103 [
2104 byte,
2105 byte.rotate_left(1),
2106 byte.rotate_left(2),
2107 byte.rotate_left(3),
2108 byte.rotate_left(4),
2109 byte.rotate_left(5),
2110 byte.rotate_left(6),
2111 byte.rotate_left(7),
2112 ]
2113}
2114
2115fn field_monolith_right_rotation_lanes(byte: u8) -> [u8; 8] {
2116 [
2117 byte,
2118 byte.rotate_right(1),
2119 byte.rotate_right(2),
2120 byte.rotate_right(3),
2121 byte.rotate_right(4),
2122 byte.rotate_right(5),
2123 byte.rotate_right(6),
2124 byte.rotate_right(7),
2125 ]
2126}
2127
2128fn field_monolith_mask_lanes(
2129 state: &FieldMonolithState,
2130 cell_index: u32,
2131 stride: usize,
2132) -> FieldMonolithMaskLanes {
2133 let phase_lanes = field_monolith_left_rotation_lanes(state.phase);
2134 let curvature_base = field_monolith_right_rotation_lanes(state.curvature);
2135 let reserve_base = field_monolith_left_rotation_lanes(state.reserve);
2136
2137 let mut curvature_lanes = [0u8; 8];
2138 let curvature_offset = stride & 0x07;
2139 for (lane, value) in curvature_lanes.iter_mut().enumerate() {
2140 *value = curvature_base[(lane + curvature_offset) & 0x07];
2141 }
2142
2143 let mut reserve_lanes = [0u8; 8];
2144 let reserve_offset = (cell_index as usize) & 0x07;
2145 for (lane, value) in reserve_lanes.iter_mut().enumerate() {
2146 *value = reserve_base[(lane + reserve_offset) & 0x07];
2147 }
2148
2149 FieldMonolithMaskLanes {
2150 phase_lanes,
2151 curvature_lanes,
2152 reserve_lanes,
2153 drift_mix: state
2154 .drift
2155 .wrapping_add((cell_index as u8).wrapping_mul(17)),
2156 echo_mix: state.echo.wrapping_add((stride as u8).wrapping_mul(11)),
2157 tension_base: state.tension,
2158 parity_base: state.parity,
2159 }
2160}
2161
2162fn field_monolith_xor_masked_bytes_in_place(bytes: &mut [u8], mask_lanes: &FieldMonolithMaskLanes) {
2163 let mut offset = 0usize;
2164 let mut tension_mix = mask_lanes.tension_base;
2165 let mut parity_mix = mask_lanes.parity_base;
2166 while offset < bytes.len() {
2167 bytes[offset] ^= mask_lanes.phase_lanes[offset & 0x07]
2168 ^ tension_mix
2169 ^ mask_lanes.curvature_lanes[offset & 0x07]
2170 ^ mask_lanes.drift_mix
2171 ^ mask_lanes.echo_mix
2172 ^ mask_lanes.reserve_lanes[offset & 0x07]
2173 ^ parity_mix;
2174 tension_mix = tension_mix.wrapping_add(3);
2175 parity_mix = parity_mix.wrapping_add(7);
2176 offset += 1;
2177 }
2178}
2179
2180fn field_monolith_apply_mutation_in_place(
2181 bytes: &mut Vec<u8>,
2182 mutation: FieldMonolithMutationMode,
2183 stride: usize,
2184 scratch: &mut Vec<u8>,
2185) {
2186 match mutation {
2187 FieldMonolithMutationMode::Direct => {}
2188 FieldMonolithMutationMode::StrideSwap => {
2189 if !bytes.is_empty() {
2190 let shift = stride % bytes.len();
2191 bytes.rotate_left(shift);
2192 }
2193 }
2194 FieldMonolithMutationMode::SplitWeave => {
2195 scratch.clear();
2196 scratch.reserve(bytes.len());
2197 let even_count = bytes.len().div_ceil(2);
2198 let odd_count = bytes.len() / 2;
2199 let mut read_index = 0usize;
2200 while scratch.len() < even_count {
2201 scratch.push(bytes[read_index]);
2202 read_index += 2;
2203 }
2204
2205 let mut odd_index = 0usize;
2206 let mut odd_read_index = 1usize;
2207 while odd_index < odd_count {
2208 scratch.push(bytes[odd_read_index]);
2209 odd_index += 1;
2210 odd_read_index += 2;
2211 }
2212
2213 core::mem::swap(bytes, scratch);
2214 }
2215 FieldMonolithMutationMode::MirrorWeave => {
2216 bytes.reverse();
2217 }
2218 }
2219}
2220
2221fn field_monolith_inverse_mutation_slice(
2222 bytes: &mut [u8],
2223 mutation: FieldMonolithMutationMode,
2224 stride: usize,
2225 scratch: &mut Vec<u8>,
2226) {
2227 match mutation {
2228 FieldMonolithMutationMode::Direct => {}
2229 FieldMonolithMutationMode::StrideSwap => {
2230 if !bytes.is_empty() {
2231 let shift = stride % bytes.len();
2232 bytes.rotate_right(shift);
2233 }
2234 }
2235 FieldMonolithMutationMode::SplitWeave => {
2236 scratch.clear();
2237 scratch.reserve(bytes.len());
2238 let even_count = bytes.len().div_ceil(2);
2239 let (evens, odds) = bytes.split_at(even_count);
2240
2241 let mut even_index = 0usize;
2242 while even_index < evens.len() || even_index < odds.len() {
2243 if even_index < evens.len() {
2244 scratch.push(evens[even_index]);
2245 }
2246 if even_index < odds.len() {
2247 scratch.push(odds[even_index]);
2248 }
2249 even_index += 1;
2250 }
2251
2252 bytes.copy_from_slice(&scratch[..bytes.len()]);
2253 }
2254 FieldMonolithMutationMode::MirrorWeave => {
2255 bytes.reverse();
2256 }
2257 }
2258}
2259
2260fn field_monolith_virtual_shift(
2261 state: &FieldMonolithState,
2262 plan: FieldMonolithCellPlan,
2263 cell_index: u32,
2264 len: usize,
2265) -> usize {
2266 if len <= 1 {
2267 0
2268 } else {
2269 let seed = usize::from(
2270 state.phase
2271 ^ state.echo.rotate_left(1)
2272 ^ state.reserve.rotate_right(1)
2273 ^ (cell_index as u8).wrapping_mul(17),
2274 );
2275 1 + ((seed + plan.stride * 5 + plan.pad_len * 11) % (len - 1))
2276 }
2277}
2278
2279fn field_monolith_virtual_block_len(
2280 state: &FieldMonolithState,
2281 plan: FieldMonolithCellPlan,
2282 cell_index: u32,
2283 len: usize,
2284) -> usize {
2285 let max_block = len.clamp(2, 8);
2286 if max_block <= 2 {
2287 2
2288 } else {
2289 2 + ((usize::from(state.parity ^ state.curvature ^ (cell_index as u8))
2290 + plan.stride
2291 + plan.pad_len)
2292 % (max_block - 1))
2293 }
2294}
2295
2296fn field_monolith_apply_virtual_pair_swaps(bytes: &mut [u8], step: usize, start: usize) {
2297 if bytes.len() < 2 {
2298 return;
2299 }
2300 let mut index = start.min(bytes.len() - 2);
2301 let jump = step.max(2);
2302 while index + 1 < bytes.len() {
2303 bytes.swap(index, index + 1);
2304 index += jump;
2305 }
2306}
2307
2308fn field_monolith_apply_virtual_window_reversal(bytes: &mut [u8], window_len: usize) {
2309 if window_len <= 1 {
2310 return;
2311 }
2312 for chunk in bytes.chunks_mut(window_len) {
2313 chunk.reverse();
2314 }
2315}
2316
2317fn field_monolith_apply_virtual_permutation(
2318 bytes: &mut [u8],
2319 state: &FieldMonolithState,
2320 plan: FieldMonolithCellPlan,
2321 cell_index: u32,
2322) {
2323 if bytes.len() <= 1 {
2324 return;
2325 }
2326 let shift = field_monolith_virtual_shift(state, plan, cell_index, bytes.len());
2327 match plan.weave {
2328 FieldMonolithWeaveMode::LeadPad => bytes.rotate_left(shift),
2329 FieldMonolithWeaveMode::TrailPad => bytes.rotate_right(shift),
2330 FieldMonolithWeaveMode::Alternating => {
2331 field_monolith_apply_virtual_pair_swaps(bytes, plan.pad_len + 2, shift % 2)
2332 }
2333 FieldMonolithWeaveMode::Clustered => field_monolith_apply_virtual_window_reversal(
2334 bytes,
2335 field_monolith_virtual_block_len(state, plan, cell_index, bytes.len()),
2336 ),
2337 }
2338}
2339
2340fn field_monolith_remove_virtual_permutation(
2341 bytes: &mut [u8],
2342 state: &FieldMonolithState,
2343 plan: FieldMonolithCellPlan,
2344 cell_index: u32,
2345) {
2346 if bytes.len() <= 1 {
2347 return;
2348 }
2349 let shift = field_monolith_virtual_shift(state, plan, cell_index, bytes.len());
2350 match plan.weave {
2351 FieldMonolithWeaveMode::LeadPad => bytes.rotate_right(shift),
2352 FieldMonolithWeaveMode::TrailPad => bytes.rotate_left(shift),
2353 FieldMonolithWeaveMode::Alternating => {
2354 field_monolith_apply_virtual_pair_swaps(bytes, plan.pad_len + 2, shift % 2)
2355 }
2356 FieldMonolithWeaveMode::Clustered => field_monolith_apply_virtual_window_reversal(
2357 bytes,
2358 field_monolith_virtual_block_len(state, plan, cell_index, bytes.len()),
2359 ),
2360 }
2361}
2362
2363#[derive(Clone, Copy)]
2364struct FieldMonolithVirtualMixContext {
2365 cell_mix: u8,
2366 weave_mix: u8,
2367 pad_mix: u8,
2368 echo_lanes: [u8; 8],
2369 parity_lanes: [u8; 8],
2370 reserve_stride: u8,
2371}
2372
2373impl FieldMonolithVirtualMixContext {
2374 fn new(state: &FieldMonolithState, plan: FieldMonolithCellPlan, cell_index: u32) -> Self {
2375 Self {
2376 cell_mix: (cell_index as u8).wrapping_mul(19),
2377 weave_mix: (plan.weave as u8).wrapping_add(3),
2378 pad_mix: (plan.pad_len as u8).wrapping_mul(29),
2379 echo_lanes: field_monolith_left_rotation_lanes(state.echo),
2380 parity_lanes: field_monolith_right_rotation_lanes(state.parity),
2381 reserve_stride: state
2382 .reserve
2383 .wrapping_add((plan.stride as u8).wrapping_mul(13)),
2384 }
2385 }
2386
2387 fn round_mix(self, cell_index: u32, plan: FieldMonolithCellPlan, round: usize) -> u8 {
2388 self.reserve_stride
2389 ^ self.parity_lanes[(cell_index as usize + plan.pad_len + round) & 7]
2390 ^ self.cell_mix
2391 ^ self.pad_mix
2392 ^ (round as u8).wrapping_mul(7)
2393 }
2394
2395 fn byte_mask(self, offset: usize, round: usize, round_mix: u8, offset_mix: u8) -> u8 {
2396 self.echo_lanes[((offset & 7) + round) & 7] ^ offset_mix ^ round_mix
2397 }
2398}
2399
2400fn field_monolith_apply_virtual_mix_rounds(
2401 bytes: &mut [u8],
2402 plan: FieldMonolithCellPlan,
2403 cell_index: u32,
2404 mix: FieldMonolithVirtualMixContext,
2405) {
2406 let len = bytes.len();
2407 let mut round = 0usize;
2408 while round < plan.pad_len {
2409 let round_mix = mix.round_mix(cell_index, plan, round);
2410 let mut offset = 0usize;
2411 let mut offset_mix = 0u8;
2412 while offset < len {
2413 bytes[offset] ^= mix.byte_mask(offset, round, round_mix, offset_mix);
2414 offset_mix = offset_mix.wrapping_add(mix.weave_mix);
2415 offset += 1;
2416 }
2417 round += 1;
2418 }
2419}
2420
2421fn field_monolith_apply_virtual_mix_with_summary(
2422 bytes: &mut [u8],
2423 state: &FieldMonolithState,
2424 plan: FieldMonolithCellPlan,
2425 cell_index: u32,
2426 descriptor: u8,
2427) -> FieldMonolithSurfaceSummary {
2428 let mix = FieldMonolithVirtualMixContext::new(state, plan, cell_index);
2429 let len = bytes.len();
2430 field_monolith_apply_virtual_mix_rounds(bytes, plan, cell_index, mix);
2431
2432 let mut digest =
2433 field_monolith_surface_checksum_seed(bytes.len(), state, cell_index, descriptor);
2434 let mut front = 0u8;
2435 let mut back = 0u8;
2436 let mut offset = 0usize;
2437 let final_mix = mix.round_mix(cell_index, plan, plan.pad_len);
2438 let mut offset_mix = 0u8;
2439 while offset < len {
2440 bytes[offset] ^= mix.byte_mask(offset, plan.pad_len, final_mix, offset_mix);
2441 let byte = bytes[offset];
2442 if offset == 0 {
2443 front = byte;
2444 }
2445 back = byte;
2446 digest = field_monolith_surface_checksum_step(digest, state, offset, byte);
2447 offset_mix = offset_mix.wrapping_add(mix.weave_mix);
2448 offset += 1;
2449 }
2450
2451 FieldMonolithSurfaceSummary {
2452 digest,
2453 front,
2454 back,
2455 }
2456}
2457
2458fn field_monolith_decode_surface_with_summary_append(
2459 surface: &[u8],
2460 state: &FieldMonolithState,
2461 plan: FieldMonolithCellPlan,
2462 cell_index: u32,
2463 descriptor: u8,
2464 output: &mut Vec<u8>,
2465 mutation_scratch: &mut Vec<u8>,
2466) -> FieldMonolithSurfaceSummary {
2467 let mix = FieldMonolithVirtualMixContext::new(state, plan, cell_index);
2468 let final_mix = mix.round_mix(cell_index, plan, plan.pad_len);
2469 let mut digest =
2470 field_monolith_surface_checksum_seed(surface.len(), state, cell_index, descriptor);
2471 let mut front = 0u8;
2472 let mut back = 0u8;
2473 let mut offset_mix = 0u8;
2474 let output_start = output.len();
2475 for (offset, &byte) in surface.iter().enumerate() {
2476 if offset == 0 {
2477 front = byte;
2478 }
2479 back = byte;
2480 digest = field_monolith_surface_checksum_step(digest, state, offset, byte);
2481 output.push(byte ^ mix.byte_mask(offset, plan.pad_len, final_mix, offset_mix));
2482 offset_mix = offset_mix.wrapping_add(mix.weave_mix);
2483 }
2484 let summary = FieldMonolithSurfaceSummary {
2485 digest,
2486 front,
2487 back,
2488 };
2489 let decoded = &mut output[output_start..];
2490 field_monolith_apply_virtual_mix_rounds(decoded, plan, cell_index, mix);
2491 field_monolith_remove_virtual_permutation(decoded, state, plan, cell_index);
2492 field_monolith_inverse_mutation_slice(decoded, plan.mutation, plan.stride, mutation_scratch);
2493 let mask_lanes = field_monolith_mask_lanes(state, cell_index, plan.stride);
2494 field_monolith_xor_masked_bytes_in_place(decoded, &mask_lanes);
2495
2496 summary
2497}
2498
2499fn field_monolith_update_state_with_summary(
2500 state: &mut FieldMonolithState,
2501 descriptor: u8,
2502 summary: FieldMonolithSurfaceSummary,
2503 cell_index: u32,
2504 cell_len: usize,
2505) {
2506 let control_a = state.phase.wrapping_add(state.tension).rotate_left(1) ^ cell_len as u8;
2507 let control_b = state.drift.wrapping_add(state.reserve).rotate_right(1) ^ cell_index as u8;
2508
2509 state.phase = state.phase.wrapping_add(3).rotate_left(1) ^ summary.digest;
2510 state.tension = state.tension.wrapping_add(control_a).rotate_left(1) ^ cell_len as u8;
2511 state.curvature = state.curvature.wrapping_add(summary.digest.rotate_left(1));
2512 state.drift = state.drift.wrapping_add(summary.front).rotate_right(1) ^ descriptor;
2513 state.echo = state.echo.wrapping_add(summary.back) ^ summary.digest.rotate_right(1);
2514 state.reserve = state.reserve.wrapping_add(control_b) ^ summary.digest.rotate_left(1);
2515 state.stride = state
2516 .stride
2517 .wrapping_add(((descriptor >> 4) & 0x03) + 1)
2518 .max(1);
2519 state.parity ^= summary.digest ^ summary.front ^ summary.back;
2520}
2521
2522fn transform_payload_v1(
2523 data: &[u8],
2524 shell_key: &Key,
2525 nonce: &[u8; SHELL_NONCE_SIZE],
2526 preset: FusedPreset,
2527 chunk_size: usize,
2528 encode: bool,
2529) -> Result<Vec<u8>> {
2530 let mut output = Vec::with_capacity(data.len());
2531 for (chunk_index, chunk) in data.chunks(chunk_size.max(1)).enumerate() {
2532 let chunk_index = chunk_index as u32;
2533 let segment_lengths = segment_lengths(preset, chunk.len(), chunk_index);
2534 let mut offset = 0usize;
2535 for (segment_index, segment_len) in segment_lengths.into_iter().enumerate() {
2536 let next_offset = offset + segment_len;
2537 let purpose = segment_purpose(preset, segment_index);
2538 let transformed = if encode {
2539 encode_segment(
2540 &chunk[offset..next_offset],
2541 shell_key,
2542 nonce,
2543 chunk_index * 8 + segment_index as u32,
2544 &purpose,
2545 )?
2546 } else {
2547 decode_segment(
2548 &chunk[offset..next_offset],
2549 shell_key,
2550 nonce,
2551 chunk_index * 8 + segment_index as u32,
2552 &purpose,
2553 )?
2554 };
2555 output.extend_from_slice(&transformed);
2556 offset = next_offset;
2557 }
2558 }
2559 Ok(output)
2560}
2561
2562fn encode_segment(
2563 segment: &[u8],
2564 shell_key: &Key,
2565 nonce: &[u8; SHELL_NONCE_SIZE],
2566 chunk_index: u32,
2567 purpose: &str,
2568) -> Result<Vec<u8>> {
2569 let seed = derive_chunk_seed(shell_key, nonce, chunk_index, purpose, 32)?;
2570 let mut rng = DeterministicRng::from_seed(&seed);
2571 let permutation = build_permutation(segment.len(), &mut rng);
2572 let mut mask = vec![0u8; segment.len()];
2573 rng.fill_bytes(&mut mask);
2574 let mut encoded = vec![0u8; segment.len()];
2575
2576 for (source_index, &target_index) in permutation.iter().enumerate() {
2577 encoded[target_index] = segment[source_index] ^ mask[source_index];
2578 }
2579
2580 Ok(encoded)
2581}
2582
2583fn decode_segment(
2584 segment: &[u8],
2585 shell_key: &Key,
2586 nonce: &[u8; SHELL_NONCE_SIZE],
2587 chunk_index: u32,
2588 purpose: &str,
2589) -> Result<Vec<u8>> {
2590 let seed = derive_chunk_seed(shell_key, nonce, chunk_index, purpose, 32)?;
2591 let mut rng = DeterministicRng::from_seed(&seed);
2592 let permutation = build_permutation(segment.len(), &mut rng);
2593 let mut mask = vec![0u8; segment.len()];
2594 rng.fill_bytes(&mut mask);
2595 let mut decoded = vec![0u8; segment.len()];
2596
2597 for (source_index, &target_index) in permutation.iter().enumerate() {
2598 decoded[source_index] = segment[target_index] ^ mask[source_index];
2599 }
2600
2601 Ok(decoded)
2602}
2603
2604fn chunk_count(payload_len: usize, chunk_size: usize) -> usize {
2605 if payload_len == 0 {
2606 0
2607 } else {
2608 1 + (payload_len - 1) / chunk_size.max(1)
2609 }
2610}
2611
2612fn segment_lengths(preset: FusedPreset, chunk_len: usize, chunk_index: u32) -> Vec<usize> {
2613 if chunk_len <= 1 {
2614 return vec![chunk_len];
2615 }
2616
2617 let weights: &[u8] = match preset {
2618 FusedPreset::Compact => &[1],
2619 FusedPreset::Balanced => match chunk_index % 3 {
2620 0 => &[3, 2, 1],
2621 1 => &[2, 1, 3],
2622 _ => &[1, 3, 2],
2623 },
2624 FusedPreset::Concealed => match chunk_index % 4 {
2625 0 => &[4, 2, 1, 1],
2626 1 => &[1, 4, 2, 1],
2627 2 => &[1, 1, 4, 2],
2628 _ => &[2, 1, 1, 4],
2629 },
2630 };
2631
2632 if weights.len() == 1 || chunk_len < weights.len() {
2633 return vec![chunk_len];
2634 }
2635
2636 split_len_by_weights(chunk_len, weights)
2637}
2638
2639fn split_len_by_weights(total_len: usize, weights: &[u8]) -> Vec<usize> {
2640 if total_len == 0 {
2641 return vec![0];
2642 }
2643
2644 let total_weight: usize = weights.iter().map(|&weight| weight as usize).sum();
2645 let mut lengths = Vec::with_capacity(weights.len());
2646 let mut consumed = 0usize;
2647 let mut remaining_weight = total_weight;
2648
2649 for (index, &weight) in weights.iter().enumerate() {
2650 let remaining_segments = weights.len() - index;
2651 let remaining_len = total_len - consumed;
2652
2653 let segment_len = if remaining_segments == 1 {
2654 remaining_len
2655 } else {
2656 let proportional = remaining_len.saturating_mul(weight as usize) / remaining_weight;
2657 proportional.max(1)
2658 };
2659
2660 lengths.push(segment_len);
2661 consumed += segment_len;
2662 remaining_weight = remaining_weight.saturating_sub(weight as usize);
2663 }
2664
2665 let mut overflow = consumed.saturating_sub(total_len);
2666 let mut cursor = lengths.len();
2667 while overflow > 0 && cursor > 0 {
2668 cursor -= 1;
2669 if lengths[cursor] > 1 {
2670 lengths[cursor] -= 1;
2671 overflow -= 1;
2672 }
2673 }
2674
2675 if consumed < total_len {
2676 if let Some(last) = lengths.last_mut() {
2677 *last += total_len - consumed;
2678 }
2679 }
2680
2681 lengths
2682}
2683
2684fn segment_purpose(preset: FusedPreset, segment_index: usize) -> String {
2685 format!("{}-segment-{}", preset.name(), segment_index)
2686}
2687
2688fn random_nonce() -> [u8; SHELL_NONCE_SIZE] {
2689 let mut nonce = [0u8; SHELL_NONCE_SIZE];
2690 rand::thread_rng().fill_bytes(&mut nonce);
2691 nonce
2692}
2693
2694fn build_permutation(len: usize, rng: &mut DeterministicRng) -> Vec<usize> {
2695 let mut permutation: Vec<usize> = (0..len).collect();
2696 if len <= 1 {
2697 return permutation;
2698 }
2699 for index in (1..len).rev() {
2700 let swap_index = rng.next_index(index + 1);
2701 permutation.swap(index, swap_index);
2702 }
2703 permutation
2704}
2705
2706#[derive(Debug, Clone, Copy)]
2707struct DeterministicRng {
2708 state: u64,
2709}
2710
2711impl DeterministicRng {
2712 fn from_seed(seed: &[u8]) -> Self {
2713 let mut state = 0x9E37_79B9_7F4A_7C15u64;
2714 for chunk in seed.chunks(8) {
2715 let mut word = [0u8; 8];
2716 word[..chunk.len()].copy_from_slice(chunk);
2717 state ^= u64::from_le_bytes(word);
2718 state = state.rotate_left(17).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2719 }
2720 Self { state }
2721 }
2722
2723 fn next_u64(&mut self) -> u64 {
2724 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
2725 let mut z = self.state;
2726 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2727 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2728 z ^ (z >> 31)
2729 }
2730
2731 fn next_u32(&mut self) -> u32 {
2732 self.next_u64() as u32
2733 }
2734
2735 fn next_index(&mut self, upper: usize) -> usize {
2736 if upper <= 1 {
2737 0
2738 } else {
2739 (self.next_u64() as usize) % upper
2740 }
2741 }
2742
2743 fn fill_bytes(&mut self, output: &mut [u8]) {
2744 let mut offset = 0usize;
2745 while offset < output.len() {
2746 let bytes = self.next_u64().to_le_bytes();
2747 let take = (output.len() - offset).min(bytes.len());
2748 output[offset..offset + take].copy_from_slice(&bytes[..take]);
2749 offset += take;
2750 }
2751 }
2752}
2753
2754#[cfg(test)]
2755mod tests {
2756 use super::*;
2757 use crate::encryption::EncryptOptions;
2758
2759 fn fixed_key() -> Key {
2760 Key::from_bytes(&[0x42; 32]).expect("valid fixed key")
2761 }
2762
2763 #[test]
2764 fn derive_domain_key_is_deterministic() {
2765 let master = fixed_key();
2766 let salt = b"shell-salt";
2767 let first = derive_domain_key(&master, Some(salt), "shell").unwrap();
2768 let second = derive_domain_key(&master, Some(salt), "shell").unwrap();
2769 let different = derive_domain_key(&master, Some(salt), "shell-tag").unwrap();
2770
2771 assert_eq!(first.as_bytes(), second.as_bytes());
2772 assert_ne!(first.as_bytes(), different.as_bytes());
2773 }
2774
2775 #[test]
2776 fn derive_chunk_seed_varies_by_chunk_and_purpose() {
2777 let shell_key = fixed_key();
2778 let nonce = [7u8; SHELL_NONCE_SIZE];
2779
2780 let first = derive_chunk_seed(&shell_key, &nonce, 0, "fused-prefix", 32).unwrap();
2781 let second = derive_chunk_seed(&shell_key, &nonce, 0, "fused-prefix", 32).unwrap();
2782 let next_chunk = derive_chunk_seed(&shell_key, &nonce, 1, "fused-prefix", 32).unwrap();
2783 let next_purpose = derive_chunk_seed(&shell_key, &nonce, 0, "compare", 32).unwrap();
2784
2785 assert_eq!(first, second);
2786 assert_ne!(first, next_chunk);
2787 assert_ne!(first, next_purpose);
2788 }
2789
2790 #[test]
2791 fn shell_tag_roundtrip_detects_wrong_key_and_tamper() {
2792 let payload = b"outer shell payload";
2793 let tag_key = fixed_key();
2794 let wrong_key = Key::from_bytes(&[0x24; 32]).unwrap();
2795
2796 let tag = compute_shell_tag(payload, &tag_key).unwrap();
2797
2798 assert!(verify_shell_tag(payload, &tag, &tag_key).unwrap());
2799 assert!(!verify_shell_tag(payload, &tag, &wrong_key).unwrap());
2800 assert!(!verify_shell_tag(b"tampered", &tag, &tag_key).unwrap());
2801 }
2802
2803 #[test]
2804 fn fused_shell_roundtrip_for_all_presets() {
2805 let master = fixed_key();
2806 let payload = vec![0xA5u8; DEFAULT_CHUNK_SIZE + 257];
2807
2808 for preset in [
2809 FusedPreset::Compact,
2810 FusedPreset::Balanced,
2811 FusedPreset::Concealed,
2812 ] {
2813 let envelope = fuse(
2814 &payload,
2815 &master,
2816 Some(FusedShellOptions {
2817 preset,
2818 chunk_size: None,
2819 shell_nonce: Some([9u8; SHELL_NONCE_SIZE]),
2820 }),
2821 )
2822 .unwrap();
2823
2824 let restored = unfuse(&envelope, &master).unwrap();
2825 assert_eq!(restored, payload);
2826 assert_eq!(envelope.info().preset, preset);
2827 }
2828 }
2829
2830 #[test]
2831 fn fused_shell_emits_field_monolith_v2_geometry() {
2832 let master = fixed_key();
2833 let payload = b"field monolith current is the shipped fuse v2 body".repeat(128);
2834
2835 let envelope = fuse(
2836 &payload,
2837 &master,
2838 Some(FusedShellOptions {
2839 preset: FusedPreset::Balanced,
2840 chunk_size: None,
2841 shell_nonce: Some([3u8; SHELL_NONCE_SIZE]),
2842 }),
2843 )
2844 .unwrap();
2845
2846 assert_eq!(envelope.version, FUSED_SHELL_VERSION);
2847 assert!(envelope.payload.len() > payload.len());
2848
2849 let geometry =
2850 field_monolith_infer_geometry(envelope.payload.len(), envelope.chunk_size as usize)
2851 .expect("valid monolith geometry");
2852 assert_eq!(geometry.original_len, payload.len());
2853 let block_count =
2854 field_monolith_block_count_for_cells(geometry.chunk_count, geometry.block_cells);
2855 assert_eq!(envelope.payload.len(), payload.len() + block_count);
2856
2857 let restored = unfuse(&envelope, &master).unwrap();
2858 assert_eq!(restored, payload);
2859 }
2860
2861 #[test]
2862 fn field_monolith_constant_time_inversion_preserves_valid_geometries() {
2863 for target_cell_size in [1usize, 7, 8, 24, 96, 128, 256, 512, 1024, 2048, 65_535] {
2864 for original_len in 0usize..=10_000 {
2865 let chunk_count = if original_len == 0 {
2866 0
2867 } else {
2868 original_len.div_ceil(target_cell_size)
2869 };
2870 let block_cells =
2871 field_monolith_current_anchor_block_cells(target_cell_size, chunk_count);
2872 let block_count = field_monolith_block_count_for_cells(chunk_count, block_cells);
2873 let payload_len = original_len + block_count;
2874
2875 assert_eq!(
2876 field_monolith_infer_geometry(payload_len, target_cell_size),
2877 Some(FieldMonolithGeometry {
2878 original_len,
2879 chunk_count,
2880 block_cells,
2881 }),
2882 "target={target_cell_size}, original={original_len}"
2883 );
2884 }
2885 }
2886
2887 let original_len = 2_000_000usize;
2888 let target_cell_size = 1usize;
2889 let chunk_count = original_len;
2890 let block_cells = FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE;
2891 let payload_len =
2892 original_len + field_monolith_block_count_for_cells(chunk_count, block_cells);
2893 assert_eq!(
2894 field_monolith_infer_geometry(payload_len, target_cell_size),
2895 Some(FieldMonolithGeometry {
2896 original_len,
2897 chunk_count,
2898 block_cells,
2899 })
2900 );
2901 }
2902
2903 #[test]
2904 fn keyless_inspect_rejects_million_cell_noncanonical_fused_geometry() {
2905 let original_len = 2_000_000usize;
2906 let block_count = field_monolith_block_count_for_cells(
2907 original_len,
2908 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE,
2909 );
2910 let payload_len = original_len + block_count;
2911 let mut artifact = vec![0u8; FUSED_SHELL_HEADER_LEN + payload_len + SHELL_TAG_SIZE];
2912 artifact[..4].copy_from_slice(&FUSED_SHELL_MAGIC);
2913 artifact[4] = FUSED_SHELL_VERSION;
2914 artifact[5] = FusedPreset::Balanced as u8;
2915 artifact[6..10].copy_from_slice(&1u32.to_be_bytes());
2916
2917 let error = inspect_fused(&artifact).unwrap_err();
2918 assert!(matches!(error, Error::InvalidConfiguration(_)));
2919 assert!(error.to_string().contains("noncanonical"));
2920 }
2921
2922 #[test]
2923 fn fused_keyless_inspect_and_roundtrip_accept_valid_edge_geometries() {
2924 let master = fixed_key();
2925 for (len, requested_chunk_size) in [
2926 (0usize, None),
2927 (1, None),
2928 (7, Some(1)),
2929 (8, Some(8)),
2930 (257, Some(24)),
2931 (1_025, Some(256)),
2932 (5_000, Some(512)),
2933 ] {
2934 let payload = vec![0x5Au8; len];
2935 let artifact = fuse_bytes(
2936 &payload,
2937 &master,
2938 Some(FusedShellOptions {
2939 chunk_size: requested_chunk_size,
2940 ..FusedShellOptions::default()
2941 }),
2942 )
2943 .unwrap();
2944 let inspected = inspect_fused(&artifact).unwrap();
2945 assert_eq!(inspected.shell_size, artifact.len());
2946 assert_eq!(unfuse_bytes(&artifact, &master).unwrap(), payload);
2947 }
2948 }
2949
2950 #[cfg(feature = "compression")]
2951 #[test]
2952 fn keyless_inspect_rejects_million_cell_masked_whole_monolith_geometry() {
2953 let original_len = 2_000_000usize;
2954 let block_count = field_monolith_block_count_for_cells(
2955 original_len,
2956 FIELD_MONOLITH_ANCHOR_BLOCK_CELLS_DENSE,
2957 );
2958 let payload_len = original_len + block_count;
2959 let seed = [0x12, 0x34];
2960 let header = WholeMonolithV0Header {
2961 preset: FusedPreset::Balanced,
2962 compression_algorithm: CompressionAlgorithm::None,
2963 encryption_algorithm: EncryptionAlgorithm::XChaCha20Poly1305,
2964 shell_cell_size: 1,
2965 original_size: 1,
2966 compressed_size: 1,
2967 shell_nonce: [0u8; SHELL_NONCE_SIZE],
2968 };
2969 let mut masked_header = header.to_bytes();
2970 whole_monolith_v0_mask_public_header(&seed, &mut masked_header);
2971 let mut artifact = Vec::with_capacity(
2972 WHOLE_MONOLITH_V0_PUBLIC_SEED_BYTES
2973 + WHOLE_MONOLITH_V0_PUBLIC_HEADER_LEN
2974 + payload_len
2975 + SHELL_TAG_SIZE,
2976 );
2977 artifact.extend_from_slice(&seed);
2978 artifact.extend_from_slice(&masked_header);
2979 artifact.resize(artifact.len() + payload_len + SHELL_TAG_SIZE, 0);
2980
2981 let error = inspect_artifact(&artifact).unwrap_err();
2982 assert!(matches!(error, Error::InvalidConfiguration(_)));
2983 assert!(error.to_string().contains("noncanonical"));
2984 }
2985
2986 #[test]
2987 fn fused_shell_still_decodes_v1_segment_envelopes() {
2988 let master = fixed_key();
2989 let payload = b"legacy fused shell payload that must remain openable".repeat(64);
2990 let preset = FusedPreset::Balanced;
2991 let chunk_size = DEFAULT_CHUNK_SIZE;
2992 let nonce = [4u8; SHELL_NONCE_SIZE];
2993 let (shell_key, tag_key) = derive_shell_keys(&master, &nonce).unwrap();
2994 let shell_payload =
2995 transform_payload_v1(&payload, &shell_key, &nonce, preset, chunk_size, true).unwrap();
2996 let mut envelope = FusedShellEnvelope {
2997 version: FUSED_SHELL_V1_VERSION,
2998 preset,
2999 chunk_size: chunk_size as u32,
3000 nonce,
3001 payload: shell_payload,
3002 tag: [0u8; SHELL_TAG_SIZE],
3003 };
3004 envelope.tag = compute_shell_tag(&envelope.to_unsigned_bytes(), &tag_key).unwrap();
3005
3006 let restored = unfuse_bytes(&envelope.to_bytes(), &master).unwrap();
3007 assert_eq!(restored, payload);
3008 }
3009
3010 #[test]
3011 fn fused_shell_detects_tamper() {
3012 let master = fixed_key();
3013 let mut bytes = fuse_bytes(b"hello fused world", &master, None).unwrap();
3014 let payload_offset = FUSED_SHELL_HEADER_LEN;
3015 bytes[payload_offset] ^= 0xFF;
3016
3017 let err = unfuse_bytes(&bytes, &master).unwrap_err();
3018 assert_eq!(err, Error::AuthenticationFailed);
3019 }
3020
3021 #[cfg(feature = "compression")]
3022 #[test]
3023 fn protect_open_roundtrip_for_all_presets() {
3024 let master = fixed_key();
3025 let payload = b"voided v3 fused flow protects this payload cleanly".repeat(1024);
3026
3027 for preset in [
3028 FusedPreset::Compact,
3029 FusedPreset::Balanced,
3030 FusedPreset::Concealed,
3031 ] {
3032 let protected = protect(
3033 &payload,
3034 &master,
3035 Some(ProtectOptions {
3036 preset,
3037 ..ProtectOptions::default()
3038 }),
3039 )
3040 .unwrap();
3041
3042 let restored = open(&protected.artifact, &master).unwrap();
3043 assert_eq!(restored, payload);
3044
3045 let inspected = inspect_artifact(&protected.artifact).unwrap();
3046 assert_eq!(inspected.preset, preset);
3047 assert_eq!(inspected.original_size, payload.len());
3048 assert_eq!(inspected.version, PROTECTED_ARTIFACT_VERSION);
3049 }
3050 }
3051
3052 #[cfg(feature = "compression")]
3053 #[test]
3054 fn protect_reports_v3_whole_monolith_metadata() {
3055 let master = fixed_key();
3056 let payload = b"protect should expose full-flow monolith geometry".repeat(512);
3057
3058 let protected = protect(&payload, &master, None).unwrap();
3059 let inspected = inspect_artifact(&protected.artifact).unwrap();
3060
3061 assert!(!artifact_starts_with_protected_magic(&protected.artifact));
3062 assert_eq!(inspected.version, PROTECTED_ARTIFACT_VERSION);
3063 assert_eq!(inspected.protected_size, protected.artifact.len());
3064 assert_eq!(inspected.original_size, payload.len());
3065 assert_eq!(inspected.encrypted_size, protected.info.encrypted_size);
3066 assert!(inspected.encrypted_size >= inspected.compressed_size);
3067 assert!(inspected.shell_chunk_count >= 1);
3068 assert_eq!(open(&protected.artifact, &master).unwrap(), payload);
3069 }
3070
3071 #[cfg(feature = "compression")]
3072 #[test]
3073 fn protect_v3_uses_one_full_flow_compression_pass() {
3074 let master = fixed_key();
3075 let payload =
3076 b"full flow monolith compression should not reset per macro chunk ".repeat(4096);
3077 let expected_compression = compression::compress(
3078 &payload,
3079 Some(CompressionOptions {
3080 algorithm: CompressionAlgorithm::Brotli,
3081 min_size_threshold: 100,
3082 level: 6,
3083 }),
3084 )
3085 .unwrap();
3086
3087 let protected = protect(
3088 &payload,
3089 &master,
3090 Some(ProtectOptions {
3091 shell_nonce: Some([0x33; SHELL_NONCE_SIZE]),
3092 ..ProtectOptions::default()
3093 }),
3094 )
3095 .unwrap();
3096 let inspected = inspect_artifact(&protected.artifact).unwrap();
3097
3098 assert_eq!(
3099 inspected.compressed_size,
3100 expected_compression.compressed.len()
3101 );
3102 assert_eq!(open(&protected.artifact, &master).unwrap(), payload);
3103 }
3104
3105 #[cfg(feature = "compression")]
3106 #[test]
3107 fn protected_v3_whole_monolith_rejects_legacy_parser_wrong_key_and_tamper() {
3108 let master = fixed_key();
3109 let wrong = Key::from_bytes(&[0x24; 32]).unwrap();
3110 let payload = b"v3 should be a masked whole monolith artifact".repeat(256);
3111 let protected = protect(
3112 &payload,
3113 &master,
3114 Some(ProtectOptions {
3115 shell_nonce: Some([8u8; SHELL_NONCE_SIZE]),
3116 ..ProtectOptions::default()
3117 }),
3118 )
3119 .unwrap();
3120
3121 assert!(ProtectedArtifactEnvelope::from_bytes(&protected.artifact).is_err());
3122 assert!(open(&protected.artifact, &wrong).is_err());
3123
3124 let mut tampered = protected.artifact.clone();
3125 let pivot = tampered.len() / 2;
3126 tampered[pivot] ^= 0xA5;
3127 assert!(open(&tampered, &master).is_err());
3128
3129 assert_eq!(open(&protected.artifact, &master).unwrap(), payload);
3130 }
3131
3132 #[cfg(feature = "compression")]
3133 #[test]
3134 fn rotation_helper_decodes_v2_shell_monolith_protected_artifacts() {
3135 let master = fixed_key();
3136 let payload = b"v2 shell-monolith protected artifacts must stay openable".repeat(256);
3137 let opts = ProtectOptions {
3138 shell_nonce: Some([6u8; SHELL_NONCE_SIZE]),
3139 ..ProtectOptions::default()
3140 };
3141 let nonce = opts.shell_nonce.unwrap();
3142 let compression_result = compression::compress(
3143 &payload,
3144 Some(CompressionOptions {
3145 algorithm: opts.compression_algorithm,
3146 min_size_threshold: opts.compression_min_size_threshold,
3147 level: opts.compression_level,
3148 }),
3149 )
3150 .unwrap();
3151 let encrypted = crate::encryption::encrypt(
3152 &compression_result.compressed,
3153 &master,
3154 Some(EncryptOptions {
3155 algorithm: opts.encryption_algorithm,
3156 aad: None,
3157 }),
3158 )
3159 .unwrap();
3160 let encrypted_bytes = encrypted.to_bytes();
3161 let (shell_key, tag_key) = derive_shell_keys(&master, &nonce).unwrap();
3162 let (shell_payload, chunk_size) =
3163 field_monolith_encode_payload(&encrypted_bytes, &shell_key, &nonce, None).unwrap();
3164 let mut envelope = ProtectedArtifactEnvelope {
3165 version: PROTECTED_ARTIFACT_V2_VERSION,
3166 preset: opts.preset,
3167 compression_algorithm: compression_result.algorithm,
3168 encryption_algorithm: encrypted.algorithm,
3169 chunk_size: chunk_size as u32,
3170 original_size: payload.len() as u64,
3171 compressed_size: compression_result.compressed.len() as u64,
3172 nonce,
3173 payload: shell_payload,
3174 tag: [0u8; SHELL_TAG_SIZE],
3175 };
3176 envelope.tag = compute_shell_tag(&envelope.to_unsigned_bytes(), &tag_key).unwrap();
3177
3178 assert!(open(&envelope.to_bytes(), &master).is_err());
3179 let restored = open_rotation_artifact(&envelope.to_bytes(), &master).unwrap();
3180 assert_eq!(restored, payload);
3181 assert_eq!(
3182 inspect_rotation_artifact(&envelope.to_bytes())
3183 .unwrap()
3184 .version,
3185 PROTECTED_ARTIFACT_V2_VERSION
3186 );
3187 }
3188
3189 #[cfg(feature = "compression")]
3190 #[test]
3191 fn rotation_helper_decodes_legacy_v3_protected_monolith_artifacts() {
3192 let master = fixed_key();
3193 let payload =
3194 b"legacy v3 protected-monolith artifacts stay explicit rotation only".repeat(256);
3195 let opts = ProtectOptions {
3196 shell_nonce: Some([7u8; SHELL_NONCE_SIZE]),
3197 ..ProtectOptions::default()
3198 };
3199 let nonce = opts.shell_nonce.unwrap();
3200 let compression_result = compression::compress(
3201 &payload,
3202 Some(CompressionOptions {
3203 algorithm: opts.compression_algorithm,
3204 min_size_threshold: opts.compression_min_size_threshold,
3205 level: opts.compression_level,
3206 }),
3207 )
3208 .unwrap();
3209 let encrypted = crate::encryption::encrypt(
3210 &compression_result.compressed,
3211 &master,
3212 Some(EncryptOptions {
3213 algorithm: opts.encryption_algorithm,
3214 aad: None,
3215 }),
3216 )
3217 .unwrap();
3218 let encrypted_bytes = encrypted.to_bytes();
3219 let (shell_key, tag_key) = derive_shell_keys(&master, &nonce).unwrap();
3220 let (shell_payload, chunk_size) = protected_monolith_encode_payload(
3221 &encrypted_bytes,
3222 &shell_key,
3223 &nonce,
3224 None,
3225 payload.len(),
3226 compression_result.compressed.len(),
3227 compression_result.algorithm,
3228 encrypted.algorithm,
3229 opts.preset,
3230 )
3231 .unwrap();
3232 let mut envelope = ProtectedArtifactEnvelope {
3233 version: PROTECTED_ARTIFACT_VERSION,
3234 preset: opts.preset,
3235 compression_algorithm: compression_result.algorithm,
3236 encryption_algorithm: encrypted.algorithm,
3237 chunk_size: chunk_size as u32,
3238 original_size: payload.len() as u64,
3239 compressed_size: compression_result.compressed.len() as u64,
3240 nonce,
3241 payload: shell_payload,
3242 tag: [0u8; SHELL_TAG_SIZE],
3243 };
3244 envelope.tag = compute_shell_tag(&envelope.to_unsigned_bytes(), &tag_key).unwrap();
3245
3246 assert!(open(&envelope.to_bytes(), &master).is_err());
3247 let restored = open_rotation_artifact(&envelope.to_bytes(), &master).unwrap();
3248 assert_eq!(restored, payload);
3249 assert_eq!(
3250 inspect_rotation_artifact(&envelope.to_bytes())
3251 .unwrap()
3252 .version,
3253 PROTECTED_ARTIFACT_VERSION
3254 );
3255 }
3256
3257 #[cfg(feature = "compression")]
3258 #[test]
3259 fn rotation_helper_decodes_v1_protected_artifacts() {
3260 let master = fixed_key();
3261 let payload = b"legacy protected artifacts must stay openable".repeat(256);
3262 let opts = ProtectOptions {
3263 shell_nonce: Some([5u8; SHELL_NONCE_SIZE]),
3264 ..ProtectOptions::default()
3265 };
3266 let nonce = opts.shell_nonce.unwrap();
3267 let chunk_size = DEFAULT_CHUNK_SIZE;
3268 let compression_result = compression::compress(
3269 &payload,
3270 Some(CompressionOptions {
3271 algorithm: opts.compression_algorithm,
3272 min_size_threshold: opts.compression_min_size_threshold,
3273 level: opts.compression_level,
3274 }),
3275 )
3276 .unwrap();
3277 let encrypted = crate::encryption::encrypt(
3278 &compression_result.compressed,
3279 &master,
3280 Some(EncryptOptions {
3281 algorithm: opts.encryption_algorithm,
3282 aad: None,
3283 }),
3284 )
3285 .unwrap();
3286 let encrypted_bytes = encrypted.to_bytes();
3287 let (shell_key, tag_key) = derive_shell_keys(&master, &nonce).unwrap();
3288 let shell_payload = transform_payload_v1(
3289 &encrypted_bytes,
3290 &shell_key,
3291 &nonce,
3292 opts.preset,
3293 chunk_size,
3294 true,
3295 )
3296 .unwrap();
3297 let mut envelope = ProtectedArtifactEnvelope {
3298 version: PROTECTED_ARTIFACT_V1_VERSION,
3299 preset: opts.preset,
3300 compression_algorithm: compression_result.algorithm,
3301 encryption_algorithm: encrypted.algorithm,
3302 chunk_size: chunk_size as u32,
3303 original_size: payload.len() as u64,
3304 compressed_size: compression_result.compressed.len() as u64,
3305 nonce,
3306 payload: shell_payload,
3307 tag: [0u8; SHELL_TAG_SIZE],
3308 };
3309 envelope.tag = compute_shell_tag(&envelope.to_unsigned_bytes(), &tag_key).unwrap();
3310
3311 assert!(open(&envelope.to_bytes(), &master).is_err());
3312 let restored = open_rotation_artifact(&envelope.to_bytes(), &master).unwrap();
3313 assert_eq!(restored, payload);
3314 assert_eq!(
3315 inspect_rotation_artifact(&envelope.to_bytes())
3316 .unwrap()
3317 .version,
3318 PROTECTED_ARTIFACT_V1_VERSION
3319 );
3320 }
3321
3322 #[cfg(feature = "compression")]
3323 #[test]
3324 fn repack_changes_preset_and_keeps_plaintext() {
3325 let master = fixed_key();
3326 let payload = vec![0x6Du8; 65_537];
3327 let protected = protect(&payload, &master, None).unwrap();
3328
3329 let repacked = repack_artifact(
3330 &protected.artifact,
3331 &master,
3332 Some(ProtectOptions {
3333 preset: FusedPreset::Concealed,
3334 ..ProtectOptions::default()
3335 }),
3336 )
3337 .unwrap();
3338
3339 assert_eq!(repacked.info.preset, FusedPreset::Concealed);
3340 assert_eq!(open(&repacked.artifact, &master).unwrap(), payload);
3341 }
3342}