From d972f3b0565b9882808de5352432dbb34347a29f Mon Sep 17 00:00:00 2001 From: roblabla Date: Mon, 27 Oct 2025 14:20:12 +0100 Subject: [PATCH] Fix AES encryption streaming support The update_aes_extra_data function requires the underlying writer to support seeking to succeed. In effect, this function is only used to switch the mode between AE2 and AE1 depending on file size. This is not strictly necessary so long as we use the strong mode by default, so we now only call this function for the non-streaming case. --- src/write.rs | 82 +++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/write.rs b/src/write.rs index 64e85a4de..b41604d0b 100644 --- a/src/write.rs +++ b/src/write.rs @@ -927,19 +927,26 @@ impl ZipWriter { new_extra_data.append(&mut extra_data); extra_data = new_extra_data; } + + // Figure out the underlying compression_method and aes mode when using + // AES encryption. + 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, + Some((mode, AesVendorVersion::Ae2, options.compression_method)), + ), + _ => (options.compression_method, None), + }; + // Write AES encryption extra data. #[allow(unused_mut)] let mut aes_extra_data_start = 0; #[cfg(feature = "aes-crypto")] - if let Some(EncryptWith::Aes { mode, .. }) = options.encrypt_with { - let aes_dummy_extra_data = [0x02, 0x00, 0x41, 0x45, mode as u8, 0x00, 0x00]; - aes_extra_data_start = extra_data.len() as u64; - ExtendedFileOptions::add_extra_data_unchecked( - &mut extra_data, - 0x9901, - &aes_dummy_extra_data, - )?; - } else if let Some((mode, vendor, underlying)) = options.aes_mode { + if let Some((mode, vendor, underlying)) = aes_mode { // For raw copies of AES entries, write the correct AES extra data immediately let mut body = [0; 7]; [body[0], body[1]] = (vendor as u16).to_le_bytes(); // vendor version (1 or 2) @@ -949,18 +956,6 @@ impl ZipWriter { aes_extra_data_start = extra_data.len() as u64; ExtendedFileOptions::add_extra_data_unchecked(&mut extra_data, 0x9901, &body)?; } - - 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, - Some((mode, AesVendorVersion::Ae2, options.compression_method)), - ), - _ => (options.compression_method, None), - }; let header_end = header_start + size_of::() as u64 + name.to_string().len() as u64; @@ -1090,29 +1085,22 @@ impl ZipWriter { let file_end = writer.stream_position()?; debug_assert!(file_end >= self.stats.start); file.compressed_size = file_end - self.stats.start; - let mut crc = true; - if let Some(aes_mode) = &mut file.aes_mode { - // We prefer using AE-1 which provides an extra CRC check, but for small files we - // switch to AE-2 to prevent being able to use the CRC value to to reconstruct the - // unencrypted contents. - // - // C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq - aes_mode.1 = if self.stats.bytes_written < 20 { - crc = false; - AesVendorVersion::Ae2 - } else { - AesVendorVersion::Ae1 - }; - } + + let crc = !matches!(file.aes_mode, Some((_, AesVendorVersion::Ae2, _))); + file.crc32 = if crc { self.stats.hasher.clone().finalize() } else { 0 }; - update_aes_extra_data(writer, file)?; + if file.using_data_descriptor { write_data_descriptor(writer, file)?; } else { + // Not using a data descriptor means the underlying writer + // supports seeking, so we can go back and update the AES Extra + // Data header to use AE1 for large files. + update_aes_extra_data(writer, file, self.stats.bytes_written)?; update_local_file_header(writer, file)?; writer.seek(SeekFrom::Start(file_end))?; } @@ -2004,11 +1992,25 @@ fn clamp_opt>( } } -fn update_aes_extra_data(writer: &mut W, file: &mut ZipFileData) -> ZipResult<()> { - let Some((aes_mode, version, compression_method)) = file.aes_mode else { +fn update_aes_extra_data(writer: &mut W, file: &mut ZipFileData, bytes_written: u64) -> ZipResult<()> { + let Some((aes_mode, version, compression_method)) = &mut file.aes_mode else { return Ok(()); }; + // We prefer using AE-1 which provides an extra CRC check, but for small files we + // switch to AE-2 to prevent being able to use the CRC value to to reconstruct the + // unencrypted contents. + // + // We can only do this when the underlying writer supports + // seek operations, so we gate this behind using_data_descriptor. + // + // C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq + *version = if bytes_written < 20 { + AesVendorVersion::Ae2 + } else { + AesVendorVersion::Ae1 + }; + let extra_data_start = file.extra_data_start.unwrap(); writer.seek(SeekFrom::Start( @@ -2023,11 +2025,11 @@ fn update_aes_extra_data(writer: &mut W, file: &mut ZipFileData // Data size. buf.write_u16_le(7)?; // Integer version number. - buf.write_u16_le(version as u16)?; + buf.write_u16_le(*version as u16)?; // Vendor ID. buf.write_all(b"AE")?; // AES encryption strength. - buf.write_all(&[aes_mode as u8])?; + buf.write_all(&[*aes_mode as u8])?; // Real compression method. buf.write_u16_le(compression_method.serialize_to_u16())?;