Merge pull request #80 from kyrias/aes-encryption3

Add AES encryption write support
This commit is contained in:
Chris Hennick 2024-05-06 18:54:37 +00:00 committed by GitHub
commit c8d1be86b3
Signed by: DevComp
GPG key ID: B5690EEEBB952194
11 changed files with 604 additions and 110 deletions

View file

@ -33,11 +33,13 @@ indexmap = "2"
hmac = { version = "0.12.1", optional = true, features = ["reset"] } hmac = { version = "0.12.1", optional = true, features = ["reset"] }
num_enum = "0.7.2" num_enum = "0.7.2"
pbkdf2 = { version = "0.12.2", optional = true } pbkdf2 = { version = "0.12.2", optional = true }
rand = { version = "0.8.5", optional = true }
sha1 = { version = "0.10.6", optional = true } sha1 = { version = "0.10.6", optional = true }
thiserror = "1.0.48" thiserror = "1.0.48"
time = { workspace = true, optional = true, features = [ time = { workspace = true, optional = true, features = [
"std", "std",
] } ] }
zeroize = { version = "1.6.0", optional = true, features = ["zeroize_derive"] }
zstd = { version = "0.13.1", optional = true, default-features = false } zstd = { version = "0.13.1", optional = true, default-features = false }
zopfli = { version = "0.8.0", optional = true } zopfli = { version = "0.8.0", optional = true }
deflate64 = { version = "0.1.8", optional = true } deflate64 = { version = "0.1.8", optional = true }
@ -58,7 +60,7 @@ anyhow = "1"
clap = { version = "=4.4.18", features = ["derive"] } clap = { version = "=4.4.18", features = ["derive"] }
[features] [features]
aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1"] aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1", "rand", "zeroize"]
chrono = ["chrono/default"] chrono = ["chrono/default"]
_deflate-any = [] _deflate-any = []
deflate = ["flate2/rust_backend", "_deflate-any"] deflate = ["flate2/rust_backend", "_deflate-any"]

View file

