Skip to main content

stream_unpack/zip/structures/
local_file_header.rs

1use std::io::Cursor;
2
3use byteorder::{ReadBytesExt, LittleEndian};
4
5use crate::{decrypt::{Decryptor, DecryptorCreationError}, zip::structures::file_header::{AEX_EXTRA_FIELD_ID, COMPRESSION_AEX, ENCRYPTED_FLAG}};
6
7use super::{CompressionMethod, file_header::{FileHeaderExtraField, Zip64ProcessedData, Zip64OriginalData}};
8
9#[cfg(feature = "zipcrypto")]
10use crate::decrypt::zipcrypto::ZipCryptoDecryptor;
11
12#[cfg(feature = "ae-x")]
13use crate::decrypt::aex::*;
14
15pub const LFH_SIGNATURE: u32 = 0x04034b50;
16pub const LFH_CONSTANT_SIZE: usize = 26;
17
18/// Represents the result of reading a ZIP local file header (LFH)
19///
20/// The layout of this object does not follow the original ZIP LFH structure
21#[derive(Debug, Clone)]
22pub struct LocalFileHeader {
23    pub version: u16,
24
25    pub flag: u16,
26
27    pub compression_method: Option<CompressionMethod>,
28
29    pub mod_time: u16,
30    pub mod_date: u16,
31
32    pub crc32: u32,
33
34    pub compressed_size: u64,
35    pub uncompressed_size: u64,
36
37    pub filename: String,
38
39    pub extra_fields: Vec<FileHeaderExtraField>,
40
41    pub encryption: EncryptionData,
42
43    pub header_size: usize
44}
45
46impl LocalFileHeader {
47    /// Attempts to read a local file header from the provided
48    /// byte buffer. Returns None if there isn't enought data
49    pub fn from_bytes(data: impl AsRef<[u8]>) -> Option<Self> {
50        let data = data.as_ref();
51        if data.len() < LFH_CONSTANT_SIZE {
52            return None;
53        }
54
55        let mut cursor = Cursor::new(data);
56
57        let version = cursor.read_u16::<LittleEndian>().unwrap();
58        let flag = cursor.read_u16::<LittleEndian>().unwrap();
59        let compression_method = cursor.read_u16::<LittleEndian>().unwrap();
60        let mod_time = cursor.read_u16::<LittleEndian>().unwrap();
61        let mod_date = cursor.read_u16::<LittleEndian>().unwrap();
62        let crc32 = cursor.read_u32::<LittleEndian>().unwrap();
63        let compressed_size = cursor.read_u32::<LittleEndian>().unwrap();
64        let uncompressed_size = cursor.read_u32::<LittleEndian>().unwrap();
65        let filename_length = cursor.read_u16::<LittleEndian>().unwrap();
66        let extra_fields_length = cursor.read_u16::<LittleEndian>().unwrap();
67
68        let filename_length = filename_length as usize;
69        let extra_fields_length = extra_fields_length as usize;
70        if data.len() < LFH_CONSTANT_SIZE + filename_length + extra_fields_length {
71            return None;
72        }
73
74        let filename_start = LFH_CONSTANT_SIZE;
75        let filename_end = filename_start + filename_length;
76        let filename = String::from_utf8_lossy(&data[filename_start..filename_end]).to_string();
77
78        let extra_fields_start = filename_end;
79        let extra_fields_end = extra_fields_start + extra_fields_length;
80        let extra_fields = FileHeaderExtraField::read_extra_fields(&data[extra_fields_start..extra_fields_end])?;
81
82        let original_zip64_data = Zip64OriginalData {
83            uncompressed_size,
84            compressed_size,
85            ..Default::default()
86        };
87
88        let Zip64ProcessedData {
89            uncompressed_size,
90            compressed_size,
91            ..
92        } = original_zip64_data.process(&extra_fields)?;
93
94        let mut compressed_size = compressed_size;
95        let mut compression_method = compression_method;
96        let mut header_size = extra_fields_end;
97        let mut encryption = EncryptionData::None;
98        if flag & ENCRYPTED_FLAG != 0 {
99            if compression_method == COMPRESSION_AEX {
100                let aex_ef = extra_fields.iter()
101                    .find(|f| f.id == AEX_EXTRA_FIELD_ID)?;
102                if aex_ef.size() < 7 {
103                    return None;
104                }
105
106                let strength = aex_ef.data[4];
107
108                let salt_length = match strength {
109                    0x01 => 8, 0x02 => 12, 0x03 => 16,
110                    _ => { return None; }
111                };
112
113                let salt_start = header_size;
114                let salt_end = salt_start + salt_length;
115                if salt_end > data.len() {
116                    return None;
117                }
118
119                let salt = &data[salt_start..salt_end];
120                let aex_variant = match strength {
121                    0x01 => AExVariableData::AES128(salt.try_into().unwrap()),
122                    0x02 => AExVariableData::AES192(salt.try_into().unwrap()),
123                    0x03 => AExVariableData::AES256(salt.try_into().unwrap()),
124                    _ => { return None; }
125                };
126
127                let pvv_start = salt_end;
128                let pvv_end = pvv_start + 2;
129                if pvv_end > data.len() {
130                    return None;
131                }
132
133                let pvv = u16::from_le_bytes([data[pvv_start], data[pvv_start + 1]]);
134
135                // FIXME this subtracts auth at the end of encrypted data
136                compressed_size -= (salt_length + 2 + 10) as u64;
137                compression_method = u16::from_le_bytes([aex_ef.data[5], aex_ef.data[6]]);
138                header_size = pvv_end;
139                encryption = EncryptionData::AEx(AExInitData { variable: aex_variant, pvv })
140            } else {
141                let zipcrypto_header_start = header_size;
142                let zipcrypto_header_end = zipcrypto_header_start + 12;
143                if zipcrypto_header_end > data.len() {
144                    return None;
145                }
146
147                compressed_size -= 12;
148                header_size = zipcrypto_header_end;
149                encryption = EncryptionData::ZipCrypto(
150                    data[zipcrypto_header_start..zipcrypto_header_end].try_into().unwrap()
151                );
152            }
153        }
154
155        Some(Self {
156            version,
157            flag,
158            compression_method: CompressionMethod::from_id(compression_method),
159            mod_time,
160            mod_date,
161            crc32,
162            compressed_size,
163            uncompressed_size,
164            filename,
165            extra_fields,
166
167            encryption,
168
169            header_size
170        })
171    }
172
173    pub fn is_directory(&self) -> bool {
174        self.filename.ends_with('/')
175    }
176
177    pub fn is_encrypted(&self) -> bool {
178        return !matches!(self.encryption, EncryptionData::None);
179    }
180
181    /// Create a [Decryptor] for the file described by this LFH with the given password. Returns
182    /// an error if the file is not encrypted.
183    pub fn create_decryptor(&self, password: &[u8]) -> Result<Box<dyn Decryptor>, DecryptorCreationError> {
184        match &self.encryption {
185            EncryptionData::None => Err(DecryptorCreationError::NotEncrypted),
186
187            EncryptionData::ZipCrypto(zipcrypto_header) => {
188                #[cfg(feature = "zipcrypto")] {
189                    Ok(Box::new(ZipCryptoDecryptor::new(password, *zipcrypto_header, self.crc32)?))
190                }
191                #[cfg(not(feature = "zipcrypto"))] {
192                    let _ = password;
193                    let _ = zipcrypto_header;
194                    Err(DecryptorCreationError::NoFeature("zipcrypto".to_string()))
195                }
196            }
197
198            EncryptionData::AEx(data) => {
199                #[cfg(feature = "ae-x")] {
200                    data.create_decryptor(password)
201                }
202                #[cfg(not(feature = "ae-x"))] {
203                    let _ = password;
204                    let _ = data;
205                    Err(DecryptorCreationError::NoFeature("ae-x".to_string()))
206                }
207            }
208        }
209    }
210}
211
212#[derive(Debug, Clone)]
213pub enum EncryptionData {
214    None,
215    ZipCrypto([u8; 12]),
216    AEx(AExInitData)
217}
218
219#[derive(Debug, Clone)]
220pub enum AExVariableData {
221    AES128([u8; 8]),
222    AES192([u8; 12]),
223    AES256([u8; 16])
224}
225
226#[derive(Debug, Clone)]
227pub struct AExInitData {
228    pub variable: AExVariableData,
229    pub pvv: u16
230}
231
232impl AExInitData {
233    #[cfg(feature = "ae-x")]
234    fn create_decryptor(&self, password: &[u8]) -> Result<Box<dyn Decryptor>, DecryptorCreationError> {
235        match &self.variable {
236            AExVariableData::AES128(salt) => Ok(Box::new(AEx128Decryptor::new(password, salt, self.pvv)?)),
237            AExVariableData::AES192(salt) => Ok(Box::new(AEx192Decryptor::new(password, salt, self.pvv)?)),
238            AExVariableData::AES256(salt) => Ok(Box::new(AEx256Decryptor::new(password, salt, self.pvv)?))
239        }
240    }
241}