From a0808a7081effc8d56da255ed460c6908ec22a20 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Tue, 2 Jan 2024 14:02:55 +0700 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 418 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 ++ LICENSE | 5 + README.md | 19 +++ src/icf.rs | 365 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 69 +++++++++ 7 files changed, 892 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/icf.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..056f24b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,418 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "binary-reader" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d173c51941d642588ed6a13d464617e3a9176b8fe00dc2de182434c36812a5e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icf-reader" +version = "0.1.0" +dependencies = [ + "aes", + "anyhow", + "binary-reader", + "cbc", + "chrono", + "crc32fast", + "hex-literal", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..47b1eb7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "icf-reader" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aes = "0.8.3" +anyhow = "1.0.75" +binary-reader = "0.4.5" +cbc = { version = "0.1.2", features = ["std"] } +chrono = "0.4.31" +crc32fast = "1.3.2" +hex-literal = "0.4.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8996204 --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +Copyright (C) 2023 by beerpsi beerpsi@noreply.gitea.tendokyu.moe + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..52f3595 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# icf-reader + +Tools for decoding, decrypting and encrypting ICFs + +## Usage +```shell +icf-reader +icf-reader +``` + +Encrypting the ICF will also correct CRC checksums, so worrying about the +correct checksum is not needed. + +## Building +``` +ICF_KEY= ICF_IV= cargo build --release + +ls target/release/icf-reader.exe +``` diff --git a/src/icf.rs b/src/icf.rs new file mode 100644 index 0000000..b30a8ee --- /dev/null +++ b/src/icf.rs @@ -0,0 +1,365 @@ +use std::fmt::Display; + +use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit, BlockEncryptMut}; +use anyhow::{anyhow, Result}; +use binary_reader::{BinaryReader, Endian}; +use chrono::{NaiveDate, NaiveDateTime}; + +type Aes128CbcDec = cbc::Decryptor; +type Aes128CbcEnc = cbc::Encryptor; + +pub const ICF_KEY: [u8; 16] = hex_literal::decode(&[env!("ICF_KEY").as_bytes()]); +pub const ICF_IV: [u8; 16] = hex_literal::decode(&[env!("ICF_IV").as_bytes()]); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Version { + pub major: u16, + pub minor: u8, + pub build: u8, +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{:0>2}.{:0>2}", self.major, self.minor, self.build) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IcfInnerData { + pub id: String, + pub version: Version, + pub required_system_version: Version, + pub datetime: NaiveDateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IcfOptionData { + pub app_id: String, + pub option_id: String, + pub required_system_version: Version, + pub datetime: NaiveDateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IcfPatchData { + pub id: String, + pub source_version: Version, + pub target_version: Version, + pub required_system_version: Version, + pub datetime: NaiveDateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IcfData { + System(IcfInnerData), + App(IcfInnerData), + Patch(IcfPatchData), + Option(IcfOptionData), +} + +pub fn decrypt_icf(data: &mut [u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result> { + let size = data.len(); + + let mut decrypted = Vec::with_capacity(size); + + for i in (0..size).step_by(4096) { + let from_start = i; + + let bufsz = std::cmp::min(4096, size - from_start); + let buf = &data[i..i + bufsz]; + let mut decbuf = vec![0; bufsz]; + + let cipher = Aes128CbcDec::new_from_slices(key.as_ref(), iv.as_ref())?; + cipher + .decrypt_padded_b2b_mut::(buf, &mut decbuf) + .map_err(|err| anyhow!(err))?; + + let xor1 = u64::from_le_bytes(decbuf[0..8].try_into()?) ^ (from_start as u64); + let xor2 = u64::from_le_bytes(decbuf[8..16].try_into()?) ^ (from_start as u64); + + decrypted.extend(xor1.to_le_bytes()); + decrypted.extend(xor2.to_le_bytes()); + decrypted.extend(&decbuf[16..]); + } + + Ok(decrypted) +} + +pub fn encrypt_icf(data: &[u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result> { + let size = data.len(); + + let mut encrypted = Vec::with_capacity(size); + + for i in (0..size).step_by(4096) { + let from_start = i; + + let bufsz = std::cmp::min(4096, size - from_start); + let buf = &data[i..i + bufsz]; + let mut to_be_encrypted = Vec::with_capacity(bufsz); + let mut encbuf = vec![0; bufsz]; + + let xor1 = u64::from_le_bytes(buf[0..8].try_into()?) ^ (from_start as u64); + let xor2 = u64::from_le_bytes(buf[8..16].try_into()?) ^ (from_start as u64); + + to_be_encrypted.extend(xor1.to_le_bytes()); + to_be_encrypted.extend(xor2.to_le_bytes()); + to_be_encrypted.extend(&buf[16..]); + + let cipher = Aes128CbcEnc::new_from_slices(key.as_ref(), iv.as_ref())?; + cipher + .encrypt_padded_b2b_mut::(&to_be_encrypted, &mut encbuf) + .map_err(|err| anyhow!(err))?; + + encrypted.extend(encbuf); + } + + Ok(encrypted) +} + +pub fn decode_icf_datetime_version( + rd: &mut BinaryReader, +) -> Result<(NaiveDateTime, Version)> { + let datetime = NaiveDate::from_ymd_opt( + rd.read_i16()? as i32, + rd.read_u8()? as u32, + rd.read_u8()? as u32, + ) + .ok_or(anyhow!("Invalid date"))? + .and_hms_milli_opt( + rd.read_u8()? as u32, + rd.read_u8()? as u32, + rd.read_u8()? as u32, + rd.read_u8()? as u32, + ) + .ok_or(anyhow!("Invalid time"))?; + + let required_system_version = Version { + build: rd.read_u8()?, + minor: rd.read_u8()?, + major: rd.read_u16()?, + }; + + Ok((datetime, required_system_version)) +} + +pub fn decode_icf_version( + rd: &mut BinaryReader, +) -> Result { + let version = Version { + build: rd.read_u8()?, + minor: rd.read_u8()?, + major: rd.read_u16()?, + }; + + Ok(version) +} + +/// Fixes incorrect CRC32s caused by hex editing the ICF +pub fn fixup_icf(data: &mut [u8]) -> Result<()> { + let mut rd = BinaryReader::from_u8(data); + rd.endian = Endian::Little; + + let reported_icf_crc = rd.read_u32()?; + + let reported_size = rd.read_u32()?; + let actual_size = data.len() as u32; + if actual_size != reported_size { + println!("[WARN] Reported size {reported_size} bytes does not match actual size {actual_size} bytes, automatically fixing"); + data[4..8].copy_from_slice(&actual_size.to_le_bytes()); + } + + let padding = rd.read_u64()?; + if padding != 0 { + return Err(anyhow!("Padding error. Expected 8 NULL bytes.")); + } + + let entry_count = rd.read_u64()?; + let expected_size = 0x40 * (entry_count + 1); + let actual_entry_count = if actual_size as u64 != expected_size { + println!("[WARN] Expected size {expected_size} ({entry_count} entries) does not match actual size {actual_size}, automatically fixing"); + + let actual_entry_count = actual_size as u64 / 0x40 - 1; + + data[16..24].copy_from_slice(&actual_entry_count.to_le_bytes()); + + actual_entry_count + } else { + entry_count + }; + + let _ = String::from_utf8(rd.read_bytes(4)?.to_vec())?; + let _ = String::from_utf8(rd.read_bytes(3)?.to_vec())?; + let _ = rd.read_u8()?; + + let reported_container_crc = rd.read_u32()?; + let mut checksum = 0; + for i in 1..=(actual_entry_count as usize) { + let container = &data[0x40 * i..0x40 * (i + 1)]; + if container[0] == 2 && container[1] == 1 { + checksum ^= crc32fast::hash(container); + } + } + + if reported_container_crc != checksum { + println!("[WARN] Reported container CRC32 ({reported_container_crc:02X}) does not match actual checksum ({checksum:02X}), automatically fixing"); + + data[32..36].copy_from_slice(&checksum.to_le_bytes()); + } + + let icf_checksum = crc32fast::hash(&data[4..]); + if icf_checksum != reported_icf_crc { + println!("[WARN] Reported CRC32 ({reported_icf_crc:02X}) does not match actual checksum ({icf_checksum:02X}), automatically fixing"); + data[0..4].copy_from_slice(&icf_checksum.to_le_bytes()); + } + + Ok(()) +} + +pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { + let decrypted = data.as_ref(); + + let mut rd = BinaryReader::from_u8(decrypted); + rd.endian = Endian::Little; + + let checksum = crc32fast::hash(&decrypted[4..]); + let reported_crc = rd.read_u32()?; + if reported_crc != checksum { + return Err(anyhow!( + "Reported CRC32 ({reported_crc:02X}) does not match actual checksum ({checksum:02X})" + )); + } + + let reported_size = rd.read_u32()? as usize; + let actual_size = decrypted.len(); + if actual_size != reported_size { + return Err(anyhow!( + "Reported size {reported_size} does not match actual size {actual_size}" + )); + } + + let padding = rd.read_u64()?; + if padding != 0 { + return Err(anyhow!("Padding error. Expected 8 NULL bytes.")); + } + + let entry_count: usize = rd.read_u64()?.try_into()?; + let expected_size = 0x40 * (entry_count + 1); + if actual_size != expected_size { + return Err(anyhow!("Expected size {expected_size} ({entry_count} entries) does not match actual size {actual_size}")); + } + + let app_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?; + let platform_id = String::from_utf8(rd.read_bytes(3)?.to_vec())?; + let _platform_generation = rd.read_u8()?; + + let reported_crc = rd.read_u32()?; + let mut checksum = 0; + for i in 1..=entry_count { + let container = &decrypted[0x40 * i..0x40 * (i + 1)]; + if container[0] == 2 && container[1] == 1 { + checksum ^= crc32fast::hash(container); + } + } + + if reported_crc != checksum { + return Err(anyhow!("Reported container CRC32 ({reported_crc:02X}) does not match actual checksum ({checksum:02X})")); + } + + for _ in 0..7 { + if rd.read_u32()? != 0 { + return Err(anyhow!("Padding error. Expected 28 NULL bytes.")); + } + } + + let mut entries: Vec = Vec::with_capacity(entry_count); + for _ in 0..entry_count { + let sig = rd.read_bytes(4)?; + if sig[0] != 2 || sig[1] != 1 { + return Err(anyhow!("Container does not start with signature (0x0102), byte {:#06x}", rd.pos)); + } + + let container_type = rd.read_u32()?; + for _ in 0..3 { + if rd.read_u64()? != 0 { + return Err(anyhow!("Padding error. Expected 24 NULL bytes.")); + } + } + + let data: IcfData = match container_type { + 0x0000 | 0x0001 => { + let version = decode_icf_version(&mut rd)?; + let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?; + + for _ in 0..2 { + if rd.read_u64()? != 0 { + return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); + } + } + + match container_type { + 0x0000 => IcfData::System(IcfInnerData { + id: platform_id.clone(), + version, + datetime, + required_system_version, + }), + 0x0001 => IcfData::App(IcfInnerData { + id: app_id.clone(), + version, + datetime, + required_system_version, + }), + _ => unreachable!() + } + } + 0x0002 => { + let option_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?; + let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?; + + for _ in 0..2 { + if rd.read_u64()? != 0 { + return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); + } + } + + IcfData::Option(IcfOptionData { + app_id: app_id.clone(), + option_id, + datetime, + required_system_version, + }) + + } + 0x0101 => { + let target_version = decode_icf_version(&mut rd)?; + let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?; + + let source_version = decode_icf_version(&mut rd)?; + let (_, _) = decode_icf_datetime_version(&mut rd)?; + + IcfData::Patch(IcfPatchData { + id: app_id.clone(), + source_version, + target_version, + required_system_version, + datetime, + }) + } + _ => { + println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos); + rd.read_bytes(32)?; + continue; + } + }; + + entries.push(data); + } + + Ok(entries) +} + +pub fn decode_icf(data: &mut [u8]) -> Result> { + let decrypted = decrypt_icf(data, ICF_KEY, ICF_IV)?; + + parse_icf(decrypted) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3255ab6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,69 @@ +use std::{env, process::exit}; + +use anyhow::anyhow; + +use icf::{decode_icf, decrypt_icf, ICF_KEY, ICF_IV, parse_icf, encrypt_icf, fixup_icf}; +use crate::icf::IcfData; + +mod icf; + +fn main() -> Result<(), anyhow::Error> { + let args: Vec = env::args().collect(); + if args.len() < 2 { + println!("Usage: icf-reader.exe "); + exit(1); + } + + if args[1] == "decrypt" { + if args.len() < 4 { + println!("Usage: icf-reader.exe decrypt "); + exit(1); + } + + let mut icf_buf = std::fs::read(args[2].clone())?; + let decrypted_icf = decrypt_icf(&mut icf_buf, ICF_KEY, ICF_IV)?; + + std::fs::write(args[3].clone(), decrypted_icf)?; + exit(0) + } + + if args[1] == "encrypt" { + if args.len() < 4 { + println!("Usage: icf-reader.exe encrypt "); + exit(1); + } + + let mut icf_buf = std::fs::read(args[2].clone())?; + fixup_icf(&mut icf_buf)?; + let icf = parse_icf(&icf_buf)?; + + for entry in icf { + match entry { + IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build), + } + } + + let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?; + + std::fs::write(args[3].clone(), encrypted_icf)?; + + exit(0) + } + + let mut icf_buf = std::fs::read(args[1].clone())?; + let icf = decode_icf(&mut icf_buf).map_err(|err| anyhow!("Reading ICF failed: {:#}", err))?; + + for entry in icf { + match entry { + IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")), + IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build), + } + } + + Ok(()) +}