diff --git a/.gitignore b/.gitignore index ea8c4bf..238c8d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +*.bin +*.icf \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 056f24b..1c8647c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -104,8 +152,9 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -118,6 +167,52 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -162,6 +257,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hex-literal" version = "0.4.1" @@ -200,8 +301,11 @@ dependencies = [ "binary-reader", "cbc", "chrono", + "clap", "crc32fast", "hex-literal", + "serde", + "serde_json", ] [[package]] @@ -214,6 +318,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "js-sys" version = "0.3.66" @@ -252,27 +362,70 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] -name = "syn" -version = "2.0.41" +name = "ryu" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -291,6 +444,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" @@ -357,7 +516,16 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -366,13 +534,28 @@ 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", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -381,38 +564,80 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[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_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[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_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[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_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index 47b1eb7..8e2275c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ aes = "0.8.3" anyhow = "1.0.75" binary-reader = "0.4.5" cbc = { version = "0.1.2", features = ["std"] } -chrono = "0.4.31" +chrono = { version = "0.4.31", features = ["serde"] } +clap = { version = "4.4.12", features = ["derive"] } crc32fast = "1.3.2" hex-literal = "0.4.1" +serde = { version = "1.0.194", features = ["derive"] } +serde_json = "1.0.110" diff --git a/README.md b/README.md index 52f3595..d1d1abc 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,69 @@ Tools for decoding, decrypting and encrypting ICFs ## Usage ```shell -icf-reader -icf-reader +Usage: icf-reader.exe + +Commands: + encrypt Fixes some common ICF errors, then encrypt the given ICF + decrypt Decrypts the given ICF + decode Decodes the given ICF (optionally to a JSON file) + encode Encodes a JSON file from the decode subcommand to an ICF + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help ``` Encrypting the ICF will also correct CRC checksums, so worrying about the correct checksum is not needed. +## Creating an ICF file from JSON +The JSON file is an array of `IcfData`, which follows the format below: +```typescript +interface Version { + major: number; + minor: number; + build: number; +} + +interface IcfInnerData { + type: "System" | "App", + id: string, // Must be 3 characters for "System" and 4 characters for "App" + version: Version, + required_system_version: Version, + datetime: string, // ISO8601 string yyyy-MM-dd'T'HH:mm:ss +} + +interface IcfOptionData { + type: "Option", + app_id: string, // Does not go into the ICF, so can be anything, but must be specified + option_id: string, // Must be 4 characters + required_system_version: Version, // Can be zeroed out, e.g. { major: 0, minor: 0, build: 0 } + datetime: string, // ISO8601 string yyyy-MM-dd'T'HH:mm:ss +} + +interface IcfPatchData { + type: "Patch", + id: string, // Does not go into the ICF, so can be anything, but must be specified + + sequence_number: number, // Incremented for every patch, starting from 1 + + source_version: Version, + source_datetime: string, // ISO8601 string yyyy-MM-dd'T'HH:mm:ss + source_required_system_version: Version, + + target_version: Version, + target_datetime: string, // ISO8601 string yyyy-MM-dd'T'HH:mm:ss + target_required_system_version: Version, +} + +type IcfData = IcfInnerData | IcfOptionData | IcfPatchData; +``` + +At least one entry of type `System` and `App` must be specified. + +When done, create your ICF using `icf-reader.exe encode input.json output.icf`. + ## Building ``` ICF_KEY= ICF_IV= cargo build --release diff --git a/src/icf/crypto.rs b/src/icf/crypto.rs new file mode 100644 index 0000000..4ab389c --- /dev/null +++ b/src/icf/crypto.rs @@ -0,0 +1,74 @@ +use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use anyhow::{anyhow, Result}; + +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()]); + +/// Decrypts an ICF using the provided key and IV. +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) +} + +/// Encrypts an ICF using the provided key and IV. +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); + let mut to_be_encrypted = Vec::with_capacity(std::cmp::min(4096, 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 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); + to_be_encrypted.clear(); + } + + Ok(encrypted) +} diff --git a/src/icf.rs b/src/icf/mod.rs similarity index 55% rename from src/icf.rs rename to src/icf/mod.rs index b30a8ee..33ba7da 100644 --- a/src/icf.rs +++ b/src/icf/mod.rs @@ -1,166 +1,22 @@ -use std::fmt::Display; +pub mod crypto; +pub mod models; +pub mod parser; -use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit, BlockEncryptMut}; use anyhow::{anyhow, Result}; use binary_reader::{BinaryReader, Endian}; -use chrono::{NaiveDate, NaiveDateTime}; +use chrono::{Datelike, Timelike, NaiveDateTime}; -type Aes128CbcDec = cbc::Decryptor; -type Aes128CbcEnc = cbc::Encryptor; +use self::crypto::{decrypt_icf, ICF_IV, ICF_KEY}; +use self::models::{IcfData, IcfInnerData, IcfOptionData, IcfPatchData, Version}; +use self::parser::{decode_icf_datetime, decode_icf_version}; -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 +/// Fixes incorrect metadata 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 { @@ -179,7 +35,7 @@ pub fn fixup_icf(data: &mut [u8]) -> Result<()> { 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 @@ -275,7 +131,10 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { 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)); + return Err(anyhow!( + "Container does not start with signature (0x0102), byte {:#06x}", + rd.pos + )); } let container_type = rd.read_u32()?; @@ -288,7 +147,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { 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)?; + let datetime = decode_icf_datetime(&mut rd)?; + let required_system_version = decode_icf_version(&mut rd)?; for _ in 0..2 { if rd.read_u64()? != 0 { @@ -309,12 +169,13 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { datetime, required_system_version, }), - _ => unreachable!() + _ => 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)?; + let datetime = decode_icf_datetime(&mut rd)?; + let required_system_version = decode_icf_version(&mut rd)?; for _ in 0..2 { if rd.read_u64()? != 0 { @@ -328,28 +189,38 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { datetime, required_system_version, }) - } - 0x0101 => { + _ => { + // PATCH container type also encode the patch's sequence number + // in the higher 16 bits. + // The lower 16 bits will always be 1. + let sequence_number = (container_type >> 8) as u8; + + if (container_type & 1) == 0 || sequence_number == 0 { + println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos); + rd.read_bytes(32)?; + continue; + } + let target_version = decode_icf_version(&mut rd)?; - let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?; + let target_datetime = decode_icf_datetime(&mut rd)?; + let target_required_system_version = decode_icf_version(&mut rd)?; let source_version = decode_icf_version(&mut rd)?; - let (_, _) = decode_icf_datetime_version(&mut rd)?; + let source_datetime = decode_icf_datetime(&mut rd)?; + let source_required_system_version = decode_icf_version(&mut rd)?; IcfData::Patch(IcfPatchData { id: app_id.clone(), + sequence_number, source_version, + source_datetime, + source_required_system_version, target_version, - required_system_version, - datetime, + target_datetime, + target_required_system_version, }) } - _ => { - println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos); - rd.read_bytes(32)?; - continue; - } }; entries.push(data); @@ -363,3 +234,109 @@ pub fn decode_icf(data: &mut [u8]) -> Result> { parse_icf(decrypted) } + +pub fn serialize_datetime(data: &mut Vec, datetime: NaiveDateTime) { + data.extend((datetime.year() as u16).to_le_bytes()); + data.extend([ + datetime.month() as u8, + datetime.day() as u8, + datetime.hour() as u8, + datetime.minute() as u8, + datetime.second() as u8, + 0x00, + ]); +} + +pub fn serialize_version(data: &mut Vec, version: Version) { + data.extend([version.build, version.minor]); + data.extend(version.major.to_le_bytes()); +} + +pub fn serialize_icf(data: &[IcfData]) -> Result> { + let entry_count = data.len(); + let icf_length = 0x40 * (entry_count + 1); + let mut icf: Vec = Vec::with_capacity(icf_length); + + icf.extend([0x00; 0x40]); + + let mut platform_id: Option = None; + let mut app_id: Option = None; + + for container in data { + icf.extend([0x02, 0x01, 0x00, 0x00]); + + match container { + IcfData::System(s) => { + platform_id = Some(s.id.clone()); + icf.extend([0x00; 4]); + } + IcfData::App(a) => { + app_id = Some(a.id.clone()); + icf.extend([0x01, 0x00, 0x00, 0x00]); + } + IcfData::Option(_) => { + icf.extend([0x02, 0x00, 0x00, 0x00]); + } + IcfData::Patch(p) => { + icf.extend([0x01, p.sequence_number, 0x00, 0x00]); + } + } + + icf.extend([0x00; 24]); + + if let IcfData::Option(o) = container { + icf.extend(o.option_id.as_bytes()); + serialize_datetime(&mut icf, o.datetime); + icf.extend([0x00; 20]); + continue; + } + + let (version, datetime, required_system_version) = match container { + IcfData::System(s) => (s.version, s.datetime, s.required_system_version), + IcfData::App(s) => (s.version, s.datetime, s.required_system_version), + IcfData::Patch(s) => (s.target_version, s.target_datetime, s.target_required_system_version), + IcfData::Option(_) => unreachable!(), + }; + + serialize_version(&mut icf, version); + serialize_datetime(&mut icf, datetime); + serialize_version(&mut icf, required_system_version); + + if let IcfData::Patch(p) = container { + serialize_version(&mut icf, p.source_version); + serialize_datetime(&mut icf, p.source_datetime); + serialize_version(&mut icf, p.source_required_system_version); + } else { + icf.extend([0x00; 16]); + } + } + + let platform_id = match platform_id { + Some(s) => s, + None => return Err(anyhow!("Missing entry of type System in provided ICF data")), + }; + + let app_id = match app_id { + Some(s) => s, + None => return Err(anyhow!("Missing entry of type App in provided ICF data")), + }; + + let mut containers_checksum: u32 = 0; + for container in icf.chunks(0x40).skip(1) { + if container[0] == 2 && container[1] == 1 { + containers_checksum ^= crc32fast::hash(container); + } + } + + icf[4..8].copy_from_slice(&(icf_length as u32).to_le_bytes()); + icf[16..24].copy_from_slice(&(entry_count as u64).to_le_bytes()); + icf[24..28].copy_from_slice(app_id.as_bytes()); + icf[28..31].copy_from_slice(platform_id.as_bytes()); + icf[32..36].copy_from_slice(&containers_checksum.to_le_bytes()); + + let icf_crc = crc32fast::hash(&icf[4..]); + + icf[0..4].copy_from_slice(&icf_crc.to_le_bytes()); + + Ok(icf) +} diff --git a/src/icf/models.rs b/src/icf/models.rs new file mode 100644 index 0000000..1798500 --- /dev/null +++ b/src/icf/models.rs @@ -0,0 +1,92 @@ +use std::fmt::Display; + +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +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, Serialize, Deserialize)] +pub struct IcfInnerData { + pub id: String, + pub version: Version, + pub required_system_version: Version, + pub datetime: NaiveDateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IcfOptionData { + pub app_id: String, + pub option_id: String, + pub required_system_version: Version, + pub datetime: NaiveDateTime, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IcfPatchData { + pub id: String, + + pub sequence_number: u8, + + pub source_version: Version, + pub source_datetime: NaiveDateTime, + pub source_required_system_version: Version, + + pub target_version: Version, + pub target_datetime: NaiveDateTime, + pub target_required_system_version: Version, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum IcfData { + System(IcfInnerData), + App(IcfInnerData), + Patch(IcfPatchData), + Option(IcfOptionData), +} + +impl IcfData { + pub fn filename(&self) -> String { + match self { + IcfData::System(data) => format!( + "{}_{: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) => format!( + "{}_{}_{}_0.app", + data.id, + data.version, + data.datetime.format("%Y%m%d%H%M%S") + ), + IcfData::Option(data) => format!( + "{}_{}_{}_0.opt", + data.app_id, + data.option_id, + data.datetime.format("%Y%m%d%H%M%S") + ), + IcfData::Patch(data) => format!( + "{}_{}_{}_{}_{}.app", + data.id, + data.target_version, + data.target_datetime.format("%Y%m%d%H%M%S"), + data.sequence_number, + data.source_version, + ), + } + } +} diff --git a/src/icf/parser.rs b/src/icf/parser.rs new file mode 100644 index 0000000..e78ac79 --- /dev/null +++ b/src/icf/parser.rs @@ -0,0 +1,33 @@ +use anyhow::{anyhow, Result}; +use binary_reader::BinaryReader; +use chrono::{NaiveDate, NaiveDateTime}; + +use super::models::Version; + +pub fn decode_icf_datetime(rd: &mut BinaryReader) -> Result { + 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"))?; + + Ok(datetime) +} + +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) +} diff --git a/src/main.rs b/src/main.rs index 3255ab6..0c3c67a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,67 +1,90 @@ -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; +use std::fs::File; + +use clap::{Parser, Subcommand}; +use icf::serialize_icf; + +use crate::icf::{ + crypto::{decrypt_icf, encrypt_icf, ICF_IV, ICF_KEY}, + decode_icf, fixup_icf, + models::IcfData, + parse_icf, +}; + +#[derive(Parser, Debug)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + #[command(about = "Fixes some common ICF errors, then encrypt the given ICF")] + Encrypt { input: String, output: String }, + + #[command(about = "Decrypts the given ICF")] + Decrypt { input: String, output: String }, + + #[command(about = "Decodes the given ICF (optionally to a JSON file)")] + Decode { + icf: String, + json_output: Option, + }, + + #[command(about = "Encodes a JSON file from the decode subcommand to an ICF")] + Encode { + json_input: String, + output: String, + }, +} + fn main() -> Result<(), anyhow::Error> { - let args: Vec = env::args().collect(); - if args.len() < 2 { - println!("Usage: icf-reader.exe "); - exit(1); - } + let cli = Cli::parse(); - if args[1] == "decrypt" { - if args.len() < 4 { - println!("Usage: icf-reader.exe decrypt "); - exit(1); + match &cli.command { + Commands::Encrypt { input, output } => { + let mut icf_buf = std::fs::read(input)?; + fixup_icf(&mut icf_buf)?; + let icf = parse_icf(&icf_buf)?; + + for entry in icf { + println!("{}", entry.filename()); + } + + let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?; + + std::fs::write(output, encrypted_icf)?; } + Commands::Decrypt { input, output } => { + let mut icf_buf = std::fs::read(input)?; + let decrypted_icf = decrypt_icf(&mut icf_buf, ICF_KEY, ICF_IV)?; - 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); + std::fs::write(output, decrypted_icf)?; } + Commands::Decode { icf, json_output } => { + let mut icf_buf = std::fs::read(icf)?; + let icf = decode_icf(&mut icf_buf)?; - let mut icf_buf = std::fs::read(args[2].clone())?; - fixup_icf(&mut icf_buf)?; - let icf = parse_icf(&icf_buf)?; + if let Some(json_output) = json_output { + let f = File::create(json_output)?; + serde_json::to_writer_pretty(f, &icf)?; + } - 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), + for entry in icf { + println!("{}", entry.filename()) } } + Commands::Encode { json_input, output } => { + let input = std::fs::read_to_string(json_input)?; + let icf: Vec = serde_json::from_str(&input)?; + let out = serialize_icf(&icf)?; - let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?; + std::fs::write("test.bin", &out)?; - std::fs::write(args[3].clone(), encrypted_icf)?; + let encrypted = encrypt_icf(&out, ICF_KEY, ICF_IV)?; - 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), + std::fs::write(output, encrypted)?; } }