Refactor + allow creating ICFs from JSON

This commit is contained in:
beerpsi 2024-01-02 17:40:16 +07:00
parent a0808a7081
commit 0dff839654
9 changed files with 723 additions and 238 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target /target
*.bin
*.icf

257
Cargo.lock generated
View File

@ -28,6 +28,54 @@ dependencies = [
"libc", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.75" version = "1.0.75"
@ -104,8 +152,9 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-targets", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -118,6 +167,52 @@ dependencies = [
"inout", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@ -162,6 +257,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hex-literal" name = "hex-literal"
version = "0.4.1" version = "0.4.1"
@ -200,8 +301,11 @@ dependencies = [
"binary-reader", "binary-reader",
"cbc", "cbc",
"chrono", "chrono",
"clap",
"crc32fast", "crc32fast",
"hex-literal", "hex-literal",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@ -214,6 +318,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.66" version = "0.3.66"
@ -252,27 +362,70 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.70" version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "syn" name = "ryu"
version = "2.0.41" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -291,6 +444,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -357,7 +516,16 @@ version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [ 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]] [[package]]
@ -366,13 +534,28 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.48.5",
"windows_i686_gnu", "windows_i686_gnu 0.48.5",
"windows_i686_msvc", "windows_i686_msvc 0.48.5",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc", "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]] [[package]]
@ -381,38 +564,80 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"

View File

@ -10,6 +10,9 @@ aes = "0.8.3"
anyhow = "1.0.75" anyhow = "1.0.75"
binary-reader = "0.4.5" binary-reader = "0.4.5"
cbc = { version = "0.1.2", features = ["std"] } 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" crc32fast = "1.3.2"
hex-literal = "0.4.1" hex-literal = "0.4.1"
serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0.110"

View File

@ -4,13 +4,69 @@ Tools for decoding, decrypting and encrypting ICFs
## Usage ## Usage
```shell ```shell
icf-reader <PATH TO ICF> Usage: icf-reader.exe <COMMAND>
icf-reader <decrypt | encrypt> <INPUT> <OUTPUT>
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 Encrypting the ICF will also correct CRC checksums, so worrying about the
correct checksum is not needed. 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 ## Building
``` ```
ICF_KEY=<key> ICF_IV=<iv> cargo build --release ICF_KEY=<key> ICF_IV=<iv> cargo build --release

74
src/icf/crypto.rs Normal file
View File

