Skip to main content

mediatek_optical/image/
cmac.rs

1//! The AES-CMAC integrity table.
2//!
3//! Every MT1959 and MT1939 image carries a 16-entry integrity table at file
4//! offset `0x10400` ([`crate::layout::CMAC_TABLE`]). Each entry is 28 bytes, little-endian:
5//!
6//! ```text
7//! struct cmac_entry {          // Python struct '<3L16s', size 28
8//!     u32 enabled;             // 0x00000001 = active, 0xFFFFFFFF = unused
9//!     u32 start;               // file offset, inclusive
10//!     u32 end;                 // file offset, inclusive
11//!     u8  cmac[16];            // stored digest (byte-reversed, see below)
12//! };
13//! ```
14//!
15//! Digest recipe (reproduced byte-for-byte against real stock images):
16//! 1. Read bytes `[start ..= end]`.
17//! 2. Byte-reverse each 16-byte block in place.
18//! 3. `digest = AES-CMAC(KEY, reversed_data)`.
19//! 4. Byte-reverse the 16-byte digest and store it at `entry + 12`.
20//!
21//! The CMAC key is symmetric and public (integrity, not confidentiality):
22//! `BD209408E35E526A36235234434FE8AB`. Anyone with the key can re-sign; the MAC
23//! exists to reject accidental corruption and unsigned third-party images.
24//!
25//! Re-sign ordering matters: the entry whose range covers the table region
26//! (`0x10400`) hashes the *other* entries' digest bytes, so it must be computed
27//! last, after every data range has been written back.
28
29use aes::Aes128;
30use cmac::{Cmac, Mac};
31
32/// Public AES-CMAC integrity key, shared by MT1959 and MT1939 firmware.
33pub const CMAC_KEY: [u8; 16] = [
34    0xBD, 0x20, 0x94, 0x08, 0xE3, 0x5E, 0x52, 0x6A, 0x36, 0x23, 0x52, 0x34, 0x43, 0x4F, 0xE8, 0xAB,
35];
36
37/// File offset of the integrity table.
38pub const TABLE_OFFSET: usize = crate::layout::CMAC_TABLE.start;
39/// Number of table entries.
40pub const ENTRY_COUNT: usize = 16;
41/// Size of a single table entry in bytes.
42pub const ENTRY_SIZE: usize = 28;
43/// `enabled` sentinel for an active entry.
44pub const ENABLED: u32 = 0x0000_0001;
45
46/// A parsed integrity-table entry.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct CmacEntry {
49    /// Index within the table (0..16).
50    pub index: usize,
51    /// File offset of this entry's 28 bytes.
52    pub entry_off: usize,
53    /// `enabled` word.
54    pub enabled: u32,
55    /// Inclusive start file offset of the covered range.
56    pub start: u32,
57    /// Inclusive end file offset of the covered range.
58    pub end: u32,
59    /// Stored (byte-reversed) digest as it sits in the file.
60    pub stored: [u8; 16],
61}
62
63impl CmacEntry {
64    /// Whether this entry participates in verification / signing.
65    pub fn is_active(&self) -> bool {
66        self.enabled == ENABLED
67    }
68
69    /// Whether this entry's covered range includes the integrity table itself.
70    pub fn covers_table(&self) -> bool {
71        (self.start as usize) <= TABLE_OFFSET && (self.end as usize) >= TABLE_OFFSET
72    }
73}
74
75fn read_u32_le(buf: &[u8], off: usize) -> u32 {
76    u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
77}
78
79/// Parse all 16 table entries. Returns an error if the image is too small.
80pub fn parse_table(image: &[u8]) -> Result<Vec<CmacEntry>, CmacError> {
81    let table_end = TABLE_OFFSET + ENTRY_COUNT * ENTRY_SIZE;
82    if image.len() < table_end {
83        return Err(CmacError::ImageTooSmall {
84            need: table_end,
85            got: image.len(),
86        });
87    }
88    let mut out = Vec::with_capacity(ENTRY_COUNT);
89    for index in 0..ENTRY_COUNT {
90        let entry_off = TABLE_OFFSET + index * ENTRY_SIZE;
91        let enabled = read_u32_le(image, entry_off);
92        let start = read_u32_le(image, entry_off + 4);
93        let end = read_u32_le(image, entry_off + 8);
94        let mut stored = [0u8; 16];
95        stored.copy_from_slice(&image[entry_off + 12..entry_off + 28]);
96        out.push(CmacEntry {
97            index,
98            entry_off,
99            enabled,
100            start,
101            end,
102            stored,
103        });
104    }
105    Ok(out)
106}
107
108/// Reverse each 16-byte block of `data` in place (final short block reversed too).
109fn reverse_blocks(data: &mut [u8]) {
110    for chunk in data.chunks_mut(16) {
111        chunk.reverse();
112    }
113}
114
115/// Compute the stored-form (byte-reversed) digest for a covered range.
116///
117/// Returns the 16 bytes exactly as they should appear in the file at `entry+12`.
118pub fn compute_stored_digest(image: &[u8], start: u32, end: u32) -> Result<[u8; 16], CmacError> {
119    let start = start as usize;
120    let end = end as usize;
121    if end < start || end >= image.len() {
122        return Err(CmacError::BadRange { start, end });
123    }
124    let mut data = image[start..=end].to_vec();
125    reverse_blocks(&mut data);
126
127    let mut mac = <Cmac<Aes128> as Mac>::new_from_slice(&CMAC_KEY)
128        .expect("AES-128 key length is always valid for CMAC");
129    mac.update(&data);
130    let tag = mac.finalize().into_bytes();
131
132    let mut digest = [0u8; 16];
133    digest.copy_from_slice(&tag);
134    digest.reverse(); // step 4: byte-reverse the digest before storing
135    Ok(digest)
136}
137
138/// Result of verifying one entry.
139#[derive(Debug, Clone, Copy)]
140pub struct EntryVerdict {
141    /// The entry that was checked.
142    pub entry: CmacEntry,
143    /// Freshly computed stored-form digest.
144    pub computed: [u8; 16],
145    /// Whether `computed == entry.stored`.
146    pub matches: bool,
147}
148
149/// Verify every active entry. Returns a per-entry verdict list.
150pub fn verify_detailed(image: &[u8]) -> Result<Vec<EntryVerdict>, CmacError> {
151    let table = parse_table(image)?;
152    let mut verdicts = Vec::new();
153    for entry in table {
154        if !entry.is_active() {
155            continue;
156        }
157        let computed = compute_stored_digest(image, entry.start, entry.end)?;
158        verdicts.push(EntryVerdict {
159            matches: computed == entry.stored,
160            entry,
161            computed,
162        });
163    }
164    Ok(verdicts)
165}
166
167/// Return `true` iff every active entry's stored digest matches a fresh compute.
168pub fn verify(image: &[u8]) -> bool {
169    match verify_detailed(image) {
170        Ok(v) => !v.is_empty() && v.iter().all(|e| e.matches),
171        Err(_) => false,
172    }
173}
174
175/// Recompute and write back every active entry's digest, returning the new image.
176///
177/// Data ranges are signed first; any entry that covers the table region
178/// (`0x10400`) is signed last, over the already-updated table bytes.
179pub fn resign(image: &[u8]) -> Result<Vec<u8>, CmacError> {
180    let mut img = image.to_vec();
181    let table = parse_table(&img)?;
182
183    // Pass 1: data ranges (those that do not cover the table).
184    for entry in table.iter().filter(|e| e.is_active() && !e.covers_table()) {
185        let digest = compute_stored_digest(&img, entry.start, entry.end)?;
186        img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
187    }
188    // Pass 2: table-covering ranges, over the now-updated table bytes.
189    for entry in table.iter().filter(|e| e.is_active() && e.covers_table()) {
190        let digest = compute_stored_digest(&img, entry.start, entry.end)?;
191        img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
192    }
193    Ok(img)
194}
195
196/// Errors from the CMAC layer.
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum CmacError {
200    /// Image is smaller than the integrity table requires.
201    ImageTooSmall {
202        /// Required minimum length.
203        need: usize,
204        /// Actual length.
205        got: usize,
206    },
207    /// A table entry described an out-of-bounds or inverted range.
208    BadRange {
209        /// Range start.
210        start: usize,
211        /// Range end.
212        end: usize,
213    },
214}
215
216impl std::fmt::Display for CmacError {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        match self {
219            CmacError::ImageTooSmall { need, got } => {
220                write!(f, "image too small: need {need} bytes, got {got}")
221            }
222            CmacError::BadRange { start, end } => {
223                write!(f, "bad CMAC range: start=0x{start:x} end=0x{end:x}")
224            }
225        }
226    }
227}
228
229impl std::error::Error for CmacError {}