@ -1,5 +1,7 @@
use std::io::prelude::*; use std::io::prelude::*;
use zip::write::SimpleFileOptions; use zip::write::SimpleFileOptions;
#[cfg(feature = "aes-crypto")]
use zip::{AesMode, CompressionMethod};
fn main() { fn main() {
std::process::exit(real_main()); std::process::exit(real_main());
@ -38,6 +40,24 @@ fn doit(filename: &str) -> zip::result::ZipResult<()> {
zip.start_file("test/lorem_ipsum.txt", options)?; zip.start_file("test/lorem_ipsum.txt", options)?;
zip.write_all(LOREM_IPSUM)?; zip.write_all(LOREM_IPSUM)?;
#[cfg(feature = "aes-crypto")]
{
zip.start_file(
"test/lorem_ipsum.aes.txt",
options
.compression_method(CompressionMethod::Zstd)
.with_aes_encryption(AesMode::Aes256, "password"),
)?;
zip.write_all(LOREM_IPSUM)?;
// This should use AE-1 due to the short file length.
zip.start_file(
"test/short.aes.txt",
options.with_aes_encryption(AesMode::Aes256, "password"),
)?;
zip.write_all(b"short text\n")?;
}
zip.finish()?; zip.finish()?;
Ok(()) Ok(())
} }

View file

@ -7,38 +7,38 @@ use std::io::{Cursor, Read, Seek, Write};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Arbitrary, Clone, Debug)] #[derive(Arbitrary, Clone, Debug)]
pub enum BasicFileOperation { pub enum BasicFileOperation<'k> {
WriteNormalFile { WriteNormalFile {
contents: Vec<Vec<u8>>, contents: Vec<Vec<u8>>,
options: zip::write::FullFileOptions, options: zip::write::FullFileOptions<'k>,
}, },
WriteDirectory(zip::write::FullFileOptions), WriteDirectory(zip::write::FullFileOptions<'k>),
WriteSymlinkWithTarget { WriteSymlinkWithTarget {
target: PathBuf, target: PathBuf,
options: zip::write::FullFileOptions, options: zip::write::FullFileOptions<'k>,
}, },
ShallowCopy(Box<FileOperation>), ShallowCopy(Box<FileOperation<'k>>),
DeepCopy(Box<FileOperation>), DeepCopy(Box<FileOperation<'k>>),
} }
#[derive(Arbitrary, Clone, Debug)] #[derive(Arbitrary, Clone, Debug)]
pub struct FileOperation { pub struct FileOperation<'k> {
basic: BasicFileOperation, basic: BasicFileOperation<'k>,
path: PathBuf, path: PathBuf,
reopen: bool, reopen: bool,
// 'abort' flag is separate, to prevent trying to copy an aborted file // 'abort' flag is separate, to prevent trying to copy an aborted file
} }
#[derive(Arbitrary, Clone, Debug)] #[derive(Arbitrary, Clone, Debug)]
pub struct FuzzTestCase { pub struct FuzzTestCase<'k> {
comment: Vec<u8>, comment: Vec<u8>,
operations: Vec<(FileOperation, bool)>, operations: Vec<(FileOperation<'k>, bool)>,
flush_on_finish_file: bool, flush_on_finish_file: bool,
} }
fn do_operation<T>( fn do_operation<'k, T>(
writer: &mut zip::ZipWriter<T>, writer: &mut zip::ZipWriter<T>,
operation: &FileOperation, operation: &FileOperation<'k>,
abort: bool, abort: bool,
flush_on_finish_file: bool, flush_on_finish_file: bool,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>>

View file

@ -4,12 +4,15 @@
//! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2. //! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2.
//! If the file is marked as encrypted with AE-2 the CRC field is ignored, even if it isn't set to 0. //! If the file is marked as encrypted with AE-2 the CRC field is ignored, even if it isn't set to 0.
use crate::aes_ctr; use crate::aes_ctr::AesCipher;
use crate::types::AesMode; use crate::types::AesMode;
use crate::{aes_ctr, result::ZipError};
use constant_time_eq::constant_time_eq; use constant_time_eq::constant_time_eq;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use rand::RngCore;
use sha1::Sha1; use sha1::Sha1;
use std::io::{self, Error, ErrorKind, Read}; use std::io::{self, Error, ErrorKind, Read, Write};
use zeroize::{Zeroize, Zeroizing};
/// The length of the password verifcation value in bytes /// The length of the password verifcation value in bytes
const PWD_VERIFY_LENGTH: usize = 2; const PWD_VERIFY_LENGTH: usize = 2;
@ -18,19 +21,38 @@ const AUTH_CODE_LENGTH: usize = 10;
/// The number of iterations used with PBKDF2 /// The number of iterations used with PBKDF2
const ITERATION_COUNT: u32 = 1000; const ITERATION_COUNT: u32 = 1000;
/// Create a AesCipher depending on the used `AesMode` and the given `key`. enum Cipher {
/// Aes128(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes128>>),
/// # Panics Aes192(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes192>>),
/// Aes256(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes256>>),
/// This panics if `key` doesn't have the correct size for the chosen aes mode. }
fn cipher_from_mode(aes_mode: AesMode, key: &[u8]) -> Box<dyn aes_ctr::AesCipher> {
match aes_mode { impl Cipher {
AesMode::Aes128 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes128>::new(key)) /// Create a `Cipher` depending on the used `AesMode` and the given `key`.
as Box<dyn aes_ctr::AesCipher>, ///
AesMode::Aes192 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes192>::new(key)) /// # Panics
as Box<dyn aes_ctr::AesCipher>, ///
AesMode::Aes256 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes256>::new(key)) /// This panics if `key` doesn't have the correct size for the chosen aes mode.
as Box<dyn aes_ctr::AesCipher>, fn from_mode(aes_mode: AesMode, key: &[u8]) -> Self {
match aes_mode {
AesMode::Aes128 => Cipher::Aes128(Box::new(aes_ctr::AesCtrZipKeyStream::<
aes_ctr::Aes128,
>::new(key))),
AesMode::Aes192 => Cipher::Aes192(Box::new(aes_ctr::AesCtrZipKeyStream::<
aes_ctr::Aes192,
>::new(key))),
AesMode::Aes256 => Cipher::Aes256(Box::new(aes_ctr::AesCtrZipKeyStream::<
aes_ctr::Aes256,
>::new(key))),
}
}
fn crypt_in_place(&mut self, target: &mut [u8]) {
match self {
Self::Aes128(cipher) => cipher.crypt_in_place(target),
Self::Aes192(cipher) => cipher.crypt_in_place(target),
Self::Aes256(cipher) => cipher.crypt_in_place(target),
}
} }
} }
@ -62,12 +84,7 @@ impl<R: Read> AesReader<R> {
/// password was provided. /// password was provided.
/// It isn't possible to check the authentication code in this step. This will be done after /// It isn't possible to check the authentication code in this step. This will be done after
/// reading and decrypting the file. /// reading and decrypting the file.
/// pub fn validate(mut self, password: &[u8]) -> Result<AesReaderValid<R>, ZipError> {
/// # Returns
///
/// If the password verification failed `Ok(None)` will be returned to match the validate
/// method of ZipCryptoReader.
pub fn validate(mut self, password: &[u8]) -> io::Result<Option<AesReaderValid<R>>> {
let salt_length = self.aes_mode.salt_length(); let salt_length = self.aes_mode.salt_length();
let key_length = self.aes_mode.key_length(); let key_length = self.aes_mode.key_length();
@ -93,19 +110,19 @@ impl<R: Read> AesReader<R> {
// the last 2 bytes should equal the password verification value // the last 2 bytes should equal the password verification value
if pwd_verification_value != pwd_verify { if pwd_verification_value != pwd_verify {
// wrong password // wrong password
return Ok(None); return Err(ZipError::InvalidPassword);
} }
let cipher = cipher_from_mode(self.aes_mode, decrypt_key); let cipher = Cipher::from_mode(self.aes_mode, decrypt_key);
let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap(); let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
Ok(Some(AesReaderValid { Ok(AesReaderValid {
reader: self.reader, reader: self.reader,
data_remaining: self.data_length, data_remaining: self.data_length,
cipher, cipher,
hmac, hmac,
finalized: false, finalized: false,
})) })
} }
} }
@ -117,7 +134,7 @@ impl<R: Read> AesReader<R> {
pub struct AesReaderValid<R: Read> { pub struct AesReaderValid<R: Read> {
reader: R, reader: R,
data_remaining: u64, data_remaining: u64,
cipher: Box<dyn aes_ctr::AesCipher>, cipher: Cipher,
hmac: Hmac<Sha1>, hmac: Hmac<Sha1>,
finalized: bool, finalized: bool,
} }
@ -184,3 +201,189 @@ impl<R: Read> AesReaderValid<R> {
self.reader self.reader
} }
} }
pub struct AesWriter<W> {
writer: W,
cipher: Cipher,
hmac: Hmac<Sha1>,
buffer: Zeroizing<Vec<u8>>,
encrypted_file_header: Option<Vec<u8>>,
}
impl<W: Write> AesWriter<W> {
pub fn new(writer: W, aes_mode: AesMode, password: &[u8]) -> io::Result<Self> {
let salt_length = aes_mode.salt_length();
let key_length = aes_mode.key_length();
let mut encrypted_file_header = Vec::with_capacity(salt_length + 2);
let mut salt = vec![0; salt_length];
rand::thread_rng().fill_bytes(&mut salt);
encrypted_file_header.write_all(&salt)?;
// Derive a key from the password and salt. The length depends on the aes key length
let derived_key_len = 2 * key_length + PWD_VERIFY_LENGTH;
let mut derived_key: Zeroizing<Vec<u8>> = Zeroizing::new(vec![0; derived_key_len]);
// Use PBKDF2 with HMAC-Sha1 to derive the key.
pbkdf2::pbkdf2::<Hmac<Sha1>>(password, &salt, ITERATION_COUNT, &mut derived_key)
.map_err(|e| Error::new(ErrorKind::InvalidInput, e))?;
let encryption_key = &derived_key[0..key_length];
let hmac_key = &derived_key[key_length..key_length * 2];
let pwd_verify = derived_key[derived_key_len - 2..].to_vec();
encrypted_file_header.write_all(&pwd_verify)?;
let cipher = Cipher::from_mode(aes_mode, encryption_key);
let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
Ok(Self {
writer,
cipher,
hmac,
buffer: Default::default(),
encrypted_file_header: Some(encrypted_file_header),
})
}
pub fn finish(mut self) -> io::Result<W> {
self.write_encrypted_file_header()?;
// Zip uses HMAC-Sha1-80, which only uses the first half of the hash
// see https://www.winzip.com/win/en/aes_info.html#auth-faq
let computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];
self.writer.write_all(computed_auth_code)?;
Ok(self.writer)
}
/// The AES encryption specification requires some metadata being written at the start of the
/// file data section, but this can only be done once the extra data writing has been finished
/// so we can't do it when the writer is constructed.
fn write_encrypted_file_header(&mut self) -> io::Result<()> {
if let Some(header) = self.encrypted_file_header.take() {
self.writer.write_all(&header)?;
}
Ok(())
}
}
impl<W: Write> Write for AesWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_encrypted_file_header()?;
// Fill the internal buffer and encrypt it in-place.
self.buffer.extend_from_slice(buf);
self.cipher.crypt_in_place(&mut self.buffer[..]);
// Update the hmac with the encrypted data.
self.hmac.update(&self.buffer[..]);
// Write the encrypted buffer to the inner writer. We need to use `write_all` here as if
// we only write parts of the data we can't easily reverse the keystream in the cipher
// implementation.
self.writer.write_all(&self.buffer[..])?;
// Zeroize the backing memory before clearing the buffer to prevent cleartext data from
// being left in memory.
self.buffer.zeroize();
self.buffer.clear();
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod tests {
use std::io::{self, Read, Write};
use crate::{
aes::{AesReader, AesWriter},
result::ZipError,
types::AesMode,
};
/// Checks whether `AesReader` can successfully decrypt what `AesWriter` produces.
fn roundtrip(aes_mode: AesMode, password: &[u8], plaintext: &[u8]) -> Result<bool, ZipError> {
let mut buf = io::Cursor::new(vec![]);
let mut read_buffer = vec![];
{
let mut writer = AesWriter::new(&mut buf, aes_mode, password)?;
writer.write_all(plaintext)?;
writer.finish()?;
}
// Reset cursor position to the beginning.
buf.set_position(0);
{
let compressed_length = buf.get_ref().len() as u64;
let mut reader =
AesReader::new(&mut buf, aes_mode, compressed_length).validate(password)?;
reader.read_to_end(&mut read_buffer)?;
}
Ok(plaintext == read_buffer)
}
#[test]
fn crypt_aes_256_0_byte() {
let plaintext = &[];
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_128_5_byte() {
let plaintext = b"asdf\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes128, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_192_5_byte() {
let plaintext = b"asdf\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes192, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_256_5_byte() {
let plaintext = b"asdf\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_128_40_byte() {
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes128, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_192_40_byte() {
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes192, password, plaintext).expect("could encrypt and decrypt"));
}
#[test]
fn crypt_aes_256_40_byte() {
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
let password = b"some super secret password";
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
}
}