@ -0,0 +1,74 @@
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use anyhow::{anyhow, Result};
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
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<Vec<u8>> {
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::<NoPadding>(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<Vec<u8>> {
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::<NoPadding>(&to_be_encrypted, &mut encbuf)
.map_err(|err| anyhow!(err))?;
encrypted.extend(encbuf);
to_be_encrypted.clear();
}
Ok(encrypted)
}

View File

@ -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 anyhow::{anyhow, Result};
use binary_reader::{BinaryReader, Endian}; use binary_reader::{BinaryReader, Endian};
use chrono::{NaiveDate, NaiveDateTime}; use chrono::{Datelike, Timelike, NaiveDateTime};
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>; use self::crypto::{decrypt_icf, ICF_IV, ICF_KEY};
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>; 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()]); /// Fixes incorrect metadata caused by hex editing the ICF
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<Vec<u8>> {
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::<NoPadding>(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<Vec<u8>> {
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::<NoPadding>(&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<Version> {
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<()> { pub fn fixup_icf(data: &mut [u8]) -> Result<()> {
let mut rd = BinaryReader::from_u8(data); let mut rd = BinaryReader::from_u8(data);
rd.endian = Endian::Little; rd.endian = Endian::Little;
let reported_icf_crc = rd.read_u32()?; let reported_icf_crc = rd.read_u32()?;
let reported_size = rd.read_u32()?; let reported_size = rd.read_u32()?;
let actual_size = data.len() as u32; let actual_size = data.len() as u32;
if actual_size != reported_size { 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"); 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; let actual_entry_count = actual_size as u64 / 0x40 - 1;
data[16..24].copy_from_slice(&actual_entry_count.to_le_bytes()); data[16..24].copy_from_slice(&actual_entry_count.to_le_bytes());
actual_entry_count actual_entry_count
@ -275,7 +131,10 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
for _ in 0..entry_count { for _ in 0..entry_count {
let sig = rd.read_bytes(4)?; let sig = rd.read_bytes(4)?;
if sig[0] != 2 || sig[1] != 1 { 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()?; let container_type = rd.read_u32()?;
@ -288,7 +147,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
let data: IcfData = match container_type { let data: IcfData = match container_type {
0x0000 | 0x0001 => { 0x0000 | 0x0001 => {
let version = decode_icf_version(&mut rd)?; 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 { for _ in 0..2 {
if rd.read_u64()? != 0 { if rd.read_u64()? != 0 {
@ -309,12 +169,13 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
datetime, datetime,
required_system_version, required_system_version,
}), }),
_ => unreachable!() _ => unreachable!(),
} }
} }
0x0002 => { 0x0002 => {
let option_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?; 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 { for _ in 0..2 {
if rd.read_u64()? != 0 { if rd.read_u64()? != 0 {
@ -328,28 +189,38 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
datetime, datetime,
required_system_version, 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 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 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 { IcfData::Patch(IcfPatchData {
id: app_id.clone(), id: app_id.clone(),
sequence_number,
source_version, source_version,
source_datetime,
source_required_system_version,
target_version, target_version,
required_system_version, target_datetime,
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); entries.push(data);
@ -363,3 +234,109 @@ pub fn decode_icf(data: &mut [u8]) -> Result<Vec<IcfData>> {
parse_icf(decrypted) parse_icf(decrypted)
} }
pub fn serialize_datetime(data: &mut Vec<u8>, 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<u8>, version: Version) {
data.extend([version.build, version.minor]);
data.extend(version.major.to_le_bytes());
}
pub fn serialize_icf(data: &[IcfData]) -> Result<Vec<u8>> {
let entry_count = data.len();
let icf_length = 0x40 * (entry_count + 1);
let mut icf: Vec<u8> = Vec::with_capacity(icf_length);
icf.extend([0x00; 0x40]);
let mut platform_id: Option<String> = None;
let mut app_id: Option<String> = 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)
}

92
src/icf/models.rs Normal file
View File

@ -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,
),
}
}
}

33
src/icf/parser.rs Normal file
View File

@ -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<NaiveDateTime> {
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<Version> {
let version = Version {
build: rd.read_u8()?,
minor: rd.read_u8()?,
major: rd.read_u16()?,
};
Ok(version)
}

View File

@ -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; 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<String>,
},
#[command(about = "Encodes a JSON file from the decode subcommand to an ICF")]
Encode {
json_input: String,
output: String,
},
}
fn main() -> Result<(), anyhow::Error> { fn main() -> Result<(), anyhow::Error> {
let args: Vec<String> = env::args().collect(); let cli = Cli::parse();
if args.len() < 2 {
println!("Usage: icf-reader.exe <PATH TO ICF>");
exit(1);
}
if args[1] == "decrypt" { match &cli.command {
if args.len() < 4 { Commands::Encrypt { input, output } => {
println!("Usage: icf-reader.exe decrypt <PATH TO ICF> <OUTPUT>"); let mut icf_buf = std::fs::read(input)?;
exit(1); 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())?; std::fs::write(output, decrypted_icf)?;
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 <PATH TO ICF> <OUTPUT>");
exit(1);
} }
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())?; if let Some(json_output) = json_output {
fixup_icf(&mut icf_buf)?; let f = File::create(json_output)?;
let icf = parse_icf(&icf_buf)?; serde_json::to_writer_pretty(f, &icf)?;
}
for entry in icf { for entry in icf {
match entry { println!("{}", entry.filename())
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),
} }
} }
Commands::Encode { json_input, output } => {
let input = std::fs::read_to_string(json_input)?;
let icf: Vec<IcfData> = 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) std::fs::write(output, encrypted)?;
}
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),
} }
} }