diff options
Diffstat (limited to 'third_party/rust/zip/src/aes.rs')
-rw-r--r-- | third_party/rust/zip/src/aes.rs | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/third_party/rust/zip/src/aes.rs b/third_party/rust/zip/src/aes.rs new file mode 100644 index 0000000000..8997705c69 --- /dev/null +++ b/third_party/rust/zip/src/aes.rs @@ -0,0 +1,185 @@ +//! Implementation of the AES decryption for zip files. +//! +//! This was implemented according to the [WinZip specification](https://www.winzip.com/win/en/aes_info.html). +//! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2. +//! If the file is marked as encrypted with AE-2 the CRC field is ignored, even if it isn't set to 0. + +use crate::aes_ctr; +use crate::types::AesMode; +use constant_time_eq::constant_time_eq; +use hmac::{Hmac, Mac}; +use sha1::Sha1; +use std::io::{self, Read}; + +/// The length of the password verifcation value in bytes +const PWD_VERIFY_LENGTH: usize = 2; +/// The length of the authentication code in bytes +const AUTH_CODE_LENGTH: usize = 10; +/// The number of iterations used with PBKDF2 +const ITERATION_COUNT: u32 = 1000; + +/// Create a AesCipher depending on the used `AesMode` and the given `key`. +/// +/// # Panics +/// +/// This panics if `key` doesn't have the correct size for the chosen aes mode. +fn cipher_from_mode(aes_mode: AesMode, key: &[u8]) -> Box<dyn aes_ctr::AesCipher> { + match aes_mode { + AesMode::Aes128 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes128>::new(key)) + as Box<dyn aes_ctr::AesCipher>, + AesMode::Aes192 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes192>::new(key)) + as Box<dyn aes_ctr::AesCipher>, + AesMode::Aes256 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes256>::new(key)) + as Box<dyn aes_ctr::AesCipher>, + } +} + +// An aes encrypted file starts with a salt, whose length depends on the used aes mode +// followed by a 2 byte password verification value +// then the variable length encrypted data +// and lastly a 10 byte authentication code +pub struct AesReader<R> { + reader: R, + aes_mode: AesMode, + data_length: u64, +} + +impl<R: Read> AesReader<R> { + pub fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader<R> { + let data_length = compressed_size + - (PWD_VERIFY_LENGTH + AUTH_CODE_LENGTH + aes_mode.salt_length()) as u64; + + Self { + reader, + aes_mode, + data_length, + } + } + + /// Read the AES header bytes and validate the password. + /// + /// Even if the validation succeeds, there is still a 1 in 65536 chance that an incorrect + /// password was provided. + /// It isn't possible to check the authentication code in this step. This will be done after + /// reading and decrypting the file. + /// + /// # Returns + /// + /// If the password verification failed `Ok(None)` will be returned to match the validate + /// method of ZipCryptoReader. + pub fn validate(mut self, password: &[u8]) -> io::Result<Option<AesReaderValid<R>>> { + let salt_length = self.aes_mode.salt_length(); + let key_length = self.aes_mode.key_length(); + + let mut salt = vec![0; salt_length]; + self.reader.read_exact(&mut salt)?; + + // next are 2 bytes used for password verification + let mut pwd_verification_value = vec![0; PWD_VERIFY_LENGTH]; + self.reader.read_exact(&mut pwd_verification_value)?; + + // derive a key from the password and salt + // the length depends on the aes key length + let derived_key_len = 2 * key_length + PWD_VERIFY_LENGTH; + let mut derived_key: Vec<u8> = vec![0; derived_key_len]; + + // use PBKDF2 with HMAC-Sha1 to derive the key + pbkdf2::pbkdf2::<Hmac<Sha1>>(password, &salt, ITERATION_COUNT, &mut derived_key); + let decrypt_key = &derived_key[0..key_length]; + let hmac_key = &derived_key[key_length..key_length * 2]; + let pwd_verify = &derived_key[derived_key_len - 2..]; + + // the last 2 bytes should equal the password verification value + if pwd_verification_value != pwd_verify { + // wrong password + return Ok(None); + } + + let cipher = cipher_from_mode(self.aes_mode, decrypt_key); + let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap(); + + Ok(Some(AesReaderValid { + reader: self.reader, + data_remaining: self.data_length, + cipher, + hmac, + finalized: false, + })) + } +} + +/// A reader for aes encrypted files, which has already passed the first password check. +/// +/// There is a 1 in 65536 chance that an invalid password passes that check. +/// After the data has been read and decrypted an HMAC will be checked and provide a final means +/// to check if either the password is invalid or if the data has been changed. +pub struct AesReaderValid<R: Read> { + reader: R, + data_remaining: u64, + cipher: Box<dyn aes_ctr::AesCipher>, + hmac: Hmac<Sha1>, + finalized: bool, +} + +impl<R: Read> Read for AesReaderValid<R> { + /// This implementation does not fulfill all requirements set in the trait documentation. + /// + /// ```txt + /// "If an error is returned then it must be guaranteed that no bytes were read." + /// ``` + /// + /// Whether this applies to errors that occur while reading the encrypted data depends on the + /// underlying reader. If the error occurs while verifying the HMAC, the reader might become + /// practically unusable, since its position after the error is not known. + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + if self.data_remaining == 0 { + return Ok(0); + } + + // get the number of bytes to read, compare as u64 to make sure we can read more than + // 2^32 bytes even on 32 bit systems. + let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize; + let read = self.reader.read(&mut buf[0..bytes_to_read])?; + self.data_remaining -= read as u64; + + // Update the hmac with the encrypted data + self.hmac.update(&buf[0..read]); + + // decrypt the data + self.cipher.crypt_in_place(&mut buf[0..read]); + + // if there is no data left to read, check the integrity of the data + if self.data_remaining == 0 { + assert!( + !self.finalized, + "Tried to use an already finalized HMAC. This is a bug!" + ); + self.finalized = true; + + // Zip uses HMAC-Sha1-80, which only uses the first half of the hash + // see https://www.winzip.com/win/en/aes_info.html#auth-faq + let mut read_auth_code = [0; AUTH_CODE_LENGTH]; + self.reader.read_exact(&mut read_auth_code)?; + let computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH]; + + // use constant time comparison to mitigate timing attacks + if !constant_time_eq(computed_auth_code, &read_auth_code) { + return Err( + io::Error::new( + io::ErrorKind::InvalidData, + "Invalid authentication code, this could be due to an invalid password or errors in the data" + ) + ); + } + } + + Ok(read) + } +} + +impl<R: Read> AesReaderValid<R> { + /// Consumes this decoder, returning the underlying reader. + pub fn into_inner(self) -> R { + self.reader + } +} |