View file

@ -29,7 +29,7 @@
#![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06 #![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06
pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
pub use crate::read::ZipArchive; pub use crate::read::ZipArchive;
pub use crate::types::DateTime; pub use crate::types::{AesMode, DateTime};
pub use crate::write::ZipWriter; pub use crate::write::ZipWriter;
#[cfg(feature = "aes-crypto")] #[cfg(feature = "aes-crypto")]

View file

@ -238,7 +238,7 @@ pub(crate) fn make_crypto_reader<'a>(
using_data_descriptor: bool, using_data_descriptor: bool,
reader: io::Take<&'a mut dyn Read>, reader: io::Take<&'a mut dyn Read>,
password: Option<&[u8]>, password: Option<&[u8]>,
aes_info: Option<(AesMode, AesVendorVersion)>, aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
#[cfg(feature = "aes-crypto")] compressed_size: u64, #[cfg(feature = "aes-crypto")] compressed_size: u64,
) -> ZipResult<CryptoReader<'a>> { ) -> ZipResult<CryptoReader<'a>> {
#[allow(deprecated)] #[allow(deprecated)]
@ -256,25 +256,17 @@ pub(crate) fn make_crypto_reader<'a>(
)) ))
} }
#[cfg(feature = "aes-crypto")] #[cfg(feature = "aes-crypto")]
(Some(password), Some((aes_mode, vendor_version))) => { (Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes {
match AesReader::new(reader, aes_mode, compressed_size).validate(password)? { reader: AesReader::new(reader, aes_mode, compressed_size).validate(password)?,
None => return Err(InvalidPassword), vendor_version,
Some(r) => CryptoReader::Aes { },
reader: r,
vendor_version,
},
}
}
(Some(password), None) => { (Some(password), None) => {
let validator = if using_data_descriptor { let validator = if using_data_descriptor {
ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart()) ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
} else { } else {
ZipCryptoValidator::PkzipCrc32(crc32) ZipCryptoValidator::PkzipCrc32(crc32)
}; };
match ZipCryptoReader::new(reader, password).validate(validator)? { CryptoReader::ZipCrypto(ZipCryptoReader::new(reader, password).validate(validator)?)
None => return Err(InvalidPassword),
Some(r) => CryptoReader::ZipCrypto(r),
}
} }
(None, Some(_)) => return Err(InvalidPassword), (None, Some(_)) => return Err(InvalidPassword),
(None, None) => CryptoReader::Plaintext(reader), (None, None) => CryptoReader::Plaintext(reader),
@ -927,11 +919,13 @@ fn central_header_to_zip_file_inner<R: Read>(
central_extra_field: None, central_extra_field: None,
file_comment, file_comment,
header_start: offset, header_start: offset,
extra_data_start: None,
central_header_start, central_header_start,
data_start: OnceLock::new(), data_start: OnceLock::new(),
external_attributes: external_file_attributes, external_attributes: external_file_attributes,
large_file: false, large_file: false,
aes_mode: None, aes_mode: None,
aes_extra_data_start: 0,
extra_fields: Vec::new(), extra_fields: Vec::new(),
}; };
@ -996,7 +990,8 @@ fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
let mut out = [0u8]; let mut out = [0u8];
reader.read_exact(&mut out)?; reader.read_exact(&mut out)?;
let aes_mode = out[0]; let aes_mode = out[0];
let compression_method = reader.read_u16_le()?; #[allow(deprecated)]
let compression_method = CompressionMethod::from_u16(reader.read_u16_le()?);
if vendor_id != 0x4541 { if vendor_id != 0x4541 {
return Err(ZipError::InvalidArchive("Invalid AES vendor")); return Err(ZipError::InvalidArchive("Invalid AES vendor"));
@ -1007,15 +1002,18 @@ fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
_ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")), _ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")),
}; };
match aes_mode { match aes_mode {
0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version)), 0x01 => {
0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version)), file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method))
0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version)), }
0x02 => {
file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method))
}
0x03 => {
file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method))
}
_ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")), _ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")),
}; };
file.compression_method = { file.compression_method = compression_method;
#[allow(deprecated)]
CompressionMethod::from_u16(compression_method)
};
} }
0x5455 => { 0x5455 => {
// extended timestamp // extended timestamp
@ -1312,6 +1310,7 @@ pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Opt
// header_start and data start are not available, but also don't matter, since seeking is // header_start and data start are not available, but also don't matter, since seeking is
// not available. // not available.
header_start: 0, header_start: 0,
extra_data_start: None,
data_start: OnceLock::new(), data_start: OnceLock::new(),
central_header_start: 0, central_header_start: 0,
// The external_attributes field is only available in the central directory. // The external_attributes field is only available in the central directory.
@ -1320,6 +1319,7 @@ pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Opt
external_attributes: 0, external_attributes: 0,
large_file: false, large_file: false,
aes_mode: None, aes_mode: None,
aes_extra_data_start: 0,
extra_fields: Vec::new(), extra_fields: Vec::new(),
}; };

