cat_dev/mion/
firmware.rs

1//! APIs for interacting with MION Firmware Files.
2
3use 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
13/// The derived AES key used for firmware encryption/decryption.
14const 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/// The MION actually contains multiple types of firmware, this enum encodes
20/// those types.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum MIONFirmwareType {
23	/// The MION has an FPGA array, these firmware files target those.
24	Fpga,
25	/// The MION has an IPL chip similar to the gamecube, these firmware files
26	/// target that chip.
27	Ipl,
28	/// The root firmware for the main MION board.
29	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/// A MION Firmware File.
42///
43/// The MION itself has 3 types of firmware:
44///
45/// - "fpga.${version}.bin" aka [`MIONFirmwareType::Fpga`]
46/// - "fw.${version}.bin" aka [`MIONFirmwareType::Ipl`]
47/// - "ipl.${version}.bin" aka [`MIONFirmwareType::Mion`]
48///
49/// Each of these are used for separate part of firmware, but all follow the
50/// same format. An encrypted blob, followed by a 4 byte version string,
51/// followed by a checksum.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct MIONFirmwareFile {
54	/// The actual firmware contents, this is *decrypted* when represented in
55	/// memory here.
56	data: Vec<u8>,
57	/// The type of firmware this is.
58	fw_type: MIONFirmwareType,
59	/// The 4 bytes that represent versions.
60	///
61	/// Each firmware type treats these bytes _slightly_
62	/// differently. Thanks Nintendo.
63	version_bytes: [u8; 4],
64	/// I have no clue what this byte is.
65	unk_byte: u8,
66	/// The checksum byte of everything else in the file. Very last byte.
67	checksum: u8,
68}
69
70impl MIONFirmwareFile {
71	/// Attempt to parse a firmware file that would be uploaded to the MION
72	/// board.
73	///
74	/// These packages would be manually uploaded to:
75	/// `http://<mionip>/update.cgi`.
76	///
77	/// ## Errors
78	///
79	/// This function will error if the original MION firmwares would throw an
80	/// error at this package, or would otherwise corrupt when installing it.
81	/// This consistetues the following checks:
82	///
83	/// - Validating the file is at least 0x26 (38) bytes long
84	///   (*note: the mion only checks for 0x16 but will fail because the AES
85	///   block size is 32 bytes.*)
86	/// - The file has an invalid checksum (the checksum is the last byte in
87	///   the file.)
88	/// - If uploading a firmware type of [`MIONFirmwareType::Mion`], the last
89	///   byte in the version must be 0x00, if it's not the upload will corrupt
90	///   heavily as the programmers treated it as a load-bearing NUL terminator
91	///   which is required for strings in C (the language the FW was written
92	///   in).
93	/// - If the main contents of the file were not encrypted with the common AES
94	///   key.
95	/// - If the decrypted contents do not end with the correct signature, this is
96	///   `PWI-SS_FW_IMAGE` for [`MionFirmwareType::Mion`] &
97	///   [`MionFirmwareType::Ipl`], and `PWI-SS_FP_IMAGE` for
98	///   [`MionFirmwareType::FPGA`].
99	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		// This check is uniquely ours, the MION does not have this.
116		//
117		// But this really fucks up a lot, because it's a load bearing NUL
118		// terminator.
119		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	/// Get the checksum for this firmware file.
150	#[must_use]
151	pub const fn checksum(&self) -> u8 {
152		self.checksum
153	}
154
155	/// Get the underlying decrypted contents of this firmware file.
156	#[must_use]
157	pub const fn contents(&self) -> &Vec<u8> {
158		&self.data
159	}
160
161	/// Get the type of firmware this is.
162	#[must_use]
163	pub const fn firmware_type(&self) -> MIONFirmwareType {
164		MIONFirmwareType::Mion
165	}
166
167	/// Get the bytes that would need to be written to a file, and uploaded to
168	/// `/update.cgi` on the MION to deploy this firmware file.
169	#[allow(
170		// This function can't actually panic, it's just raw_encrypt doesn't know
171		// we've pre-validated the length by decrypting it successfully and not
172		// offering mutable APIs.
173		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	/// Get the version for the actual firmware file.
186	///
187	/// Each firmware type has a slightly different format than the rest of the
188	/// firmware types. An example of known following firmware types are:
189	///
190	/// - [`MIONFirmwareType::Mion`] -> `0.00.14.80`
191	/// - [`MIONFirmwareType::Fpga`] -> `13052071`
192	/// - [`MIONFirmwareType::Ipl`] -> `0.5`
193	#[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/// Errors related to handling mion firmwares.
217#[derive(Error, Diagnostic, Debug, PartialEq, Eq)]
218pub enum MIONFirmwareAPIError {
219	/// You attempted to encrypt data that we could not encrypt.
220	#[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	/// You attempted to decrypt data that we could not decrypt.
224	#[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	/// The MION Firmware files end with a final byte that acts as a checksum
228	/// to validate the content before it was correct. Your checksum was not
229	/// correct.
230	#[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	/// All MION Firmware files must be at a minimum 0x26 bytes long.
234	///
235	/// This covers a single AES-256 block (32 bytes), plus the 6 byte footer
236	/// that they contain.
237	#[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	/// The MION Firmware version string must end with a `0x00`, as this is a
241	/// load-bearing NUL terminator for many parts of the firmware.
242	#[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	/// All MION FW files must end with:
246	///
247	/// - `PWI-SS_FW_IMAGE` for IPL/MION firmware types
248	/// - `PWI-SS_FP_IMAGE` for FPGA firmware types.
249	///
250	/// If they do not, they are immediately considered invalid.
251	#[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
256/// Calculate a checksum for a given _encrypted_ blob.
257///
258/// The checksum present as the last byte in the file takes in all the content
259/// before the final byte of the encrypted file, and spits out a single `u8`
260/// that represents the final checksum of the firmware.
261///
262/// ## Panics
263///
264/// This function should never panic, however there is an `expect` incase math
265/// ever fundamenetally breaks and performing a `& 0xFF` returns us _more_ than
266/// the last 8 bits.
267fn calculate_checksum(encrypted_blob: &[u8]) -> u8 {
268	let mut chksum = 0_u32;
269	// For every byte except the last byte (CHKSUM), use it's value to calculate
270	// the checksum.
271	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/// Encrypt a firmware files contents so it can be uploaded.
282///
283/// ## Errors
284///
285/// If there is a problem encrypting your data. See error codes
286/// from the [`aes`], and [`ecb`] crates.
287#[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/// Decrypt a firmware files contents so it can be uploaded.
304///
305/// ## Errors
306///
307/// If your data is not the correct size to be decrypted.
308#[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		// These test constants were all taken from real life MION files.
392		//
393		// You can see what the MION would "expect" by connecting to a running
394		// MION device with telnet (the username is `mion`, and password is
395		// `/Multi_I/O_Network/`, the same as the HTTP interface.) Then run the
396		// following commands:
397		//
398		// ```bash
399		// INET> enable_telnet_trace
400		// Telnet port is ENABLED to transmit debug logs...
401		//
402		// INET> enable_debug SATA_H|SATA_D|FPGA|PCIE|EEPROM|HTTP|FWUPD|FPUPD|BIOS|KERNEL
403		// enable_debug SATA_H|SATA_D|FPGA|PCIE|EEPROM|HTTP|FWUPD|FPUPD|BIOS|KERNEL modules are enabled to produce debug logs...
404		// ```
405		//
406		// Then upload a firmware file with a purposefully bad checksum (the last
407		// byte in the file). And in your existing telnet session run:
408		//
409		// ```bash
410		// INET> task_trace
411		// [...snipped...]
412		// 9, TS - 191574, Msg - *** ERR: [HTTP]  >Checck sum : Calc, Image 21, 20
413		//
414		// 9, TS - 193977, Msg - === INF: [HTTP]  >_http_update_fw_html Enter, seq = -2, Mode = Firmware
415		// ```
416		//
417		// The numbers will most likely be different at the beginning but what we
418		// care about is the: `Checck sum : Calc, Image 21, 20` line. In this case
419		// it's letting us know it calculated a checksum of 0x21, but the file had
420		// a checksum of 0x20. So it would have showed the error page.
421		//
422		// You can use this to validate the output of any file.
423
424		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		// Append the real life footers that would've been on these files.
470		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}