1use aes::{cipher::KeyInit, Aes256};
4use cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, BlockSizeUser};
5use ecb::{Decryptor, Encryptor};
6use miette::Diagnostic;
7use std::fmt::{Display, Formatter, Result as FmtResult};
8use thiserror::Error;
9
10type Aes256EcbEnc = Encryptor<Aes256>;
11type Aes256EcbDec = Decryptor<Aes256>;
12
13const STOCK_FW_KEY: [u8; 32] = [
15 0xA9, 0xFE, 0x4F, 0x78, 0x26, 0x3A, 0xE0, 0xE0, 0xC8, 0xFF, 0x39, 0x95, 0xE4, 0x43, 0x1F, 0x74,
16 0x87, 0x9D, 0x1C, 0x67, 0x04, 0x29, 0xBC, 0x79, 0xA5, 0xE3, 0x35, 0x47, 0x8A, 0x60, 0x3B, 0x22,
17];
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum MIONFirmwareType {
23 Fpga,
25 Ipl,
28 Mion,
30}
31impl Display for MIONFirmwareType {
32 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
33 match *self {
34 Self::Fpga => write!(fmt, "fpga"),
35 Self::Ipl => write!(fmt, "ipl"),
36 Self::Mion => write!(fmt, "fw"),
37 }
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct MIONFirmwareFile {
54 data: Vec<u8>,
57 fw_type: MIONFirmwareType,
59 version_bytes: [u8; 4],
64 unk_byte: u8,
66 checksum: u8,
68}
69
70impl MIONFirmwareFile {
71 pub fn parse(
100 firmware: &[u8],
101 firmware_type: MIONFirmwareType,
102 ) -> Result<Self, MIONFirmwareAPIError> {
103 let firmware_length = firmware.len();
104 if firmware_length < 0x26 {
105 return Err(MIONFirmwareAPIError::TooSmall(firmware_length));
106 }
107 let chksum_in_file = firmware[firmware_length - 1];
108 let got_chksum = calculate_checksum(firmware);
109 if chksum_in_file != got_chksum {
110 return Err(MIONFirmwareAPIError::BadChecksum(
111 chksum_in_file,
112 got_chksum,
113 ));
114 }
115 if matches!(firmware_type, MIONFirmwareType::Mion) && firmware[firmware_length - 3] != 0x00
120 {
121 return Err(MIONFirmwareAPIError::MissingNULTerminator(
122 firmware[firmware_length - 3],
123 ));
124 }
125 let decrypted = raw_decrypt(&firmware[..firmware_length - 6])?;
126
127 let expected_footer = match firmware_type {
128 MIONFirmwareType::Fpga => b"PWI-SS_FP_IMAGE\0",
129 _ => b"PWI-SS_FW_IMAGE\0",
130 };
131 if !decrypted.ends_with(expected_footer) {
132 return Err(MIONFirmwareAPIError::MissingSignature);
133 }
134
135 Ok(Self {
136 data: decrypted,
137 fw_type: firmware_type,
138 version_bytes: [
139 firmware[firmware_length - 6],
140 firmware[firmware_length - 5],
141 firmware[firmware_length - 4],
142 firmware[firmware_length - 3],
143 ],
144 unk_byte: firmware[firmware_length - 2],
145 checksum: chksum_in_file,
146 })
147 }
148
149 #[must_use]
151 pub const fn checksum(&self) -> u8 {
152 self.checksum
153 }
154
155 #[must_use]
157 pub const fn contents(&self) -> &Vec<u8> {
158 &self.data
159 }
160
161 #[must_use]
163 pub const fn firmware_type(&self) -> MIONFirmwareType {
164 MIONFirmwareType::Mion
165 }
166
167 #[allow(
170 clippy::missing_panics_doc,
174 )]
175 #[must_use]
176 pub fn get_deployable_firmware_data(&self) -> Vec<u8> {
177 let mut encrypted =
178 raw_encrypt(&self.data).expect("We validate the block size at parse time.");
179 encrypted.extend_from_slice(&self.version_bytes);
180 encrypted.push(self.unk_byte);
181 encrypted.push(self.checksum);
182 encrypted
183 }
184
185 #[must_use]
194 pub fn version(&self) -> String {
195 match self.fw_type {
196 MIONFirmwareType::Mion => format!(
197 "0.{:02}.{}.{}",
198 self.version_bytes[0], self.version_bytes[1], self.version_bytes[2],
199 ),
200 MIONFirmwareType::Fpga => format!(
201 "{:02X}{:02X}{:02X}{:02X}",
202 self.version_bytes[3],
203 self.version_bytes[2],
204 self.version_bytes[1],
205 self.version_bytes[0],
206 ),
207 MIONFirmwareType::Ipl => format!(
208 "{}.{}",
209 u16::from_le_bytes([self.version_bytes[0], self.version_bytes[1]]),
210 u16::from_le_bytes([self.version_bytes[2], self.version_bytes[3]]),
211 ),
212 }
213 }
214}
215
216#[derive(Error, Diagnostic, Debug, PartialEq, Eq)]
218pub enum MIONFirmwareAPIError {
219 #[error("We could not encrypt your data, because it was not padded to the correct length, expected a block size of: {0}")]
221 #[diagnostic(code(cat_dev::api::mion::firmware::bad_decrypted_data_length))]
222 BadDecryptedDataLength(usize),
223 #[error("We could not decrypt your data, because it was not padded to the correct length, expected a block size of: {0}")]
225 #[diagnostic(code(cat_dev::api::mion::firmware::bad_encrypted_data_length))]
226 BadEncryptedDataLength(usize),
227 #[error("The MION Firmware file you provided had an invalid checksum, we expected: {1:02x}, but got: {0:02x}")]
231 #[diagnostic(code(cat_dev::api::mion::firmware::bad_checksum))]
232 BadChecksum(u8, u8),
233 #[error("The MION Firmware file provided was too small, it must be at least 0x26 bytes long, was {0:02x}")]
238 #[diagnostic(code(cat_dev::api::mion::firmware::too_small))]
239 TooSmall(usize),
240 #[error("The Version String for MION Firmware Files Typed 'MION', must have their version bytes end with a NUL terminator (0x00) due to an oversight in programming. Your file ended with: ({0:02x})")]
243 #[diagnostic(code(cat_dev::api::mion::firmware::missing_nul_terminator))]
244 MissingNULTerminator(u8),
245 #[error("While validating the decrypted contents of your FW we were not able to identify the required ending bytes, this firmware is corrupt.")]
252 #[diagnostic(code(cat_dev::api::mion_fw::missing_signature))]
253 MissingSignature,
254}
255
256fn calculate_checksum(encrypted_blob: &[u8]) -> u8 {
268 let mut chksum = 0_u32;
269 for byte in encrypted_blob.iter().take(encrypted_blob.len() - 1) {
272 chksum = chksum.wrapping_add((*byte).into());
273 }
274 while chksum & 0xFFFF_FF00_u32 != 0 {
275 chksum = (chksum & 0xFF) + (chksum >> 8);
276 }
277 u8::try_from(!chksum & 0xFF)
278 .expect("&0xFF did not just give us the last 8 bits??? is math broken?")
279}
280
281#[doc(hidden)]
288pub fn raw_encrypt(file_contents: &[u8]) -> Result<Vec<u8>, MIONFirmwareAPIError> {
289 let encryptor = Aes256EcbEnc::new(&STOCK_FW_KEY.into());
290 let mut decrypted = vec![
291 0x0;
292 Aes256EcbEnc::block_size()
293 * (file_contents.len() / Aes256EcbEnc::block_size() + 1)
294 ];
295 let actual_len = encryptor
296 .encrypt_padded_b2b_mut::<NoPadding>(file_contents, &mut decrypted)
297 .map_err(|_| MIONFirmwareAPIError::BadDecryptedDataLength(Aes256EcbEnc::block_size()))?
298 .len();
299 decrypted.truncate(actual_len);
300 Ok(decrypted)
301}
302
303#[doc(hidden)]
309pub fn raw_decrypt(file_contents: &[u8]) -> Result<Vec<u8>, MIONFirmwareAPIError> {
310 let decryptor = Aes256EcbDec::new(&STOCK_FW_KEY.into());
311
312 decryptor
313 .decrypt_padded_vec_mut::<NoPadding>(file_contents)
314 .map_err(|_| MIONFirmwareAPIError::BadEncryptedDataLength(Aes256EcbDec::block_size()))
315}
316
317#[cfg(test)]
318mod unit_tests {
319 use super::*;
320 use std::path::PathBuf;
321
322 #[must_use]
323 pub fn get_test_data_path(relative_to_test_data: &str) -> PathBuf {
324 let mut final_path = PathBuf::from(
325 std::env::var("CARGO_MANIFEST_DIR")
326 .expect("Failed to read `CARGO_MANIFEST_DIR` to locate t est files!"),
327 );
328 final_path.push("src");
329 final_path.push("mion");
330 final_path.push("test-data");
331 for file_part in relative_to_test_data.split('/') {
332 if file_part.is_empty() {
333 continue;
334 }
335 final_path.push(file_part);
336 }
337 final_path
338 }
339
340 #[test]
341 pub fn can_decrypt_and_reencrypt_fw() {
342 for (source_file_name, dest_file_name) in vec![
343 ("/fpga.13052071.bin", "/fpga.13052071_d.bin"),
344 ("/fw.0.00.14.80.bin", "/fw.0.00.14.80_d.bin"),
345 ("/ipl.0.5.bin", "/ipl.0.5_d.bin"),
346 ] {
347 let encrypted_path = get_test_data_path(source_file_name);
348 let decrypted_path = get_test_data_path(dest_file_name);
349
350 let full_encrypted_contents =
351 std::fs::read(&encrypted_path).expect("Failed to read encrypted file to decrypt!");
352 let decrypted =
353 raw_decrypt(&full_encrypted_contents[..]).expect("Failed to decrypt data!");
354 let expected_decrypted_contents = std::fs::read(&decrypted_path)
355 .expect("Failed to read expected decrypted contents!");
356
357 assert_eq!(
358 decrypted.len(),
359 expected_decrypted_contents.len(),
360 "Decrypted data length did not match expected decrypted data length, file: {}",
361 encrypted_path.display(),
362 );
363 for (idx, byte) in decrypted.iter().enumerate() {
364 if *byte != expected_decrypted_contents[idx] {
365 panic!(
366 "Decrypted Byte at Location: {idx} did not match expected contents! (total: {})",
367 decrypted.len(),
368 );
369 }
370 }
371
372 let re_encrypted = raw_encrypt(&decrypted).expect("Failed to encrypt firmware!");
373 assert_eq!(
374 re_encrypted.len(),
375 full_encrypted_contents.len(),
376 "Encrypted data length did not match expected encrypted data length!",
377 );
378 for (idx, byte) in re_encrypted.iter().enumerate() {
379 if *byte != full_encrypted_contents[idx] {
380 panic!(
381 "Re-Encrypted Byte at Location: {idx} did not match expected contents! (total: {})",
382 re_encrypted.len(),
383 );
384 }
385 }
386 }
387 }
388
389 #[test]
390 pub fn correctly_calculates_checksums() {
391 for (file_path, footer_data, name, expected_value) in vec![
425 (
426 "/fw.0.00.14.80.bin",
427 vec![0x00_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
428 "$encrypted.0.14.80.0.$0xA1",
429 0x21_u8,
430 ),
431 (
432 "/fw.0.00.14.80.bin",
433 vec![0x01_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
434 "$encrypted.1.14.80.0.$0xA1",
435 0x20_u8,
436 ),
437 (
438 "/fw.0.00.14.80.bin",
439 vec![0xFF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
440 "$encrypted.255.14.80.0.$0xA1",
441 0x21_u8,
442 ),
443 (
444 "/fw.0.00.14.80.bin",
445 vec![0xCF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
446 "$encrypted.207.14.80.0.$0xA1",
447 0x51_u8,
448 ),
449 ] {
450 let mut encrypted_contents = std::fs::read(get_test_data_path(file_path))
451 .expect("Failed to read test data file!");
452 encrypted_contents.extend(footer_data);
453 assert_eq!(
454 calculate_checksum(&encrypted_contents),
455 expected_value,
456 "Checksum did not match for named contents: {name}! Please check checksum code!",
457 );
458 }
459 }
460
461 #[test]
462 pub fn can_successfully_parse_real_fw_file() {
463 let mut mion_fw = std::fs::read(get_test_data_path("/fw.0.00.14.80.bin"))
464 .expect("Failed to read encrypted MION FW!");
465 let mut ipl_fw = std::fs::read(get_test_data_path("/ipl.0.5.bin"))
466 .expect("Failed to read encrypted IPL FW!");
467 let mut fpga_fw = std::fs::read(get_test_data_path("/fpga.13052071.bin"))
468 .expect("Failed to read encrypted FPGA FW!");
469 mion_fw.extend([0x00, 0x0E, 0x50, 0x00, 0xA1, 0x21]);
471 ipl_fw.extend([0x00, 0x00, 0x05, 0x00, 0xA1, 0x3E]);
472 fpga_fw.extend([0x71, 0x20, 0x05, 0x13, 0xA2, 0xBF]);
473
474 let parsed_mion = MIONFirmwareFile::parse(&mion_fw, MIONFirmwareType::Mion)
475 .expect("Failed to parse MION firmware!");
476 let parsed_ipl = MIONFirmwareFile::parse(&ipl_fw, MIONFirmwareType::Ipl)
477 .expect("Failed to parse IPL firmware!");
478 let parsed_fpga = MIONFirmwareFile::parse(&fpga_fw, MIONFirmwareType::Fpga)
479 .expect("Failed to parse FPGA firmware!");
480
481 assert_eq!(parsed_mion.version(), "0.00.14.80");
482 assert_eq!(parsed_ipl.version(), "0.5");
483 assert_eq!(parsed_fpga.version(), "13052071");
484 }
485}