Skip to main content

mediatek_optical/receiver/
layout.rs

1//! Solve wire encoding from ordered receiver operations, not an AES signature.
2//!
3//! This is an offline proof component. Operations must come from a separately
4//! validated, complete receiver path; a layout alone never authorizes I/O.
5//! Scratch header decoding and in-place body decoding have different effects.
6use core::ops::Range;
7
8const BLOCK: usize = 16;
9
10/// An operation whose inputs, range and order the receiver analyzer established.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Operation {
13    /// Decode to temporary storage and compare the result with plaintext.
14    /// Staging memory remains unchanged.
15    CheckDecrypted(Range<usize>),
16    /// Decode staged blocks in place with the transport AES key.
17    DecryptInPlace(Range<usize>),
18    /// A signature, descriptor or integrity check consumes plaintext staging.
19    CheckPlaintext(Range<usize>),
20    /// These staged bytes become the installed image and must be plaintext.
21    Program(Range<usize>),
22}
23
24/// Contradictory or unsupported receiver operations cannot supply a wire plan.
25///
26/// Hosts must keep a wildcard arm; the set of variants may grow.
27///
28/// ```compile_fail,E0004
29/// fn f(e: mediatek_optical::receiver::layout::Error) -> u8 { match e { mediatek_optical::receiver::layout::Error::Alignment { .. } => 0, mediatek_optical::receiver::layout::Error::Range { .. } => 1, mediatek_optical::receiver::layout::Error::Conflict { .. } => 2, mediatek_optical::receiver::layout::Error::MultipleDecryptions { .. } => 3, mediatek_optical::receiver::layout::Error::NoProgram { .. } => 4, mediatek_optical::receiver::layout::Error::ImageSize { .. } => 5, } }
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum Error {
34    /// Canonical images and AES transforms require complete blocks.
35    Alignment,
36    /// An operation escapes the supplied image or has an empty/reversed range.
37    Range,
38    /// The same block is required in both ciphertext and plaintext form.
39    Conflict {
40        /// Byte offset of the conflicting AES block.
41        block: usize,
42    },
43    /// The receiver would decrypt the same block more than once.
44    MultipleDecryptions {
45        /// Byte offset of the block requiring unsupported nested encoding.
46        block: usize,
47    },
48    /// No program operation was supplied, so there is no installed-image proof.
49    NoProgram,
50    /// Encoding was requested for a different-sized image.
51    ImageSize,
52}
53
54impl core::fmt::Display for Error {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        match self {
57            Self::Alignment => f.write_str("receiver encoding requires complete AES blocks"),
58            Self::Range => f.write_str("receiver operation has an empty or out-of-bounds range"),
59            Self::Conflict { block } => write!(f, "conflicting wire encodings at {block:#x}"),
60            Self::MultipleDecryptions { block } => write!(f, "repeated AES decoding at {block:#x}"),
61            Self::NoProgram => f.write_str("receiver layout contains no programming operation"),
62            Self::ImageSize => f.write_str("image size differs from the receiver layout"),
63        }
64    }
65}
66
67impl std::error::Error for Error {}
68
69/// Encoding ranges derived from constraints on every observed/programmed block.
70/// Unconsumed bytes stay canonical. This does not attest to parser completeness.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Layout {
73    size: usize,
74    encrypted: Vec<Range<usize>>,
75    programmed: Vec<Range<usize>>,
76}
77
78impl Layout {
79    /// Contiguous AES-aligned ranges to encrypt once in the outgoing image.
80    pub fn encrypted_ranges(&self) -> &[Range<usize>] {
81        &self.encrypted
82    }
83
84    /// Ranges for which the operation sequence establishes plaintext at write.
85    pub fn programmed_ranges(&self) -> &[Range<usize>] {
86        &self.programmed
87    }
88
89    /// Create wire bytes without changing the canonical image.
90    /// The caller still needs a complete, bound receiver execution contract.
91    pub fn encode(&self, image: &[u8]) -> Result<Vec<u8>, Error> {
92        if image.len() != self.size {
93            return Err(Error::ImageSize);
94        }
95        let mut wire = image.to_vec();
96        for range in &self.encrypted {
97            crate::enc::encrypt(&mut wire[range.clone()]).map_err(|_| Error::Alignment)?;
98        }
99        Ok(wire)
100    }
101}
102
103fn blocks(range: &Range<usize>, size: usize, aligned: bool) -> Result<Range<usize>, Error> {
104    if range.start >= range.end || range.end > size {
105        return Err(Error::Range);
106    }
107    if aligned && (range.start % BLOCK != 0 || range.end % BLOCK != 0) {
108        return Err(Error::Alignment);
109    }
110    Ok(range.start / BLOCK..range.end.div_ceil(BLOCK))
111}
112
113/// Derive the initial encoding required by the ordered checks and writes.
114/// A scratch decode adds a requirement without altering staging state; an
115/// in-place decode changes the requirements of all subsequent consumers.
116pub fn analyze(size: usize, operations: &[Operation]) -> Result<Layout, Error> {
117    if size == 0 || size % BLOCK != 0 {
118        return Err(Error::Alignment);
119    }
120    let mut decoded = vec![false; size / BLOCK];
121    let mut required = vec![None; size / BLOCK];
122    let mut programmed = Vec::new();
123    for operation in operations {
124        let (range, aligned) = match operation {
125            Operation::CheckDecrypted(r) | Operation::DecryptInPlace(r) => (r, true),
126            Operation::CheckPlaintext(r) | Operation::Program(r) => (r, false),
127        };
128        for block in blocks(range, size, aligned)? {
129            if matches!(operation, Operation::DecryptInPlace(_)) {
130                if decoded[block] {
131                    return Err(Error::MultipleDecryptions {
132                        block: block * BLOCK,
133                    });
134                }
135                decoded[block] = true;
136                continue;
137            }
138            let scratch = matches!(operation, Operation::CheckDecrypted(_));
139            if scratch && decoded[block] {
140                return Err(Error::MultipleDecryptions {
141                    block: block * BLOCK,
142                });
143            }
144            let encrypted = scratch || decoded[block];
145            if required[block].is_some_and(|previous| previous != encrypted) {
146                return Err(Error::Conflict {
147                    block: block * BLOCK,
148                });
149            }
150            required[block] = Some(encrypted);
151        }
152        if let Operation::Program(range) = operation {
153            programmed.push(range.clone());
154        }
155    }
156    if programmed.is_empty() {
157        return Err(Error::NoProgram);
158    }
159    let mut encrypted: Vec<Range<usize>> = Vec::new();
160    for (block, value) in required.into_iter().enumerate() {
161        if value != Some(true) {
162            continue;
163        }
164        let start = block * BLOCK;
165        match encrypted.last_mut() {
166            Some(previous) if previous.end == start => previous.end += BLOCK,
167            _ => encrypted.push(start..start + BLOCK),
168        }
169    }
170    Ok(Layout {
171        size,
172        encrypted,
173        programmed,
174    })
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    const SIZE: usize = 0x20_0000;
182    const HEADER: Range<usize> = 0x3000..0x3010;
183
184    #[test]
185    fn scratch_decode_does_not_make_programmed_header_plaintext() {
186        let operations = [
187            Operation::CheckDecrypted(HEADER),
188            Operation::Program(0..SIZE),
189        ];
190        assert_eq!(
191            analyze(SIZE, &operations),
192            Err(Error::Conflict { block: 0x3000 })
193        );
194    }
195
196    #[test]
197    fn boot_integrity_check_also_prevents_header_only_encoding() {
198        let operations = [
199            Operation::CheckDecrypted(HEADER),
200            Operation::CheckPlaintext(0x1000..0xfff0),
201            Operation::Program(0x10000..SIZE),
202        ];
203        assert_eq!(
204            analyze(SIZE, &operations),
205            Err(Error::Conflict { block: 0x3000 })
206        );
207    }
208
209    #[test]
210    fn preserved_unchecked_boot_allows_header_only_encoding() {
211        let operations = [
212            Operation::CheckDecrypted(HEADER),
213            Operation::CheckPlaintext(0x10600..0x10608),
214            Operation::Program(0x10000..SIZE),
215        ];
216        let plan = analyze(SIZE, &operations).unwrap();
217        assert_eq!(plan.encrypted_ranges(), &[HEADER]);
218        let image = vec![0x5a; SIZE];
219        let wire = plan.encode(&image).unwrap();
220        assert_eq!(wire[..HEADER.start], image[..HEADER.start]);
221        assert_ne!(wire[HEADER.clone()], image[HEADER.clone()]);
222        assert_eq!(wire[HEADER.end..], image[HEADER.end..]);
223        let mut header = wire[HEADER.clone()].to_vec();
224        crate::enc::decrypt(&mut header).unwrap();
225        assert_eq!(header, image[HEADER]);
226    }
227
228    #[test]
229    fn full_decode_before_checks_and_programming_requires_full_encryption() {
230        let operations = [
231            Operation::CheckDecrypted(HEADER),
232            Operation::DecryptInPlace(0..SIZE),
233            Operation::CheckPlaintext(0x10600..0x10608),
234            Operation::Program(0..SIZE),
235        ];
236        let plan = analyze(SIZE, &operations).unwrap();
237        assert_eq!(plan.encrypted_ranges().len(), 1);
238        assert_eq!(plan.encrypted_ranges()[0], 0..SIZE);
239        let mut reordered = operations.to_vec();
240        reordered.swap(1, 2);
241        assert_eq!(
242            analyze(SIZE, &reordered),
243            Err(Error::Conflict { block: 0x10600 })
244        );
245    }
246
247    #[test]
248    fn plaintext_and_body_only_are_distinct_constraints() {
249        let plain = analyze(SIZE, &[Operation::Program(0..SIZE)]).unwrap();
250        assert!(plain.encrypted_ranges().is_empty());
251        let body = analyze(
252            SIZE,
253            &[
254                Operation::CheckPlaintext(HEADER),
255                Operation::DecryptInPlace(0x10000..SIZE),
256                Operation::Program(0x10000..SIZE),
257            ],
258        )
259        .unwrap();
260        assert_eq!(body.encrypted_ranges().len(), 1);
261        assert_eq!(body.encrypted_ranges()[0], 0x10000..SIZE);
262    }
263
264    #[test]
265    fn reject_unbounded_unaligned_repeated_and_incomplete_operations() {
266        assert_eq!(
267            analyze(SIZE, &[Operation::Program(0..SIZE + 1)]),
268            Err(Error::Range)
269        );
270        assert_eq!(
271            analyze(SIZE, &[Operation::CheckDecrypted(1..17)]),
272            Err(Error::Alignment)
273        );
274        assert_eq!(
275            analyze(SIZE, &[Operation::CheckPlaintext(HEADER)]),
276            Err(Error::NoProgram)
277        );
278        assert_eq!(
279            analyze(
280                SIZE,
281                &[
282                    Operation::DecryptInPlace(HEADER),
283                    Operation::CheckDecrypted(HEADER),
284                ]
285            ),
286            Err(Error::MultipleDecryptions { block: 0x3000 })
287        );
288    }
289}