initial commit
This commit is contained in:
commit
dbbe287b1c
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/target
|
||||
*.exe
|
||||
*.zip
|
||||
*.app
|
||||
*.opt
|
||||
*.vhd
|
||||
*.ntfs
|
||||
*.exfat
|
||||
flamegraph.svg
|
276
Cargo.lock
generated
Normal file
276
Cargo.lock
generated
Normal file
@ -0,0 +1,276 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[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 = "cbc"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[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 = "console"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
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 = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "fsdecrypt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"cbc",
|
||||
"crc32fast",
|
||||
"hex-literal",
|
||||
"indicatif",
|
||||
]
|
||||
|
||||
[[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 = "indicatif"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
|
||||
dependencies = [
|
||||
"console",
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[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 = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "fsdecrypt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
anyhow = "1.0.86"
|
||||
cbc = "0.1.2"
|
||||
crc32fast = "1.4.2"
|
||||
hex-literal = "0.4.1"
|
||||
indicatif = "0.17.8"
|
14
LICENSE
Normal file
14
LICENSE
Normal file
@ -0,0 +1,14 @@
|
||||
BSD Zero Clause License
|
||||
|
||||
Copyright (c) 2024 beerpsi
|
||||
|
||||
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.
|
76
src/bootid.rs
Normal file
76
src/bootid.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use hex_literal::hex;
|
||||
|
||||
pub const BOOTID_KEY: [u8; 16] = hex!("09ca5efd30c9aaef3804d0a7e3fa7120");
|
||||
pub const BOOTID_IV: [u8; 16] = hex!("b155c22c2e7f0491fa7f0fdc217aff90");
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub mod ContainerType {
|
||||
pub const OS: u16 = 0x0000;
|
||||
pub const APP: u16 = 0x0101;
|
||||
pub const OPTION: u16 = 0x0201;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Timestamp {
|
||||
pub year: u16,
|
||||
pub month: u8,
|
||||
pub day: u8,
|
||||
pub hour: u8,
|
||||
pub minute: u8,
|
||||
pub second: u8,
|
||||
unk1: u8,
|
||||
}
|
||||
|
||||
impl Display for Timestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:<04}{:<02}{:<02}{:<02}{:<02}{:<02}",
|
||||
self.year, self.month, self.day, self.hour, self.minute, self.second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Version {
|
||||
pub release: u8,
|
||||
pub minor: u8,
|
||||
pub major: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub union GameVersion {
|
||||
pub version: Version,
|
||||
pub option: [u8; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct BootId {
|
||||
pub crc32: u32,
|
||||
pub length: u32,
|
||||
pub signature: [u8; 4],
|
||||
pub container_type: u16,
|
||||
pub sequence_number: u8,
|
||||
pub use_custom_iv: bool,
|
||||
pub game_id: [u8; 4],
|
||||
pub target_timestamp: Timestamp,
|
||||
pub target_version: GameVersion,
|
||||
pub block_count: u64,
|
||||
pub block_size: u64,
|
||||
pub header_block_count: u64,
|
||||
unk1: u64,
|
||||
pub os_id: [u8; 3],
|
||||
pub os_generation: u8,
|
||||
pub source_timestamp: Timestamp,
|
||||
pub source_version: Version,
|
||||
pub os_version: Version,
|
||||
pub padding: [u8; 8],
|
||||
// We don't need the entire bootID, so keep size down by not including the string table.
|
||||
// pub strings: [u8; 10156],
|
||||
}
|
516
src/crypto.rs
Normal file
516
src/crypto.rs
Normal file
@ -0,0 +1,516 @@
|
||||
use std::path::Path;
|
||||
|
||||
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use hex_literal::hex;
|
||||
|
||||
pub const NTFS_HEADER: [u8; 16] = hex!("eb52904e544653202020200010010000");
|
||||
pub const EXFAT_HEADER: [u8; 16] = hex!("eb769045584641542020200000000000");
|
||||
|
||||
pub const OPTION_KEY: [u8; 16] = hex!("5c84a9e726eaa5dd351f2b0750c23697");
|
||||
pub const OPTION_IV: [u8; 16] = hex!("c063bf6f562d084d7963c987f5281761");
|
||||
|
||||
pub type Aes128CbcDec = cbc::Decryptor<aes::Aes128Dec>;
|
||||
|
||||
pub struct GameKeys {
|
||||
pub key: [u8; 16],
|
||||
pub iv: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
pub fn calculate_page_iv(file_offset: u64, file_iv: &[u8], page_iv: &mut [u8]) {
|
||||
for (i, (fbyte, pbyte)) in file_iv.iter().zip(page_iv.iter_mut()).enumerate() {
|
||||
*pbyte = fbyte ^ (file_offset >> (8 * (i % 8))) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_file_iv(
|
||||
key: [u8; 16],
|
||||
expected_header: [u8; 16],
|
||||
first_page: &[u8],
|
||||
) -> Result<[u8; 16]> {
|
||||
let mut iv = [0u8; 16];
|
||||
let mut header = [0u8; 16];
|
||||
|
||||
header.copy_from_slice(&first_page[..16]);
|
||||
|
||||
calculate_page_iv(0, &expected_header, &mut iv);
|
||||
|
||||
let cipher = Aes128CbcDec::new_from_slices(&key, &iv).map_err(|e| anyhow!(e))?;
|
||||
|
||||
cipher
|
||||
.decrypt_padded_mut::<NoPadding>(&mut header)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
pub fn get_game_keys(game_id: &str) -> Option<GameKeys> {
|
||||
match game_id {
|
||||
// Nu Firmware / Hardware Test
|
||||
"SBZS" => Some(GameKeys {
|
||||
key: hex!("2ecbcff65ce0abecc10547f8ac8351d8"),
|
||||
iv: Some(hex!("f2ac6c2817d0574bba113d497e319f3e")),
|
||||
}),
|
||||
|
||||
// KEY CHIP NU FACTORY / KEY CHIP NUSX FACTORY
|
||||
"SBZT" => Some(GameKeys {
|
||||
key: hex!("9ab9ce55ed9c194a715a73a7699f795b"),
|
||||
iv: Some(hex!("8552de88fedda6e859369fb000f44d5b")),
|
||||
}),
|
||||
|
||||
// Nu Firmware / Hardware Test
|
||||
"SBZU" => Some(GameKeys {
|
||||
key: hex!("eb1228254cdd3077eb3e441c0227bf40"),
|
||||
iv: Some(hex!("3f9b4676118cee129fe2f1cb2747bca5")),
|
||||
}),
|
||||
|
||||
// Project DIVA Arcade Future Tone
|
||||
"SBZV" => Some(GameKeys {
|
||||
key: hex!("3274a399594d84779625940b69c02d3f"),
|
||||
iv: Some(hex!("675ba66d29c87923f5f154c406afee42")),
|
||||
}),
|
||||
|
||||
// Wonderland Wars
|
||||
"SDAP" => Some(GameKeys {
|
||||
key: hex!("41b5027c5e99d94aa9335d6d71838ecf"),
|
||||
iv: Some(hex!("41b5027c5e99d94aa9335d6d71838ecf")),
|
||||
}),
|
||||
|
||||
// Herobank Arcade
|
||||
"SDAQ" => Some(GameKeys {
|
||||
key: hex!("c28f22bc1b339ae64180739886dc83d6"),
|
||||
iv: Some(hex!("0a29fd145d72bf8dedd436025df0a9fc")),
|
||||
}),
|
||||
|
||||
// Uranai Collection: Torotte
|
||||
"SDAV" => Some(GameKeys {
|
||||
key: hex!("eed95513266a499a55e265b049169c44"),
|
||||
iv: Some(hex!("84c0e5931d91a6a477d62c271546056e")),
|
||||
}),
|
||||
|
||||
// Shin Kouchuu Ouja Mushiking
|
||||
"SDBE" => Some(GameKeys {
|
||||
key: hex!("7053fb944572e5b631a665cef4b5bcdd"),
|
||||
iv: Some(hex!("ae4d7e884002c79eb35711554d613057")),
|
||||
}),
|
||||
|
||||
// E-DEL Sand
|
||||
"SDBN" => Some(GameKeys {
|
||||
key: hex!("c1f14ae2e85b095e313c8baec125805e"),
|
||||
iv: Some(hex!("3c538eea66251acd5404b93f8976a7f7")),
|
||||
}),
|
||||
|
||||
// CHUNITHM
|
||||
"SDBT" => Some(GameKeys {
|
||||
key: hex!("a6a870671fd432ec637adf7a822f97da"),
|
||||
iv: Some(hex!("2c277f31cd550cfa2c993b4dd56b85ae")),
|
||||
}),
|
||||
|
||||
// Sonic Dash Extreme
|
||||
"SDBX" => Some(GameKeys {
|
||||
key: hex!("3dc19c2d0c20ac199d5fa46e7f6335a6"),
|
||||
iv: Some(hex!("d8f029ec90fe55be67584f742c55ef8b")),
|
||||
}),
|
||||
|
||||
// Kancolle Arcade
|
||||
"SDBZ" => Some(GameKeys {
|
||||
key: hex!("521bde4460f4184edd879136adeea5ee"),
|
||||
iv: Some(hex!("1b8324032db69d7b0954794aa229fe68")),
|
||||
}),
|
||||
|
||||
// crossbeats REV.
|
||||
"SDCA" => Some(GameKeys {
|
||||
key: hex!("1649490a03d6c2aec1c496982cb0405c"),
|
||||
iv: Some(hex!("4680711c7e67a26f9230d5af74b5dcfb")),
|
||||
}),
|
||||
|
||||
// nailpuri
|
||||
"SDCD" => Some(GameKeys {
|
||||
key: hex!("43b38502d8f6d3c7b02b95fc28db5308"),
|
||||
iv: Some(hex!("6dfcb94bf74f152b55f3e0c7f35b44b5")),
|
||||
}),
|
||||
|
||||
// Luigi's Mansion Arcade
|
||||
"SDCF" => Some(GameKeys {
|
||||
key: hex!("df986883da837538e37b959a3e4117cd"),
|
||||
iv: Some(hex!("dabf539738852f17714811af70435a83")),
|
||||
}),
|
||||
|
||||
// Mario & Sonic at Rio Olympic Games
|
||||
"SDCH" => Some(GameKeys {
|
||||
key: hex!("e2da769e94f1d3aca1930cdbe0708c9f"),
|
||||
iv: Some(hex!("c7dcce203c84ab0477236d697570dadc")),
|
||||
}),
|
||||
|
||||
// KEY CHIP NUSX EDB SOC
|
||||
"SDCR" => Some(GameKeys {
|
||||
key: hex!("4961a51fd36f14e72664f52373052160"),
|
||||
iv: Some(hex!("25d7d1341a282c5e0a34c64562c023ec")),
|
||||
}),
|
||||
|
||||
// Celevie
|
||||
"SDCT" => Some(GameKeys {
|
||||
key: hex!("d6ae51f10ec76da93c981800fc3ad3cb"),
|
||||
iv: Some(hex!("fb8e43e280d330d06581732f2e11a6dc")),
|
||||
}),
|
||||
|
||||
// CYTUS Ω
|
||||
"SDCX" => Some(GameKeys {
|
||||
key: hex!("79504ccc509b67d1f7a3f593e6f9d9d6"),
|
||||
iv: Some(hex!("1551ea8926f2aee233eec309de3e5f3c")),
|
||||
}),
|
||||
|
||||
// KEY CHIP NUSX1.1 TDW
|
||||
"SDDB" => Some(GameKeys {
|
||||
key: hex!("875679b2cd1637962b0db25c51fb21a6"),
|
||||
iv: Some(hex!("8ef44722a0566e8f572356245687fbe5")),
|
||||
}),
|
||||
|
||||
// Sangokushi Taisen
|
||||
"SDDD" => Some(GameKeys {
|
||||
key: hex!("564e967873de6cbcd22efeca6952e9dc"),
|
||||
iv: Some(hex!("4e3dd465cf09cd82b259f7bed5fc2d6d")),
|
||||
}),
|
||||
|
||||
// Initial D Arcade Stage Zero
|
||||
"SDDF" => Some(GameKeys {
|
||||
key: hex!("65058573a0cb81749e694ae164c61b04"),
|
||||
iv: Some(hex!("981c4f45e3c6958f054e5d00916bdf2b")),
|
||||
}),
|
||||
|
||||
// KEY CHIP NU1.1 ESC
|
||||
"SDDJ" => Some(GameKeys {
|
||||
key: hex!("630fe52276537bd7fb267adf175f4e99"),
|
||||
iv: Some(hex!("dc5755be57ded2cdb34433bbba2204ff")),
|
||||
}),
|
||||
|
||||
// APM3 Sample Program 2
|
||||
"SDDL" => Some(GameKeys {
|
||||
key: hex!("992458295fd06d6a8af0dfb3f6854c19"),
|
||||
iv: Some(hex!("8484906d4cd5fd225e032843ed37495d")),
|
||||
}),
|
||||
|
||||
// ALLS MX Factory Dummy
|
||||
"SDDM" => Some(GameKeys {
|
||||
key: hex!("0127958210f6ae9bdeb8975018b5af24"),
|
||||
iv: Some(hex!("181716badccff4bc2b1e29ae02a1bbbb")),
|
||||
}),
|
||||
|
||||
// Demo ID
|
||||
"SDDN" => Some(GameKeys {
|
||||
key: hex!("41dd8e66290117ac67d311a2f0a6416e"),
|
||||
iv: Some(hex!("73e18e8418f6ceefb11e2767fdea190c")),
|
||||
}),
|
||||
|
||||
// Soul Reverse
|
||||
"SDDP" => Some(GameKeys {
|
||||
key: hex!("cf6d64427eeca47674e17bcd46d1ea8c"),
|
||||
iv: Some(hex!("ce5174093d26ca2a31b58541e85ac276")),
|
||||
}),
|
||||
|
||||
// SEGA World Driver Championship
|
||||
"SDDS" => Some(GameKeys {
|
||||
key: hex!("161bec6d90989d0e26d791170607a440"),
|
||||
iv: Some(hex!("81dc26a27028e2092332038aa1bffc47")),
|
||||
}),
|
||||
|
||||
// O.N.G.E.K.I.
|
||||
"SDDT" => Some(GameKeys {
|
||||
key: hex!("3f7658728b9517d3314e684fa2e2a045"),
|
||||
iv: Some(hex!("41578833c547aaff04db597a6e9eb784")),
|
||||
}),
|
||||
|
||||
"SDDU" => Some(GameKeys {
|
||||
key: hex!("649ae9982625f90c55af86713c55d3fd"),
|
||||
iv: Some(hex!("187116fc4647a7d3b6f2303a34f0a2fe")),
|
||||
}),
|
||||
|
||||
// ALLS X / X2 Research & Development
|
||||
"SDDW" => Some(GameKeys {
|
||||
key: hex!("118565d344f3e14ca69299eeac049bb9"),
|
||||
iv: Some(hex!("9d6d392ec35ed94ef9fe0a5be0573981")),
|
||||
}),
|
||||
|
||||
// KEY CHIP ALLS X FACTORY
|
||||
"SDDX" => Some(GameKeys {
|
||||
key: hex!("428bff0f9e7aafc169a7a75751ffda98"),
|
||||
iv: Some(hex!("f8250594f425332c6d349d7ea0e86669")),
|
||||
}),
|
||||
|
||||
// Shin Kouchuu Ouja Mushiking TWN
|
||||
"SDEA" => Some(GameKeys {
|
||||
key: hex!("9f9cf148ac3c50aaf925af1dfb27f58b"),
|
||||
iv: Some(hex!("4d8ebbd971896b8a4a3dd84a23b329fc")),
|
||||
}),
|
||||
|
||||
// WCCF FOOTISTA
|
||||
"SDEB" => Some(GameKeys {
|
||||
key: hex!("d511ed690415f6359843a134fd47836a"),
|
||||
iv: Some(hex!("ac139b382acdd112e31564ea7f38186c")),
|
||||
}),
|
||||
|
||||
// Chrono Regalia
|
||||
"SDEC" => Some(GameKeys {
|
||||
key: hex!("f272e5016863af2ba0337f50de686f6e"),
|
||||
iv: Some(hex!("5327e132631e7f71b61be7cc0df382ce")),
|
||||
}),
|
||||
|
||||
// CARD MAKER
|
||||
"SDED" => Some(GameKeys {
|
||||
key: hex!("21fcec779a16769f5277a36fb542992c"),
|
||||
iv: Some(hex!("22b50239f1b40ccc3e55a2d69c69b160")),
|
||||
}),
|
||||
|
||||
// House of the Dead: Scarlet Dawn
|
||||
"SDEE" => Some(GameKeys {
|
||||
key: hex!("191eb7440672dab08ddbb7195efb356f"),
|
||||
iv: Some(hex!("c278b5386dc38bd76d71dbcd826954cf")),
|
||||
}),
|
||||
|
||||
// FiZ
|
||||
"SDEG" => Some(GameKeys {
|
||||
key: hex!("721853dbe2d30bafe24f0edbd210deeb"),
|
||||
iv: Some(hex!("4dfb0bcec86159aab297166bcd509e6f")),
|
||||
}),
|
||||
|
||||
// Fate/Grand Order Arcade
|
||||
"SDEJ" => Some(GameKeys {
|
||||
key: hex!("9de1ea6ae38d9011f55d8ee864395d24"),
|
||||
iv: Some(hex!("f60cde21982876d12d17662a48d90836")),
|
||||
}),
|
||||
|
||||
// ALL.Net P.ras multi Ver.3
|
||||
"SDEM" => Some(GameKeys {
|
||||
key: hex!("700617f293696c07fb9f356d3b99240d"),
|
||||
iv: Some(hex!("667d026d6cdf329ff351dbaf7098e81d")),
|
||||
}),
|
||||
|
||||
// StarHorse4 (Server) / MESTA Medal Station
|
||||
"SDEP" => Some(GameKeys {
|
||||
key: hex!("fa2b7ca53a823c152d940972cbf532f5"),
|
||||
iv: Some(hex!("f4af35120c48617704bb5b8471797a62")),
|
||||
}),
|
||||
|
||||
// Initial D Arcade Stage Zero (CHN)
|
||||
"SDER" => Some(GameKeys {
|
||||
key: hex!("7d73367ebb218ec82930d58dc6d7950b"),
|
||||
iv: Some(hex!("9788c3eca2db6ba92bac4f6f7b706308")),
|
||||
}),
|
||||
|
||||
// House of the Dead: Scarlet Dawn (EXP)
|
||||
"SDET" => Some(GameKeys {
|
||||
key: hex!("4643e7b2c3006e0264163edc8545fb72"),
|
||||
iv: Some(hex!("612bca81ea2958ffbac36f780f1ed688")),
|
||||
}),
|
||||
|
||||
// KEY CHIP ALLS X HDZ
|
||||
"SDEU" => Some(GameKeys {
|
||||
key: hex!("23b3e9bb47e3ac9998f6e6c1adc4ae33"),
|
||||
iv: Some(hex!("a964714cea60688407bf554bd1c27ec2")),
|
||||
}),
|
||||
|
||||
// House of the Dead: Scarlet Dawn (CHN)
|
||||
"SDEV" => Some(GameKeys {
|
||||
key: hex!("3c1f018d88926d98163b07a1563a4818"),
|
||||
iv: Some(hex!("ca7373c9c7dfebac0fc24254c030e4ad")),
|
||||
}),
|
||||
|
||||
// maimai DX
|
||||
"SDEZ" => Some(GameKeys {
|
||||
key: hex!("d136eba05d40e82682e6aad8d9e8688c"),
|
||||
iv: Some(hex!("c484deeaa0249ef46695f63694b7372f")),
|
||||
}),
|
||||
|
||||
// KEY CHIP ALLS X REC
|
||||
"SDFA" => Some(GameKeys {
|
||||
key: hex!("8e816b4362db24a230877885864d206d"),
|
||||
iv: Some(hex!("8e5a0ba6a0a1150d47d12bdb64debba7")),
|
||||
}),
|
||||
|
||||
// WACCA
|
||||
"SDFE" => Some(GameKeys {
|
||||
key: hex!("f61719c371e5bca6788c139a53091617"),
|
||||
iv: Some(hex!("67d43173e343813fa2097fd32992a8e2")),
|
||||
}),
|
||||
|
||||
// SANDRA
|
||||
"SDFG" => Some(GameKeys {
|
||||
key: hex!("3398fb86bfe630a14979411879861ac7"),
|
||||
iv: Some(hex!("a794c49c2c7639cd80571807c17246ff")),
|
||||
}),
|
||||
|
||||
// Kemono Friends 3: Planet Tours
|
||||
"SDFL" => Some(GameKeys {
|
||||
key: hex!("2449b48067b9176a6e0f9563481e97f4"),
|
||||
iv: Some(hex!("616f8710454632eb4fb1d89d8c19c19a")),
|
||||
}),
|
||||
|
||||
// KEY CHIP ALLS X CASJ
|
||||
"SDFN" => Some(GameKeys {
|
||||
key: hex!("29f62e22c6a9fd8be327631c68546405"),
|
||||
iv: Some(hex!("2a860976e6d98513825f291e56cfb5ee")),
|
||||
}),
|
||||
|
||||
// KEY CHIP NUSX FUTURE
|
||||
"SDFP" => Some(GameKeys {
|
||||
key: hex!("570b87263a7ca0aa4c1388e204ee6d4b"),
|
||||
iv: Some(hex!("7640886011a2300a91fad9f36a8c4775")),
|
||||
}),
|
||||
|
||||
// StarHorse4
|
||||
"SDFT" => Some(GameKeys {
|
||||
key: hex!("92a25f388c50737e39c3c2f006645f31"),
|
||||
iv: Some(hex!("a97e72f990417488cb4c67f8f0c3fb25")),
|
||||
}),
|
||||
|
||||
// Mario & Sonic at TOKYO Olympic
|
||||
"SDFV" => Some(GameKeys {
|
||||
key: hex!("fe82db9a60295d829b95f03c2276018b"),
|
||||
iv: Some(hex!("34d82772ae18174f0a181dc53399ea9c")),
|
||||
}),
|
||||
|
||||
// maimai DX (EXP)
|
||||
"SDGA" => Some(GameKeys {
|
||||
key: hex!("0a6610a62ef670c65b7e7b1750ffb7a1"),
|
||||
iv: Some(hex!("17a2a22915f81c5896edbba4c412585e")),
|
||||
}),
|
||||
|
||||
// maimai DX (CHN)
|
||||
"SDGB" => Some(GameKeys {
|
||||
key: hex!("7ca4e6b6f3d6e8b26472973887d7fa3a"),
|
||||
iv: Some(hex!("53fe7135762de3f97e7fe76b0fef3f27")),
|
||||
}),
|
||||
|
||||
// Puyo Puyo e-Sports Arcade
|
||||
"SDGH" => Some(GameKeys {
|
||||
key: hex!("b3e30e7eabac3767ade13c69c9b2f22b"),
|
||||
iv: Some(hex!("03deaea3742d69675b36cddc8b15ac91")),
|
||||
}),
|
||||
|
||||
// WCCF FOOTISTA (EXP)
|
||||
"SDGK" => Some(GameKeys {
|
||||
key: hex!("9dc4a17fc39fca5a8a358984801caaa7"),
|
||||
iv: Some(hex!("e0445b11dcfa0dae56c85e8787e11d9b")),
|
||||
}),
|
||||
|
||||
// KEY CHIP ALLS X HDZ CASJ
|
||||
"SDGP" => Some(GameKeys {
|
||||
key: hex!("c87ab31247e7b6ff95fdd79fb91f9f37"),
|
||||
iv: Some(hex!("2467ab3c031e3dc0568b7077efd27c36")),
|
||||
}),
|
||||
|
||||
// ROKUMEN
|
||||
"SDGQ" => Some(GameKeys {
|
||||
key: hex!("c5356dae7b066bce88984aec36deb62d"),
|
||||
iv: Some(hex!("4e9f2982460e2fd907bde15709edfba7")),
|
||||
}),
|
||||
|
||||
// CHUNITHM (EXP)
|
||||
"SDGS" => Some(GameKeys {
|
||||
key: hex!("a5150cc5065d2c59ee2f8f332cbd29d5"),
|
||||
iv: Some(hex!("84014d26696f290ad7ead70c7549bd81")),
|
||||
}),
|
||||
|
||||
// Initial D THE ARCADE
|
||||
"SDGT" => Some(GameKeys {
|
||||
key: hex!("9d0bba20d1e84f2459399f5383beee72"),
|
||||
iv: Some(hex!("5d340013fdfb2464d253093602fe4b64")),
|
||||
}),
|
||||
|
||||
"SDGV" => Some(GameKeys {
|
||||
key: hex!("573f5c8cc44f10f31ec749b695ebe886"),
|
||||
iv: Some(hex!("6bfca86f9d208a7944cdfc25ea3cd220")),
|
||||
}),
|
||||
|
||||
// "Eiketsu Taisen: Sanzensekai no Hadou
|
||||
"SDGY" => Some(GameKeys {
|
||||
key: hex!("c04b663a59055acbdfebc6d3df0e6a04"),
|
||||
iv: Some(hex!("76fc5f1d88605107947d0c1ff347022d")),
|
||||
}),
|
||||
|
||||
// Hori a Tale
|
||||
"SDGZ" => Some(GameKeys {
|
||||
key: hex!("9ad74efb208d6ee4fe5ee770331712cf"),
|
||||
iv: Some(hex!("45f6e53f0eb8fae6665b45444a61e266")),
|
||||
}),
|
||||
|
||||
// CHUNITHM NEW!!
|
||||
"SDHD" => Some(GameKeys {
|
||||
key: hex!("3abd00d7a820ce862eaf474bf6c8f33e"),
|
||||
iv: Some(hex!("0f1e7eea78da7e037e0552c2843e1b6a")),
|
||||
}),
|
||||
|
||||
"SDHH" => Some(GameKeys {
|
||||
key: hex!("fc6f887f3717c5d6713113b92fa3fb27"),
|
||||
iv: Some(hex!("ad76606460dbe1e91e41bef7ab0c1535")),
|
||||
}),
|
||||
|
||||
// CHUNITHM (CHN)
|
||||
"SDHJ" => Some(GameKeys {
|
||||
key: hex!("985ea66ecb5b1f208c90e2b898f0b073"),
|
||||
iv: Some(hex!("164a65422e7f01b7f1b0849fc7737cdb")),
|
||||
}),
|
||||
|
||||
// UFO CATCHER LINK STATION
|
||||
"SDHK" => Some(GameKeys {
|
||||
key: hex!("bc92d63c2a099ca2315a483c3041fdd7"),
|
||||
iv: Some(hex!("b14d8449b6d4325d83a2774b13dd21ff")),
|
||||
}),
|
||||
|
||||
// WACCA (CHN)
|
||||
"SDHN" => Some(GameKeys {
|
||||
key: hex!("892123a26d7c03d49edd12a80ee0c58f"),
|
||||
iv: Some(hex!("76aa15a6868b8dbdf7207906354d5169")),
|
||||
}),
|
||||
|
||||
// meityromantic
|
||||
"SDHR" => Some(GameKeys {
|
||||
key: hex!("1fb897cab97c8170a6ac0a21685c58d9"),
|
||||
iv: Some(hex!("f9b60f65b01e8e836a4bc20f7d39faf5")),
|
||||
}),
|
||||
|
||||
// ALLS System
|
||||
"ACA" => Some(GameKeys {
|
||||
key: hex!("e4281bcf48c4d28eb05772ce6f98587a"),
|
||||
iv: Some(hex!("6cee7f5a2c4b5f1e93c5949114ff0b74")),
|
||||
}),
|
||||
|
||||
// Read from {game_id}.bin for unknown keys.
|
||||
_ => {
|
||||
let filename = format!("{}.bin", game_id);
|
||||
let path = Path::new(&filename);
|
||||
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(metadata) = path.metadata() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let size = metadata.len();
|
||||
|
||||
match size {
|
||||
16 => Some(GameKeys {
|
||||
key: std::fs::read(filename)
|
||||
.map(|v| v.try_into().unwrap())
|
||||
.unwrap(),
|
||||
iv: None,
|
||||
}),
|
||||
32 => {
|
||||
let keyiv = std::fs::read(filename).unwrap();
|
||||
let key = keyiv[..16].try_into().unwrap();
|
||||
let iv: [u8; 16] = keyiv[16..].try_into().unwrap();
|
||||
let iv = if iv == NTFS_HEADER || iv == EXFAT_HEADER {
|
||||
None
|
||||
} else {
|
||||
Some(iv)
|
||||
};
|
||||
|
||||
Some(GameKeys { key, iv })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
193
src/main.rs
Normal file
193
src/main.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use aes::{
|
||||
cipher::{block_padding::NoPadding, BlockDecryptMut, InnerIvInit, KeyInit, KeyIvInit},
|
||||
Aes128Dec,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY};
|
||||
use crypto::{
|
||||
calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER,
|
||||
NTFS_HEADER, OPTION_IV, OPTION_KEY,
|
||||
};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
mod bootid;
|
||||
mod crypto;
|
||||
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("Usage: fsdecrypt <input_file1> [<input_file2> ...]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bootid_cipher =
|
||||
Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV).map_err(|e| anyhow!(e))?;
|
||||
let mut bootid_bytes = [0u8; std::mem::size_of::<BootId>()];
|
||||
let mut page: Vec<u8> = Vec::with_capacity(PAGE_SIZE as usize);
|
||||
let mut page_iv = [0u8; 16];
|
||||
|
||||
for path in args.iter().skip(1) {
|
||||
let path = Path::new(path);
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::with_capacity(0x40000, file);
|
||||
|
||||
reader.read_exact(&mut bootid_bytes)?;
|
||||
|
||||
if let Err(e) = bootid_cipher
|
||||
.clone()
|
||||
.decrypt_padded_mut::<NoPadding>(&mut bootid_bytes)
|
||||
{
|
||||
println!("ERROR: Could not decrypt BootID: {e:#?}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let bootid = unsafe { std::mem::transmute::<[u8; 96], BootId>(bootid_bytes) };
|
||||
|
||||
if bootid.container_type != ContainerType::OS
|
||||
&& bootid.container_type != ContainerType::APP
|
||||
&& bootid.container_type != ContainerType::OPTION
|
||||
{
|
||||
println!("ERROR: Unknown container type {}", bootid.container_type);
|
||||
continue;
|
||||
}
|
||||
|
||||
let os_id = std::str::from_utf8(&bootid.os_id)?;
|
||||
let game_id = std::str::from_utf8(&bootid.game_id)?;
|
||||
let id = match bootid.container_type {
|
||||
ContainerType::OS => os_id,
|
||||
_ => game_id,
|
||||
};
|
||||
|
||||
let keys = match bootid.container_type {
|
||||
ContainerType::OS => get_game_keys(os_id),
|
||||
ContainerType::APP => get_game_keys(game_id),
|
||||
_ => Some(GameKeys {
|
||||
key: OPTION_KEY,
|
||||
iv: Some(OPTION_IV),
|
||||
}),
|
||||
};
|
||||
|
||||
let Some(keys) = keys else {
|
||||
println!("ERROR: Key not found for {id}. If you're using a custom key file, ensure the key file is 16/32 bytes and named {id}.bin.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let data_offset = bootid.header_block_count * bootid.block_size;
|
||||
let key = keys.key;
|
||||
let iv = if bootid.use_custom_iv {
|
||||
None
|
||||
} else {
|
||||
keys.iv
|
||||
};
|
||||
let iv = match iv {
|
||||
Some(iv) => iv,
|
||||
None => {
|
||||
reader.seek(SeekFrom::Start(data_offset))?;
|
||||
|
||||
let reference = Read::by_ref(&mut reader);
|
||||
|
||||
reference.take(4096).read_to_end(&mut page)?;
|
||||
|
||||
if bootid.container_type == ContainerType::OPTION {
|
||||
calculate_file_iv(key, EXFAT_HEADER, &page)?
|
||||
} else {
|
||||
calculate_file_iv(key, NTFS_HEADER, &page)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output_filename = match bootid.container_type {
|
||||
ContainerType::OS => format!(
|
||||
"{os_id}_{:<04}.{:<02}.{:<02}_{}_{}.ntfs",
|
||||
bootid.os_version.major,
|
||||
bootid.os_version.minor,
|
||||
bootid.os_version.release,
|
||||
bootid.target_timestamp,
|
||||
bootid.sequence_number
|
||||
),
|
||||
ContainerType::APP => {
|
||||
if bootid.sequence_number > 0 {
|
||||
format!(
|
||||
"{game_id}_{}.{:<02}.{:<02}_{}_{}_{}.{:<02}.{:<02}.ntfs",
|
||||
unsafe { bootid.target_version.version.major },
|
||||
unsafe { bootid.target_version.version.minor },
|
||||
unsafe { bootid.target_version.version.release },
|
||||
bootid.target_timestamp,
|
||||
bootid.sequence_number,
|
||||
bootid.source_version.major,
|
||||
bootid.source_version.minor,
|
||||
bootid.source_version.release,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{game_id}_{}.{:<02}.{:<02}_{}_{}.ntfs",
|
||||
unsafe { bootid.target_version.version.major },
|
||||
unsafe { bootid.target_version.version.minor },
|
||||
unsafe { bootid.target_version.version.release },
|
||||
bootid.target_timestamp,
|
||||
bootid.sequence_number,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => format!(
|
||||
"{game_id}_{}_{}_{}.exfat",
|
||||
unsafe { std::str::from_utf8(&bootid.target_version.option)? },
|
||||
bootid.target_timestamp,
|
||||
bootid.sequence_number,
|
||||
),
|
||||
};
|
||||
let output_path = path.with_file_name(&output_filename);
|
||||
let output_file = File::create(&output_path)?;
|
||||
let output_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size;
|
||||
|
||||
output_file.set_len(output_size)?;
|
||||
|
||||
let mut writer = BufWriter::with_capacity(0x40000, output_file);
|
||||
let cipher = Aes128Dec::new_from_slice(&key).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let pb = ProgressBar::new(output_size)
|
||||
.with_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")?
|
||||
);
|
||||
|
||||
pb.set_prefix(output_filename);
|
||||
reader.seek(SeekFrom::Start(data_offset))?;
|
||||
|
||||
for _ in 0..output_size / PAGE_SIZE {
|
||||
let file_offset = reader.stream_position()? - data_offset;
|
||||
let reference = Read::by_ref(&mut reader);
|
||||
|
||||
calculate_page_iv(file_offset, &iv, &mut page_iv);
|
||||
page.clear();
|
||||
reference.take(PAGE_SIZE).read_to_end(&mut page)?;
|
||||
|
||||
let page_cipher = Aes128CbcDec::inner_iv_slice_init(cipher.clone(), &page_iv)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
page_cipher
|
||||
.decrypt_padded_mut::<NoPadding>(&mut page)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
|
||||
writer.write_all(&page)?;
|
||||
pb.inc(PAGE_SIZE);
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
pb.finish();
|
||||
|
||||
page.clear();
|
||||
page_iv.fill(0);
|
||||
bootid_bytes.fill(0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user