Skip to content

Commit b611a59

Browse files
authored
fix: aes was not copied correctly in raw copy methods (#417)
* fix: aes encrypted files raw copy * fix: preserve aes header data for raw copy
1 parent e1d74a0 commit b611a59

File tree

4 files changed

+128
-2
lines changed

4 files changed

+128
-2
lines changed

src/read.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,12 @@ impl<'a, R: Read> ZipFile<'a, R> {
18271827
);
18281828

18291829
options.normalize();
1830+
#[cfg(feature = "aes-crypto")]
1831+
if let Some(aes) = self.get_metadata().aes_mode {
1832+
// Preserve AES metadata in options for downstream writers.
1833+
// This is metadata-only and does not trigger encryption.
1834+
options.aes_mode = Some(aes);
1835+
}
18301836
options
18311837
}
18321838
}

src/types.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,16 @@ impl ZipFileData {
710710
let mut local_block = ZipFileData {
711711
system: System::Unix,
712712
version_made_by: DEFAULT_VERSION,
713-
encrypted: options.encrypt_with.is_some(),
713+
encrypted: options.encrypt_with.is_some() || {
714+
#[cfg(feature = "aes-crypto")]
715+
{
716+
options.aes_mode.is_some()
717+
}
718+
#[cfg(not(feature = "aes-crypto"))]
719+
{
720+
false
721+
}
722+
},
714723
using_data_descriptor: false,
715724
is_utf8: !file_name.is_ascii(),
716725
compression_method,
@@ -1213,7 +1222,7 @@ impl FixedSizeBlock for Zip64DataDescriptorBlock {
12131222
///
12141223
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
12151224
/// does not make use of the CRC check.
1216-
#[derive(Copy, Clone, Debug)]
1225+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
12171226
#[repr(u16)]
12181227
pub enum AesVendorVersion {
12191228
Ae1 = 0x0001,

src/write.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ pub struct FileOptions<'k, T: FileOptionExtension> {
273273
pub(crate) alignment: u16,
274274
#[cfg(feature = "deflate-zopfli")]
275275
pub(super) zopfli_buffer_size: Option<usize>,
276+
#[cfg(feature = "aes-crypto")]
277+
pub(crate) aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
276278
}
277279
/// Simple File Options. Can be copied and good for simple writing zip files
278280
pub type SimpleFileOptions = FileOptions<'static, ()>;
@@ -566,6 +568,8 @@ impl<T: FileOptionExtension> Default for FileOptions<'_, T> {
566568
alignment: 1,
567569
#[cfg(feature = "deflate-zopfli")]
568570
zopfli_buffer_size: Some(1 << 15),
571+
#[cfg(feature = "aes-crypto")]
572+
aes_mode: None,
569573
}
570574
}
571575
}
@@ -932,9 +936,25 @@ impl<W: Write + Seek> ZipWriter<W> {
932936
0x9901,
933937
aes_dummy_extra_data,
934938
)?;
939+
} else if let Some((mode, vendor, underlying)) = options.aes_mode {
940+
// For raw copies of AES entries, write the correct AES extra data immediately
941+
let mut body = Vec::with_capacity(7);
942+
body.write_u16_le(vendor as u16)?; // vendor version (1 or 2)
943+
body.extend_from_slice(b"AE"); // vendor id
944+
body.push(mode as u8); // strength
945+
body.write_u16_le(underlying.serialize_to_u16())?; // real compression method
946+
aes_extra_data_start = extra_data.len() as u64;
947+
ExtendedFileOptions::add_extra_data_unchecked(
948+
&mut extra_data,
949+
0x9901,
950+
body.into_boxed_slice(),
951+
)?;
935952
}
936953

