diff options
author | Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> | 2024-02-01 11:57:53 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-02-01 11:57:53 +0000 |
commit | 5baa525f72e2eace28d60e8c7d9617e65cd503ef (patch) | |
tree | f22a08f84fabca451e486e8a3460064a9f75ec8c | |
parent | 99470abf006d30ce5d56c1f9cd90a96e44ec2e64 (diff) | |
parent | e3276d8a2b197611278f7552860757a1e221fec2 (diff) | |
download | zip-5baa525f72e2eace28d60e8c7d9617e65cd503ef.tar.gz |
Merge "Upgrade zip to 0.6.6" into mainemu-34-3-release
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | Android.bp | 4 | ||||
-rw-r--r-- | CHANGELOG.md | 10 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | Cargo.toml.orig | 7 | ||||
-rw-r--r-- | METADATA | 21 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | src/aes_ctr.rs | 11 | ||||
-rw-r--r-- | src/lib.rs | 11 | ||||
-rw-r--r-- | src/read.rs | 64 | ||||
-rw-r--r-- | src/read/stream.rs | 372 | ||||
-rw-r--r-- | src/types.rs | 72 | ||||
-rw-r--r-- | src/unstable.rs | 20 | ||||
-rw-r--r-- | src/write.rs | 67 | ||||
-rw-r--r-- | src/zipcrypto.rs | 46 | ||||
-rw-r--r-- | tests/issue_234.rs | 2 | ||||
-rw-r--r-- | tests/zip_crypto.rs | 17 |
17 files changed, 616 insertions, 124 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 535489c..cb0d9ad 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "f7dcc666b75256e766295589a5ac5dc5a9617c39" + "sha1": "21a20584bc9e05dfa4f3c5b0bc420a1389fae2c3" }, "path_in_vcs": "" }
\ No newline at end of file @@ -23,9 +23,9 @@ rust_library { host_supported: true, crate_name: "zip", cargo_env_compat: true, - cargo_pkg_version: "0.6.4", + cargo_pkg_version: "0.6.6", srcs: ["src/lib.rs"], - edition: "2018", + edition: "2021", features: [ "deflate-zlib", "flate2", diff --git a/CHANGELOG.md b/CHANGELOG.md index cd79e39..96c6994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.6.6] +### Changed + +- Updated `aes` dependency to `0.8.2` (https://github.com/zip-rs/zip/pull/354) + +## [0.6.5] +### Changed + +- Added experimental [`zip::unstable::write::FileOptions::with_deprecated_encryption`] API to enable encrypting files with PKWARE encryption. + ## [0.6.4] ### Changed @@ -10,9 +10,10 @@ # See Cargo.toml.orig for the original contents. [package] -edition = "2018" +edition = "2021" +rust-version = "1.59.0" name = "zip" -version = "0.6.4" +version = "0.6.6" authors = [ "Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", @@ -38,7 +39,7 @@ name = "read_metadata" harness = false [dependencies.aes] -version = "0.7.5" +version = "0.8.2" optional = true [dependencies.byteorder] diff --git a/Cargo.toml.orig b/Cargo.toml.orig index caf6a07..510df9c 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "zip" -version = "0.6.4" +version = "0.6.6" authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"] license = "MIT" repository = "https://github.com/zip-rs/zip.git" @@ -8,10 +8,11 @@ keywords = ["zip", "archive"] description = """ Library to support the reading and writing of zip files. """ -edition = "2018" +edition = "2021" +rust-version = "1.59.0" [dependencies] -aes = { version = "0.7.5", optional = true } +aes = { version = "0.8.2", optional = true } byteorder = "1.4.3" bzip2 = { version = "0.4.3", optional = true } constant_time_eq = { version = "0.1.5", optional = true } @@ -1,23 +1,20 @@ # This project was upgraded with external_updater. # Usage: tools/external_updater/updater.sh update rust/crates/zip -# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md +# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md name: "zip" description: "Library to support the reading and writing of zip files." third_party { - url { - type: HOMEPAGE - value: "https://crates.io/crates/zip" - } - url { - type: ARCHIVE - value: "https://static.crates.io/crates/zip/zip-0.6.4.crate" - } - version: "0.6.4" license_type: NOTICE last_upgrade_date { year: 2023 - month: 2 - day: 17 + month: 12 + day: 4 + } + homepage: "https://crates.io/crates/zip" + identifier { + type: "Archive" + value: "https://static.crates.io/crates/zip/zip-0.6.6.crate" + version: "0.6.6" } } @@ -7,9 +7,6 @@ zip-rs [Documentation](https://docs.rs/zip/0.6.3/zip/) -> PSA: This version of the ZIP crate will not gain any new features, -> and will only be updated if major security issues are found. - Info ---- @@ -35,14 +32,14 @@ With all default features: ```toml [dependencies] -zip = "0.6.4" +zip = "0.6" ``` Without the default features: ```toml [dependencies] -zip = { version = "0.6.4", default-features = false } +zip = { version = "0.6.6", default-features = false } ``` The features available are: diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs index 0f34335..211727c 100644 --- a/src/aes_ctr.rs +++ b/src/aes_ctr.rs @@ -2,10 +2,11 @@ //! //! This was implemented since the zip specification requires the mode to not use a nonce and uses a //! different byte order (little endian) than NIST (big endian). -//! See [AesCtrZipKeyStream](./struct.AesCtrZipKeyStream.html) for more information. +//! See [AesCtrZipKeyStream] for more information. use aes::cipher::generic_array::GenericArray; -use aes::{BlockEncrypt, NewBlockCipher}; +// use aes::{BlockEncrypt, NewBlockCipher}; +use aes::cipher::{BlockEncrypt, KeyInit}; use byteorder::WriteBytesExt; use std::{any, fmt}; @@ -82,7 +83,7 @@ where impl<C> AesCtrZipKeyStream<C> where C: AesKind, - C::Cipher: NewBlockCipher, + C::Cipher: KeyInit, { /// Creates a new zip variant AES-CTR key stream. /// @@ -150,14 +151,14 @@ fn xor(dest: &mut [u8], src: &[u8]) { #[cfg(test)] mod tests { use super::{Aes128, Aes192, Aes256, AesCipher, AesCtrZipKeyStream, AesKind}; - use aes::{BlockEncrypt, NewBlockCipher}; + use aes::cipher::{BlockEncrypt, KeyInit}; /// Checks whether `crypt_in_place` produces the correct plaintext after one use and yields the /// cipertext again after applying it again. fn roundtrip<Aes>(key: &[u8], ciphertext: &mut [u8], expected_plaintext: &[u8]) where Aes: AesKind, - Aes::Cipher: NewBlockCipher + BlockEncrypt, + Aes::Cipher: KeyInit + BlockEncrypt, { let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key); @@ -42,3 +42,14 @@ mod spec; mod types; pub mod write; mod zipcrypto; + +/// Unstable APIs +/// +/// All APIs accessible by importing this module are unstable; They may be changed in patch releases. +/// You MUST you an exact version specifier in `Cargo.toml`, to indicate the version of this API you're using: +/// +/// ```toml +/// [dependencies] +/// zip = "=0.6.6" +/// ``` +pub mod unstable; diff --git a/src/read.rs b/src/read.rs index dad20c2..b702b4f 100644 --- a/src/read.rs +++ b/src/read.rs @@ -13,7 +13,7 @@ use byteorder::{LittleEndian, ReadBytesExt}; use std::borrow::Cow; use std::collections::HashMap; use std::io::{self, prelude::*}; -use std::path::{Component, Path}; +use std::path::Path; use std::sync::Arc; #[cfg(any( @@ -29,10 +29,8 @@ use bzip2::read::BzDecoder; #[cfg(feature = "zstd")] use zstd::stream::read::Decoder as ZstdDecoder; -mod ffi { - pub const S_IFDIR: u32 = 0o0040000; - pub const S_IFREG: u32 = 0o0100000; -} +/// Provides high level API for reading from a stream. +pub(crate) mod stream; // Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely pub(crate) mod zip_archive { @@ -650,12 +648,22 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>( archive_offset: u64, ) -> ZipResult<ZipFileData> { let central_header_start = reader.stream_position()?; + // Parse central header let signature = reader.read_u32::<LittleEndian>()?; if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid Central Directory header")); + Err(ZipError::InvalidArchive("Invalid Central Directory header")) + } else { + central_header_to_zip_file_inner(reader, archive_offset, central_header_start) } +} +/// Parse a central directory entry to collect the information for the file. +fn central_header_to_zip_file_inner<R: Read>( + reader: &mut R, + archive_offset: u64, + central_header_start: u64, +) -> ZipResult<ZipFileData> { let version_made_by = reader.read_u16::<LittleEndian>()?; let _version_to_extract = reader.read_u16::<LittleEndian>()?; let flags = reader.read_u16::<LittleEndian>()?; @@ -896,20 +904,7 @@ impl<'a> ZipFile<'a> { /// to path-based exploits. It is recommended over /// [`ZipFile::mangled_name`]. pub fn enclosed_name(&self) -> Option<&Path> { - if self.data.file_name.contains('\0') { - return None; - } - let path = Path::new(&self.data.file_name); - let mut depth = 0usize; - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir => return None, - Component::ParentDir => depth = depth.checked_sub(1)?, - Component::Normal(_) => depth += 1, - Component::CurDir => (), - } - } - Some(path) + self.data.enclosed_name() } /// Get the comment of the file @@ -952,27 +947,7 @@ impl<'a> ZipFile<'a> { /// Get unix mode for the file pub fn unix_mode(&self) -> Option<u32> { - if self.data.external_attributes == 0 { - return None; - } - - match self.data.system { - System::Unix => Some(self.data.external_attributes >> 16), - System::Dos => { - // Interpret MS-DOS directory bit - let mut mode = if 0x10 == (self.data.external_attributes & 0x10) { - ffi::S_IFDIR | 0o0775 - } else { - ffi::S_IFREG | 0o0664 - }; - if 0x01 == (self.data.external_attributes & 0x01) { - // Read-only bit; strip write permissions - mode &= 0o0555; - } - Some(mode) - } - _ => None, - } + self.data.unix_mode() } /// Get the CRC32 hash of the original file @@ -1029,10 +1004,9 @@ impl<'a> Drop for ZipFile<'a> { match reader.read(&mut buffer) { Ok(0) => break, Ok(_) => (), - Err(e) => panic!( - "Could not consume all of the output of the current ZipFile: {:?}", - e - ), + Err(e) => { + panic!("Could not consume all of the output of the current ZipFile: {e:?}") + } } } } diff --git a/src/read/stream.rs b/src/read/stream.rs new file mode 100644 index 0000000..5a01b23 --- /dev/null +++ b/src/read/stream.rs @@ -0,0 +1,372 @@ +use std::fs; +use std::io::{self, Read}; +use std::path::Path; + +use super::{ + central_header_to_zip_file_inner, read_zipfile_from_stream, spec, ZipError, ZipFile, + ZipFileData, ZipResult, +}; + +use byteorder::{LittleEndian, ReadBytesExt}; + +/// Stream decoder for zip. +#[derive(Debug)] +pub struct ZipStreamReader<R>(R); + +impl<R> ZipStreamReader<R> { + /// Create a new ZipStreamReader + pub fn new(reader: R) -> Self { + Self(reader) + } +} + +impl<R: Read> ZipStreamReader<R> { + fn parse_central_directory(&mut self) -> ZipResult<Option<ZipStreamFileMetadata>> { + // Give archive_offset and central_header_start dummy value 0, since + // they are not used in the output. + let archive_offset = 0; + let central_header_start = 0; + + // Parse central header + let signature = self.0.read_u32::<LittleEndian>()?; + if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { + Ok(None) + } else { + central_header_to_zip_file_inner(&mut self.0, archive_offset, central_header_start) + .map(ZipStreamFileMetadata) + .map(Some) + } + } + + /// Iteraate over the stream and extract all file and their + /// metadata. + pub fn visit<V: ZipStreamVisitor>(mut self, visitor: &mut V) -> ZipResult<()> { + while let Some(mut file) = read_zipfile_from_stream(&mut self.0)? { + visitor.visit_file(&mut file)?; + } + + while let Some(metadata) = self.parse_central_directory()? { + visitor.visit_additional_metadata(&metadata)?; + } + + Ok(()) + } + + /// Extract a Zip archive into a directory, overwriting files if they + /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. + /// + /// Extraction is not atomic; If an error is encountered, some of the files + /// may be left on disk. + pub fn extract<P: AsRef<Path>>(self, directory: P) -> ZipResult<()> { + struct Extractor<'a>(&'a Path); + impl ZipStreamVisitor for Extractor<'_> { + fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> { + let filepath = file + .enclosed_name() + .ok_or(ZipError::InvalidArchive("Invalid file path"))?; + + let outpath = self.0.join(filepath); + + if file.name().ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + fs::create_dir_all(p)?; + } + let mut outfile = fs::File::create(&outpath)?; + io::copy(file, &mut outfile)?; + } + + Ok(()) + } + + #[allow(unused)] + fn visit_additional_metadata( + &mut self, + metadata: &ZipStreamFileMetadata, + ) -> ZipResult<()> { + #[cfg(unix)] + { + let filepath = metadata + .enclosed_name() + .ok_or(ZipError::InvalidArchive("Invalid file path"))?; + + let outpath = self.0.join(filepath); + + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = metadata.unix_mode() { + fs::set_permissions(outpath, fs::Permissions::from_mode(mode))?; + } + } + + Ok(()) + } + } + + self.visit(&mut Extractor(directory.as_ref())) + } +} + +/// Visitor for ZipStreamReader +pub trait ZipStreamVisitor { + /// * `file` - contains the content of the file and most of the metadata, + /// except: + /// - `comment`: set to an empty string + /// - `data_start`: set to 0 + /// - `external_attributes`: `unix_mode()`: will return None + fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()>; + + /// This function is guranteed to be called after all `visit_file`s. + /// + /// * `metadata` - Provides missing metadata in `visit_file`. + fn visit_additional_metadata(&mut self, metadata: &ZipStreamFileMetadata) -> ZipResult<()>; +} + +/// Additional metadata for the file. +#[derive(Debug)] +pub struct ZipStreamFileMetadata(ZipFileData); + +impl ZipStreamFileMetadata { + /// Get the name of the file + /// + /// # Warnings + /// + /// It is dangerous to use this name directly when extracting an archive. + /// It may contain an absolute path (`/etc/shadow`), or break out of the + /// current directory (`../runtime`). Carelessly writing to these paths + /// allows an attacker to craft a ZIP archive that will overwrite critical + /// files. + /// + /// You can use the [`ZipFile::enclosed_name`] method to validate the name + /// as a safe path. + pub fn name(&self) -> &str { + &self.0.file_name + } + + /// Get the name of the file, in the raw (internal) byte representation. + /// + /// The encoding of this data is currently undefined. + pub fn name_raw(&self) -> &[u8] { + &self.0.file_name_raw + } + + /// Rewrite the path, ignoring any path components with special meaning. + /// + /// - Absolute paths are made relative + /// - [`ParentDir`]s are ignored + /// - Truncates the filename at a NULL byte + /// + /// This is appropriate if you need to be able to extract *something* from + /// any archive, but will easily misrepresent trivial paths like + /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this, + /// [`ZipFile::enclosed_name`] is the better option in most scenarios. + /// + /// [`ParentDir`]: `Component::ParentDir` + pub fn mangled_name(&self) -> ::std::path::PathBuf { + self.0.file_name_sanitized() + } + + /// Ensure the file path is safe to use as a [`Path`]. + /// + /// - It can't contain NULL bytes + /// - It can't resolve to a path outside the current directory + /// > `foo/../bar` is fine, `foo/../../bar` is not. + /// - It can't be an absolute path + /// + /// This will read well-formed ZIP files correctly, and is resistant + /// to path-based exploits. It is recommended over + /// [`ZipFile::mangled_name`]. + pub fn enclosed_name(&self) -> Option<&Path> { + self.0.enclosed_name() + } + + /// Returns whether the file is actually a directory + pub fn is_dir(&self) -> bool { + self.name() + .chars() + .rev() + .next() + .map_or(false, |c| c == '/' || c == '\\') + } + + /// Returns whether the file is a regular file + pub fn is_file(&self) -> bool { + !self.is_dir() + } + + /// Get the comment of the file + pub fn comment(&self) -> &str { + &self.0.file_comment + } + + /// Get the starting offset of the data of the compressed file + pub fn data_start(&self) -> u64 { + self.0.data_start.load() + } + + /// Get unix mode for the file + pub fn unix_mode(&self) -> Option<u32> { + self.0.unix_mode() + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::collections::BTreeSet; + use std::io; + + struct DummyVisitor; + impl ZipStreamVisitor for DummyVisitor { + fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> { + Ok(()) + } + + fn visit_additional_metadata( + &mut self, + _metadata: &ZipStreamFileMetadata, + ) -> ZipResult<()> { + Ok(()) + } + } + + #[derive(Default, Debug, Eq, PartialEq)] + struct CounterVisitor(u64, u64); + impl ZipStreamVisitor for CounterVisitor { + fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> { + self.0 += 1; + Ok(()) + } + + fn visit_additional_metadata( + &mut self, + _metadata: &ZipStreamFileMetadata, + ) -> ZipResult<()> { + self.1 += 1; + Ok(()) + } + } + + #[test] + fn invalid_offset() { + ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/invalid_offset.zip" + ))) + .visit(&mut DummyVisitor) + .unwrap_err(); + } + + #[test] + fn invalid_offset2() { + ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/invalid_offset2.zip" + ))) + .visit(&mut DummyVisitor) + .unwrap_err(); + } + + #[test] + fn zip_read_streaming() { + let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/mimetype.zip" + ))); + + #[derive(Default)] + struct V { + filenames: BTreeSet<Box<str>>, + } + impl ZipStreamVisitor for V { + fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> { + if file.is_file() { + self.filenames.insert(file.name().into()); + } + + Ok(()) + } + fn visit_additional_metadata( + &mut self, + metadata: &ZipStreamFileMetadata, + ) -> ZipResult<()> { + if metadata.is_file() { + assert!( + self.filenames.contains(metadata.name()), + "{} is missing its file content", + metadata.name() + ); + } + + Ok(()) + } + } + + reader.visit(&mut V::default()).unwrap(); + } + + #[test] + fn file_and_dir_predicates() { + let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/files_and_dirs.zip" + ))); + + #[derive(Default)] + struct V { + filenames: BTreeSet<Box<str>>, + } + impl ZipStreamVisitor for V { + fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> { + let full_name = file.enclosed_name().unwrap(); + let file_name = full_name.file_name().unwrap().to_str().unwrap(); + assert!( + (file_name.starts_with("dir") && file.is_dir()) + || (file_name.starts_with("file") && file.is_file()) + ); + + if file.is_file() { + self.filenames.insert(file.name().into()); + } + + Ok(()) + } + fn visit_additional_metadata( + &mut self, + metadata: &ZipStreamFileMetadata, + ) -> ZipResult<()> { + if metadata.is_file() { + assert!( + self.filenames.contains(metadata.name()), + "{} is missing its file content", + metadata.name() + ); + } + + Ok(()) + } + } + + reader.visit(&mut V::default()).unwrap(); + } + + /// test case to ensure we don't preemptively over allocate based on the + /// declared number of files in the CDE of an invalid zip when the number of + /// files declared is more than the alleged offset in the CDE + #[test] + fn invalid_cde_number_of_files_allocation_smaller_offset() { + ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip" + ))) + .visit(&mut DummyVisitor) + .unwrap_err(); + } + + /// test case to ensure we don't preemptively over allocate based on the + /// declared number of files in the CDE of an invalid zip when the number of + /// files declared is less than the alleged offset in the CDE + #[test] + fn invalid_cde_number_of_files_allocation_greater_offset() { + ZipStreamReader::new(io::Cursor::new(include_bytes!( + "../../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip" + ))) + .visit(&mut DummyVisitor) + .unwrap_err(); + } +} diff --git a/src/types.rs b/src/types.rs index ad3a570..c3d0a45 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ //! Types that specify what is contained in a ZIP. -#[cfg(feature = "time")] -use std::convert::{TryFrom, TryInto}; +use std::path; + #[cfg(not(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", @@ -12,6 +12,11 @@ use std::time::SystemTime; #[cfg(doc)] use {crate::read::ZipFile, crate::write::FileOptions}; +mod ffi { + pub const S_IFDIR: u32 = 0o0040000; + pub const S_IFREG: u32 = 0o0100000; +} + #[cfg(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", @@ -375,6 +380,48 @@ impl ZipFileData { }) } + pub(crate) fn enclosed_name(&self) -> Option<&path::Path> { + if self.file_name.contains('\0') { + return None; + } + let path = path::Path::new(&self.file_name); + let mut depth = 0usize; + for component in path.components() { + match component { + path::Component::Prefix(_) | path::Component::RootDir => return None, + path::Component::ParentDir => depth = depth.checked_sub(1)?, + path::Component::Normal(_) => depth += 1, + path::Component::CurDir => (), + } + } + Some(path) + } + + /// Get unix mode for the file + pub(crate) fn unix_mode(&self) -> Option<u32> { + if self.external_attributes == 0 { + return None; + } + + match self.system { + System::Unix => Some(self.external_attributes >> 16), + System::Dos => { + // Interpret MS-DOS directory bit + let mut mode = if 0x10 == (self.external_attributes & 0x10) { + ffi::S_IFDIR | 0o0775 + } else { + ffi::S_IFREG | 0o0664 + }; + if 0x01 == (self.external_attributes & 0x01) { + // Read-only bit; strip write permissions + mode &= 0o0555; + } + Some(mode) + } + _ => None, + } + } + pub fn zip64_extension(&self) -> bool { self.uncompressed_size > 0xFFFFFFFF || self.compressed_size > 0xFFFFFFFF @@ -510,27 +557,6 @@ mod test { #[cfg(feature = "time")] #[test] - fn datetime_from_time_bounds() { - use std::convert::TryFrom; - - use super::DateTime; - use time::macros::datetime; - - // 1979-12-31 23:59:59 - assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err()); - - // 1980-01-01 00:00:00 - assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); - - // 2107-12-31 23:59:59 - assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); - - // 2108-01-01 00:00:00 - assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err()); - } - - #[cfg(feature = "time")] - #[test] fn datetime_try_from_bounds() { use std::convert::TryFrom; diff --git a/src/unstable.rs b/src/unstable.rs new file mode 100644 index 0000000..f8b46a9 --- /dev/null +++ b/src/unstable.rs @@ -0,0 +1,20 @@ +/// Provides high level API for reading from a stream. +pub mod stream { + pub use crate::read::stream::*; +} +/// Types for creating ZIP archives. +pub mod write { + use crate::write::FileOptions; + /// Unstable methods for [`FileOptions`]. + pub trait FileOptionsExt { + /// Write the file with the given password using the deprecated ZipCrypto algorithm. + /// + /// This is not recommended for new archives, as ZipCrypto is not secure. + fn with_deprecated_encryption(self, password: &[u8]) -> Self; + } + impl FileOptionsExt for FileOptions { + fn with_deprecated_encryption(self, password: &[u8]) -> Self { + self.with_deprecated_encryption(password) + } + } +}
\ No newline at end of file diff --git a/src/write.rs b/src/write.rs index 14252b4..4cdc031 100644 --- a/src/write.rs +++ b/src/write.rs @@ -29,19 +29,37 @@ use time::OffsetDateTime; #[cfg(feature = "zstd")] use zstd::stream::write::Encoder as ZstdEncoder; +enum MaybeEncrypted<W> { + Unencrypted(W), + Encrypted(crate::zipcrypto::ZipCryptoWriter<W>), +} +impl<W: Write> Write for MaybeEncrypted<W> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + match self { + MaybeEncrypted::Unencrypted(w) => w.write(buf), + MaybeEncrypted::Encrypted(w) => w.write(buf), + } + } + fn flush(&mut self) -> io::Result<()> { + match self { + MaybeEncrypted::Unencrypted(w) => w.flush(), + MaybeEncrypted::Encrypted(w) => w.flush(), + } + } +} enum GenericZipWriter<W: Write + io::Seek> { Closed, - Storer(W), + Storer(MaybeEncrypted<W>), #[cfg(any( feature = "deflate", feature = "deflate-miniz", feature = "deflate-zlib" ))] - Deflater(DeflateEncoder<W>), + Deflater(DeflateEncoder<MaybeEncrypted<W>>), #[cfg(feature = "bzip2")] - Bzip2(BzEncoder<W>), + Bzip2(BzEncoder<MaybeEncrypted<W>>), #[cfg(feature = "zstd")] - Zstd(ZstdEncoder<'static, W>), + Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>), } // Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely pub(crate) mod zip_writer { @@ -108,6 +126,7 @@ pub struct FileOptions { last_modified_time: DateTime, permissions: Option<u32>, large_file: bool, + encrypt_with: Option<crate::zipcrypto::ZipCryptoKeys>, } impl FileOptions { @@ -171,6 +190,10 @@ impl FileOptions { self.large_file = large; self } + pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions { + self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password)); + self + } } impl Default for FileOptions { @@ -196,6 +219,7 @@ impl Default for FileOptions { last_modified_time: DateTime::default(), permissions: None, large_file: false, + encrypt_with: None, } } } @@ -284,7 +308,7 @@ impl<A: Read + Write + io::Seek> ZipWriter<A> { let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it Ok(ZipWriter { - inner: GenericZipWriter::Storer(readwriter), + inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), files, stats: Default::default(), writing_to_file: false, @@ -302,7 +326,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. pub fn new(inner: W) -> ZipWriter<W> { ZipWriter { - inner: GenericZipWriter::Storer(inner), + inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)), files: Vec::new(), stats: Default::default(), writing_to_file: false, @@ -355,7 +379,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { let mut file = ZipFileData { system: System::Unix, version_made_by: DEFAULT_VERSION, - encrypted: false, + encrypted: options.encrypt_with.is_some(), using_data_descriptor: false, compression_method: options.compression_method, compression_level: options.compression_level, @@ -385,7 +409,13 @@ impl<W: Write + io::Seek> ZipWriter<W> { self.files.push(file); } + if let Some(keys) = options.encrypt_with { + let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(), buffer: vec![], keys }; + let mut crypto_header = [0u8; 12]; + zipwriter.write_all(&crypto_header)?; + self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter)); + } Ok(()) } @@ -395,6 +425,14 @@ impl<W: Write + io::Seek> ZipWriter<W> { self.end_extra_data()?; } self.inner.switch_to(CompressionMethod::Stored, None)?; + match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) { + GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => { + let crc32 = self.stats.hasher.clone().finalize(); + self.inner = GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?)) + } + GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w), + _ => unreachable!() + } let writer = self.inner.get_plain(); if !self.writing_raw { @@ -699,7 +737,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { /// Add a directory entry. /// - /// You can't write data to the file afterwards. + /// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file. pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> where S: Into<String>, @@ -985,8 +1023,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> { fn get_plain(&mut self) -> &mut W { match *self { - GenericZipWriter::Storer(ref mut w) => w, - _ => panic!("Should have switched to stored beforehand"), + GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w, + _ => panic!("Should have switched to stored and unencrypted beforehand"), } } @@ -1009,8 +1047,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> { fn unwrap(self) -> W { match self { - GenericZipWriter::Storer(w) => w, - _ => panic!("Should have switched to stored beforehand"), + GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w, + _ => panic!("Should have switched to stored and unencrypted beforehand"), } } } @@ -1058,7 +1096,7 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR 1u16 << 11 } else { 0 - }; + } | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16::<LittleEndian>(flag)?; // Compression method #[allow(deprecated)] @@ -1133,7 +1171,7 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) 1u16 << 11 } else { 0 - }; + } | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16::<LittleEndian>(flag)?; // compression method #[allow(deprecated)] @@ -1428,6 +1466,7 @@ mod test { last_modified_time: DateTime::default(), permissions: Some(33188), large_file: false, + encrypt_with: None, }; writer.start_file("mimetype", options).unwrap(); writer diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs index 91d4039..c3696e4 100644 --- a/src/zipcrypto.rs +++ b/src/zipcrypto.rs @@ -6,7 +6,8 @@ use std::num::Wrapping; /// A container to hold the current key state -struct ZipCryptoKeys { +#[derive(Clone, Copy)] +pub(crate) struct ZipCryptoKeys { key_0: Wrapping<u32>, key_1: Wrapping<u32>, key_2: Wrapping<u32>, @@ -49,6 +50,13 @@ impl ZipCryptoKeys { fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> { (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize]) } + pub(crate) fn derive(password: &[u8]) -> ZipCryptoKeys { + let mut keys = ZipCryptoKeys::new(); + for byte in password.iter() { + keys.update(*byte); + } + keys + } } /// A ZipCrypto reader with unverified password @@ -70,17 +78,10 @@ impl<R: std::io::Read> ZipCryptoReader<R> { /// would be impossible to decrypt files that were encrypted with a /// password byte sequence that is unrepresentable in UTF-8. pub fn new(file: R, password: &[u8]) -> ZipCryptoReader<R> { - let mut result = ZipCryptoReader { + ZipCryptoReader { file, - keys: ZipCryptoKeys::new(), - }; - - // Key the cipher by updating the keys with the password. - for byte in password.iter() { - result.keys.update(*byte); + keys: ZipCryptoKeys::derive(password), } - - result } /// Read the ZipCrypto header bytes and validate the password. @@ -122,6 +123,31 @@ impl<R: std::io::Read> ZipCryptoReader<R> { Ok(Some(ZipCryptoReaderValid { reader: self })) } } +pub(crate) struct ZipCryptoWriter<W> { + pub(crate) writer: W, + pub(crate) buffer: Vec<u8>, + pub(crate) keys: ZipCryptoKeys, +} +impl<W: std::io::Write> ZipCryptoWriter<W> { + pub(crate) fn finish(mut self, crc32: u32) -> std::io::Result<W> { + self.buffer[11] = (crc32 >> 24) as u8; + for byte in self.buffer.iter_mut() { + *byte = self.keys.encrypt_byte(*byte); + } + self.writer.write_all(&self.buffer)?; + self.writer.flush()?; + Ok(self.writer) + } +} +impl<W: std::io::Write> std::io::Write for ZipCryptoWriter<W> { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + self.buffer.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} /// A ZipCrypto reader with verified password pub struct ZipCryptoReaderValid<R> { diff --git a/tests/issue_234.rs b/tests/issue_234.rs index bd01d1d..f8c1d2c 100644 --- a/tests/issue_234.rs +++ b/tests/issue_234.rs @@ -26,6 +26,6 @@ fn invalid_header() { let archive = zip::ZipArchive::new(reader); match archive { Err(ZipError::InvalidArchive(_)) => {} - value => panic!("Unexpected value: {:?}", value), + value => panic!("Unexpected value: {value:?}"), } } diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs index 6c4d6b8..d831c1e 100644 --- a/tests/zip_crypto.rs +++ b/tests/zip_crypto.rs @@ -21,6 +21,23 @@ use std::io::Cursor; use std::io::Read; #[test] +fn encrypting_file() { + use zip::unstable::write::FileOptionsExt; + use std::io::{Read, Write}; + let mut buf = vec![0; 2048]; + let mut archive = zip::write::ZipWriter::new(std::io::Cursor::new(&mut buf)); + archive.start_file("name", zip::write::FileOptions::default().with_deprecated_encryption(b"password")).unwrap(); + archive.write_all(b"test").unwrap(); + archive.finish().unwrap(); + drop(archive); + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&mut buf)).unwrap(); + let mut file = archive.by_index_decrypt(0, b"password").unwrap().unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + assert_eq!(buf, b"test"); + +} +#[test] fn encrypted_file() { let zip_file_bytes = &mut Cursor::new(vec![ 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x54, 0xbd, 0xb5, 0x50, 0x2f, |