From cc18a545a87ca616f05114d174690e5cc9614669 Mon Sep 17 00:00:00 2001 From: Igor Tolmachev Date: Tue, 16 Jul 2024 17:24:33 +0900 Subject: Optimize encryption - Add archive for testing encryption of compressed files - Implement incorrect password check - Use custom crc32 function --- .vscode/settings.json | 7 +++- src/archive.rs | 42 +++----------------- src/driver/driver.rs | 5 +-- src/driver/file.rs | 2 - src/file.rs | 38 ------------------ src/lib.rs | 6 +-- src/zip/archive.rs | 19 +++++++++ src/zip/driver.rs | 25 +++++++----- src/zip/encryption.rs | 49 +++++++++++++++++------ src/zip/error.rs | 2 + src/zip/file/info.rs | 16 +++++--- src/zip/file/read.rs | 88 +++++++++++++++++++---------------------- src/zip/file/write.rs | 6 +-- src/zip/structs.rs | 6 +-- tests/files/archive_passwd.zip | Bin 200 -> 1059 bytes tests/zip.rs | 40 +++++++++++++++---- 16 files changed, 174 insertions(+), 177 deletions(-) delete mode 100644 src/file.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index d344ae3..8ab0a05 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,14 +8,17 @@ "decompressor", "eocdr", "flate", - "Hasher", + "hasher", "lzma", "newtype", "ntfs", + "refin", + "refout", "repr", "rposition", "seekable", - "unseekable" + "unseekable", + "xorout" ], "rust-analyzer.linkedProjects": ["./Cargo.toml", "./Cargo.toml"] } diff --git a/src/archive.rs b/src/archive.rs index 73c515c..d49689d 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,5 +1,5 @@ use crate::driver::{ArchiveRead, ArchiveWrite, Driver}; -use crate::{ArchiveFile, ArchiveResult}; +use crate::ArchiveResult; use std::fs::File; use std::io::{Read, Write}; use std::path::Path; @@ -48,51 +48,19 @@ where self.get_file_info_by_index(self.get_file_index(name)?) } - fn get_file_reader_by_index_with_optional_password<'d>( - &'d mut self, - index: usize, - password: Option<&str>, - ) -> ArchiveResult>, D::Error> { - Ok(ArchiveFile::new( - self.driver.get_file_reader(index, password)?, - )) - } - - #[inline] pub fn get_file_reader_by_index<'d>( &'d mut self, index: usize, - ) -> ArchiveResult>, D::Error> { - self.get_file_reader_by_index_with_optional_password(index, None) - } - - #[inline] - pub fn get_file_reader_by_index_with_password<'d>( - &'d mut self, - index: usize, - password: &str, - ) -> ArchiveResult>, D::Error> { - self.get_file_reader_by_index_with_optional_password(index, Some(password)) + ) -> ArchiveResult, D::Error> { + self.driver.get_file_reader(index) } #[inline] pub fn get_file_reader_by_name<'d>( &'d mut self, name: &str, - ) -> ArchiveResult>, D::Error> { - self.get_file_reader_by_index_with_optional_password(self.get_file_index(name)?, None) - } - - #[inline] - pub fn get_file_reader_by_name_with_password<'d>( - &'d mut self, - name: &str, - password: &str, - ) -> ArchiveResult>, D::Error> { - self.get_file_reader_by_index_with_optional_password( - self.get_file_index(name)?, - Some(password), - ) + ) -> ArchiveResult, D::Error> { + self.get_file_reader_by_index(self.get_file_index(name)?) } } diff --git a/src/driver/driver.rs b/src/driver/driver.rs index f0f93a9..359793d 100644 --- a/src/driver/driver.rs +++ b/src/driver/driver.rs @@ -14,7 +14,7 @@ pub trait ArchiveRead: Driver where Self::Io: Read, { - type FileReader<'d>: FileDriver + type FileReader<'d>: FileDriver where Self: 'd; @@ -34,7 +34,6 @@ where fn get_file_reader<'d>( &'d mut self, index: usize, - password: Option<&str>, ) -> ArchiveResult, Self::Error>; } @@ -42,7 +41,7 @@ pub trait ArchiveWrite: Driver where Self::Io: Read + Write, { - type FileWriter<'d>: FileDriver + type FileWriter<'d>: FileDriver where Self: 'd; } diff --git a/src/driver/file.rs b/src/driver/file.rs index 5c6ea43..3d562da 100644 --- a/src/driver/file.rs +++ b/src/driver/file.rs @@ -3,6 +3,4 @@ pub trait ArchiveFileInfo: Clone {} pub trait FileDriver { type Io; type FileInfo: ArchiveFileInfo; - - fn info(&self) -> &Self::FileInfo; } diff --git a/src/file.rs b/src/file.rs deleted file mode 100644 index f284b98..0000000 --- a/src/file.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::driver::FileDriver; -use std::io::{Read, Result as IoResult, Seek, Write}; - -pub struct ArchiveFile { - pub(crate) driver: D, -} - -impl ArchiveFile { - pub fn new(driver: D) -> Self { - Self { driver } - } - - pub fn info(&self) -> &D::FileInfo { - self.driver.info() - } -} - -impl Read for ArchiveFile { - fn read(&mut self, buf: &mut [u8]) -> IoResult { - self.driver.read(buf) - } -} - -impl Write for ArchiveFile { - fn write(&mut self, buf: &[u8]) -> IoResult { - self.driver.write(buf) - } - - fn flush(&mut self) -> IoResult<()> { - self.driver.flush() - } -} - -impl Seek for ArchiveFile { - fn seek(&mut self, pos: std::io::SeekFrom) -> IoResult { - self.driver.seek(pos) - } -} diff --git a/src/lib.rs b/src/lib.rs index 9ec8b34..7490cc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,12 @@ mod archive; +mod driver; mod error; -mod file; +mod structs; mod utils; -pub mod driver; -pub mod structs; pub mod zip; pub use archive::Archive; pub use error::{ArchiveError, ArchiveResult}; -pub use file::ArchiveFile; pub use zip::Zip; diff --git a/src/zip/archive.rs b/src/zip/archive.rs index 79e8ca1..569ad87 100644 --- a/src/zip/archive.rs +++ b/src/zip/archive.rs @@ -1,3 +1,4 @@ +use crate::zip::{ZipFileReader, ZipResult}; use crate::{Archive, Zip}; use std::io::{Read, Seek, Write}; @@ -5,6 +6,24 @@ impl Archive> { pub fn comment(&self) -> &String { self.driver.comment() } + + pub fn get_file_reader_by_index_with_password<'d>( + &'d mut self, + index: usize, + password: &[u8], + ) -> ZipResult> { + self.driver + .get_file_reader_with_optional_password(index, Some(password)) + } + + #[inline] + pub fn get_file_reader_by_name_with_password<'d>( + &'d mut self, + name: &str, + password: &[u8], + ) -> ZipResult> { + self.get_file_reader_by_index_with_password(self.get_file_index(name)?, password) + } } impl Archive> {} diff --git a/src/zip/driver.rs b/src/zip/driver.rs index 87f9c1a..62da39f 100644 --- a/src/zip/driver.rs +++ b/src/zip/driver.rs @@ -221,7 +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), + EncryptionMethod::from_bif_flag(bit_flag, cdr.crc, cdr.dos_time), bit_flag, mtime, atime, @@ -258,11 +258,22 @@ impl ArchiveRead for Zip { self.files.get(index).ok_or(ZipError::FileNotFound.into()) } - fn get_file_reader<'d>( + #[inline] + fn get_file_reader<'d>(&'d mut self, index: usize) -> ZipResult> { + self.get_file_reader_with_optional_password(index, None) + } +} + +impl Zip { + pub fn comment(&self) -> &String { + &self.comment + } + + pub fn get_file_reader_with_optional_password<'d>( &'d mut self, index: usize, - password: Option<&str>, - ) -> ZipResult> { + password: Option<&[u8]>, + ) -> ZipResult<::FileReader<'d>> { Ok(ZipFileReader::new( &mut self.io, self.files.get(index).ok_or(ZipError::FileNotFound)?, @@ -271,12 +282,6 @@ impl ArchiveRead for Zip { } } -impl Zip { - pub fn comment(&self) -> &String { - &self.comment - } -} - impl ArchiveWrite for Zip { type FileWriter<'d> = ZipFileWriter<'d, Io> where Io: 'd; } diff --git a/src/zip/encryption.rs b/src/zip/encryption.rs index 84e30d5..28a6bdb 100644 --- a/src/zip/encryption.rs +++ b/src/zip/encryption.rs @@ -1,12 +1,35 @@ use crate::utils::ReadUtils; -use crate::zip::ZipResult; -use crc32fast::Hasher; +use crate::zip::{ZipError, ZipResult}; 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 +const TABLE: [u32; 256] = generate_table(); + +const fn generate_table() -> [u32; 256] { + let mut table = [0; 256]; + + let mut b = 0; + while b <= 255 { + let mut crc = b as u32; + + let mut i = 0; + while i < 8 { + if (crc & 1) > 0 { + crc = (crc >> 1) ^ 0xEDB88320 + } else { + crc >>= 1 + } + i += 1; + } + + table[b] = crc; + b += 1 + } + + table +} + +fn crc32(byte: u8, crc: u32) -> u32 { + (crc >> 8) ^ TABLE[((crc & 0xFF) as u8 ^ byte) as usize] } pub struct WeakDecoder { @@ -17,7 +40,7 @@ pub struct WeakDecoder { } impl WeakDecoder { - pub fn new(io: Io, password: &str) -> ZipResult { + pub fn new(io: Io, check: u8, password: &[u8]) -> ZipResult { let mut decoder = Self { key0: 305419896, key1: 591751049, @@ -25,11 +48,14 @@ impl WeakDecoder { io, }; - for c in password.chars() { - decoder.update_keys(c as u8) + 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) } @@ -46,7 +72,9 @@ impl WeakDecoder { fn decode_byte(&mut self, byte: u8) -> u8 { let key = self.key2 | 2; - byte ^ (((key * (key ^ 1)) >> 8) as u8) + let byte = byte ^ ((key * (key ^ 1)) >> 8) as u8; + self.update_keys(byte); + byte } } @@ -55,7 +83,6 @@ impl Read for WeakDecoder { 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 9ec0956..a4b8c2b 100644 --- a/src/zip/error.rs +++ b/src/zip/error.rs @@ -20,6 +20,7 @@ pub enum ZipError { InvalidFileComment, FileNotFound, + IncorrectPassword, PasswordIsNotSpecified, CompressedDataIsUnseekable, EncryptedDataIsUnseekable, @@ -66,6 +67,7 @@ impl Display for ZipError { Self::InvalidFileComment => write!(f, "Invalid file comment"), Self::FileNotFound => write!(f, "File not found"), + Self::IncorrectPassword => write!(f, "Incorrect password"), 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 f5d4d8a..599dcc3 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, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum CompressionMethod { Store, Deflate, @@ -28,18 +28,22 @@ impl CompressionMethod { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum EncryptionMethod { None, - Weak, + Weak(u8), Unsupported, } impl EncryptionMethod { - pub(crate) fn from_bit_flag(bit_flag: BitFlag) -> 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, + (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"), } @@ -52,7 +56,7 @@ pub struct BitFlag { } pub mod bit { - #[derive(Debug, PartialEq, Eq, Clone, Copy)] + #[derive(Debug, PartialEq, Eq, Clone)] pub enum DeflateMode { Normal, Maximum, diff --git a/src/zip/file/read.rs b/src/zip/file/read.rs index aa665c3..c26b304 100644 --- a/src/zip/file/read.rs +++ b/src/zip/file/read.rs @@ -17,11 +17,12 @@ enum Encryption { } impl Encryption { - pub fn new(io: Io, method: EncryptionMethod, password: Option<&str>) -> ZipResult { - Ok(match method { + pub fn new(io: Io, info: &ZipFileInfo, password: Option<&[u8]>) -> ZipResult { + Ok(match info.encryption_method { EncryptionMethod::None => Self::None(io), - EncryptionMethod::Weak => Self::Weak(WeakDecoder::new( + EncryptionMethod::Weak(check) => Self::Weak(WeakDecoder::new( io, + check, password.ok_or(ZipError::PasswordIsNotSpecified)?, )?), EncryptionMethod::Unsupported => { @@ -61,12 +62,29 @@ enum Compression { } impl Compression { - pub fn new(io: Io, method: CompressionMethod) -> ZipResult { - Ok(match method { + pub fn new(mut io: Io, info: &ZipFileInfo) -> ZipResult { + Ok(match info.compression_method { CompressionMethod::Store => Self::Store(io), CompressionMethod::Deflate => Self::Deflate(DeflateDecoder::new(io)), CompressionMethod::BZip2 => Self::BZip2(BzDecoder::new(io)), - CompressionMethod::Lzma => panic!(), + CompressionMethod::Lzma => { + let buf = io.read_arr::<9>()?; + Compression::Xz(XzDecoder::new_stream( + io, + Stream::new_raw_decoder( + Filters::new().lzma1( + LzmaOptions::new() + .literal_context_bits((buf[4] % 9) as u32) + .literal_position_bits((buf[4] / 9 % 5) as u32) + .position_bits((buf[4] / 45) as u32) + .dict_size( + u32::from_le_bytes(buf[5..9].try_into().unwrap()).max(4096), + ), + ), + ) + .unwrap(), + )) + } CompressionMethod::Zstd => Self::Zstd(ZstdDecoder::new(io)?), CompressionMethod::Xz => Self::Xz(XzDecoder::new(io)), CompressionMethod::Unsupported(id) => { @@ -108,14 +126,14 @@ pub struct ZipFileReader<'d, Io: Read> { impl<'d, Io: Read> FileDriver for ZipFileReader<'d, Io> { type Io = Io; type FileInfo = ZipFileInfo; - - fn info(&self) -> &Self::FileInfo { - self.info - } } impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { - pub fn new(io: &'d mut Io, info: &'d ZipFileInfo, password: Option<&str>) -> ZipResult { + pub(crate) fn new( + io: &'d mut Io, + info: &'d ZipFileInfo, + password: Option<&[u8]>, + ) -> ZipResult { io.seek(SeekFrom::Start(info.header_pointer))?; let buf = io.read_arr::<30>()?; @@ -127,49 +145,23 @@ 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::Lzma => { - let buf = io.read_arr::<9>()?; - Compression::Xz(XzDecoder::new_stream( - 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() - .literal_context_bits((buf[4] % 9) as u32) - .literal_position_bits((buf[4] / 9 % 5) as u32) - .position_bits((buf[4] / 45) as u32) - .dict_size( - u32::from_le_bytes(buf[5..9].try_into().unwrap()).max(4096), - ), - ), - ) - .unwrap(), - )) - } - _ => Compression::new( - Encryption::new( - IoCursor::new(io, data_pointer, data_pointer + info.compressed_size)?, - info.encryption_method, - password, - )?, - info.compression_method, + io: Compression::new( + Encryption::new( + IoCursor::new(io, data_pointer, data_pointer + info.compressed_size)?, + info, + password, )?, - }, + info, + )?, info, }) } + pub fn info(&self) -> &ZipFileInfo { + self.info + } + pub fn is_seekable(&self) -> bool { match self.io { Compression::Store(Encryption::None(..)) => true, diff --git a/src/zip/file/write.rs b/src/zip/file/write.rs index d5b686c..20e53b3 100644 --- a/src/zip/file/write.rs +++ b/src/zip/file/write.rs @@ -2,8 +2,8 @@ use crate::driver::FileDriver; use crate::zip::ZipFileInfo; use std::io::Write; +#[allow(dead_code)] pub struct ZipFileWriter<'d, Io: Write> { - #[allow(dead_code)] io: &'d mut Io, info: &'d ZipFileInfo, } @@ -11,8 +11,4 @@ pub struct ZipFileWriter<'d, Io: Write> { impl<'d, Io: Write> FileDriver for ZipFileWriter<'d, Io> { type Io = Io; type FileInfo = ZipFileInfo; - - fn info(&self) -> &Self::FileInfo { - self.info - } } diff --git a/src/zip/structs.rs b/src/zip/structs.rs index a44659b..9fd1aeb 100644 --- a/src/zip/structs.rs +++ b/src/zip/structs.rs @@ -1,4 +1,4 @@ -use crate::structs::{Settings, StructResult}; +use crate::structs::{ByteOrder, Settings, StructResult, VariantIndexType}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -60,9 +60,9 @@ pub struct ExtraHeader { #[allow(dead_code)] pub fn serialize(object: &mut T) -> StructResult> { - Settings::default().serialize(object) + Settings::new(ByteOrder::Le, VariantIndexType::U8).serialize(object) } pub fn deserialize<'de, T: Deserialize<'de>>(object: &'de [u8]) -> StructResult { - Settings::default().deserialize(object) + Settings::new(ByteOrder::Le, VariantIndexType::U8).deserialize(object) } diff --git a/tests/files/archive_passwd.zip b/tests/files/archive_passwd.zip index 700a343..291bd12 100644 Binary files a/tests/files/archive_passwd.zip and b/tests/files/archive_passwd.zip differ diff --git a/tests/zip.rs b/tests/zip.rs index 4f0c985..d02e96e 100644 --- a/tests/zip.rs +++ b/tests/zip.rs @@ -4,15 +4,38 @@ use std::io::{Read, Seek, SeekFrom}; #[test] fn test_zip_passwd() { let mut archive = Archive::::read_from_file("tests/files/archive_passwd.zip").unwrap(); - let mut f = archive - .get_file_reader_by_index_with_password(0, "passwd") - .unwrap(); - let mut data = String::new(); - f.read_to_string(&mut data).unwrap(); - assert_eq!(data, "test file data"); + + assert_eq!(archive.comment(), "archive comment"); + assert_eq!( + archive + .files() + .iter() + .map(|f| &f.name) + .collect::>(), + vec!["store", "deflate", "bzip"] + ); + + assert!(archive.get_file_reader_by_name("store").is_err()); + assert!(archive + .get_file_reader_by_name_with_password("store", b"wrong_passwd") + .is_err()); + + for (name, check_data) in [ + ("store", "1e643774f40510e37c6f3c451d9d"), + ("deflate", "a70aff4b6b2754ad47852503236a"), + ("bzip", "f7085f4f8ecc512a8c2c3cbe8227"), + ] { + let mut f = archive + .get_file_reader_by_name_with_password(name, b"passwd") + .unwrap(); + let mut data = String::new(); + f.read_to_string(&mut data).unwrap(); + assert_eq!(&data[..24], "test encrypted file data"); + assert_eq!(&data[172..], check_data); + } } -// #[test] +#[test] fn test_zip() { let mut archive = Archive::::read_from_file("tests/files/archive.zip").unwrap(); @@ -54,7 +77,7 @@ fn test_zip() { f.seek(SeekFrom::Start(7)).unwrap(); assert_eq!(f.seek(SeekFrom::Current(0)).unwrap(), 7); - f.seek(SeekFrom::End(-100)).unwrap_err(); + assert!(f.seek(SeekFrom::End(-100)).is_err()); assert_eq!(f.seek(SeekFrom::Current(0)).unwrap(), 7); assert_eq!(f.seek(SeekFrom::Start(100)).unwrap(), 14); @@ -64,5 +87,6 @@ fn test_zip() { let mut data = String::new(); f.read_to_string(&mut data).unwrap(); assert_eq!(data, "test file data"); + assert!(!f.is_seekable() || f.info().name == "store") } } -- cgit v1.2.3