View file

@ -48,8 +48,8 @@ mod atomic {
use crate::extra_fields::ExtraField; use crate::extra_fields::ExtraField;
use crate::result::DateTimeRangeError; use crate::result::DateTimeRangeError;
use crate::types::ffi::S_IFDIR;
use crate::CompressionMethod; use crate::CompressionMethod;
use crate::types::ffi::S_IFDIR;
#[cfg(feature = "time")] #[cfg(feature = "time")]
use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
@ -353,6 +353,8 @@ pub struct ZipFileData {
pub file_comment: Box<str>, pub file_comment: Box<str>,
/// Specifies where the local header of the file starts /// Specifies where the local header of the file starts
pub header_start: u64, pub header_start: u64,
/// Specifies where the extra data of the file starts
pub extra_data_start: Option<u64>,
/// Specifies where the central header of the file starts /// Specifies where the central header of the file starts
/// ///
/// Note that when this is not known, it is set to 0 /// Note that when this is not known, it is set to 0
@ -364,7 +366,9 @@ pub struct ZipFileData {
/// Reserve local ZIP64 extra field /// Reserve local ZIP64 extra field
pub large_file: bool, pub large_file: bool,
/// AES mode if applicable /// AES mode if applicable
pub aes_mode: Option<(AesMode, AesVendorVersion)>, pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
/// Specifies where in the extra data the AES metadata starts
pub aes_extra_data_start: u64,
/// extra fields, see <https://libzip.org/specifications/extrafld.txt> /// extra fields, see <https://libzip.org/specifications/extrafld.txt>
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
@ -498,25 +502,33 @@ impl ZipFileData {
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2 /// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
/// does not make use of the CRC check. /// does not make use of the CRC check.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
#[repr(u16)]
pub enum AesVendorVersion { pub enum AesVendorVersion {
Ae1, Ae1 = 0x0001,
Ae2, Ae2 = 0x0002,
} }
/// AES variant used. /// AES variant used.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum AesMode { pub enum AesMode {
Aes128, /// 128-bit AES encryption.
Aes192, Aes128 = 0x01,
Aes256, /// 192-bit AES encryption.
Aes192 = 0x02,
/// 256-bit AES encryption.
Aes256 = 0x03,
} }
#[cfg(feature = "aes-crypto")] #[cfg(feature = "aes-crypto")]
impl AesMode { impl AesMode {
/// Length of the salt for the given AES mode.
pub const fn salt_length(&self) -> usize { pub const fn salt_length(&self) -> usize {
self.key_length() / 2 self.key_length() / 2
} }
/// Length of the key for the given AES mode.
pub const fn key_length(&self) -> usize { pub const fn key_length(&self) -> usize {
match self { match self {
Self::Aes128 => 16, Self::Aes128 => 16,
@ -562,11 +574,13 @@ mod test {
central_extra_field: None, central_extra_field: None,
file_comment: String::with_capacity(0).into_boxed_str(), file_comment: String::with_capacity(0).into_boxed_str(),
header_start: 0, header_start: 0,
extra_data_start: None,
data_start: OnceLock::new(), data_start: OnceLock::new(),
central_header_start: 0, central_header_start: 0,
external_attributes: 0, external_attributes: 0,
large_file: false, large_file: false,
aes_mode: None, aes_mode: None,
aes_extra_data_start: 0,
extra_fields: Vec::new(), extra_fields: Vec::new(),
}; };
assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd")); assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));

View file

@ -17,8 +17,8 @@ pub mod write {
/// This is not recommended for new archives, as ZipCrypto is not secure. /// This is not recommended for new archives, as ZipCrypto is not secure.
fn with_deprecated_encryption(self, password: &[u8]) -> Self; fn with_deprecated_encryption(self, password: &[u8]) -> Self;
} }
impl<T: FileOptionExtension> FileOptionsExt for FileOptions<T> { impl<'k, T: FileOptionExtension> FileOptionsExt for FileOptions<'k, T> {
fn with_deprecated_encryption(self, password: &[u8]) -> Self { fn with_deprecated_encryption(self, password: &[u8]) -> FileOptions<'static, T> {
self.with_deprecated_encryption(password) self.with_deprecated_encryption(password)
} }
} }

View file

@ -1,10 +1,14 @@
//! Types for creating ZIP archives //! Types for creating ZIP archives
#[cfg(feature = "aes-crypto")]
use crate::aes::AesWriter;
use crate::compression::CompressionMethod; use crate::compression::CompressionMethod;
use crate::read::{find_content, ZipArchive, ZipFile, ZipFileReader}; use crate::read::{find_content, ZipArchive, ZipFile, ZipFileReader};
use crate::result::{ZipError, ZipResult}; use crate::result::{ZipError, ZipResult};
use crate::spec; use crate::spec;
use crate::types::{ffi, DateTime, System, ZipFileData, DEFAULT_VERSION}; #[cfg(feature = "aes-crypto")]
use crate::types::AesMode;
use crate::types::{ffi, AesVendorVersion, DateTime, System, ZipFileData, DEFAULT_VERSION};
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))] #[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))]
use core::num::NonZeroU64; use core::num::NonZeroU64;
use crc32fast::Hasher; use crc32fast::Hasher;
@ -13,6 +17,7 @@ use std::default::Default;
use std::io; use std::io;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::{BufReader, SeekFrom}; use std::io::{BufReader, SeekFrom};
use std::marker::PhantomData;
use std::mem; use std::mem;
use std::str::{from_utf8, Utf8Error}; use std::str::{from_utf8, Utf8Error};
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
@ -42,19 +47,25 @@ use zstd::stream::write::Encoder as ZstdEncoder;
enum MaybeEncrypted<W> { enum MaybeEncrypted<W> {
Unencrypted(W), Unencrypted(W),
Encrypted(crate::zipcrypto::ZipCryptoWriter<W>), #[cfg(feature = "aes-crypto")]
Aes(crate::aes::AesWriter<W>),
ZipCrypto(crate::zipcrypto::ZipCryptoWriter<W>),
} }
impl<W: Write> Write for MaybeEncrypted<W> { impl<W: Write> Write for MaybeEncrypted<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self { match self {
MaybeEncrypted::Unencrypted(w) => w.write(buf), MaybeEncrypted::Unencrypted(w) => w.write(buf),
MaybeEncrypted::Encrypted(w) => w.write(buf), #[cfg(feature = "aes-crypto")]
MaybeEncrypted::Aes(w) => w.write(buf),
MaybeEncrypted::ZipCrypto(w) => w.write(buf),
} }
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
match self { match self {
MaybeEncrypted::Unencrypted(w) => w.flush(), MaybeEncrypted::Unencrypted(w) => w.flush(),
MaybeEncrypted::Encrypted(w) => w.flush(), #[cfg(feature = "aes-crypto")]
MaybeEncrypted::Aes(w) => w.flush(),
MaybeEncrypted::ZipCrypto(w) => w.flush(),
} }
} }
} }
@ -177,24 +188,52 @@ mod sealed {
} }
} }
#[derive(Copy, Clone, Debug)]
enum EncryptWith<'k> {
#[cfg(feature = "aes-crypto")]
Aes {
mode: AesMode,
password: &'k str,
},
ZipCrypto(ZipCryptoKeys, PhantomData<&'k ()>),
}
#[cfg(fuzzing)]
impl<'a> arbitrary::Arbitrary<'a> for EncryptWith<'a> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
#[cfg(feature = "aes-crypto")]
if bool::arbitrary(u)? {
return Ok(EncryptWith::Aes {
mode: AesMode::arbitrary(u)?,
password: u.arbitrary::<&str>()?,
});
}
Ok(EncryptWith::ZipCrypto(
ZipCryptoKeys::arbitrary(u)?,
PhantomData,
))
}
}
/// Metadata for a file to be written /// Metadata for a file to be written
#[derive(Clone, Debug, Copy)] #[derive(Clone, Debug, Copy)]
pub struct FileOptions<T: FileOptionExtension> { pub struct FileOptions<'k, T: FileOptionExtension> {
pub(crate) compression_method: CompressionMethod, pub(crate) compression_method: CompressionMethod,
pub(crate) compression_level: Option<i64>, pub(crate) compression_level: Option<i64>,
pub(crate) last_modified_time: DateTime, pub(crate) last_modified_time: DateTime,
pub(crate) permissions: Option<u32>, pub(crate) permissions: Option<u32>,
pub(crate) large_file: bool, pub(crate) large_file: bool,
encrypt_with: Option<ZipCryptoKeys>, encrypt_with: Option<EncryptWith<'k>>,
extended_options: T, extended_options: T,
alignment: u16, alignment: u16,
#[cfg(feature = "deflate-zopfli")] #[cfg(feature = "deflate-zopfli")]
pub(super) zopfli_buffer_size: Option<usize>, pub(super) zopfli_buffer_size: Option<usize>,
} }
/// Simple File Options. Can be copied and good for simple writing zip files /// Simple File Options. Can be copied and good for simple writing zip files
pub type SimpleFileOptions = FileOptions<()>; pub type SimpleFileOptions = FileOptions<'static, ()>;
/// Adds Extra Data and Central Extra Data. It does not implement copy. /// Adds Extra Data and Central Extra Data. It does not implement copy.
pub type FullFileOptions = FileOptions<ExtendedFileOptions>; pub type FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>;
/// The Extension for Extra Data and Central Extra Data /// The Extension for Extra Data and Central Extra Data
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct ExtendedFileOptions { pub struct ExtendedFileOptions {
@ -203,15 +242,15 @@ pub struct ExtendedFileOptions {
} }
#[cfg(fuzzing)] #[cfg(fuzzing)]
impl arbitrary::Arbitrary<'_> for FileOptions<ExtendedFileOptions> { impl<'a> arbitrary::Arbitrary<'a> for FileOptions<'a, ExtendedFileOptions> {
fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Self> { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let mut options = FullFileOptions { let mut options = FullFileOptions {
compression_method: CompressionMethod::arbitrary(u)?, compression_method: CompressionMethod::arbitrary(u)?,
compression_level: None, compression_level: None,
last_modified_time: DateTime::arbitrary(u)?, last_modified_time: DateTime::arbitrary(u)?,
permissions: Option::<u32>::arbitrary(u)?, permissions: Option::<u32>::arbitrary(u)?,
large_file: bool::arbitrary(u)?, large_file: bool::arbitrary(u)?,
encrypt_with: Option::<ZipCryptoKeys>::arbitrary(u)?, encrypt_with: Option::<EncryptWith>::arbitrary(u)?,
alignment: u16::arbitrary(u)?, alignment: u16::arbitrary(u)?,
#[cfg(feature = "deflate-zopfli")] #[cfg(feature = "deflate-zopfli")]
zopfli_buffer_size: None, zopfli_buffer_size: None,
@ -253,7 +292,7 @@ impl arbitrary::Arbitrary<'_> for FileOptions<ExtendedFileOptions> {
} }
} }
impl<T: FileOptionExtension> FileOptions<T> { impl<'k, T: FileOptionExtension> FileOptions<'k, T> {
/// Set the compression method for the new file /// Set the compression method for the new file
/// ///
/// The default is `CompressionMethod::Deflated` if it is enabled. If not, /// The default is `CompressionMethod::Deflated` if it is enabled. If not,
@ -317,9 +356,24 @@ impl<T: FileOptionExtension> FileOptions<T> {
self.large_file = large; self.large_file = large;
self self
} }
pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> Self {
self.encrypt_with = Some(ZipCryptoKeys::derive(password)); pub(crate) fn with_deprecated_encryption(self, password: &[u8]) -> FileOptions<'static, T> {
self FileOptions {
encrypt_with: Some(EncryptWith::ZipCrypto(
ZipCryptoKeys::derive(password),
PhantomData,
)),
..self
}
}
/// Set the AES encryption parameters.
#[cfg(feature = "aes-crypto")]
pub fn with_aes_encryption(self, mode: AesMode, password: &str) -> FileOptions<'_, T> {
FileOptions {
encrypt_with: Some(EncryptWith::Aes { mode, password }),
..self
}
} }
/// Sets the size of the buffer used to hold the next block that Zopfli will compress. The /// Sets the size of the buffer used to hold the next block that Zopfli will compress. The
@ -344,7 +398,7 @@ impl<T: FileOptionExtension> FileOptions<T> {
self self
} }
} }
impl FileOptions<ExtendedFileOptions> { impl<'k> FileOptions<'k, ExtendedFileOptions> {
/// Adds an extra data field. /// Adds an extra data field.
pub fn add_extra_data( pub fn add_extra_data(
&mut self, &mut self,
@ -396,7 +450,7 @@ impl FileOptions<ExtendedFileOptions> {
self self
} }
} }
impl<T: FileOptionExtension> Default for FileOptions<T> { impl<'k, T: FileOptionExtension> Default for FileOptions<'k, T> {
/// Construct a new FileOptions object /// Construct a new FileOptions object
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -700,16 +754,57 @@ impl<W: Write + Seek> ZipWriter<W> {
uncompressed_size: 0, uncompressed_size: 0,
}); });
#[allow(unused_mut)]
let mut extra_field = options.extended_options.extra_data().cloned();
// Write AES encryption extra data.
#[allow(unused_mut)]
let mut aes_extra_data_start = 0;
#[cfg(feature = "aes-crypto")]
if let Some(EncryptWith::Aes { .. }) = options.encrypt_with {
const AES_DUMMY_EXTRA_DATA: [u8; 11] = [
0x01, 0x99, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let extra_data = extra_field.get_or_insert_with(Default::default);
let extra_data = match Arc::get_mut(extra_data) {
Some(exclusive) => exclusive,
None => {
let new = Arc::new(extra_data.to_vec());
Arc::get_mut(extra_field.insert(new)).unwrap()
}
};
if extra_data.len() + AES_DUMMY_EXTRA_DATA.len() > u16::MAX as usize {
let _ = self.abort_file();
return Err(InvalidArchive("Extra data field is too large"));
}
aes_extra_data_start = extra_data.len() as u64;
// We write zero bytes for now since we need to update the data when finishing the
// file.
extra_data.write_all(&AES_DUMMY_EXTRA_DATA)?;
}
{ {
let header_start = self.inner.get_plain().stream_position()?; let header_start = self.inner.get_plain().stream_position()?;
let permissions = options.permissions.unwrap_or(0o100644); let permissions = options.permissions.unwrap_or(0o100644);
let (compression_method, aes_mode) = match options.encrypt_with {
#[cfg(feature = "aes-crypto")]
Some(EncryptWith::Aes { mode, .. }) => (
CompressionMethod::Aes,
Some((mode, AesVendorVersion::Ae2, options.compression_method)),
),
_ => (options.compression_method, None),
};
let file = ZipFileData { let file = ZipFileData {
system: System::Unix, system: System::Unix,
version_made_by: DEFAULT_VERSION, version_made_by: DEFAULT_VERSION,
encrypted: options.encrypt_with.is_some(), encrypted: options.encrypt_with.is_some(),
using_data_descriptor: false, using_data_descriptor: false,
compression_method: options.compression_method, compression_method,
compression_level: options.compression_level, compression_level: options.compression_level,
last_modified_time: options.last_modified_time, last_modified_time: options.last_modified_time,
crc32: raw_values.crc32, crc32: raw_values.crc32,
@ -717,15 +812,18 @@ impl<W: Write + Seek> ZipWriter<W> {
uncompressed_size: raw_values.uncompressed_size, uncompressed_size: raw_values.uncompressed_size,
file_name: name.into(), file_name: name.into(),
file_name_raw: vec![].into_boxed_slice(), // Never used for saving file_name_raw: vec![].into_boxed_slice(), // Never used for saving
extra_field: options.extended_options.extra_data().cloned(), extra_field,
central_extra_field: options.extended_options.central_extra_data().cloned(), central_extra_field: options.extended_options.central_extra_data().cloned(),
file_comment: String::with_capacity(0).into_boxed_str(), file_comment: String::with_capacity(0).into_boxed_str(),
header_start, header_start,
extra_data_start: None,
data_start: OnceLock::new(), data_start: OnceLock::new(),
central_header_start: 0, central_header_start: 0,
external_attributes: permissions << 16, external_attributes: permissions << 16,
large_file: options.large_file, large_file: options.large_file,
aes_mode: None, aes_mode,
aes_extra_data_start,
extra_fields: Vec::new(), extra_fields: Vec::new(),
}; };
let index = self.insert_file_data(file)?; let index = self.insert_file_data(file)?;
@ -777,6 +875,7 @@ impl<W: Write + Seek> ZipWriter<W> {
write_local_zip64_extra_field(writer, file)?; write_local_zip64_extra_field(writer, file)?;
} }
if let Some(extra_field) = &file.extra_field { if let Some(extra_field) = &file.extra_field {
file.extra_data_start = Some(writer.stream_position()?);
writer.write_all(extra_field)?; writer.write_all(extra_field)?;
} }
let mut header_end = writer.stream_position()?; let mut header_end = writer.stream_position()?;
@ -816,17 +915,29 @@ impl<W: Write + Seek> ZipWriter<W> {
debug_assert_eq!(header_end % align, 0); debug_assert_eq!(header_end % align, 0);
} }
} }
if let Some(keys) = options.encrypt_with { match options.encrypt_with {
let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { #[cfg(feature = "aes-crypto")]
writer: mem::replace(&mut self.inner, Closed).unwrap(), Some(EncryptWith::Aes { mode, password }) => {
buffer: vec![], let aeswriter = AesWriter::new(
keys, mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(),
}; mode,
let crypto_header = [0u8; 12]; password.as_bytes(),
)?;
self.inner = GenericZipWriter::Storer(MaybeEncrypted::Aes(aeswriter));
}
Some(EncryptWith::ZipCrypto(keys, ..)) => {
let mut zipwriter = crate::zipcrypto::ZipCryptoWriter {
writer: mem::replace(&mut self.inner, Closed).unwrap(),
buffer: vec![],
keys,
};
let crypto_header = [0u8; 12];
zipwriter.write_all(&crypto_header)?; zipwriter.write_all(&crypto_header)?;
header_end = zipwriter.writer.stream_position()?; header_end = zipwriter.writer.stream_position()?;
self.inner = Storer(MaybeEncrypted::Encrypted(zipwriter)); self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter));
}
None => {}
} }
self.stats.start = header_end; self.stats.start = header_end;
debug_assert!(file.data_start.get().is_none()); debug_assert!(file.data_start.get().is_none());
@ -867,13 +978,28 @@ impl<W: Write + Seek> ZipWriter<W> {
None => return Ok(()), None => return Ok(()),
Some((_, f)) => f, Some((_, f)) => f,
}; };
file.crc32 = self.stats.hasher.clone().finalize();
file.uncompressed_size = self.stats.bytes_written; file.uncompressed_size = self.stats.bytes_written;
let file_end = writer.stream_position()?; let file_end = writer.stream_position()?;
debug_assert!(file_end >= self.stats.start); debug_assert!(file_end >= self.stats.start);
file.compressed_size = file_end - self.stats.start; file.compressed_size = file_end - self.stats.start;
file.crc32 = self.stats.hasher.clone().finalize();
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 {
file.crc32 = 0;
AesVendorVersion::Ae2
} else {
AesVendorVersion::Ae1
}
}
update_aes_extra_data(writer, file)?;
update_local_file_header(writer, file)?; update_local_file_header(writer, file)?;
writer.seek(SeekFrom::Start(file_end))?; writer.seek(SeekFrom::Start(file_end))?;
} }
@ -890,7 +1016,11 @@ impl<W: Write + Seek> ZipWriter<W> {
fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> { fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> {
match mem::replace(&mut self.inner, Closed) { match mem::replace(&mut self.inner, Closed) {
Storer(MaybeEncrypted::Encrypted(writer)) => { #[cfg(feature = "aes-crypto")]
Storer(MaybeEncrypted::Aes(writer)) => {
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish()?));
}
Storer(MaybeEncrypted::ZipCrypto(writer)) => {
let crc32 = self.stats.hasher.clone().finalize(); let crc32 = self.stats.hasher.clone().finalize();
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?)) self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
} }
@ -1157,6 +1287,7 @@ impl<W: Write + Seek> ZipWriter<W> {
} }
*options.permissions.as_mut().unwrap() |= 0o40000; *options.permissions.as_mut().unwrap() |= 0o40000;
options.compression_method = Stored; options.compression_method = Stored;
options.encrypt_with = None;
let name_as_string = name.into(); let name_as_string = name.into();
// Append a slash to the filename if it does not end with it. // Append a slash to the filename if it does not end with it.
@ -1469,7 +1600,7 @@ impl<W: Write + Seek> GenericZipWriter<W> {
})) }))
} }
CompressionMethod::AES => Err(ZipError::UnsupportedArchive( CompressionMethod::AES => Err(ZipError::UnsupportedArchive(
"AES compression is not supported for writing", "AES encryption is enabled through FileOptions::with_aes_encryption",
)), )),
#[cfg(feature = "zstd")] #[cfg(feature = "zstd")]
CompressionMethod::Zstd => { CompressionMethod::Zstd => {
@ -1607,6 +1738,50 @@ fn clamp_opt<T: Ord + Copy, U: Ord + Copy + TryFrom<T>>(
} }
} }
fn update_aes_extra_data<W: Write + io::Seek>(
writer: &mut W,
file: &mut ZipFileData,
) -> ZipResult<()> {
let Some((aes_mode, version, compression_method)) = file.aes_mode else {
return Ok(());
};
let extra_data_start = file.extra_data_start.unwrap();
writer.seek(io::SeekFrom::Start(
extra_data_start + file.aes_extra_data_start,
))?;
let mut buf = Vec::new();
// Extra field header ID.
buf.write_u16_le(0x9901)?;
// Data size.
buf.write_u16_le(7)?;
// Integer version number.
buf.write_u16_le(version as u16)?;
// Vendor ID.
buf.write_all(b"AE")?;
// AES encryption strength.
buf.write_all(&[aes_mode as u8])?;
// Real compression method.
#[allow(deprecated)]
buf.write_u16_le(compression_method.to_u16())?;
writer.write_all(&buf)?;
let aes_extra_data_start = file.aes_extra_data_start as usize;
let extra_field = Arc::get_mut(file.extra_field.as_mut().unwrap()).unwrap();
extra_field
.splice(
aes_extra_data_start..(aes_extra_data_start + buf.len()),
buf,
)
.count();
Ok(())
}
fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
const CRC32_OFFSET: u64 = 14; const CRC32_OFFSET: u64 = 14;
writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?; writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?;

