From a83767f9fbd51df654901b52bdba7838f6a10bf9 Mon Sep 17 00:00:00 2001 From: Igor Tolmachev Date: Tue, 16 Jul 2024 01:59:53 +0900 Subject: Add traditional PKWARE decryption. - Compression and encryption may not work together - Password check is not yet implemented - Unoptimized crc32 function --- src/zip/driver.rs | 11 +++- src/zip/encryption.rs | 62 ++++++++++++++++++++ src/zip/error.rs | 20 +++++-- src/zip/file/info.rs | 47 ++++++++++----- src/zip/file/mod.rs | 2 +- src/zip/file/read.rs | 159 ++++++++++++++++++++++++++++++++++---------------- src/zip/mod.rs | 5 +- 7 files changed, 234 insertions(+), 72 deletions(-) create mode 100644 src/zip/encryption.rs (limited to 'src/zip') diff --git a/src/zip/driver.rs b/src/zip/driver.rs index 0905d9a..87f9c1a 100644 --- a/src/zip/driver.rs +++ b/src/zip/driver.rs @@ -3,7 +3,8 @@ use crate::utils::ReadUtils; use crate::zip::cp437::FromCp437; use crate::zip::structs::{deserialize, Cdr, Eocdr, Eocdr64, Eocdr64Locator, ExtraHeader}; use crate::zip::{ - BitFlag, CompressionMethod, ZipError, ZipFileInfo, ZipFileReader, ZipFileWriter, ZipResult, + BitFlag, CompressionMethod, EncryptionMethod, ZipError, ZipFileInfo, ZipFileReader, + ZipFileWriter, ZipResult, }; use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime}; use std::collections::HashMap as Map; @@ -220,6 +221,7 @@ impl ArchiveRead for Zip { indexes.insert(name.clone(), i); files.push(ZipFileInfo::new( CompressionMethod::from_struct_id(cdr.compression_method)?, + EncryptionMethod::from_bit_flag(bit_flag), bit_flag, mtime, atime, @@ -256,10 +258,15 @@ impl ArchiveRead for Zip { self.files.get(index).ok_or(ZipError::FileNotFound.into()) } - fn get_file_reader<'d>(&'d mut self, index: usize) -> ZipResult> { + fn get_file_reader<'d>( + &'d mut self, + index: usize, + password: Option<&str>, + ) -> ZipResult> { Ok(ZipFileReader::new( &mut self.io, self.files.get(index).ok_or(ZipError::FileNotFound)?, + password, )?) } } diff --git a/src/zip/encryption.rs b/src/zip/encryption.rs new file mode 100644 index 0000000..84e30d5 --- /dev/null +++ b/src/zip/encryption.rs @@ -0,0 +1,62 @@ +use crate::utils::ReadUtils; +use crate::zip::ZipResult; +use crc32fast::Hasher; +use std::io::{Read, Result as IoResult}; + +fn crc32(byte: u8, crc32: u32) -> u32 { + let mut hasher = Hasher::new_with_initial(crc32 ^ 0xFFFFFFFF); + hasher.update(&[byte]); + hasher.finalize() ^ 0xFFFFFFFF +} + +pub struct WeakDecoder { + key0: u32, + key1: u32, + key2: u32, + io: Io, +} + +impl WeakDecoder { + pub fn new(io: Io, password: &str) -> ZipResult { + let mut decoder = Self { + key0: 305419896, + key1: 591751049, + key2: 878082192, + io, + }; + + for c in password.chars() { + decoder.update_keys(c as u8) + } + + let buf = decoder.read_arr::<12>()?; + + Ok(decoder) + } + + fn update_keys(&mut self, byte: u8) { + self.key0 = crc32(byte, self.key0); + self.key1 = self + .key1 + .wrapping_add(self.key0 & 0xFF) + .wrapping_mul(134775813) + .wrapping_add(1); + self.key2 = crc32((self.key1 >> 24) as u8, self.key2); + } + + fn decode_byte(&mut self, byte: u8) -> u8 { + let key = self.key2 | 2; + byte ^ (((key * (key ^ 1)) >> 8) as u8) + } +} + +impl Read for WeakDecoder { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + let bytes = self.io.read(buf)?; + for i in 0..bytes { + buf[i] = self.decode_byte(buf[i]); + self.update_keys(buf[i]) + } + Ok(bytes) + } +} diff --git a/src/zip/error.rs b/src/zip/error.rs index 3eb68b8..9ec0956 100644 --- a/src/zip/error.rs +++ b/src/zip/error.rs @@ -11,15 +11,18 @@ pub enum ZipError { InvalidFileHeaderSignature, InvalidCDRSignature, - InvalidCompressionMethod, - UnsupportedCompressionMethod, + InvalidCompressionMethod(u16), + UnsupportedCompressionMethod(u16), + UnsupportedEncryptionMethod, InvalidDate, InvalidTime, InvalidFileName, InvalidFileComment, FileNotFound, + PasswordIsNotSpecified, CompressedDataIsUnseekable, + EncryptedDataIsUnseekable, } impl From for ArchiveError { @@ -48,15 +51,24 @@ impl Display for ZipError { write!(f, "Invalid signature of central directory record") } - Self::InvalidCompressionMethod => writeln!(f, "Invalid compression method"), - Self::UnsupportedCompressionMethod => writeln!(f, "Unsupported compression method"), + Self::InvalidCompressionMethod(id) => { + writeln!(f, "Invalid compression method {}", id) + } + Self::UnsupportedCompressionMethod(id) => { + writeln!(f, "Unsupported compression method {}", id) + } + Self::UnsupportedEncryptionMethod => { + writeln!(f, "Unsupported encryption method") + } Self::InvalidDate => write!(f, "Invalid date"), Self::InvalidTime => write!(f, "Invalid time"), Self::InvalidFileName => write!(f, "Invalid file name"), Self::InvalidFileComment => write!(f, "Invalid file comment"), Self::FileNotFound => write!(f, "File not found"), + Self::PasswordIsNotSpecified => write!(f, "Password is not specified"), Self::CompressedDataIsUnseekable => write!(f, "Compressed data is unseekable"), + Self::EncryptedDataIsUnseekable => write!(f, "Encrypted data is unseekable"), } } } diff --git a/src/zip/file/info.rs b/src/zip/file/info.rs index 4e1b293..f5d4d8a 100644 --- a/src/zip/file/info.rs +++ b/src/zip/file/info.rs @@ -2,7 +2,7 @@ use crate::driver::ArchiveFileInfo; use crate::zip::{ZipError, ZipResult}; use chrono::{DateTime, Local}; -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CompressionMethod { Store, Deflate, @@ -10,31 +10,49 @@ pub enum CompressionMethod { Lzma, Zstd, Xz, - Unsupported, + Unsupported(u16), } impl CompressionMethod { pub(crate) fn from_struct_id(id: u16) -> ZipResult { - match id { - 0 => Ok(Self::Store), - 8 => Ok(Self::Deflate), - 12 => Ok(Self::BZip2), - 14 => Ok(Self::Lzma), - 93 => Ok(Self::Zstd), - 95 => Ok(Self::Xz), - 1..=7 | 9..=11 | 13 | 15..=20 | 94 | 96..=99 => Ok(Self::Unsupported), - 21..=92 | 100.. => Err(ZipError::InvalidCompressionMethod.into()), + Ok(match id { + 0 => Self::Store, + 8 => Self::Deflate, + 12 => Self::BZip2, + 14 => Self::Lzma, + 93 => Self::Zstd, + 95 => Self::Xz, + 1..=7 | 9..=11 | 13 | 15..=20 | 94 | 96..=99 => Self::Unsupported(id), + 21..=92 | 100.. => return Err(ZipError::InvalidCompressionMethod(id).into()), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum EncryptionMethod { + None, + Weak, + Unsupported, +} + +impl EncryptionMethod { + pub(crate) fn from_bit_flag(bit_flag: BitFlag) -> EncryptionMethod { + match (bit_flag.is_encrypted(), bit_flag.is_strong_encryption()) { + (false, false) => EncryptionMethod::None, + (true, false) => EncryptionMethod::Weak, + (true, true) => EncryptionMethod::Unsupported, + _ => panic!("impossible"), } } } -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct BitFlag { flag: u16, } pub mod bit { - #[derive(Debug, PartialEq, Eq)] + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum DeflateMode { Normal, Maximum, @@ -123,6 +141,7 @@ impl BitFlag { #[derive(Debug, Clone)] pub struct ZipFileInfo { pub compression_method: CompressionMethod, + pub encryption_method: EncryptionMethod, pub bit_flag: BitFlag, pub mtime: DateTime, pub atime: Option>, @@ -138,6 +157,7 @@ pub struct ZipFileInfo { impl ZipFileInfo { pub fn new( compression_method: CompressionMethod, + encryption_method: EncryptionMethod, bit_flag: BitFlag, mtime: DateTime, atime: Option>, @@ -151,6 +171,7 @@ impl ZipFileInfo { ) -> Self { Self { compression_method, + encryption_method, bit_flag, mtime, atime, diff --git a/src/zip/file/mod.rs b/src/zip/file/mod.rs index 43ccc04..ce3c21d 100644 --- a/src/zip/file/mod.rs +++ b/src/zip/file/mod.rs @@ -2,6 +2,6 @@ mod info; mod read; mod write; -pub use info::{bit, BitFlag, CompressionMethod, ZipFileInfo}; +pub use info::{bit, BitFlag, CompressionMethod, EncryptionMethod, ZipFileInfo}; pub use read::ZipFileReader; pub use write::ZipFileWriter; diff --git a/src/zip/file/read.rs b/src/zip/file/read.rs index f5a54f3..aa665c3 100644 --- a/src/zip/file/read.rs +++ b/src/zip/file/read.rs @@ -1,6 +1,7 @@ use crate::driver::FileDriver; use crate::utils::{IoCursor, ReadUtils}; -use crate::zip::{CompressionMethod, ZipError, ZipFileInfo, ZipResult}; +use crate::zip::encryption::WeakDecoder; +use crate::zip::{CompressionMethod, EncryptionMethod, ZipError, ZipFileInfo, ZipResult}; use bzip2::read::BzDecoder; use flate2::read::DeflateDecoder; use liblzma::read::XzDecoder; @@ -10,6 +11,47 @@ use std::io::{ }; use zstd::stream::Decoder as ZstdDecoder; +enum Encryption { + None(Io), + Weak(WeakDecoder), +} + +impl Encryption { + pub fn new(io: Io, method: EncryptionMethod, password: Option<&str>) -> ZipResult { + Ok(match method { + EncryptionMethod::None => Self::None(io), + EncryptionMethod::Weak => Self::Weak(WeakDecoder::new( + io, + password.ok_or(ZipError::PasswordIsNotSpecified)?, + )?), + EncryptionMethod::Unsupported => { + return Err(ZipError::UnsupportedEncryptionMethod.into()) + } + }) + } +} + +impl Read for Encryption { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + match self { + Self::None(io) => io.read(buf), + Self::Weak(io) => io.read(buf), + } + } +} + +impl Seek for Encryption { + fn seek(&mut self, pos: SeekFrom) -> IoResult { + match self { + Self::None(io) => io.seek(pos), + _ => Err(IoError::new( + IoErrorKind::Unsupported, + ZipError::EncryptedDataIsUnseekable, + )), + } + } +} + enum Compression { Store(Io), Deflate(DeflateDecoder), @@ -18,8 +60,48 @@ enum Compression { Xz(XzDecoder), } +impl Compression { + pub fn new(io: Io, method: CompressionMethod) -> ZipResult { + Ok(match method { + CompressionMethod::Store => Self::Store(io), + CompressionMethod::Deflate => Self::Deflate(DeflateDecoder::new(io)), + CompressionMethod::BZip2 => Self::BZip2(BzDecoder::new(io)), + CompressionMethod::Lzma => panic!(), + CompressionMethod::Zstd => Self::Zstd(ZstdDecoder::new(io)?), + CompressionMethod::Xz => Self::Xz(XzDecoder::new(io)), + CompressionMethod::Unsupported(id) => { + return Err(ZipError::UnsupportedCompressionMethod(id).into()) + } + }) + } +} + +impl Read for Compression { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + match self { + Compression::Store(io) => io.read(buf), + Compression::Deflate(io) => io.read(buf), + Compression::BZip2(io) => io.read(buf), + Compression::Zstd(io) => io.read(buf), + Compression::Xz(io) => io.read(buf), + } + } +} + +impl Seek for Compression { + fn seek(&mut self, pos: SeekFrom) -> IoResult { + match self { + Compression::Store(io) => io.seek(pos), + _ => Err(IoError::new( + IoErrorKind::Unsupported, + ZipError::CompressedDataIsUnseekable, + )), + } + } +} + pub struct ZipFileReader<'d, Io: Read> { - io: Compression>, + io: Compression>>, info: &'d ZipFileInfo, } @@ -33,7 +115,7 @@ impl<'d, Io: Read> FileDriver for ZipFileReader<'d, Io> { } impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { - pub fn new(io: &'d mut Io, info: &'d ZipFileInfo) -> ZipResult { + pub fn new(io: &'d mut Io, info: &'d ZipFileInfo, password: Option<&str>) -> ZipResult { io.seek(SeekFrom::Start(info.header_pointer))?; let buf = io.read_arr::<30>()?; @@ -45,27 +127,22 @@ impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { + u16::from_le_bytes(buf[26..28].try_into().unwrap()) as u64 + u16::from_le_bytes(buf[28..30].try_into().unwrap()) as u64; + io.seek(SeekFrom::Start(data_pointer))?; + Ok(Self { io: match info.compression_method { - CompressionMethod::Store => Compression::Store(IoCursor::new( - io, - data_pointer, - data_pointer + info.compressed_size, - )?), - CompressionMethod::Deflate => Compression::Deflate(DeflateDecoder::new( - IoCursor::new(io, data_pointer, data_pointer + info.compressed_size)?, - )), - CompressionMethod::BZip2 => Compression::BZip2(BzDecoder::new(IoCursor::new( - io, - data_pointer, - data_pointer + info.compressed_size, - )?)), CompressionMethod::Lzma => { - io.seek(SeekFrom::Start(data_pointer))?; let buf = io.read_arr::<9>()?; - Compression::Xz(XzDecoder::new_stream( - IoCursor::new(io, data_pointer + 9, data_pointer + info.compressed_size)?, + Encryption::new( + IoCursor::new( + io, + data_pointer + 9, + data_pointer + info.compressed_size, + )?, + info.encryption_method, + password, + )?, Stream::new_raw_decoder( Filters::new().lzma1( LzmaOptions::new() @@ -80,30 +157,22 @@ impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { .unwrap(), )) } - CompressionMethod::Zstd => Compression::Zstd( - ZstdDecoder::new(IoCursor::new( - io, - data_pointer, - data_pointer + info.compressed_size, - )?) - .unwrap(), - ), - CompressionMethod::Xz => Compression::Xz(XzDecoder::new(IoCursor::new( - io, - data_pointer, - data_pointer + info.compressed_size, - )?)), - CompressionMethod::Unsupported => { - return Err(ZipError::UnsupportedCompressionMethod.into()) - } + _ => Compression::new( + Encryption::new( + IoCursor::new(io, data_pointer, data_pointer + info.compressed_size)?, + info.encryption_method, + password, + )?, + info.compression_method, + )?, }, info, }) } - pub fn seekable(&self) -> bool { + pub fn is_seekable(&self) -> bool { match self.io { - Compression::Store(..) => true, + Compression::Store(Encryption::None(..)) => true, _ => false, } } @@ -111,24 +180,12 @@ impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { impl<'d, Io: Read> Read for ZipFileReader<'d, Io> { fn read(&mut self, buf: &mut [u8]) -> IoResult { - match &mut self.io { - Compression::Store(io) => io.read(buf), - Compression::Deflate(io) => io.read(buf), - Compression::BZip2(io) => io.read(buf), - Compression::Zstd(io) => io.read(buf), - Compression::Xz(io) => io.read(buf), - } + self.io.read(buf) } } impl<'d, Io: Read + Seek> Seek for ZipFileReader<'d, Io> { fn seek(&mut self, pos: SeekFrom) -> IoResult { - match &mut self.io { - Compression::Store(io) => io.seek(pos), - _ => Err(IoError::new( - IoErrorKind::Unsupported, - ZipError::CompressedDataIsUnseekable, - )), - } + self.io.seek(pos) } } diff --git a/src/zip/mod.rs b/src/zip/mod.rs index bcc34ed..fcc6161 100644 --- a/src/zip/mod.rs +++ b/src/zip/mod.rs @@ -1,13 +1,16 @@ mod archive; mod cp437; mod driver; +mod encryption; mod error; mod file; mod structs; pub use driver::Zip; pub use error::{ZipError, ZipResult}; -pub use file::{bit, BitFlag, CompressionMethod, ZipFileInfo, ZipFileReader, ZipFileWriter}; +pub use file::{ + bit, BitFlag, CompressionMethod, EncryptionMethod, ZipFileInfo, ZipFileReader, ZipFileWriter, +}; #[cfg(test)] mod tests; -- cgit v1.2.3