937954
let (compression_method, aes_mode) = match options.encrypt_with {
955+
// Preserve AES method for raw copies without needing a password
956+
#[cfg(feature = "aes-crypto")]
957+
None if options.aes_mode.is_some() => (CompressionMethod::Aes, options.aes_mode),
938958
#[cfg(feature = "aes-crypto")]
939959
Some(EncryptWith::Aes { mode, .. }) => (
940960
CompressionMethod::Aes,
@@ -2317,6 +2337,8 @@ mod test {
23172337
alignment: 1,
23182338
#[cfg(feature = "deflate-zopfli")]
23192339
zopfli_buffer_size: None,
2340+
#[cfg(feature = "aes-crypto")]
2341+
aes_mode: None,
23202342
};
23212343
writer.start_file("mimetype", options).unwrap();
23222344
writer
@@ -2358,6 +2380,8 @@ mod test {
23582380
alignment: 1,
23592381
#[cfg(feature = "deflate-zopfli")]
23602382
zopfli_buffer_size: None,
2383+
#[cfg(feature = "aes-crypto")]
2384+
aes_mode: None,
23612385
};
23622386

23632387
// GB18030
@@ -2411,6 +2435,8 @@ mod test {
24112435
alignment: 0,
24122436
#[cfg(feature = "deflate-zopfli")]
24132437
zopfli_buffer_size: None,
2438+
#[cfg(feature = "aes-crypto")]
2439+
aes_mode: None,
24142440
};
24152441
writer.start_file(RT_TEST_FILENAME, options).unwrap();
24162442
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();
@@ -2462,6 +2488,8 @@ mod test {
24622488
alignment: 0,
24632489
#[cfg(feature = "deflate-zopfli")]
24642490
zopfli_buffer_size: None,
2491+
#[cfg(feature = "aes-crypto")]
2492+
aes_mode: None,
24652493
};
24662494
writer.start_file(RT_TEST_FILENAME, options).unwrap();
24672495
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();

tests/aes_encryption.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![cfg(feature = "aes-crypto")]
22

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

67
const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
@@ -152,3 +153,85 @@ fn test_extract_encrypted_file<R: io::Read + io::Seek>(
152153
assert_eq!(SECRET_CONTENT, content);
153154
}
154155
}
156+
157+
#[test]
158+
fn raw_copy_from_aes_zip() {
159+
let mut v = Vec::new();
160+
v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
161+
162+
let dst_cursor = {
163+
let mut src =
164+
ZipArchive::new(io::Cursor::new(v.as_slice())).expect("couldn't open source zip");
165+
let mut dst = ZipWriter::new(io::Cursor::new(Vec::new()));
166+
167+
let total = src.len();
168+
for i in 0..total {
169+
let file = src.by_index_raw(i).expect("read source entry");
170+
let name = file.name().to_string();
171+
if file.is_dir() {
172+
dst.add_directory(&name, SimpleFileOptions::default())
173+
.expect("add directory");
174+
} else {
175+
dst.raw_copy_file(file).expect("raw copy file");
176+
}
177+
}
178+
dst.finish().expect("finish dst")
179+
};
180+
181+
let mut src_zip = ZipArchive::new(io::Cursor::new(v.as_slice())).expect("reopen src zip");
182+
let mut dst_zip = ZipArchive::new(dst_cursor).expect("reopen dst zip");
183+
184+
let total = src_zip.len();
185+
186+
for i in 0..total {
187+
// Copy out simple header fields without holding borrows across later reads
188+
let (name, is_dir, s_encrypted, d_encrypted, s_comp, d_comp) = {
189+
let s = src_zip.by_index_raw(i).expect("src by_index_raw");
190+
let name = s.name().to_string();
191+
let is_dir = s.is_dir();
192+
let s_encrypted = s.encrypted();
193+
let s_comp = s.compression();
194+
let d = dst_zip.by_index_raw(i).expect("dst by_index_raw");
195+
let d_encrypted = d.encrypted();
196+
let d_comp = d.compression();
197+
(name, is_dir, s_encrypted, d_encrypted, s_comp, d_comp)
198+
};
199+
200+
// AES-critical invariants preserved by raw copy
201+
assert_eq!(
202+
s_encrypted, d_encrypted,
203+
"encrypted flag differs for {name}"
204+
);
205+
assert_eq!(s_comp, d_comp, "compression method differs for {name}");
206+
207+
// For files, verify content bytes match. For encrypted entries, use the shared fixture password
208+
if !is_dir {
209+
let mut s_buf = Vec::new();
210+
let mut d_buf = Vec::new();
211+
if s_encrypted {
212+
src_zip
213+
.by_index_decrypt(i, PASSWORD)
214+
.expect("decrypt src")
215+
.read_to_end(&mut s_buf)
216+
.expect("read src");
217+
dst_zip
218+
.by_index_decrypt(i, PASSWORD)
219+
.expect("decrypt dst")
220+
.read_to_end(&mut d_buf)
221+
.expect("read dst");
222+
} else {
223+
src_zip
224+
.by_index(i)
225+
.expect("open src")
226+
.read_to_end(&mut s_buf)
227+
.expect("read src");
228+
dst_zip
229+
.by_index(i)
230+
.expect("open dst")
231+
.read_to_end(&mut d_buf)
232+
.expect("read dst");
233+
}
234+
assert_eq!(s_buf, d_buf, "content differs for {name}");
235+
}
236+
}
237+
}

0 commit comments

Comments
 (0)