View file

@ -7,6 +7,8 @@ use std::fmt::{Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::num::Wrapping; use std::num::Wrapping;
use crate::result::ZipError;
/// A container to hold the current key state /// A container to hold the current key state
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] #[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
#[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)] #[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)]
@ -110,7 +112,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
pub fn validate( pub fn validate(
mut self, mut self,
validator: ZipCryptoValidator, validator: ZipCryptoValidator,
) -> Result<Option<ZipCryptoReaderValid<R>>, std::io::Error> { ) -> Result<ZipCryptoReaderValid<R>, ZipError> {
// ZipCrypto prefixes a file with a 12 byte header // ZipCrypto prefixes a file with a 12 byte header
let mut header_buf = [0u8; 12]; let mut header_buf = [0u8; 12];
self.file.read_exact(&mut header_buf)?; self.file.read_exact(&mut header_buf)?;
@ -125,7 +127,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
// We also use 1 byte CRC. // We also use 1 byte CRC.
if (crc32_plaintext >> 24) as u8 != header_buf[11] { if (crc32_plaintext >> 24) as u8 != header_buf[11] {
return Ok(None); // Wrong password return Err(ZipError::InvalidPassword);
} }
} }
ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => { ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => {
@ -137,12 +139,12 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
// We check only 1 byte. // We check only 1 byte.
if (last_mod_time >> 8) as u8 != header_buf[11] { if (last_mod_time >> 8) as u8 != header_buf[11] {
return Ok(None); // Wrong password return Err(ZipError::InvalidPassword);
} }
} }
} }
Ok(Some(ZipCryptoReaderValid { reader: self })) Ok(ZipCryptoReaderValid { reader: self })
} }
} }
#[allow(unused)] #[allow(unused)]

