From 2eeb47ce56855859fd265eecb61d76305492d362 Mon Sep 17 00:00:00 2001 From: Marli Frost Date: Sat, 6 May 2023 15:48:52 +0100 Subject: [PATCH] add support for writing files with PKWARE encryption --- src/unstable.rs | 16 +++++++++++ src/write.rs | 65 ++++++++++++++++++++++++++++++++++++--------- src/zipcrypto.rs | 46 +++++++++++++++++++++++++------- tests/zip_crypto.rs | 17 ++++++++++++ 4 files changed, 121 insertions(+), 23 deletions(-) diff --git a/src/unstable.rs b/src/unstable.rs index 2cbfa5bb..f8b46a97 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -2,3 +2,19 @@ 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 3f41c4d6..4cdc031b 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 { + Unencrypted(W), + Encrypted(crate::zipcrypto::ZipCryptoWriter), +} +impl Write for MaybeEncrypted { + fn write(&mut self, buf: &[u8]) -> io::Result { + 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 { Closed, - Storer(W), + Storer(MaybeEncrypted), #[cfg(any( feature = "deflate", feature = "deflate-miniz", feature = "deflate-zlib" ))] - Deflater(DeflateEncoder), + Deflater(DeflateEncoder>), #[cfg(feature = "bzip2")] - Bzip2(BzEncoder), + Bzip2(BzEncoder>), #[cfg(feature = "zstd")] - Zstd(ZstdEncoder<'static, W>), + Zstd(ZstdEncoder<'static, MaybeEncrypted>), } // 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, large_file: bool, + encrypt_with: Option, } 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 ZipWriter { 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 ZipWriter { /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. pub fn new(inner: W) -> ZipWriter { 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 ZipWriter { 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 ZipWriter { 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 ZipWriter { 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 { @@ -985,8 +1023,8 @@ impl GenericZipWriter { 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 GenericZipWriter { 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(writer: &mut T, file: &ZipFileData) -> ZipR 1u16 << 11 } else { 0 - }; + } | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16::(flag)?; // Compression method #[allow(deprecated)] @@ -1133,7 +1171,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) 1u16 << 11 } else { 0 - }; + } | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16::(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 91d40395..c3696e4d 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, key_1: Wrapping, key_2: Wrapping, @@ -49,6 +50,13 @@ impl ZipCryptoKeys { fn crc32(crc: Wrapping, input: u8) -> Wrapping { (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 ZipCryptoReader { /// 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 { - 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 ZipCryptoReader { Ok(Some(ZipCryptoReaderValid { reader: self })) } } +pub(crate) struct ZipCryptoWriter { + pub(crate) writer: W, + pub(crate) buffer: Vec, + pub(crate) keys: ZipCryptoKeys, +} +impl ZipCryptoWriter { + pub(crate) fn finish(mut self, crc32: u32) -> std::io::Result { + 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 std::io::Write for ZipCryptoWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + 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 { diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs index 6c4d6b81..d831c1e6 100644 --- a/tests/zip_crypto.rs +++ b/tests/zip_crypto.rs @@ -20,6 +20,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![