Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,12 @@ impl<'a, R: Read> ZipFile<'a, R> {
);

options.normalize();
#[cfg(feature = "aes-crypto")]
if let Some(aes) = self.get_metadata().aes_mode {
// Preserve AES metadata in options for downstream writers.
// This is metadata-only and does not trigger encryption.
options.aes_mode = Some(aes);
}
options
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,16 @@ impl ZipFileData {
let mut local_block = ZipFileData {
system: System::Unix,
version_made_by: DEFAULT_VERSION,
encrypted: options.encrypt_with.is_some(),
encrypted: options.encrypt_with.is_some() || {
#[cfg(feature = "aes-crypto")]
{
options.aes_mode.is_some()
}
#[cfg(not(feature = "aes-crypto"))]
{
false
}
},
using_data_descriptor: false,
is_utf8: !file_name.is_ascii(),
compression_method,
Expand Down Expand Up @@ -1213,7 +1222,7 @@ impl FixedSizeBlock for Zip64DataDescriptorBlock {
///
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
/// does not make use of the CRC check.
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u16)]
pub enum AesVendorVersion {
Ae1 = 0x0001,
Expand Down
28 changes: 28 additions & 0 deletions src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ pub struct FileOptions<'k, T: FileOptionExtension> {
pub(crate) alignment: u16,
#[cfg(feature = "deflate-zopfli")]
pub(super) zopfli_buffer_size: Option<usize>,
#[cfg(feature = "aes-crypto")]
pub(crate) aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
}
/// Simple File Options. Can be copied and good for simple writing zip files
pub type SimpleFileOptions = FileOptions<'static, ()>;
Expand Down Expand Up @@ -566,6 +568,8 @@ impl<T: FileOptionExtension> Default for FileOptions<'_, T> {
alignment: 1,
#[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: Some(1 << 15),
#[cfg(feature = "aes-crypto")]
aes_mode: None,
}
}
}
Expand Down Expand Up @@ -932,9 +936,25 @@ impl<W: Write + Seek> ZipWriter<W> {
0x9901,
aes_dummy_extra_data,
)?;
} else if let Some((mode, vendor, underlying)) = options.aes_mode {
// For raw copies of AES entries, write the correct AES extra data immediately
let mut body = Vec::with_capacity(7);
body.write_u16_le(vendor as u16)?; // vendor version (1 or 2)
body.extend_from_slice(b"AE"); // vendor id
body.push(mode as u8); // strength
body.write_u16_le(underlying.serialize_to_u16())?; // real compression method
aes_extra_data_start = extra_data.len() as u64;
ExtendedFileOptions::add_extra_data_unchecked(
&mut extra_data,
0x9901,
body.into_boxed_slice(),
)?;
}

let (compression_method, aes_mode) = match options.encrypt_with {
// Preserve AES method for raw copies without needing a password
#[cfg(feature = "aes-crypto")]
None if options.aes_mode.is_some() => (CompressionMethod::Aes, options.aes_mode),
#[cfg(feature = "aes-crypto")]
Some(EncryptWith::Aes { mode, .. }) => (
CompressionMethod::Aes,
Expand Down Expand Up @@ -2317,6 +2337,8 @@ mod test {
alignment: 1,
#[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: None,
#[cfg(feature = "aes-crypto")]
aes_mode: None,
};
writer.start_file("mimetype", options).unwrap();
writer
Expand Down Expand Up @@ -2358,6 +2380,8 @@ mod test {
alignment: 1,
#[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: None,
#[cfg(feature = "aes-crypto")]
aes_mode: None,
};

// GB18030
Expand Down Expand Up @@ -2411,6 +2435,8 @@ mod test {
alignment: 0,
#[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: None,
#[cfg(feature = "aes-crypto")]
aes_mode: None,
};
writer.start_file(RT_TEST_FILENAME, options).unwrap();
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();
Expand Down Expand Up @@ -2462,6 +2488,8 @@ mod test {
alignment: 0,
#[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: None,
#[cfg(feature = "aes-crypto")]
aes_mode: None,
};
writer.start_file(RT_TEST_FILENAME, options).unwrap();
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();
Expand Down
83 changes: 83 additions & 0 deletions tests/aes_encryption.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![cfg(feature = "aes-crypto")]

use std::io::{self, Read, Write};
use zip::write::ZipWriter;
use zip::{result::ZipError, write::SimpleFileOptions, AesMode, CompressionMethod, ZipArchive};

const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
Expand Down Expand Up @@ -152,3 +153,85 @@ fn test_extract_encrypted_file<R: io::Read + io::Seek>(
assert_eq!(SECRET_CONTENT, content);
}
}

#[test]
fn raw_copy_from_aes_zip() {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("data/aes_archive.zip"));

let dst_cursor = {
let mut src =
ZipArchive::new(io::Cursor::new(v.as_slice())).expect("couldn't open source zip");
let mut dst = ZipWriter::new(io::Cursor::new(Vec::new()));

let total = src.len();
for i in 0..total {
let file = src.by_index_raw(i).expect("read source entry");
let name = file.name().to_string();
if file.is_dir() {
dst.add_directory(&name, SimpleFileOptions::default())
.expect("add directory");
} else {
dst.raw_copy_file(file).expect("raw copy file");
}
}
dst.finish().expect("finish dst")
};

let mut src_zip = ZipArchive::new(io::Cursor::new(v.as_slice())).expect("reopen src zip");
let mut dst_zip = ZipArchive::new(dst_cursor).expect("reopen dst zip");

let total = src_zip.len();

for i in 0..total {
// Copy out simple header fields without holding borrows across later reads
let (name, is_dir, s_encrypted, d_encrypted, s_comp, d_comp) = {
let s = src_zip.by_index_raw(i).expect("src by_index_raw");
let name = s.name().to_string();
let is_dir = s.is_dir();
let s_encrypted = s.encrypted();
let s_comp = s.compression();
let d = dst_zip.by_index_raw(i).expect("dst by_index_raw");
let d_encrypted = d.encrypted();
let d_comp = d.compression();
(name, is_dir, s_encrypted, d_encrypted, s_comp, d_comp)
};

// AES-critical invariants preserved by raw copy
assert_eq!(
s_encrypted, d_encrypted,
"encrypted flag differs for {name}"
);
assert_eq!(s_comp, d_comp, "compression method differs for {name}");

// For files, verify content bytes match. For encrypted entries, use the shared fixture password
if !is_dir {
let mut s_buf = Vec::new();
let mut d_buf = Vec::new();
if s_encrypted {
src_zip
.by_index_decrypt(i, PASSWORD)
.expect("decrypt src")
.read_to_end(&mut s_buf)
.expect("read src");
dst_zip
.by_index_decrypt(i, PASSWORD)
.expect("decrypt dst")
.read_to_end(&mut d_buf)
.expect("read dst");
} else {
src_zip
.by_index(i)
.expect("open src")
.read_to_end(&mut s_buf)
.expect("read src");
dst_zip
.by_index(i)
.expect("open dst")
.read_to_end(&mut d_buf)
.expect("read dst");
}
assert_eq!(s_buf, d_buf, "content differs for {name}");
}
}
}
Loading