From 7bcdc3b4ca460aec2b98fb2dca6165788c562b05 Mon Sep 17 00:00:00 2001 From: Igor Tolmachev Date: Sat, 20 Jul 2024 16:52:39 +0900 Subject: Partial aes implementation and others improvements --- src/zip/datetime.rs | 45 ++++++++++++ src/zip/driver.rs | 179 ++++++++++++++++++++++----------------------- src/zip/encryption.rs | 89 ---------------------- src/zip/encryption/aes.rs | 46 ++++++++++++ src/zip/encryption/mod.rs | 5 ++ src/zip/encryption/weak.rs | 105 ++++++++++++++++++++++++++ src/zip/error.rs | 38 +++++++--- src/zip/file/info.rs | 43 +++++------ src/zip/file/read.rs | 99 ++++++++++++++++++++++--- src/zip/mod.rs | 1 + src/zip/structs.rs | 14 ++++ src/zip/tests.rs | 19 +++++ 12 files changed, 457 insertions(+), 226 deletions(-) create mode 100644 src/zip/datetime.rs delete mode 100644 src/zip/encryption.rs create mode 100644 src/zip/encryption/aes.rs create mode 100644 src/zip/encryption/mod.rs create mode 100644 src/zip/encryption/weak.rs (limited to 'src/zip') diff --git a/src/zip/datetime.rs b/src/zip/datetime.rs new file mode 100644 index 0000000..a4b1b55 --- /dev/null +++ b/src/zip/datetime.rs @@ -0,0 +1,45 @@ +use crate::zip::{ZipError, ZipResult}; +use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Timelike}; + +pub trait DosDateTime: Sized { + fn from_dos_date_time(date: u16, time: u16, tz: Tz) -> ZipResult; + + #[allow(dead_code)] + fn to_dos_date(&self) -> u16; + + fn to_dos_time(&self) -> u16; +} + +impl DosDateTime for DateTime { + #[inline] + fn from_dos_date_time(date: u16, time: u16, tz: Tz) -> ZipResult { + Ok(NaiveDate::from_ymd_opt( + (date as i32 >> 9 & 0x7F) + 1980, + date as u32 >> 5 & 0xF, + date as u32 & 0x1F, + ) + .ok_or(ZipError::InvalidDate)? + .and_hms_opt( + (time as u32 >> 11) & 0x1F, + (time as u32 >> 5) & 0x3F, + (time as u32 & 0x1F) * 2, + ) + .ok_or(ZipError::InvalidTime)? + .and_local_timezone(tz) + .unwrap()) + } + + #[inline] + fn to_dos_date(&self) -> u16 { + (self.year() as u16 - 1980 & 0x7F) << 9 + | (self.month() as u16 & 0xF) << 5 + | self.day() as u16 & 0x1F + } + + #[inline] + fn to_dos_time(&self) -> u16 { + (self.hour() as u16 & 0x1F) << 11 + | (self.minute() as u16 & 0x3F) << 5 + | self.second() as u16 / 2 & 0x1F + } +} diff --git a/src/zip/driver.rs b/src/zip/driver.rs index e7878cf..8dc902f 100644 --- a/src/zip/driver.rs +++ b/src/zip/driver.rs @@ -1,48 +1,51 @@ use crate::driver::{ArchiveRead, ArchiveWrite, Driver}; use crate::utils::ReadUtils; use crate::zip::cp437::FromCp437; -use crate::zip::structs::{deserialize, Cdr, Eocdr, Eocdr64, Eocdr64Locator, ExtraHeader}; +use crate::zip::datetime::DosDateTime; +use crate::zip::structs::{ + deserialize, AesField, Cdr, Eocdr, Eocdr64, Eocdr64Locator, ExtraHeader, CDR_SIGNATURE, + EOCDR64_LOCATOR_SIGNATURE, EOCDR64_SIGNATURE, EOCDR_SIGNATURE, +}; use crate::zip::{ BitFlag, CompressionMethod, EncryptionMethod, ZipError, ZipFileInfo, ZipFileReader, ZipFileWriter, ZipResult, }; -use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime}; +use chrono::{DateTime, Local}; use std::collections::HashMap as Map; use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom, Write}; -fn dos_to_local(date: u16, time: u16) -> ZipResult> { - Ok(NaiveDateTime::new( - NaiveDate::from_ymd_opt( - (date as i32 >> 9 & 0x7F) + 1980, - date as u32 >> 5 & 0xF, - date as u32 & 0x1F, - ) - .ok_or(ZipError::InvalidDate)?, - NaiveTime::from_hms_opt( - (time as u32 >> 11) & 0x1F, - (time as u32 >> 5) & 0x3F, - (time as u32 & 0x1F) * 2, - ) - .ok_or(ZipError::InvalidTime)?, - ) - .and_local_timezone(Local) - .unwrap()) +#[inline] +fn split_fields(bytes: &[u8]) -> Option> { + let mut fields = Vec::new(); + + let mut p = 0; + while p < bytes.len() { + let header: ExtraHeader = deserialize(bytes.get(p..p + 4)?).unwrap(); + p += 4; + let data = bytes.get(p..p + header.size as usize)?; + p += header.size as usize; + + fields.push((header.id, data)); + } + + Some(fields) } -fn ntfs_to_local(time: u64) -> ZipResult> { - Ok(DateTime::from_timestamp( - (time / 10000000 - 11644473600) as i64, - (time % 10000000) as u32, +#[inline] +fn ntfs_to_local(time: u64) -> Option> { + Some( + DateTime::from_timestamp( + (time / 10000000 - 11644473600) as i64, + (time % 10000000) as u32, + )? + .with_timezone(&Local), ) - .ok_or(ZipError::InvalidTime)? - .with_timezone(&Local)) } -fn timestamp_to_local(time: i32) -> ZipResult> { - Ok(DateTime::from_timestamp(time as i64, 0) - .ok_or(ZipError::InvalidTime)? - .with_timezone(&Local)) +#[inline] +fn timestamp_to_local(time: i32) -> Option> { + Some(DateTime::from_timestamp(time as i64, 0)?.with_timezone(&Local)) } pub struct Zip { @@ -74,7 +77,7 @@ impl ArchiveRead for Zip { .ok_or(ZipError::EocdrNotFound)?, )? .windows(4) - .rposition(|v| u32::from_le_bytes(v.try_into().unwrap()) == 0x06054b50) + .rposition(|v| v == EOCDR_SIGNATURE) .ok_or(ZipError::EocdrNotFound)? as u64; // Read eocdr @@ -88,13 +91,13 @@ impl ArchiveRead for Zip { let buf = io.read_arr::<20>()?; let (cd_pointer, cd_size, cd_records) = // If locator found then read eocdr64 - if u32::from_le_bytes(buf[0..4].try_into().unwrap()) == 0x07064b50 { + if buf[0..4] == EOCDR64_LOCATOR_SIGNATURE { let eocdr64locator: Eocdr64Locator = deserialize(&buf[4..]).unwrap(); io.seek(SeekFrom::Start(eocdr64locator.eocdr64_pointer))?; let buf = io.read_arr::<56>()?; - if u32::from_le_bytes(buf[0..4].try_into().unwrap()) != 0x06064b50 { - return Err(ZipError::InvalidEOCDR64Signature.into()); + if buf[0..4] != EOCDR64_SIGNATURE { + return Err(ZipError::InvalidEOCDR64Signature); } let eocdr64: Eocdr64 = deserialize(&buf[4..]).unwrap(); @@ -116,8 +119,8 @@ impl ArchiveRead for Zip { for i in 0..cd_records as usize { let buf = buf_reader.read_arr::<46>()?; - if u32::from_le_bytes(buf[..4].try_into().unwrap()) != 0x02014b50 { - return Err(ZipError::InvalidCDRSignature.into()); + if buf[..4] != CDR_SIGNATURE { + return Err(ZipError::InvalidCDRSignature); } let cdr: Cdr = deserialize(&buf[4..46]).unwrap(); let bit_flag = BitFlag::new(cdr.bit_flag); @@ -138,91 +141,87 @@ impl ArchiveRead for Zip { String::from_cp437(comment) }; + let mut compression_method = cdr.compression_method; + let mut encryption_method = if !bit_flag.is_encrypted() { + EncryptionMethod::None + } else if !bit_flag.is_strong_encryption() { + EncryptionMethod::Weak + } else { + EncryptionMethod::Unsupported + }; + let mut compressed_size = cdr.compressed_size as u64; let mut size = cdr.size as u64; let mut header_pointer = cdr.header_pointer as u64; - let mut mtime = dos_to_local(cdr.dos_date, cdr.dos_time)?; + let mut mtime = DateTime::from_dos_date_time(cdr.dos_date, cdr.dos_time, Local)?; let mut atime = None; let mut ctime = None; // Parse extensible data fields - let mut ep: usize = 0; - while ep < cdr.extra_field_len as usize { - let header: ExtraHeader = deserialize(&extra_fields[ep..ep + 4]).unwrap(); - ep += 4; - - match header.id { + for (id, mut data) in split_fields(&extra_fields).ok_or(ZipError::InvalidExtraFields)? { + match id { // Zip64 0x0001 => { if size == 0xFFFFFFFF { - compressed_size = - u64::from_le_bytes(extra_fields[ep..ep + 8].try_into().unwrap()); - ep += 8; + size = u64::from_le_bytes(data.read_arr()?); } if compressed_size == 0xFFFFFFFF { - size = u64::from_le_bytes(extra_fields[ep..ep + 8].try_into().unwrap()); - ep += 8; + compressed_size = u64::from_le_bytes(data.read_arr()?); } if header_pointer == 0xFFFFFFFF { - header_pointer = - u64::from_le_bytes(extra_fields[ep..ep + 8].try_into().unwrap()); - ep += 8; - } - if cdr.disk == 0xFFFF { - ep += 4 + header_pointer = u64::from_le_bytes(data.read_arr()?); } } // NTFS 0x000a => { - let mut tp = ep + 4; - ep += header.size as usize; - - while tp < ep { - let header: ExtraHeader = - deserialize(&extra_fields[tp..tp + 4]).unwrap(); - tp += 4; - - match header.id { + for (id, mut data) in + split_fields(&data).ok_or(ZipError::InvalidExtraFields)? + { + match id { 0x0001 => { - mtime = ntfs_to_local(u64::from_le_bytes( - extra_fields[tp..tp + 8].try_into().unwrap(), - ))?; - tp += 8; - atime = Some(ntfs_to_local(u64::from_le_bytes( - extra_fields[tp..tp + 8].try_into().unwrap(), - ))?); - tp += 8; - ctime = Some(ntfs_to_local(u64::from_le_bytes( - extra_fields[tp..tp + 8].try_into().unwrap(), - ))?); - tp += 8; - } - _ => { - tp += header.size as usize; + mtime = ntfs_to_local(u64::from_le_bytes(data.read_arr()?)) + .unwrap_or(mtime); + atime = ntfs_to_local(u64::from_le_bytes(data.read_arr()?)); + ctime = ntfs_to_local(u64::from_le_bytes(data.read_arr()?)); } + _ => {} } } } // Unix 0x000d => { - atime = Some(timestamp_to_local(i32::from_le_bytes( - extra_fields[ep..ep + 4].try_into().unwrap(), - ))?); - mtime = timestamp_to_local(i32::from_le_bytes( - extra_fields[ep + 4..ep + 8].try_into().unwrap(), - ))?; - ep += header.size as usize + atime = timestamp_to_local(i32::from_le_bytes(data.read_arr()?)); + mtime = timestamp_to_local(i32::from_le_bytes(data.read_arr()?)) + .unwrap_or(mtime); + } + // AES + 0x9901 => { + let aes: AesField = deserialize(&data.read_arr::<7>()?).unwrap(); + if aes.id != 0x4541 { + return Err(ZipError::InvalidExtraFields); + } + encryption_method = match aes.strength { + 0x01 => EncryptionMethod::Aes128, + 0x02 => EncryptionMethod::Aes192, + 0x03 => EncryptionMethod::Aes256, + _ => EncryptionMethod::Unsupported, + }; + compression_method = aes.compression_method; } // Skip unrecognized header - _ => ep += header.size as usize, + _ => {} } } + if compression_method == 99 { + return Err(ZipError::AesExtraFieldNotFound); + } + indexes.insert(name.clone(), i); files.push(ZipFileInfo::new( - CompressionMethod::from_struct_id(cdr.compression_method)?, - EncryptionMethod::from_bif_flag(bit_flag, cdr.crc, cdr.dos_time), + CompressionMethod::from_struct_id(compression_method), + encryption_method, bit_flag, mtime, atime, @@ -248,15 +247,15 @@ impl ArchiveRead for Zip { &self.files } - fn get_file_index(&self, name: &str) -> crate::ArchiveResult { + fn get_file_index(&self, name: &str) -> ZipResult { self.indexes .get(name) - .ok_or(ZipError::FileNotFound.into()) + .ok_or(ZipError::FileNotFound) .copied() } fn get_file_info(&self, index: usize) -> ZipResult<&Self::FileInfo> { - self.files.get(index).ok_or(ZipError::FileNotFound.into()) + self.files.get(index).ok_or(ZipError::FileNotFound) } #[inline] diff --git a/src/zip/encryption.rs b/src/zip/encryption.rs deleted file mode 100644 index f317245..0000000 --- a/src/zip/encryption.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::utils::ReadUtils; -use crate::zip::{ZipError, ZipResult}; -use std::io::{Read, Result as IoResult}; - -const TABLE: [u32; 256] = generate_table(); - -const fn generate_table() -> [u32; 256] { - let mut table = [0; 256]; - - let mut i = 0; - while i <= 255 { - let mut t = i as u32; - - let mut j = 0; - while j < 8 { - if (t & 1) > 0 { - t = (t >> 1) ^ 0xEDB88320 - } else { - t >>= 1 - } - j += 1; - } - - table[i] = t; - i += 1 - } - - table -} - -fn crc32(byte: u8, crc: u32) -> u32 { - (crc >> 8) ^ TABLE[((crc & 0xFF) as u8 ^ byte) as usize] -} - -pub struct WeakDecoder { - key0: u32, - key1: u32, - key2: u32, - io: Io, -} - -impl WeakDecoder { - pub fn new(io: Io, check: u8, password: &[u8]) -> ZipResult { - let mut decoder = Self { - key0: 305419896, - key1: 591751049, - key2: 878082192, - io, - }; - - for c in password { - decoder.update_keys(*c) - } - - let buf = decoder.read_arr::<12>()?; - if check != buf[11] { - return Err(ZipError::IncorrectPassword.into()); - } - - 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; - let byte = byte ^ ((key.wrapping_mul(key ^ 1)) >> 8) as u8; - self.update_keys(byte); - byte - } -} - -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]); - } - Ok(bytes) - } -} diff --git a/src/zip/encryption/aes.rs b/src/zip/encryption/aes.rs new file mode 100644 index 0000000..6f41aaa --- /dev/null +++ b/src/zip/encryption/aes.rs @@ -0,0 +1,46 @@ +use crate::utils::ReadUtils; +use aes::cipher::generic_array::GenericArray; +use aes::cipher::BlockEncrypt; +use std::io::{Read, Result as IoResult}; + +#[allow(dead_code)] +pub struct AesDecoder { + io: Io, + aes: Aes, + + counter: u128, + block: [u8; 16], + cursor: usize, +} + +impl AesDecoder { + pub fn new(mut io: Io, aes: Aes) -> IoResult { + let block = io.read_arr::<16>()?; + let mut decoder = Self { + io, + aes, + counter: 1, + block, + cursor: 0, + }; + decoder.decrypt_block(); + Ok(decoder) + } + + #[inline] + fn decrypt_block(&mut self) { + let mut mask = self.counter.to_le_bytes(); + self.aes + .encrypt_block(GenericArray::from_mut_slice(&mut mask)); + for (b, m) in self.block.iter_mut().zip(mask) { + *b ^= m + } + self.counter += 1; + } +} + +impl Read for AesDecoder { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + todo!() + } +} diff --git a/src/zip/encryption/mod.rs b/src/zip/encryption/mod.rs new file mode 100644 index 0000000..8cacaf9 --- /dev/null +++ b/src/zip/encryption/mod.rs @@ -0,0 +1,5 @@ +mod aes; +mod weak; + +pub use aes::AesDecoder; +pub use weak::{Keys, WeakDecoder}; diff --git a/src/zip/encryption/weak.rs b/src/zip/encryption/weak.rs new file mode 100644 index 0000000..144cd53 --- /dev/null +++ b/src/zip/encryption/weak.rs @@ -0,0 +1,105 @@ +use std::io::{Read, Result as IoResult}; + +const TABLE: [u32; 256] = generate_table(); + +const fn generate_table() -> [u32; 256] { + let mut table = [0; 256]; + + let mut i = 0; + while i <= 255 { + let mut t = i as u32; + + let mut j = 0; + while j < 8 { + if (t & 1) > 0 { + t = (t >> 1) ^ 0xEDB88320 + } else { + t >>= 1 + } + j += 1; + } + + table[i] = t; + i += 1 + } + + table +} + +fn crc32(byte: u8, crc: u32) -> u32 { + (crc >> 8) ^ TABLE[((crc & 0xFF) as u8 ^ byte) as usize] +} + +pub struct Keys { + key0: u32, + key1: u32, + key2: u32, +} + +impl Keys { + pub fn new() -> Self { + Self { + key0: 305419896, + key1: 591751049, + key2: 878082192, + } + } + + fn update(&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); + } + + pub fn set_password(&mut self, passwd: &[u8]) { + for b in passwd { + self.update(*b) + } + } + + pub fn set_header(&mut self, header: [u8; 12]) -> u8 { + for b in &header[..11] { + self.decode_byte(*b); + } + self.decode_byte(header[11]) + } + + #[allow(dead_code)] + pub fn encode_bytes(&mut self, byte: u8) -> u8 { + let key = self.key2 | 2; + self.update(byte); + byte ^ ((key.wrapping_mul(key ^ 1)) >> 8) as u8 + } + + pub fn decode_byte(&mut self, byte: u8) -> u8 { + let key = self.key2 | 2; + let byte = byte ^ ((key.wrapping_mul(key ^ 1)) >> 8) as u8; + self.update(byte); + byte + } +} + +pub struct WeakDecoder { + io: Io, + keys: Keys, +} + +impl WeakDecoder { + pub fn new(io: Io, keys: Keys) -> Self { + WeakDecoder { io, keys } + } +} + +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.keys.decode_byte(buf[i]); + } + Ok(bytes) + } +} diff --git a/src/zip/error.rs b/src/zip/error.rs index 5573cf8..5508177 100644 --- a/src/zip/error.rs +++ b/src/zip/error.rs @@ -1,22 +1,25 @@ -use crate::{ArchiveError, ArchiveResult}; use std::error::Error; use std::fmt::Display; +use std::io::Error as IoError; -pub type ZipResult = ArchiveResult; +pub type ZipResult = Result; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub enum ZipError { + Io(IoError), + EocdrNotFound, InvalidEOCDR64Signature, InvalidFileHeaderSignature, InvalidCDRSignature, - InvalidCompressionMethod(u16), UnsupportedCompressionMethod(u16), UnsupportedEncryptionMethod, InvalidDate, InvalidTime, InvalidFileName, + InvalidExtraFields, + AesExtraFieldNotFound, InvalidFileComment, FileNotFound, @@ -26,18 +29,30 @@ pub enum ZipError { EncryptedDataIsUnseekable, } -impl From for ArchiveError { - fn from(value: ZipError) -> Self { - Self::Archivator { - module: "Zip", - error: value, +impl From for ZipError { + fn from(value: IoError) -> Self { + Self::Io(value) + } +} + +impl PartialEq for ZipError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Io(l0), Self::Io(r0)) => l0.kind() == r0.kind(), + (Self::UnsupportedCompressionMethod(l0), Self::UnsupportedCompressionMethod(r0)) => { + l0 == r0 + } + _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } } +impl Eq for ZipError {} + impl Display for ZipError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Io(error) => write!(f, "{}", error), Self::EocdrNotFound => write!(f, "End of central directory record not found"), Self::InvalidEOCDR64Signature => { write!( @@ -52,9 +67,6 @@ impl Display for ZipError { write!(f, "Invalid signature of central directory record") } - Self::InvalidCompressionMethod(id) => { - writeln!(f, "Invalid compression method {}", id) - } Self::UnsupportedCompressionMethod(id) => { writeln!(f, "Unsupported compression method {}", id) } @@ -64,6 +76,8 @@ impl Display for ZipError { Self::InvalidDate => write!(f, "Invalid date"), Self::InvalidTime => write!(f, "Invalid time"), Self::InvalidFileName => write!(f, "Invalid file name"), + Self::InvalidExtraFields => write!(f, "Invalid extra fields"), + Self::AesExtraFieldNotFound => write!(f, "Aes extra field not found"), Self::InvalidFileComment => write!(f, "Invalid file comment"), Self::FileNotFound => write!(f, "File not found"), diff --git a/src/zip/file/info.rs b/src/zip/file/info.rs index 599dcc3..38ea984 100644 --- a/src/zip/file/info.rs +++ b/src/zip/file/info.rs @@ -1,5 +1,5 @@ use crate::driver::ArchiveFileInfo; -use crate::zip::{ZipError, ZipResult}; +use crate::zip::datetime::DosDateTime; use chrono::{DateTime, Local}; #[derive(Debug, PartialEq, Eq, Clone)] @@ -14,42 +14,30 @@ pub enum CompressionMethod { } impl CompressionMethod { - pub(crate) fn from_struct_id(id: u16) -> ZipResult { - Ok(match id { + #[inline] + pub(crate) fn from_struct_id(id: u16) -> Self { + 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()), - }) + _ => Self::Unsupported(id), + } } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum EncryptionMethod { None, - Weak(u8), + Weak, // ZipCrypto + Aes128, // WinZip encryption + Aes192, + Aes256, Unsupported, } -impl EncryptionMethod { - pub(crate) fn from_bif_flag(bit_flag: BitFlag, crc: u32, dos_time: u16) -> EncryptionMethod { - match (bit_flag.is_encrypted(), bit_flag.is_strong_encryption()) { - (false, false) => EncryptionMethod::None, - (true, false) => EncryptionMethod::Weak(if bit_flag.is_has_data_descriptor() { - (dos_time >> 8) as u8 // Info-ZIP modification - } else { - (crc >> 24) as u8 - }), - (true, true) => EncryptionMethod::Unsupported, - _ => panic!("impossible"), - } - } -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct BitFlag { flag: u16, @@ -159,7 +147,7 @@ pub struct ZipFileInfo { } impl ZipFileInfo { - pub fn new( + pub(crate) fn new( compression_method: CompressionMethod, encryption_method: EncryptionMethod, bit_flag: BitFlag, @@ -189,6 +177,15 @@ impl ZipFileInfo { } } + #[inline] + pub(crate) fn password_check(&self) -> u8 { + if self.bit_flag.is_has_data_descriptor() { + (self.mtime.to_dos_time() >> 8) as u8 + } else { + (self.crc >> 24) as u8 + } + } + pub fn is_dir(&self) -> bool { self.name.ends_with("/") } diff --git a/src/zip/file/read.rs b/src/zip/file/read.rs index c26b304..80bdcfb 100644 --- a/src/zip/file/read.rs +++ b/src/zip/file/read.rs @@ -1,11 +1,16 @@ use crate::driver::FileDriver; use crate::utils::{IoCursor, ReadUtils}; -use crate::zip::encryption::WeakDecoder; +use crate::zip::encryption::{AesDecoder, Keys, WeakDecoder}; +use crate::zip::structs::FILE_HEADER_SIGNATURE; use crate::zip::{CompressionMethod, EncryptionMethod, ZipError, ZipFileInfo, ZipResult}; +use aes::cipher::KeyInit; +use aes::{Aes128, Aes192, Aes256}; use bzip2::read::BzDecoder; use flate2::read::DeflateDecoder; use liblzma::read::XzDecoder; use liblzma::stream::{Filters, LzmaOptions, Stream}; +use pbkdf2::pbkdf2_hmac_array; +use sha1::Sha1; use std::io::{ BufReader, Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, }; @@ -14,34 +19,101 @@ use zstd::stream::Decoder as ZstdDecoder; enum Encryption { None(Io), Weak(WeakDecoder), + Aes128(AesDecoder), + Aes192(AesDecoder), + Aes256(AesDecoder), } impl Encryption { - pub fn new(io: Io, info: &ZipFileInfo, password: Option<&[u8]>) -> ZipResult { + #[inline] + pub fn new(mut io: Io, info: &ZipFileInfo, password: Option<&[u8]>) -> ZipResult { Ok(match info.encryption_method { EncryptionMethod::None => Self::None(io), - EncryptionMethod::Weak(check) => Self::Weak(WeakDecoder::new( - io, - check, - password.ok_or(ZipError::PasswordIsNotSpecified)?, - )?), - EncryptionMethod::Unsupported => { - return Err(ZipError::UnsupportedEncryptionMethod.into()) + EncryptionMethod::Weak => { + let mut keys = Keys::new(); + keys.set_password(password.ok_or(ZipError::PasswordIsNotSpecified)?); + + let check = keys.set_header(io.read_arr()?); + if check != info.password_check() { + return Err(ZipError::IncorrectPassword); + } + + Self::Weak(WeakDecoder::new(io, keys)) + } + EncryptionMethod::Aes128 => { + let header = io.read_arr::<10>()?; + let salt = &header[..8]; + let check = &header[8..]; + + let hash = pbkdf2_hmac_array::( + password.ok_or(ZipError::PasswordIsNotSpecified)?, + salt, + 1000, + ); + let key = &hash[..16]; + + if check != &hash[32..] { + return Err(ZipError::IncorrectPassword); + } + + Self::Aes128(AesDecoder::new(io, Aes128::new(key.into()))?) + } + EncryptionMethod::Aes192 => { + let header = io.read_arr::<14>()?; + let salt = &header[..12]; + let check = &header[12..]; + + let hash = pbkdf2_hmac_array::( + password.ok_or(ZipError::PasswordIsNotSpecified)?, + salt, + 1000, + ); + let key = &hash[..24]; + + if check != &hash[48..] { + return Err(ZipError::IncorrectPassword); + } + + Self::Aes192(AesDecoder::new(io, Aes192::new(key.into()))?) + } + EncryptionMethod::Aes256 => { + let header = io.read_arr::<18>()?; + let salt = &header[..16]; + let check = &header[16..]; + + let hash = pbkdf2_hmac_array::( + password.ok_or(ZipError::PasswordIsNotSpecified)?, + salt, + 1000, + ); + let key = &hash[..32]; + + if check != &hash[64..] { + return Err(ZipError::IncorrectPassword); + } + + Self::Aes256(AesDecoder::new(io, Aes256::new(key.into()))?) } + EncryptionMethod::Unsupported => return Err(ZipError::UnsupportedEncryptionMethod), }) } } impl Read for Encryption { + #[inline] fn read(&mut self, buf: &mut [u8]) -> IoResult { match self { Self::None(io) => io.read(buf), Self::Weak(io) => io.read(buf), + Self::Aes128(io) => io.read(buf), + Self::Aes192(io) => io.read(buf), + Self::Aes256(io) => io.read(buf), } } } impl Seek for Encryption { + #[inline] fn seek(&mut self, pos: SeekFrom) -> IoResult { match self { Self::None(io) => io.seek(pos), @@ -62,6 +134,7 @@ enum Compression { } impl Compression { + #[inline] pub fn new(mut io: Io, info: &ZipFileInfo) -> ZipResult { Ok(match info.compression_method { CompressionMethod::Store => Self::Store(io), @@ -88,13 +161,14 @@ impl Compression { CompressionMethod::Zstd => Self::Zstd(ZstdDecoder::new(io)?), CompressionMethod::Xz => Self::Xz(XzDecoder::new(io)), CompressionMethod::Unsupported(id) => { - return Err(ZipError::UnsupportedCompressionMethod(id).into()) + return Err(ZipError::UnsupportedCompressionMethod(id)) } }) } } impl Read for Compression { + #[inline] fn read(&mut self, buf: &mut [u8]) -> IoResult { match self { Compression::Store(io) => io.read(buf), @@ -107,6 +181,7 @@ impl Read for Compression { } impl Seek for Compression { + #[inline] fn seek(&mut self, pos: SeekFrom) -> IoResult { match self { Compression::Store(io) => io.seek(pos), @@ -137,8 +212,8 @@ impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { io.seek(SeekFrom::Start(info.header_pointer))?; let buf = io.read_arr::<30>()?; - if u32::from_le_bytes(buf[..4].try_into().unwrap()) != 0x04034b50 { - return Err(ZipError::InvalidFileHeaderSignature.into()); + if buf[..4] != FILE_HEADER_SIGNATURE { + return Err(ZipError::InvalidFileHeaderSignature); } let data_pointer = info.header_pointer + 30 diff --git a/src/zip/mod.rs b/src/zip/mod.rs index fcc6161..488c06d 100644 --- a/src/zip/mod.rs +++ b/src/zip/mod.rs @@ -1,5 +1,6 @@ mod archive; mod cp437; +mod datetime; mod driver; mod encryption; mod error; diff --git a/src/zip/structs.rs b/src/zip/structs.rs index ebecae7..8b25400 100644 --- a/src/zip/structs.rs +++ b/src/zip/structs.rs @@ -1,6 +1,9 @@ use crate::structs::{ByteOrder, Settings, StructResult, VariantIndexType}; use serde::{Deserialize, Serialize}; +pub const FILE_HEADER_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x03, 0x04]; + +pub const EOCDR_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x05, 0x06]; #[derive(Serialize, Deserialize)] pub struct Eocdr { pub eocdr_disk: u16, @@ -12,6 +15,7 @@ pub struct Eocdr { pub comment_len: u16, } +pub const EOCDR64_LOCATOR_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x06, 0x07]; #[derive(Serialize, Deserialize)] pub struct Eocdr64Locator { pub eocdr64_disk: u32, @@ -19,6 +23,7 @@ pub struct Eocdr64Locator { pub disks: u32, } +pub const EOCDR64_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x06, 0x06]; #[derive(Serialize, Deserialize)] pub struct Eocdr64 { pub eocdr64_size: u64, @@ -32,6 +37,7 @@ pub struct Eocdr64 { pub cd_pointer: u64, } +pub const CDR_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x01, 0x02]; #[derive(Serialize, Deserialize)] pub struct Cdr { pub version: u16, @@ -58,6 +64,14 @@ pub struct ExtraHeader { pub size: u16, } +#[derive(Serialize, Deserialize)] +pub struct AesField { + pub version: u16, + pub id: u16, + pub strength: u8, + pub compression_method: u16, +} + #[inline] #[allow(dead_code)] pub fn serialize(object: &mut T) -> StructResult> { diff --git a/src/zip/tests.rs b/src/zip/tests.rs index e24cdfe..a8ac634 100644 --- a/src/zip/tests.rs +++ b/src/zip/tests.rs @@ -1,5 +1,7 @@ use crate::zip::cp437::{from_char, is_cp437, to_char, FromCp437}; +use crate::zip::datetime::DosDateTime; use crate::zip::{bit::DeflateMode, BitFlag}; +use chrono::{DateTime, Local, TimeZone}; #[test] fn test_bit_flag() { @@ -74,3 +76,20 @@ fn test_cp437() { "abcdefghijklmnopqrstuvwxyz" ); } + +#[test] +fn test_dos_datetime() { + let datetime = Local.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap(); + assert_eq!( + DateTime::from_dos_date_time(datetime.to_dos_date(), datetime.to_dos_time(), Local) + .unwrap(), + datetime + ); + + let datetime = Local.with_ymd_and_hms(1999, 9, 9, 9, 9, 9).unwrap(); + assert_eq!( + DateTime::from_dos_date_time(datetime.to_dos_date(), datetime.to_dos_time(), Local) + .unwrap(), + Local.with_ymd_and_hms(1999, 9, 9, 9, 9, 8).unwrap() + ); // Dos format stores seconds with an accuracy of 2s +} -- cgit v1.2.3