mirror of
https://gitea.tendokyu.moe/beerpsi/fsdecrypt.git
synced 2026-10-07 14:18:02 +03:00
Merge branch 'trunk' of git.tendokyu.moe:beerpsi/fsdecrypt into trunk
This commit is contained in:
Generated
+713
-100
File diff suppressed because it is too large
Load Diff
+15
-5
@@ -1,13 +1,23 @@
|
||||
[package]
|
||||
name = "fsdecrypt"
|
||||
version = "0.1.1"
|
||||
version = "0.1.7"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
anyhow = "1.0.86"
|
||||
anyhow = "1.0.100"
|
||||
cbc = "0.1.2"
|
||||
crc32fast = "1.4.2"
|
||||
hex-literal = "0.4.1"
|
||||
indicatif = "0.17.8"
|
||||
crc32fast = "1.5.0"
|
||||
hex-literal = "1.0.0"
|
||||
indicatif = "0.18.0"
|
||||
exfat-fs = "0.1.3"
|
||||
chrono = "0.4.42"
|
||||
clap = { version = "4.5.48", features = ["derive"] }
|
||||
ntfs = "0.4.0"
|
||||
thiserror = "2.0.18"
|
||||
cipher = { version = "0.4.4", features = ["alloc"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = "none"
|
||||
|
||||
+28
-1
@@ -1,4 +1,4 @@
|
||||
use std::fmt::Display;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use hex_literal::hex;
|
||||
|
||||
@@ -75,3 +75,30 @@ pub struct BootId {
|
||||
// We don't need the entire bootID, so keep size down by not including the string table.
|
||||
// pub strings: [u8; 10156],
|
||||
}
|
||||
|
||||
impl Debug for BootId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BootId")
|
||||
.field("crc32", &self.crc32)
|
||||
.field("length", &self.length)
|
||||
.field("signature", &self.signature)
|
||||
.field("unk1", &self.unk1)
|
||||
.field("container_type", &self.container_type)
|
||||
.field("sequence_number", &self.sequence_number)
|
||||
.field("use_custom_iv", &self.use_custom_iv)
|
||||
.field("game_id", &self.game_id)
|
||||
.field("target_timestamp", &self.target_timestamp)
|
||||
//.field("target_version", &self.target_version)
|
||||
.field("block_count", &self.block_count)
|
||||
.field("block_size", &self.block_size)
|
||||
.field("header_block_count", &self.header_block_count)
|
||||
.field("unk2", &self.unk2)
|
||||
.field("os_id", &self.os_id)
|
||||
.field("os_generation", &self.os_generation)
|
||||
.field("source_timestamp", &self.source_timestamp)
|
||||
.field("source_version", &self.source_version)
|
||||
.field("os_version", &self.os_version)
|
||||
.field("padding", &self.padding)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub const OPTION_IV: [u8; 16] = hex!("c063bf6f562d084d7963c987f5281761");
|
||||
|
||||
pub type Aes128CbcDec = cbc::Decryptor<aes::Aes128Dec>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GameKeys {
|
||||
pub key: [u8; 16],
|
||||
pub iv: Option<[u8; 16]>,
|
||||
|
||||
+227
-183
@@ -1,58 +1,123 @@
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
fs::{create_dir_all, File, FileTimes},
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use clap::Parser;
|
||||
use exfat_fs::dir::{entry::fs::FsElement, Root};
|
||||
|
||||
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 bootid::ContainerType;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use ntfs::{
|
||||
indexes::NtfsFileNameIndex, structured_values::NtfsStandardInformation, Ntfs,
|
||||
NtfsAttributeType, NtfsTime,
|
||||
};
|
||||
|
||||
use crate::stream::FscryptDecryptor;
|
||||
|
||||
mod bootid;
|
||||
mod crypto;
|
||||
mod stream;
|
||||
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
fn exfat_timestamp_to_system_time(
|
||||
timestamp: &exfat_fs::timestamp::Timestamp,
|
||||
) -> Result<SystemTime> {
|
||||
let exfat_date = timestamp.date();
|
||||
let exfat_time = timestamp.time();
|
||||
// exFAT UTC offset is in 15-minute intervals, so 1 = UTC+00:15, 2 = UTC+00:30, etc.
|
||||
let exfat_utc_offset = timestamp.utc_offset() as i32 * 15 * 60;
|
||||
let chrono_date_time = FixedOffset::east_opt(exfat_utc_offset)
|
||||
.ok_or_else(|| anyhow!("invaid utc offset: {}", timestamp.utc_offset()))?
|
||||
.with_ymd_and_hms(
|
||||
exfat_date.year as i32,
|
||||
exfat_date.month as u32,
|
||||
exfat_date.day as u32,
|
||||
exfat_time.hour as u32,
|
||||
exfat_time.minute as u32,
|
||||
exfat_time.second as u32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
return Ok(SystemTime::UNIX_EPOCH
|
||||
+ Duration::from_micros(chrono_date_time.timestamp_micros().try_into()?));
|
||||
}
|
||||
|
||||
fn extract_exfat_contents(exfat_path: &Path) -> Result<()> {
|
||||
println!("Extracting contents of {}", exfat_path.display());
|
||||
|
||||
let file = File::open(exfat_path)?;
|
||||
let mut root = Root::open(file)?;
|
||||
|
||||
// Create output directory with same name as exfat file (without extension)
|
||||
let output_dir = exfat_path.with_extension("");
|
||||
|
||||
let file = FscryptDecryptor::new(File::open(exfat_path)?).map_err(|e| anyhow!(e))?;
|
||||
let mut root = Root::open(file)?;
|
||||
|
||||
let pb = ProgressBar::new(calculate_exfat_size(root.items())?)
|
||||
.with_style(
|
||||
ProgressStyle::default_bar().template(
|
||||
"{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]",
|
||||
)?
|
||||
);
|
||||
pb.set_prefix(format!(
|
||||
"Extracting {}",
|
||||
exfat_path.file_name().unwrap().display(),
|
||||
));
|
||||
|
||||
create_dir_all(&output_dir)?;
|
||||
extract_fs_elements(root.items(), &output_dir)?;
|
||||
extract_exfat_elements(root.items(), &output_dir, &pb)?;
|
||||
|
||||
pb.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_fs_elements(elements: &mut [FsElement<File>], output_dir: &Path) -> Result<()> {
|
||||
fn calculate_exfat_size(elements: &[FsElement<FscryptDecryptor<File>>]) -> Result<u64> {
|
||||
let mut total = 0;
|
||||
|
||||
for element in elements {
|
||||
match element {
|
||||
FsElement::F(ref file) => total += file.len(),
|
||||
FsElement::D(directory) => total += calculate_exfat_size(&directory.open()?)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn extract_exfat_elements(
|
||||
elements: &mut [FsElement<FscryptDecryptor<File>>],
|
||||
output_dir: &Path,
|
||||
pb: &ProgressBar,
|
||||
) -> Result<()> {
|
||||
for element in elements {
|
||||
match element {
|
||||
FsElement::F(ref mut file) => {
|
||||
let dest_path = output_dir.join(file.name());
|
||||
let mut dest = File::create(dest_path)?;
|
||||
|
||||
std::io::copy(file, &mut dest)?;
|
||||
dest.set_times(
|
||||
FileTimes::new()
|
||||
.set_accessed(exfat_timestamp_to_system_time(
|
||||
file.timestamps().accessed(),
|
||||
)?)
|
||||
.set_modified(exfat_timestamp_to_system_time(
|
||||
file.timestamps().modified(),
|
||||
)?),
|
||||
)?;
|
||||
|
||||
let mut writer = BufWriter::with_capacity(256 * 1024, &mut dest);
|
||||
|
||||
std::io::copy(file, &mut writer)?;
|
||||
writer.flush()?;
|
||||
pb.inc(file.len());
|
||||
}
|
||||
FsElement::D(directory) => {
|
||||
let dest_path = output_dir.join(directory.name());
|
||||
create_dir_all(&dest_path)?;
|
||||
|
||||
let mut children = directory.open()?;
|
||||
extract_fs_elements(&mut children, &dest_path)?;
|
||||
extract_exfat_elements(&mut children, &dest_path, &pb)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,182 +125,161 @@ fn extract_fs_elements(elements: &mut [FsElement<File>], output_dir: &Path) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ntfs_time_to_system_time(ntfs_time: NtfsTime) -> SystemTime {
|
||||
// An NTFS "interval" is 100 nanoseconds.
|
||||
// The Windows epoch is 1601-01-01, while the Unix epoch is 1970-01-01.
|
||||
let intervals_since_windows_epoch = ntfs_time.nt_timestamp();
|
||||
let intervals_since_unix_epoch = intervals_since_windows_epoch - 116_444_736_000_000_000;
|
||||
let nanos_since_unix_epoch = intervals_since_unix_epoch * 100;
|
||||
|
||||
return SystemTime::UNIX_EPOCH + Duration::from_nanos(nanos_since_unix_epoch);
|
||||
}
|
||||
|
||||
fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result<PathBuf> {
|
||||
let vhd_filename = format!("internal_{sequence_number}.vhd");
|
||||
let output_path = image_path.with_extension("vhd");
|
||||
|
||||
let mut fs = FscryptDecryptor::new(File::open(image_path)?).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let mut ntfs = Ntfs::new(&mut fs)?;
|
||||
|
||||
ntfs.read_upcase_table(&mut fs)?;
|
||||
|
||||
let root_directory = ntfs.root_directory(&mut fs)?;
|
||||
let index = root_directory.directory_index(&mut fs)?;
|
||||
let mut finder = index.finder();
|
||||
let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut fs, &vhd_filename)
|
||||
.ok_or_else(|| anyhow!("could not find VHD {vhd_filename}"))??;
|
||||
let file = entry.to_file(&ntfs, &mut fs)?;
|
||||
let data_item = file
|
||||
.data(&mut fs, "")
|
||||
.ok_or_else(|| anyhow!("file data does not exist"))??;
|
||||
let data_attribute = data_item.to_attribute()?;
|
||||
let mut data_value =
|
||||
BufReader::with_capacity(256 * 1024, data_attribute.value(&mut fs)?.attach(&mut fs));
|
||||
|
||||
let mut output_file = File::create(&output_path)?;
|
||||
|
||||
let pb = ProgressBar::new(data_attribute.value_length() as u64)
|
||||
.with_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")?
|
||||
);
|
||||
pb.set_prefix(format!("{}", output_path.file_name().unwrap().display()));
|
||||
|
||||
loop {
|
||||
let buffer = data_value.fill_buf()?;
|
||||
let length = buffer.len();
|
||||
|
||||
if length == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
output_file.write_all(buffer)?;
|
||||
data_value.consume(length);
|
||||
pb.inc(length as u64);
|
||||
}
|
||||
output_file.flush()?;
|
||||
|
||||
pb.finish();
|
||||
|
||||
let mut attributes_iterator = file.attributes();
|
||||
|
||||
while let Some(attribute) = attributes_iterator.next(&mut fs) {
|
||||
let attribute = attribute?;
|
||||
let attribute = attribute.to_attribute()?;
|
||||
|
||||
match attribute.ty() {
|
||||
Ok(NtfsAttributeType::StandardInformation) => {
|
||||
let info = attribute.resident_structured_value::<NtfsStandardInformation>()?;
|
||||
|
||||
output_file.set_times(
|
||||
FileTimes::new()
|
||||
.set_accessed(ntfs_time_to_system_time(info.access_time()))
|
||||
.set_modified(ntfs_time_to_system_time(info.modification_time())),
|
||||
)?;
|
||||
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "decryptor for some SEGA containers", long_about = None)]
|
||||
struct Cli {
|
||||
#[arg(long, help = "do not extract contents of decrypted image")]
|
||||
no_extract: bool,
|
||||
|
||||
#[arg(required = true)]
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
let cli = Cli::parse();
|
||||
|
||||
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,
|
||||
),
|
||||
};
|
||||
for path in &cli.files {
|
||||
let file = FscryptDecryptor::new(File::open(path)?).map_err(|e| anyhow!(e))?;
|
||||
let bootid = file.bootid.clone();
|
||||
let output_filename = file.filename()?;
|
||||
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)?;
|
||||
// Can't directly extract options since we can't impl exfat_fs::dir::ReadOffset
|
||||
// for our wrapper decryptor
|
||||
if cli.no_extract {
|
||||
let mut output_file = File::create(&output_path)?;
|
||||
|
||||
let mut writer = BufWriter::with_capacity(0x40000, output_file);
|
||||
let cipher = Aes128Dec::new_from_slice(&key).map_err(|e| anyhow!(e))?;
|
||||
output_file.set_len(file.len())?;
|
||||
|
||||
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}]")?
|
||||
);
|
||||
let pb = ProgressBar::new(file.len())
|
||||
.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.clone());
|
||||
reader.seek(SeekFrom::Start(data_offset))?;
|
||||
pb.set_prefix(output_filename.clone());
|
||||
|
||||
for _ in 0..output_size / PAGE_SIZE {
|
||||
let file_offset = reader.stream_position()? - data_offset;
|
||||
let reference = Read::by_ref(&mut reader);
|
||||
let mut reader = BufReader::with_capacity(0x40000, file);
|
||||
|
||||
calculate_page_iv(file_offset, &iv, &mut page_iv);
|
||||
page.clear();
|
||||
reference.take(PAGE_SIZE).read_to_end(&mut page)?;
|
||||
loop {
|
||||
let buffer = reader.fill_buf()?;
|
||||
let length = buffer.len();
|
||||
|
||||
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))?;
|
||||
if length == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.write_all(&page)?;
|
||||
pb.inc(PAGE_SIZE);
|
||||
}
|
||||
output_file.write_all(&buffer)?;
|
||||
reader.consume(length);
|
||||
|
||||
writer.flush()?;
|
||||
pb.finish();
|
||||
pb.inc(length as u64);
|
||||
}
|
||||
|
||||
// Extract exfat contents if this is an exfat file
|
||||
if bootid.container_type == ContainerType::OPTION
|
||||
&& output_path.extension().unwrap_or_default() == "exfat"
|
||||
{
|
||||
if let Err(e) = extract_exfat_contents(&output_path) {
|
||||
println!("WARNING: Failed to extract exfat contents: {e:#?}");
|
||||
} else {
|
||||
println!("Extracted exfat contents: {:?}", output_path);
|
||||
println!("Deleting exfat file: {:?}", output_path);
|
||||
|
||||
std::fs::remove_file(output_path)?;
|
||||
output_file.flush()?;
|
||||
} else {
|
||||
match bootid.container_type {
|
||||
ContainerType::OS | ContainerType::APP => {
|
||||
match extract_internal_vhd(&path, bootid.sequence_number) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("WARNING: Failed to extract internal VHD: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
ContainerType::OPTION => match extract_exfat_contents(&path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("WARNING: Failed to extract exfat contents: {e:#?}");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!("WARNING: Unknown container type: {}", bootid.container_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page.clear();
|
||||
page_iv.fill(0);
|
||||
bootid_bytes.fill(0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Read, Seek, SeekFrom},
|
||||
str::Utf8Error,
|
||||
};
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::{NoPadding, UnpadError},
|
||||
BlockDecryptMut, InvalidLength,
|
||||
};
|
||||
use cipher::KeyIvInit;
|
||||
use exfat_fs::disk::ReadOffset;
|
||||
|
||||
use crate::{
|
||||
bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY},
|
||||
crypto::{
|
||||
calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER,
|
||||
NTFS_HEADER, OPTION_IV, OPTION_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DecryptError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
|
||||
#[error("Invalid IV length: {0:?}")]
|
||||
AesInvalidLength(InvalidLength),
|
||||
|
||||
#[error("Malformed padding: {0:?}")]
|
||||
AesUnpadError(UnpadError),
|
||||
|
||||
#[error("Unknown container type {0}")]
|
||||
InvalidContainerType(u8),
|
||||
|
||||
#[error("There were no decryption keys for this container.")]
|
||||
NoMatchingKeys,
|
||||
}
|
||||
|
||||
/// Decryptor for an fscrypt container.
|
||||
#[derive(Debug)]
|
||||
pub struct FscryptDecryptor<R> {
|
||||
/// The fscrypt file.
|
||||
input: R,
|
||||
|
||||
/// The fscrypt file's bootid.
|
||||
pub bootid: BootId,
|
||||
|
||||
key: [u8; 16],
|
||||
iv: [u8; 16],
|
||||
|
||||
encrypted_page: Vec<u8>,
|
||||
plaintext_pos: u64,
|
||||
page: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl<R> FscryptDecryptor<R> {
|
||||
pub fn len(&self) -> u64 {
|
||||
self.bootid.block_size * (self.bootid.block_count - self.bootid.header_block_count)
|
||||
}
|
||||
|
||||
pub fn filename(&self) -> Result<String, Utf8Error> {
|
||||
let bootid = self.bootid;
|
||||
let os_id = std::str::from_utf8(&bootid.os_id)?;
|
||||
let game_id = std::str::from_utf8(&bootid.game_id)?;
|
||||
|
||||
let 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,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(filename)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> FscryptDecryptor<R> {
|
||||
pub fn new(mut input: R) -> Result<Self, DecryptError> {
|
||||
let bootid_cipher = Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV)
|
||||
.map_err(|e| DecryptError::AesInvalidLength(e))?;
|
||||
let mut bootid_bytes = [0u8; std::mem::size_of::<BootId>()];
|
||||
|
||||
input.read_exact(&mut bootid_bytes)?;
|
||||
bootid_cipher
|
||||
.decrypt_padded_mut::<NoPadding>(&mut bootid_bytes)
|
||||
.map_err(|e| DecryptError::AesUnpadError(e))?;
|
||||
|
||||
let bootid: BootId = unsafe { std::mem::transmute(bootid_bytes) };
|
||||
|
||||
if bootid.container_type != ContainerType::OS
|
||||
&& bootid.container_type != ContainerType::APP
|
||||
&& bootid.container_type != ContainerType::OPTION
|
||||
{
|
||||
return Err(DecryptError::InvalidContainerType(bootid.container_type));
|
||||
}
|
||||
|
||||
let data_offset = bootid.header_block_count * bootid.block_size;
|
||||
let mut page: Vec<u8> = Vec::with_capacity(PAGE_SIZE as usize);
|
||||
let keys = match bootid.container_type {
|
||||
ContainerType::OS => get_game_keys(std::str::from_utf8(&bootid.os_id)?),
|
||||
ContainerType::APP => get_game_keys(std::str::from_utf8(&bootid.game_id)?),
|
||||
_ => Some(GameKeys {
|
||||
key: OPTION_KEY,
|
||||
iv: Some(OPTION_IV),
|
||||
}),
|
||||
};
|
||||
let Some(keys) = keys else {
|
||||
return Err(DecryptError::NoMatchingKeys);
|
||||
};
|
||||
let iv = if bootid.use_custom_iv { None } else { keys.iv };
|
||||
let iv = match iv {
|
||||
Some(iv) => iv,
|
||||
None => {
|
||||
input.seek(SeekFrom::Start(data_offset))?;
|
||||
|
||||
let reference = Read::by_ref(&mut input);
|
||||
|
||||
reference.take(PAGE_SIZE).read_to_end(&mut page)?;
|
||||
|
||||
if bootid.container_type == ContainerType::OPTION {
|
||||
calculate_file_iv(keys.key, EXFAT_HEADER, &page)?
|
||||
} else {
|
||||
calculate_file_iv(keys.key, NTFS_HEADER, &page)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
input.seek(SeekFrom::Start(data_offset))?;
|
||||
|
||||
Ok(Self {
|
||||
input,
|
||||
bootid,
|
||||
key: keys.key,
|
||||
iv,
|
||||
encrypted_page: Vec::with_capacity(PAGE_SIZE as usize),
|
||||
plaintext_pos: 0,
|
||||
page: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn decrypt_page(&mut self) -> io::Result<()> {
|
||||
let page = &self.encrypted_page;
|
||||
let file_offset = self.input.stream_position()?
|
||||
- (self.bootid.header_block_count * self.bootid.block_size)
|
||||
- PAGE_SIZE;
|
||||
let mut page_iv = [0u8; 16];
|
||||
|
||||
calculate_page_iv(file_offset, &self.iv, &mut page_iv);
|
||||
|
||||
let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap();
|
||||
|
||||
self.page = Some(
|
||||
page_cipher
|
||||
.decrypt_padded_vec_mut::<NoPadding>(&page)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?,
|
||||
);
|
||||
self.encrypted_page.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_page(&mut self, buf: &mut [u8]) -> usize {
|
||||
let Some(ref page) = self.page else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let page_offset = (self.plaintext_pos % PAGE_SIZE) as usize;
|
||||
let to_read = std::cmp::min(page.len() - page_offset, buf.len());
|
||||
|
||||
buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
|
||||
|
||||
self.plaintext_pos += to_read as u64;
|
||||
|
||||
if self.plaintext_pos % PAGE_SIZE == 0 {
|
||||
self.page = None;
|
||||
}
|
||||
|
||||
to_read
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for FscryptDecryptor<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.page.is_none() {
|
||||
if (self.encrypted_page.len() as u64) < PAGE_SIZE {
|
||||
Read::by_ref(&mut self.input)
|
||||
.take(PAGE_SIZE)
|
||||
.read_to_end(&mut self.encrypted_page)?;
|
||||
}
|
||||
|
||||
self.decrypt_page()?;
|
||||
}
|
||||
|
||||
Ok(self.read_from_page(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for FscryptDecryptor<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let start = self.bootid.header_block_count * self.bootid.block_size;
|
||||
let target_pos = match pos {
|
||||
SeekFrom::Start(offset) => offset,
|
||||
SeekFrom::Current(offset) => {
|
||||
let res = (self.plaintext_pos as i64) + offset;
|
||||
|
||||
if res < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"cannot seek before the start",
|
||||
));
|
||||
}
|
||||
|
||||
res as u64
|
||||
}
|
||||
SeekFrom::End(offset) => {
|
||||
let length = (self.bootid.block_count - self.bootid.header_block_count)
|
||||
* self.bootid.block_size;
|
||||
let res = (length as i64) + offset;
|
||||
|
||||
if res < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"cannot seek before the start",
|
||||
));
|
||||
}
|
||||
|
||||
res as u64
|
||||
}
|
||||
};
|
||||
let page_index = self.plaintext_pos / PAGE_SIZE;
|
||||
let target_index = target_pos / PAGE_SIZE;
|
||||
let target_offset = target_pos % PAGE_SIZE;
|
||||
|
||||
if page_index == target_index {
|
||||
self.plaintext_pos = target_pos;
|
||||
return Ok(target_pos);
|
||||
}
|
||||
|
||||
self.page = None;
|
||||
self.input
|
||||
.seek(SeekFrom::Start(start + target_index * PAGE_SIZE))?;
|
||||
self.plaintext_pos = target_index * PAGE_SIZE;
|
||||
|
||||
if target_offset > 0 {
|
||||
let mut to_drop = vec![0; target_offset as usize];
|
||||
self.read_exact(&mut to_drop)?;
|
||||
}
|
||||
|
||||
Ok(target_pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadOffset for FscryptDecryptor<File> {
|
||||
type Err = std::io::Error;
|
||||
|
||||
fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result<usize, Self::Err> {
|
||||
let start = self.bootid.header_block_count * self.bootid.block_size;
|
||||
let target_index = offset / PAGE_SIZE;
|
||||
let target_offset = offset % PAGE_SIZE;
|
||||
let mut page = [0u8; 4096];
|
||||
let mut page_iv = [0u8; 16];
|
||||
let page_offset = target_index * PAGE_SIZE;
|
||||
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::FileExt::read_at(&self.input, &mut page, start + page_offset)?;
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::FileExt::seek_read(&self.input, &mut page, start + page_offset)?;
|
||||
|
||||
calculate_page_iv(page_offset, &self.iv, &mut page_iv);
|
||||
|
||||
let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap();
|
||||
|
||||
page_cipher
|
||||
.decrypt_padded_mut::<NoPadding>(&mut page)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?;
|
||||
|
||||
let to_read = std::cmp::min(buffer.len(), (PAGE_SIZE - target_offset) as usize);
|
||||
|
||||
buffer[..to_read]
|
||||
.copy_from_slice(&page[target_offset as usize..target_offset as usize + to_read]);
|
||||
|
||||
Ok(to_read)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user