View file

@ -1,7 +1,7 @@
#![cfg(feature = "aes-crypto")] #![cfg(feature = "aes-crypto")]
use std::io::{self, Read}; use std::io::{self, Read, Write};
use zip::ZipArchive; use zip::{result::ZipError, write::SimpleFileOptions, AesMode, CompressionMethod, ZipArchive};
const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet"; const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
@ -74,3 +74,81 @@ fn aes128_encrypted_file() {
.expect("couldn't read encrypted file"); .expect("couldn't read encrypted file");
assert_eq!(SECRET_CONTENT, content); assert_eq!(SECRET_CONTENT, content);
} }
#[test]
fn aes128_stored_roundtrip() {
let cursor = {
let mut zip = zip::ZipWriter::new(io::Cursor::new(Vec::new()));
zip.start_file(
"test.txt",
SimpleFileOptions::default().with_aes_encryption(AesMode::Aes128, "some password"),
)
.unwrap();
zip.write_all(SECRET_CONTENT.as_bytes()).unwrap();
zip.finish().unwrap()
};
let mut archive = ZipArchive::new(cursor).expect("couldn't open test zip file");
test_extract_encrypted_file(&mut archive, "test.txt", "some password", "other password");
}
#[test]
fn aes256_deflated_roundtrip() {
let cursor = {
let mut zip = zip::ZipWriter::new(io::Cursor::new(Vec::new()));
zip.start_file(
"test.txt",
SimpleFileOptions::default()
.compression_method(CompressionMethod::Deflated)
.with_aes_encryption(AesMode::Aes256, "some password"),
)
.unwrap();
zip.write_all(SECRET_CONTENT.as_bytes()).unwrap();
zip.finish().unwrap()
};
let mut archive = ZipArchive::new(cursor).expect("couldn't open test zip file");
test_extract_encrypted_file(&mut archive, "test.txt", "some password", "other password");
}
fn test_extract_encrypted_file<R: io::Read + io::Seek>(
archive: &mut ZipArchive<R>,
file_name: &str,
correct_password: &str,
incorrect_password: &str,
) {
{
let file = archive.by_name(file_name).map(|_| ());
match file {
Err(ZipError::UnsupportedArchive("Password required to decrypt file")) => {}
Err(err) => {
panic!("Failed to read file for unknown reason: {err:?}");
}
Ok(_) => {
panic!("Was able to successfully read encrypted file without password");
}
}
}
{
match archive.by_name_decrypt(file_name, incorrect_password.as_bytes()) {
Err(ZipError::InvalidPassword) => {}
Err(err) => panic!("Expected invalid password error, got: {err:?}"),
Ok(_) => panic!("Expected invalid password, got decrypted file"),
}
}
{
let mut content = String::new();
archive
.by_name_decrypt(file_name, correct_password.as_bytes())
.expect("couldn't read encrypted file")
.read_to_string(&mut content)
.expect("couldn't read encrypted file");
assert_eq!(SECRET_CONTENT, content);
}
}