Skip to content

Commit

Permalink
feat: sligthly strengthen size_increasing_zip_obfuscation
Browse files Browse the repository at this point in the history
It has come to my attention that there is a relatively novel technique
we can apply to slightly improve how hard are packs to extract, for a
small subset of pack files.
  • Loading branch information
AlexTMjugador committed Sep 24, 2023
1 parent 2d075e4 commit ea933d5
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 31 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ Versioning](https://semver.org/spec/v2.0.0.html).
Action and Docker container also saw performance improvements. (Thanks
_@xMikux_ for reporting the performance differences!)

#### Protection

- PackSquash now adds an extra layer of protection when
`size_increasing_zip_obfuscation` is enabled on a small subset of pack files,
as far as it safe to do so due to the inner workings of Minecraft. (Thanks to
a Discord user for bringing this idea to my attention)
- Select textures may optionally be more protected by changing the new
`may_be_atlas_texture` PNG-specific option, but it is advised that you only
do this if you have detailed knowledge of how the game processes textures,
as otherwise the game may not load the pack correctly.

#### Fixed

- The UTF-8 BOM is no longer automatically stripped from properties files, as
Expand Down
18 changes: 18 additions & 0 deletions packages/packsquash/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,23 @@ pub struct PngFileOptions {
///
/// **Default value**: `false`
pub downsize_if_single_color: bool,
/// Controls whether PackSquash should assume that this texture may be stitched by the game as
/// a part of an internal or custom atlas. For performance reasons, Minecraft stitches most
/// game textures into atlases, including those of item and block models, but there are a few
/// exceptions, such as main menu panorama textures, or those sampled directly from shaders.
///
/// Currently, this option only affects how PackSquash protects images when
/// [`size_increasing_zip_obfuscation`](GlobalOptions::size_increasing_zip_obfuscation) is in
/// effect. `true` is a safe default value that causes textures to be less protected. Setting
/// it to `false` for better protection is, however, only recommended if you have detailed
/// knowledge of how the game stitches textures and are willing to test the correctness of this
/// assumption on a case-by-case basis.
///
/// This option may be changed in the future to have more side effects. It may also be removed,
/// depending on how PackSquash improves its atlas texture detection capabilities.
///
/// **Default value**: `true`
pub may_be_atlas_texture: bool,
/// Crate-private option set by the [MinecraftQuirk::GrayscaleImagesGammaMiscorrection]
/// workaround to not reduce color images to grayscale.
///
Expand Down Expand Up @@ -951,6 +968,7 @@ impl Default for PngFileOptions {
maximum_width_and_height: NonZeroU16::new(8192).unwrap(),
skip_alpha_optimizations: false,
downsize_if_single_color: false,
may_be_atlas_texture: true,
working_around_grayscale_reduction_quirk: false,
working_around_color_type_change_quirk: false,
working_around_transparent_pixel_colors_change_quirk: false
Expand Down
8 changes: 6 additions & 2 deletions packages/packsquash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,10 @@ async fn process_pack_file<F: AsyncRead + AsyncSeek + Unpin>(

if copy_previous_file {
optimization_error = squash_zip
.add_previous_file(&pack_file_path)
.add_previous_file(
&pack_file_path,
pack_file_process_data.listing_circumstances
)
.await
.err()
.map(|err| err.to_string());
Expand Down Expand Up @@ -844,7 +847,8 @@ async fn process_pack_file<F: AsyncRead + AsyncSeek + Unpin>(
&pack_file_path,
processed_pack_file_chunks,
!compress_already_compressed && pack_file_process_data.is_compressed,
file_size_hint.try_into().unwrap_or(0)
file_size_hint.try_into().unwrap_or(0),
pack_file_process_data.listing_circumstances
)
.await
.err()
Expand Down
14 changes: 12 additions & 2 deletions packages/packsquash/src/pack_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use tokio_stream::Stream;
pub use util::strip_utf8_bom;

use crate::pack_file::asset_type::PackFileAssetType;
use crate::squash_zip::FileListingCircumstances;

pub mod asset_type;

Expand Down Expand Up @@ -96,6 +97,12 @@ trait PackFile {
/// Returns whether the contents of this pack file are already internally compressed, and as such any
/// attempt to further compress them will likely result in lower than usual space savings.
fn is_compressed(&self) -> bool;

/// Returns whether this pack file may be stitched into an atlas texture by the game. Most pack files
/// should return `false` here, except for texture (i.e., PNG) files.
fn may_be_atlas_texture(&self) -> bool {
false
}
}

/// Factory trait for a [`PackFile`] that allows it to be instantiated in an standard way. It is separated
Expand All @@ -122,7 +129,7 @@ trait PackFileConstructor<R: AsyncRead + Unpin + 'static>: PackFile + Sized {
) -> Option<Self>;
}

/// Contains the different pieces of data obtained by processing some pack file.
/// Contains the different pieces of data obtained or related to processing some pack file.
pub struct PackFileProcessData {
/// A stream that contains the byte chunks of the processed pack file data.
pub optimized_byte_chunks_stream: Box<dyn Stream<Item = OptimizedBoxedBytesChunk> + Send + Unpin>,
Expand All @@ -132,5 +139,8 @@ pub struct PackFileProcessData {
pub is_compressed: bool,
/// The canonical extension for the pack file, which Minecraft expects. It might be `None`
/// if the pack file is already known to have a canonical extension.
pub canonical_extension: Option<&'static str>
pub canonical_extension: Option<&'static str>,
/// The circumstances affecting how this file is listed (i.e., enumerated) alongside other
/// pack files of its type by the game.
pub listing_circumstances: FileListingCircumstances
}
4 changes: 4 additions & 0 deletions packages/packsquash/src/pack_file/asset_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::pack_file::png_file::PngFile;
#[cfg(feature = "optifine-support")]
use crate::pack_file::properties_file::PropertiesFile;
use crate::pack_file::shader_file::ShaderFile;
use crate::squash_zip::FileListingCircumstances;
use crate::{
config::{compile_pack_file_glob_pattern, CustomFileOptions, FileOptions},
RelativePath
Expand Down Expand Up @@ -732,6 +733,9 @@ fn pack_file_to_process_data(
pack_file.map(|pack_file| PackFileProcessData {
is_compressed: pack_file.is_compressed(),
canonical_extension: asset_type.canonical_extension(),
listing_circumstances: FileListingCircumstances {
is_atlas_texture: pack_file.may_be_atlas_texture()
},
optimized_byte_chunks_stream: Box::new(pack_file.process().map(|byte_chunk_result| {
match byte_chunk_result {
Ok((optimization_strategy, optimized_bytes)) => Ok((
Expand Down
4 changes: 4 additions & 0 deletions packages/packsquash/src/pack_file/png_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ impl<T: AsyncRead + Send + Unpin + 'static> PackFile for PngFile<T> {
fn is_compressed(&self) -> bool {
true
}

fn may_be_atlas_texture(&self) -> bool {
self.optimization_settings.may_be_atlas_texture
}
}

impl<T: AsyncRead + Send + Unpin + 'static> PackFileConstructor<T> for PngFile<T> {
Expand Down
33 changes: 27 additions & 6 deletions packages/packsquash/src/squash_zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ use self::{
}
};

pub use self::obfuscation_engine::FileListingCircumstances;

mod buffered_async_spooled_temp_file;
mod obfuscation_engine;
pub mod relative_path;
Expand Down Expand Up @@ -81,7 +83,8 @@ struct PartialCentralDirectoryHeader {
squash_time: [u8; 4],
crc32: u32,
compressed_size: u32,
uncompressed_size: u32
uncompressed_size: u32,
listing_circumstances: FileListingCircumstances
}

/// Represents a ZIP file hash and size pair.
Expand Down Expand Up @@ -277,7 +280,8 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
path: &RelativePath<'_>,
processed_data: S,
skip_compression: bool,
file_size_hint: usize
file_size_hint: usize,
listing_circumstances: FileListingCircumstances
) -> Result<(), SquashZipError> {
let (mut local_file_header, mut compressed_data_scratch_file) = self
.compress_and_generate_local_header(
Expand Down Expand Up @@ -355,6 +359,7 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
path,
&local_file_header,
*matching_header_offset,
listing_circumstances,
&mut state.central_directory_data
)?;

Expand Down Expand Up @@ -384,6 +389,7 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
path,
&local_file_header,
new_local_file_header_offset,
listing_circumstances,
&mut state.central_directory_data
)?;

Expand Down Expand Up @@ -435,7 +441,11 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
/// was not present in the previous ZIP file. In this case it is guaranteed that no bad
/// state was introduced in the result output ZIP file, and the instance can still used
/// normally.
pub async fn add_previous_file(&self, path: &RelativePath<'_>) -> Result<(), SquashZipError> {
pub async fn add_previous_file(
&self,
path: &RelativePath<'_>,
listing_circumstances: FileListingCircumstances
) -> Result<(), SquashZipError> {
// For this method we implement a simpler version of the algorithm of add_file. It can be
// summarised as follows:
// 1. Check if the file is in map 1) (hash, size) -> (LOC offset list).
Expand Down Expand Up @@ -559,6 +569,7 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
path,
&local_file_header,
*matching_header_offset,
listing_circumstances,
&mut state.central_directory_data
)?;

Expand Down Expand Up @@ -588,6 +599,7 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
path,
&local_file_header,
new_local_file_header_offset,
listing_circumstances,
&mut state.central_directory_data
)?;

Expand Down Expand Up @@ -644,8 +656,10 @@ impl<F: AsyncRead + AsyncSeek + Unpin> SquashZip<F> {
spoof_version_made_by: false
};

self.obfuscation_engine
.obfuscate_central_directory_header(&mut central_directory_header);
self.obfuscation_engine.obfuscate_central_directory_header(
&mut central_directory_header,
header_data.listing_circumstances
);

central_directory_header.write(&mut output_zip).await?;
}
Expand Down Expand Up @@ -930,6 +944,11 @@ async fn read_previous_zip_contents<F: AsyncRead + AsyncSeek + Unpin>(

previous_zip.read_exact(&mut filename_buf).await?;

// Normalize directories
if filename_buf.ends_with(&[b'/']) {
filename_buf.pop();
}

// In the unlikely case this relative path is corrupt and/or invalid, but
// still valid UTF-8, it'll be effectively ignored, so it doesn't really
// matter
Expand Down Expand Up @@ -1028,6 +1047,7 @@ fn add_partial_central_directory_header(
path: &RelativePath<'_>,
local_file_header: &LocalFileHeader<'_>,
local_file_header_offset: u64,
listing_circumstances: FileListingCircumstances,
central_directory_data: &mut AHashMap<RelativePath<'static>, PartialCentralDirectoryHeader>
) -> Result<(), SquashZipError> {
match central_directory_data.entry(path.as_owned()) {
Expand All @@ -1038,7 +1058,8 @@ fn add_partial_central_directory_header(
squash_time: local_file_header.squash_time,
crc32: local_file_header.crc32,
compressed_size: local_file_header.compressed_size,
uncompressed_size: local_file_header.uncompressed_size
uncompressed_size: local_file_header.uncompressed_size,
listing_circumstances
});

Ok(())
Expand Down
Loading

0 comments on commit ea933d5

Please sign in to comment.