Skip to main content

voided_core/
shell.rs

1//! Fused shell primitives and whole-monolith protect/open flows for Voided v3.
2
3use 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
20/// Truncated outer shell tag size in bytes.
21pub const SHELL_TAG_SIZE: usize = 16;
22
23/// Fused shell envelope magic bytes.
24pub const FUSED_SHELL_MAGIC: [u8; 4] = *b"VFS2";
25
26/// Fused shell envelope version.
27pub const FUSED_SHELL_VERSION: u8 = 0x02;
28const FUSED_SHELL_V1_VERSION: u8 = 0x01;
29
30/// Protected artifact magic bytes.
31pub const PROTECTED_ARTIFACT_MAGIC: [u8; 4] = *b"VOF2";
32
33/// Protected artifact version.
34pub const PROTECTED_ARTIFACT_VERSION: u8 = 0x03;
35const PROTECTED_ARTIFACT_V2_VERSION: u8 = 0x02;
36const PROTECTED_ARTIFACT_V1_VERSION: u8 = 0x01;
37
38/// Shell nonce size in bytes.
39pub const SHELL_NONCE_SIZE: usize = 12;
40
41/// Default fused shell chunk size in bytes.
42pub 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/// Stable fused preset ids.
91#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
92#[repr(u8)]
93pub enum FusedPreset {
94    /// Cheapest fused shell lane.
95    Compact = 0x01,
96    /// Default fused shell lane.
97    Balanced = 0x02,
98    /// Highest-variation fused shell lane.
99    Concealed = 0x03,
100}
101
102impl FusedPreset {
103    /// Parse a preset from its serialized byte.
104    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    /// Parse a preset from its stable user-facing label.
114    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    /// Get the stable user-facing label for this preset.
126    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/// Fused shell primitive options.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct FusedShellOptions {
144    /// Fused preset to use.
145    pub preset: FusedPreset,
146    /// Override the default chunk size.
147    pub chunk_size: Option<usize>,
148    /// Optional deterministic shell nonce.
149    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/// Lightweight fused shell metadata available without a key.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct FusedShellInfo {
165    /// Envelope version.
166    pub version: u8,
167    /// Selected fused preset.
168    pub preset: FusedPreset,
169    /// Stable preset label.
170    pub preset_label: String,
171    /// Effective chunk size.
172    pub chunk_size: u32,
173    /// Chunk count implied by the payload length.
174    pub chunk_count: usize,
175    /// Obfuscated payload bytes.
176    pub payload_size: usize,
177    /// Total envelope bytes.
178    pub shell_size: usize,
179    /// Header bytes.
180    pub metadata_size: usize,
181    /// Tag bytes.
182    pub tag_size: usize,
183}
184
185/// Serialized fused shell envelope.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct FusedShellEnvelope {
188    /// Envelope version.
189    pub version: u8,
190    /// Fused preset id.
191    pub preset: FusedPreset,
192    /// Effective chunk size.
193    pub chunk_size: u32,
194    /// Shell nonce.
195    pub nonce: [u8; SHELL_NONCE_SIZE],
196    /// Shell-transformed bytes.
197    pub payload: Vec<u8>,
198    /// Truncated authentication tag.
199    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    /// Serialize the envelope to bytes.
283    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    /// Serialize the envelope without the trailing tag.
290    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    /// Parse a serialized fused shell envelope.
302    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    /// Return lightweight envelope metadata.
319    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/// Protect/open pipeline options.
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ProtectOptions {
346    /// Fused preset for the outer shell.
347    pub preset: FusedPreset,
348    /// Compression algorithm preference.
349    pub compression_algorithm: CompressionAlgorithm,
350    /// Compression level.
351    pub compression_level: u32,
352    /// Minimum size before compression is attempted.
353    pub compression_min_size_threshold: usize,
354    /// Preferred encryption algorithm.
355    pub encryption_algorithm: Option<EncryptionAlgorithm>,
356    /// Optional override for shell chunk size.
357    pub shell_chunk_size: Option<usize>,
358    /// Optional deterministic shell nonce.
359    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/// Keyless metadata for a Voided v3 monolith protected artifact.
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct ProtectedArtifactInfo {
381    /// Artifact version.
382    pub version: u8,
383    /// Fused preset id.
384    pub preset: FusedPreset,
385    /// Stable preset label.
386    pub preset_label: String,
387    /// Compression algorithm stored in the artifact.
388    pub compression_algorithm: CompressionAlgorithm,
389    /// Encryption algorithm used for the sealed payload.
390    pub encryption_algorithm: EncryptionAlgorithm,
391    /// Original plaintext size.
392    pub original_size: usize,
393    /// Size after compression and before encryption.
394    pub compressed_size: usize,
395    /// Size of the encrypted bytes before shell transformation.
396    pub encrypted_size: usize,
397    /// Total artifact bytes.
398    pub protected_size: usize,
399    /// Effective shell chunk size.
400    pub shell_chunk_size: u32,
401    /// Shell chunk count.
402    pub shell_chunk_count: usize,
403    /// Shell nonce.
404    pub shell_nonce: [u8; SHELL_NONCE_SIZE],
405}
406
407#[cfg(feature = "compression")]
408/// Result of a protect or repack operation.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct ProtectResult {
411    /// Serialized protected artifact bytes.
412    pub artifact: Vec<u8>,
413    /// Parsed keyless metadata for the returned artifact.
414    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
598/// Derive a 256-bit domain key for shell-related operations.
599pub 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
607/// Derive arbitrary bytes for a shell-related domain.
608pub 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
622/// Derive a per-chunk shell seed from the shell key, nonce, chunk index, and purpose.
623pub 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
638/// Compute a truncated outer tag for shell payloads.
639pub 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
653/// Verify a truncated shell tag in constant time.
654pub 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
662/// Apply the fused shell primitive to arbitrary bytes.
663pub 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
686/// Invert the fused shell primitive.
687pub 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
712/// Serialize a fused shell envelope directly.
713pub 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
721/// Parse and invert a serialized fused shell envelope.
722pub fn unfuse_bytes(data: &[u8], master_key: &Key) -> Result<Vec<u8>> {
723    unfuse(&FusedShellEnvelope::from_bytes(data)?, master_key)
724}
725
726/// Inspect a serialized fused shell envelope without a key.
727pub fn inspect_fused(data: &[u8]) -> Result<FusedShellInfo> {
728    Ok(parse_fused_shell_header(data)?.info(data.len()))
729}
730
731#[cfg(feature = "compression")]
732/// Run the Voided v3 whole-monolith full flow.
733pub 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")]
743/// Open a serialized Voided v3 whole-monolith artifact.
744pub fn open(artifact: &[u8], master_key: &Key) -> Result<Vec<u8>> {
745    whole_monolith_v0_open(artifact, master_key)
746}
747
748#[cfg(feature = "compression")]
749/// Open either the current v3 artifact or an explicit legacy VOF2 rotation artifact.
750pub 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")]
817/// Inspect a current Voided v3 artifact without decrypting it.
818pub fn inspect_artifact(artifact: &[u8]) -> Result<ProtectedArtifactInfo> {
819    whole_monolith_v0_inspect_artifact(artifact)
820}
821
822#[cfg(feature = "compression")]
823/// Inspect either the current v3 artifact or an explicit legacy VOF2 rotation artifact.
824pub 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")]
833/// Repack a current v3 monolith artifact under a new full-flow configuration.
834pub 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    // Validate attacker-controlled geometry before any caller clones or decodes
1540    // the body. The inversion is constant work even for one-byte cell headers.
1541    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    // A block contains at most `target_cell_size * block_cells` original bytes
1704    // plus one anchor byte. Therefore, for a valid payload, its anchor count is
1705    // exactly ceil(payload_len / (target_cell_size * block_cells + 1)). Once
1706    // that count is known, both the original length and cell count are direct.
1707    // Trying the only two possible block widths keeps inversion O(1), including
1708    // the special 256-byte geometry whose width changes after four cells.
1709    [
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    // Current encoders clamp cells below eight bytes to eight, except when the
1757    // entire non-empty input is itself shorter. They also store the shortened
1758    // input length rather than a cell size larger than the input. Enforcing the
1759    // same canonical form rejects attacker-selected one-byte cells over large
1760    // payloads while preserving every geometry the encoder can emit.
1761    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}