Merge pull request #80 from kyrias/aes-encryption3
Add AES encryption write support
This commit is contained in:
commit
c8d1be86b3
11 changed files with 604 additions and 110 deletions
|
@ -33,11 +33,13 @@ indexmap = "2"
|
||||||
hmac = { version = "0.12.1", optional = true, features = ["reset"] }
|
hmac = { version = "0.12.1", optional = true, features = ["reset"] }
|
||||||
num_enum = "0.7.2"
|
num_enum = "0.7.2"
|
||||||
pbkdf2 = { version = "0.12.2", optional = true }
|
pbkdf2 = { version = "0.12.2", optional = true }
|
||||||
|
rand = { version = "0.8.5", optional = true }
|
||||||
sha1 = { version = "0.10.6", optional = true }
|
sha1 = { version = "0.10.6", optional = true }
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
time = { workspace = true, optional = true, features = [
|
time = { workspace = true, optional = true, features = [
|
||||||
"std",
|
"std",
|
||||||
] }
|
] }
|
||||||
|
zeroize = { version = "1.6.0", optional = true, features = ["zeroize_derive"] }
|
||||||
zstd = { version = "0.13.1", optional = true, default-features = false }
|
zstd = { version = "0.13.1", optional = true, default-features = false }
|
||||||
zopfli = { version = "0.8.0", optional = true }
|
zopfli = { version = "0.8.0", optional = true }
|
||||||
deflate64 = { version = "0.1.8", optional = true }
|
deflate64 = { version = "0.1.8", optional = true }
|
||||||
|
@ -58,7 +60,7 @@ anyhow = "1"
|
||||||
clap = { version = "=4.4.18", features = ["derive"] }
|
clap = { version = "=4.4.18", features = ["derive"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1"]
|
aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1", "rand", "zeroize"]
|
||||||
chrono = ["chrono/default"]
|
chrono = ["chrono/default"]
|
||||||
_deflate-any = []
|
_deflate-any = []
|
||||||
deflate = ["flate2/rust_backend", "_deflate-any"]
|
deflate = ["flate2/rust_backend", "_deflate-any"]
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
use zip::{AesMode, CompressionMethod};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
std::process::exit(real_main());
|
std::process::exit(real_main());
|
||||||
|
@ -38,6 +40,24 @@ fn doit(filename: &str) -> zip::result::ZipResult<()> {
|
||||||
zip.start_file("test/lorem_ipsum.txt", options)?;
|
zip.start_file("test/lorem_ipsum.txt", options)?;
|
||||||
zip.write_all(LOREM_IPSUM)?;
|
zip.write_all(LOREM_IPSUM)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
{
|
||||||
|
zip.start_file(
|
||||||
|
"test/lorem_ipsum.aes.txt",
|
||||||
|
options
|
||||||
|
.compression_method(CompressionMethod::Zstd)
|
||||||
|
.with_aes_encryption(AesMode::Aes256, "password"),
|
||||||
|
)?;
|
||||||
|
zip.write_all(LOREM_IPSUM)?;
|
||||||
|
|
||||||
|
// This should use AE-1 due to the short file length.
|
||||||
|
zip.start_file(
|
||||||
|
"test/short.aes.txt",
|
||||||
|
options.with_aes_encryption(AesMode::Aes256, "password"),
|
||||||
|
)?;
|
||||||
|
zip.write_all(b"short text\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
zip.finish()?;
|
zip.finish()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,38 +7,38 @@ use std::io::{Cursor, Read, Seek, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Arbitrary, Clone, Debug)]
|
#[derive(Arbitrary, Clone, Debug)]
|
||||||
pub enum BasicFileOperation {
|
pub enum BasicFileOperation<'k> {
|
||||||
WriteNormalFile {
|
WriteNormalFile {
|
||||||
contents: Vec<Vec<u8>>,
|
contents: Vec<Vec<u8>>,
|
||||||
options: zip::write::FullFileOptions,
|
options: zip::write::FullFileOptions<'k>,
|
||||||
},
|
},
|
||||||
WriteDirectory(zip::write::FullFileOptions),
|
WriteDirectory(zip::write::FullFileOptions<'k>),
|
||||||
WriteSymlinkWithTarget {
|
WriteSymlinkWithTarget {
|
||||||
target: PathBuf,
|
target: PathBuf,
|
||||||
options: zip::write::FullFileOptions,
|
options: zip::write::FullFileOptions<'k>,
|
||||||
},
|
},
|
||||||
ShallowCopy(Box<FileOperation>),
|
ShallowCopy(Box<FileOperation<'k>>),
|
||||||
DeepCopy(Box<FileOperation>),
|
DeepCopy(Box<FileOperation<'k>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Arbitrary, Clone, Debug)]
|
#[derive(Arbitrary, Clone, Debug)]
|
||||||
pub struct FileOperation {
|
pub struct FileOperation<'k> {
|
||||||
basic: BasicFileOperation,
|
basic: BasicFileOperation<'k>,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
reopen: bool,
|
reopen: bool,
|
||||||
// 'abort' flag is separate, to prevent trying to copy an aborted file
|
// 'abort' flag is separate, to prevent trying to copy an aborted file
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Arbitrary, Clone, Debug)]
|
#[derive(Arbitrary, Clone, Debug)]
|
||||||
pub struct FuzzTestCase {
|
pub struct FuzzTestCase<'k> {
|
||||||
comment: Vec<u8>,
|
comment: Vec<u8>,
|
||||||
operations: Vec<(FileOperation, bool)>,
|
operations: Vec<(FileOperation<'k>, bool)>,
|
||||||
flush_on_finish_file: bool,
|
flush_on_finish_file: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_operation<T>(
|
fn do_operation<'k, T>(
|
||||||
writer: &mut zip::ZipWriter<T>,
|
writer: &mut zip::ZipWriter<T>,
|
||||||
operation: &FileOperation,
|
operation: &FileOperation<'k>,
|
||||||
abort: bool,
|
abort: bool,
|
||||||
flush_on_finish_file: bool,
|
flush_on_finish_file: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
|
255
src/aes.rs
255
src/aes.rs
|
@ -4,12 +4,15 @@
|
||||||
//! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2.
|
//! 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.
|
//! 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::aes_ctr::AesCipher;
|
||||||
use crate::types::AesMode;
|
use crate::types::AesMode;
|
||||||
|
use crate::{aes_ctr, result::ZipError};
|
||||||
use constant_time_eq::constant_time_eq;
|
use constant_time_eq::constant_time_eq;
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
|
use rand::RngCore;
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use std::io::{self, Error, ErrorKind, Read};
|
use std::io::{self, Error, ErrorKind, Read, Write};
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
/// The length of the password verifcation value in bytes
|
/// The length of the password verifcation value in bytes
|
||||||
const PWD_VERIFY_LENGTH: usize = 2;
|
const PWD_VERIFY_LENGTH: usize = 2;
|
||||||
|
@ -18,19 +21,38 @@ const AUTH_CODE_LENGTH: usize = 10;
|
||||||
/// The number of iterations used with PBKDF2
|
/// The number of iterations used with PBKDF2
|
||||||
const ITERATION_COUNT: u32 = 1000;
|
const ITERATION_COUNT: u32 = 1000;
|
||||||
|
|
||||||
/// Create a AesCipher depending on the used `AesMode` and the given `key`.
|
enum Cipher {
|
||||||
///
|
Aes128(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes128>>),
|
||||||
/// # Panics
|
Aes192(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes192>>),
|
||||||
///
|
Aes256(Box<aes_ctr::AesCtrZipKeyStream<aes_ctr::Aes256>>),
|
||||||
/// 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 {
|
impl Cipher {
|
||||||
AesMode::Aes128 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes128>::new(key))
|
/// Create a `Cipher` depending on the used `AesMode` and the given `key`.
|
||||||
as Box<dyn aes_ctr::AesCipher>,
|
///
|
||||||
AesMode::Aes192 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes192>::new(key))
|
/// # Panics
|
||||||
as Box<dyn aes_ctr::AesCipher>,
|
///
|
||||||
AesMode::Aes256 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes256>::new(key))
|
/// This panics if `key` doesn't have the correct size for the chosen aes mode.
|
||||||
as Box<dyn aes_ctr::AesCipher>,
|
fn from_mode(aes_mode: AesMode, key: &[u8]) -> Self {
|
||||||
|
match aes_mode {
|
||||||
|
AesMode::Aes128 => Cipher::Aes128(Box::new(aes_ctr::AesCtrZipKeyStream::<
|
||||||
|
aes_ctr::Aes128,
|
||||||
|
>::new(key))),
|
||||||
|
AesMode::Aes192 => Cipher::Aes192(Box::new(aes_ctr::AesCtrZipKeyStream::<
|
||||||
|
aes_ctr::Aes192,
|
||||||
|
>::new(key))),
|
||||||
|
AesMode::Aes256 => Cipher::Aes256(Box::new(aes_ctr::AesCtrZipKeyStream::<
|
||||||
|
aes_ctr::Aes256,
|
||||||
|
>::new(key))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crypt_in_place(&mut self, target: &mut [u8]) {
|
||||||
|
match self {
|
||||||
|
Self::Aes128(cipher) => cipher.crypt_in_place(target),
|
||||||
|
Self::Aes192(cipher) => cipher.crypt_in_place(target),
|
||||||
|
Self::Aes256(cipher) => cipher.crypt_in_place(target),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,12 +84,7 @@ impl<R: Read> AesReader<R> {
|
||||||
/// password was provided.
|
/// password was provided.
|
||||||
/// It isn't possible to check the authentication code in this step. This will be done after
|
/// It isn't possible to check the authentication code in this step. This will be done after
|
||||||
/// reading and decrypting the file.
|
/// reading and decrypting the file.
|
||||||
///
|
pub fn validate(mut self, password: &[u8]) -> Result<AesReaderValid<R>, ZipError> {
|
||||||
/// # 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 salt_length = self.aes_mode.salt_length();
|
||||||
let key_length = self.aes_mode.key_length();
|
let key_length = self.aes_mode.key_length();
|
||||||
|
|
||||||
|
@ -93,19 +110,19 @@ impl<R: Read> AesReader<R> {
|
||||||
// the last 2 bytes should equal the password verification value
|
// the last 2 bytes should equal the password verification value
|
||||||
if pwd_verification_value != pwd_verify {
|
if pwd_verification_value != pwd_verify {
|
||||||
// wrong password
|
// wrong password
|
||||||
return Ok(None);
|
return Err(ZipError::InvalidPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cipher = cipher_from_mode(self.aes_mode, decrypt_key);
|
let cipher = Cipher::from_mode(self.aes_mode, decrypt_key);
|
||||||
let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
|
let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
|
||||||
|
|
||||||
Ok(Some(AesReaderValid {
|
Ok(AesReaderValid {
|
||||||
reader: self.reader,
|
reader: self.reader,
|
||||||
data_remaining: self.data_length,
|
data_remaining: self.data_length,
|
||||||
cipher,
|
cipher,
|
||||||
hmac,
|
hmac,
|
||||||
finalized: false,
|
finalized: false,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +134,7 @@ impl<R: Read> AesReader<R> {
|
||||||
pub struct AesReaderValid<R: Read> {
|
pub struct AesReaderValid<R: Read> {
|
||||||
reader: R,
|
reader: R,
|
||||||
data_remaining: u64,
|
data_remaining: u64,
|
||||||
cipher: Box<dyn aes_ctr::AesCipher>,
|
cipher: Cipher,
|
||||||
hmac: Hmac<Sha1>,
|
hmac: Hmac<Sha1>,
|
||||||
finalized: bool,
|
finalized: bool,
|
||||||
}
|
}
|
||||||
|
@ -184,3 +201,189 @@ impl<R: Read> AesReaderValid<R> {
|
||||||
self.reader
|
self.reader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AesWriter<W> {
|
||||||
|
writer: W,
|
||||||
|
cipher: Cipher,
|
||||||
|
hmac: Hmac<Sha1>,
|
||||||
|
buffer: Zeroizing<Vec<u8>>,
|
||||||
|
encrypted_file_header: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> AesWriter<W> {
|
||||||
|
pub fn new(writer: W, aes_mode: AesMode, password: &[u8]) -> io::Result<Self> {
|
||||||
|
let salt_length = aes_mode.salt_length();
|
||||||
|
let key_length = aes_mode.key_length();
|
||||||
|
|
||||||
|
let mut encrypted_file_header = Vec::with_capacity(salt_length + 2);
|
||||||
|
|
||||||
|
let mut salt = vec![0; salt_length];
|
||||||
|
rand::thread_rng().fill_bytes(&mut salt);
|
||||||
|
encrypted_file_header.write_all(&salt)?;
|
||||||
|
|
||||||
|
// 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: Zeroizing<Vec<u8>> = Zeroizing::new(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)
|
||||||
|
.map_err(|e| Error::new(ErrorKind::InvalidInput, e))?;
|
||||||
|
let encryption_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..].to_vec();
|
||||||
|
encrypted_file_header.write_all(&pwd_verify)?;
|
||||||
|
|
||||||
|
let cipher = Cipher::from_mode(aes_mode, encryption_key);
|
||||||
|
let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
writer,
|
||||||
|
cipher,
|
||||||
|
hmac,
|
||||||
|
buffer: Default::default(),
|
||||||
|
encrypted_file_header: Some(encrypted_file_header),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(mut self) -> io::Result<W> {
|
||||||
|
self.write_encrypted_file_header()?;
|
||||||
|
|
||||||
|
// 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 computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];
|
||||||
|
self.writer.write_all(computed_auth_code)?;
|
||||||
|
|
||||||
|
Ok(self.writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The AES encryption specification requires some metadata being written at the start of the
|
||||||
|
/// file data section, but this can only be done once the extra data writing has been finished
|
||||||
|
/// so we can't do it when the writer is constructed.
|
||||||
|
fn write_encrypted_file_header(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(header) = self.encrypted_file_header.take() {
|
||||||
|
self.writer.write_all(&header)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for AesWriter<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.write_encrypted_file_header()?;
|
||||||
|
|
||||||
|
// Fill the internal buffer and encrypt it in-place.
|
||||||
|
self.buffer.extend_from_slice(buf);
|
||||||
|
self.cipher.crypt_in_place(&mut self.buffer[..]);
|
||||||
|
|
||||||
|
// Update the hmac with the encrypted data.
|
||||||
|
self.hmac.update(&self.buffer[..]);
|
||||||
|
|
||||||
|
// Write the encrypted buffer to the inner writer. We need to use `write_all` here as if
|
||||||
|
// we only write parts of the data we can't easily reverse the keystream in the cipher
|
||||||
|
// implementation.
|
||||||
|
self.writer.write_all(&self.buffer[..])?;
|
||||||
|
|
||||||
|
// Zeroize the backing memory before clearing the buffer to prevent cleartext data from
|
||||||
|
// being left in memory.
|
||||||
|
self.buffer.zeroize();
|
||||||
|
self.buffer.clear();
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
aes::{AesReader, AesWriter},
|
||||||
|
result::ZipError,
|
||||||
|
types::AesMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Checks whether `AesReader` can successfully decrypt what `AesWriter` produces.
|
||||||
|
fn roundtrip(aes_mode: AesMode, password: &[u8], plaintext: &[u8]) -> Result<bool, ZipError> {
|
||||||
|
let mut buf = io::Cursor::new(vec![]);
|
||||||
|
let mut read_buffer = vec![];
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut writer = AesWriter::new(&mut buf, aes_mode, password)?;
|
||||||
|
writer.write_all(plaintext)?;
|
||||||
|
writer.finish()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset cursor position to the beginning.
|
||||||
|
buf.set_position(0);
|
||||||
|
|
||||||
|
{
|
||||||
|
let compressed_length = buf.get_ref().len() as u64;
|
||||||
|
let mut reader =
|
||||||
|
AesReader::new(&mut buf, aes_mode, compressed_length).validate(password)?;
|
||||||
|
reader.read_to_end(&mut read_buffer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(plaintext == read_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_256_0_byte() {
|
||||||
|
let plaintext = &[];
|
||||||
|
let password = b"some super secret password";
|
||||||
|
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_128_5_byte() {
|
||||||
|
let plaintext = b"asdf\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes128, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_192_5_byte() {
|
||||||
|
let plaintext = b"asdf\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes192, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_256_5_byte() {
|
||||||
|
let plaintext = b"asdf\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_128_40_byte() {
|
||||||
|
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes128, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_192_40_byte() {
|
||||||
|
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes192, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crypt_aes_256_40_byte() {
|
||||||
|
let plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
|
||||||
|
let password = b"some super secret password";
|
||||||
|
|
||||||
|
assert!(roundtrip(AesMode::Aes256, password, plaintext).expect("could encrypt and decrypt"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
#![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06
|
#![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06
|
||||||
pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
|
pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
|
||||||
pub use crate::read::ZipArchive;
|
pub use crate::read::ZipArchive;
|
||||||
pub use crate::types::DateTime;
|
pub use crate::types::{AesMode, DateTime};
|
||||||
pub use crate::write::ZipWriter;
|
pub use crate::write::ZipWriter;
|
||||||
|
|
||||||
#[cfg(feature = "aes-crypto")]
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
|
44
src/read.rs
44
src/read.rs
|
@ -238,7 +238,7 @@ pub(crate) fn make_crypto_reader<'a>(
|
||||||
using_data_descriptor: bool,
|
using_data_descriptor: bool,
|
||||||
reader: io::Take<&'a mut dyn Read>,
|
reader: io::Take<&'a mut dyn Read>,
|
||||||
password: Option<&[u8]>,
|
password: Option<&[u8]>,
|
||||||
aes_info: Option<(AesMode, AesVendorVersion)>,
|
aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
|
||||||
#[cfg(feature = "aes-crypto")] compressed_size: u64,
|
#[cfg(feature = "aes-crypto")] compressed_size: u64,
|
||||||
) -> ZipResult<CryptoReader<'a>> {
|
) -> ZipResult<CryptoReader<'a>> {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
|
@ -256,25 +256,17 @@ pub(crate) fn make_crypto_reader<'a>(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "aes-crypto")]
|
#[cfg(feature = "aes-crypto")]
|
||||||
(Some(password), Some((aes_mode, vendor_version))) => {
|
(Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes {
|
||||||
match AesReader::new(reader, aes_mode, compressed_size).validate(password)? {
|
reader: AesReader::new(reader, aes_mode, compressed_size).validate(password)?,
|
||||||
None => return Err(InvalidPassword),
|
vendor_version,
|
||||||
Some(r) => CryptoReader::Aes {
|
},
|
||||||
reader: r,
|
|
||||||
vendor_version,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(password), None) => {
|
(Some(password), None) => {
|
||||||
let validator = if using_data_descriptor {
|
let validator = if using_data_descriptor {
|
||||||
ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
|
ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
|
||||||
} else {
|
} else {
|
||||||
ZipCryptoValidator::PkzipCrc32(crc32)
|
ZipCryptoValidator::PkzipCrc32(crc32)
|
||||||
};
|
};
|
||||||
match ZipCryptoReader::new(reader, password).validate(validator)? {
|
CryptoReader::ZipCrypto(ZipCryptoReader::new(reader, password).validate(validator)?)
|
||||||
None => return Err(InvalidPassword),
|
|
||||||
Some(r) => CryptoReader::ZipCrypto(r),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(None, Some(_)) => return Err(InvalidPassword),
|
(None, Some(_)) => return Err(InvalidPassword),
|
||||||
(None, None) => CryptoReader::Plaintext(reader),
|
(None, None) => CryptoReader::Plaintext(reader),
|
||||||
|
@ -927,11 +919,13 @@ fn central_header_to_zip_file_inner<R: Read>(
|
||||||
central_extra_field: None,
|
central_extra_field: None,
|
||||||
file_comment,
|
file_comment,
|
||||||
header_start: offset,
|
header_start: offset,
|
||||||
|
extra_data_start: None,
|
||||||
central_header_start,
|
central_header_start,
|
||||||
data_start: OnceLock::new(),
|
data_start: OnceLock::new(),
|
||||||
external_attributes: external_file_attributes,
|
external_attributes: external_file_attributes,
|
||||||
large_file: false,
|
large_file: false,
|
||||||
aes_mode: None,
|
aes_mode: None,
|
||||||
|
aes_extra_data_start: 0,
|
||||||
extra_fields: Vec::new(),
|
extra_fields: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -996,7 +990,8 @@ fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
|
||||||
let mut out = [0u8];
|
let mut out = [0u8];
|
||||||
reader.read_exact(&mut out)?;
|
reader.read_exact(&mut out)?;
|
||||||
let aes_mode = out[0];
|
let aes_mode = out[0];
|
||||||
let compression_method = reader.read_u16_le()?;
|
#[allow(deprecated)]
|
||||||
|
let compression_method = CompressionMethod::from_u16(reader.read_u16_le()?);
|
||||||
|
|
||||||
if vendor_id != 0x4541 {
|
if vendor_id != 0x4541 {
|
||||||
return Err(ZipError::InvalidArchive("Invalid AES vendor"));
|
return Err(ZipError::InvalidArchive("Invalid AES vendor"));
|
||||||
|
@ -1007,15 +1002,18 @@ fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
|
||||||
_ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")),
|
_ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")),
|
||||||
};
|
};
|
||||||
match aes_mode {
|
match aes_mode {
|
||||||
0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version)),
|
0x01 => {
|
||||||
0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version)),
|
file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method))
|
||||||
0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version)),
|
}
|
||||||
|
0x02 => {
|
||||||
|
file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method))
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method))
|
||||||
|
}
|
||||||
_ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")),
|
_ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")),
|
||||||
};
|
};
|
||||||
file.compression_method = {
|
file.compression_method = compression_method;
|
||||||
#[allow(deprecated)]
|
|
||||||
CompressionMethod::from_u16(compression_method)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
0x5455 => {
|
0x5455 => {
|
||||||
// extended timestamp
|
// extended timestamp
|
||||||
|
@ -1312,6 +1310,7 @@ pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Opt
|
||||||
// header_start and data start are not available, but also don't matter, since seeking is
|
// header_start and data start are not available, but also don't matter, since seeking is
|
||||||
// not available.
|
// not available.
|
||||||
header_start: 0,
|
header_start: 0,
|
||||||
|
extra_data_start: None,
|
||||||
data_start: OnceLock::new(),
|
data_start: OnceLock::new(),
|
||||||
central_header_start: 0,
|
central_header_start: 0,
|
||||||
// The external_attributes field is only available in the central directory.
|
// The external_attributes field is only available in the central directory.
|
||||||
|
@ -1320,6 +1319,7 @@ pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Opt
|
||||||
external_attributes: 0,
|
external_attributes: 0,
|
||||||
large_file: false,
|
large_file: false,
|
||||||
aes_mode: None,
|
aes_mode: None,
|
||||||
|
aes_extra_data_start: 0,
|
||||||
extra_fields: Vec::new(),
|
extra_fields: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
28
src/types.rs
28
src/types.rs
|
@ -48,8 +48,8 @@ mod atomic {
|
||||||
|
|
||||||
use crate::extra_fields::ExtraField;
|
use crate::extra_fields::ExtraField;
|
||||||
use crate::result::DateTimeRangeError;
|
use crate::result::DateTimeRangeError;
|
||||||
use crate::types::ffi::S_IFDIR;
|
|
||||||
use crate::CompressionMethod;
|
use crate::CompressionMethod;
|
||||||
|
use crate::types::ffi::S_IFDIR;
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
|
use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
|
||||||
|
|
||||||
|
@ -353,6 +353,8 @@ pub struct ZipFileData {
|
||||||
pub file_comment: Box<str>,
|
pub file_comment: Box<str>,
|
||||||
/// Specifies where the local header of the file starts
|
/// Specifies where the local header of the file starts
|
||||||
pub header_start: u64,
|
pub header_start: u64,
|
||||||
|
/// Specifies where the extra data of the file starts
|
||||||
|
pub extra_data_start: Option<u64>,
|
||||||
/// Specifies where the central header of the file starts
|
/// Specifies where the central header of the file starts
|
||||||
///
|
///
|
||||||
/// Note that when this is not known, it is set to 0
|
/// Note that when this is not known, it is set to 0
|
||||||
|
@ -364,7 +366,9 @@ pub struct ZipFileData {
|
||||||
/// Reserve local ZIP64 extra field
|
/// Reserve local ZIP64 extra field
|
||||||
pub large_file: bool,
|
pub large_file: bool,
|
||||||
/// AES mode if applicable
|
/// AES mode if applicable
|
||||||
pub aes_mode: Option<(AesMode, AesVendorVersion)>,
|
pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
|
||||||
|
/// Specifies where in the extra data the AES metadata starts
|
||||||
|
pub aes_extra_data_start: u64,
|
||||||
|
|
||||||
/// extra fields, see <https://libzip.org/specifications/extrafld.txt>
|
/// extra fields, see <https://libzip.org/specifications/extrafld.txt>
|
||||||
pub extra_fields: Vec<ExtraField>,
|
pub extra_fields: Vec<ExtraField>,
|
||||||
|
@ -498,25 +502,33 @@ impl ZipFileData {
|
||||||
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
|
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
|
||||||
/// does not make use of the CRC check.
|
/// does not make use of the CRC check.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
#[repr(u16)]
|
||||||
pub enum AesVendorVersion {
|
pub enum AesVendorVersion {
|
||||||
Ae1,
|
Ae1 = 0x0001,
|
||||||
Ae2,
|
Ae2 = 0x0002,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AES variant used.
|
/// AES variant used.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
|
||||||
|
#[repr(u8)]
|
||||||
pub enum AesMode {
|
pub enum AesMode {
|
||||||
Aes128,
|
/// 128-bit AES encryption.
|
||||||
Aes192,
|
Aes128 = 0x01,
|
||||||
Aes256,
|
/// 192-bit AES encryption.
|
||||||
|
Aes192 = 0x02,
|
||||||
|
/// 256-bit AES encryption.
|
||||||
|
Aes256 = 0x03,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "aes-crypto")]
|
#[cfg(feature = "aes-crypto")]
|
||||||
impl AesMode {
|
impl AesMode {
|
||||||
|
/// Length of the salt for the given AES mode.
|
||||||
pub const fn salt_length(&self) -> usize {
|
pub const fn salt_length(&self) -> usize {
|
||||||
self.key_length() / 2
|
self.key_length() / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Length of the key for the given AES mode.
|
||||||
pub const fn key_length(&self) -> usize {
|
pub const fn key_length(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::Aes128 => 16,
|
Self::Aes128 => 16,
|
||||||
|
@ -562,11 +574,13 @@ mod test {
|
||||||
central_extra_field: None,
|
central_extra_field: None,
|
||||||
file_comment: String::with_capacity(0).into_boxed_str(),
|
file_comment: String::with_capacity(0).into_boxed_str(),
|
||||||
header_start: 0,
|
header_start: 0,
|
||||||
|
extra_data_start: None,
|
||||||
data_start: OnceLock::new(),
|
data_start: OnceLock::new(),
|
||||||
central_header_start: 0,
|
central_header_start: 0,
|
||||||
external_attributes: 0,
|
external_attributes: 0,
|
||||||
large_file: false,
|
large_file: false,
|
||||||
aes_mode: None,
|
aes_mode: None,
|
||||||
|
aes_extra_data_start: 0,
|
||||||
extra_fields: Vec::new(),
|
extra_fields: Vec::new(),
|
||||||
};
|
};
|
||||||
assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
|
assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
|
||||||
|
|
|
@ -17,8 +17,8 @@ pub mod write {
|
||||||
/// This is not recommended for new archives, as ZipCrypto is not secure.
|
/// This is not recommended for new archives, as ZipCrypto is not secure.
|
||||||
fn with_deprecated_encryption(self, password: &[u8]) -> Self;
|
fn with_deprecated_encryption(self, password: &[u8]) -> Self;
|
||||||
}
|
}
|
||||||
impl<T: FileOptionExtension> FileOptionsExt for FileOptions<T> {
|
impl<'k, T: FileOptionExtension> FileOptionsExt for FileOptions<'k, T> {
|
||||||
fn with_deprecated_encryption(self, password: &[u8]) -> Self {
|
fn with_deprecated_encryption(self, password: &[u8]) -> FileOptions<'static, T> {
|
||||||
self.with_deprecated_encryption(password)
|
self.with_deprecated_encryption(password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
241
src/write.rs
241
src/write.rs
|
@ -1,10 +1,14 @@
|
||||||
//! Types for creating ZIP archives
|
//! Types for creating ZIP archives
|
||||||
|
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
use crate::aes::AesWriter;
|
||||||
use crate::compression::CompressionMethod;
|
use crate::compression::CompressionMethod;
|
||||||
use crate::read::{find_content, ZipArchive, ZipFile, ZipFileReader};
|
use crate::read::{find_content, ZipArchive, ZipFile, ZipFileReader};
|
||||||
use crate::result::{ZipError, ZipResult};
|
use crate::result::{ZipError, ZipResult};
|
||||||
use crate::spec;
|
use crate::spec;
|
||||||
use crate::types::{ffi, DateTime, System, ZipFileData, DEFAULT_VERSION};
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
use crate::types::AesMode;
|
||||||
|
use crate::types::{ffi, AesVendorVersion, DateTime, System, ZipFileData, DEFAULT_VERSION};
|
||||||
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))]
|
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))]
|
||||||
use core::num::NonZeroU64;
|
use core::num::NonZeroU64;
|
||||||
use crc32fast::Hasher;
|
use crc32fast::Hasher;
|
||||||
|
@ -13,6 +17,7 @@ use std::default::Default;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::io::{BufReader, SeekFrom};
|
use std::io::{BufReader, SeekFrom};
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::str::{from_utf8, Utf8Error};
|
use std::str::{from_utf8, Utf8Error};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
@ -42,19 +47,25 @@ use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
enum MaybeEncrypted<W> {
|
enum MaybeEncrypted<W> {
|
||||||
Unencrypted(W),
|
Unencrypted(W),
|
||||||
Encrypted(crate::zipcrypto::ZipCryptoWriter<W>),
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
Aes(crate::aes::AesWriter<W>),
|
||||||
|
ZipCrypto(crate::zipcrypto::ZipCryptoWriter<W>),
|
||||||
}
|
}
|
||||||
impl<W: Write> Write for MaybeEncrypted<W> {
|
impl<W: Write> Write for MaybeEncrypted<W> {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
MaybeEncrypted::Unencrypted(w) => w.write(buf),
|
MaybeEncrypted::Unencrypted(w) => w.write(buf),
|
||||||
MaybeEncrypted::Encrypted(w) => w.write(buf),
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
MaybeEncrypted::Aes(w) => w.write(buf),
|
||||||
|
MaybeEncrypted::ZipCrypto(w) => w.write(buf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
MaybeEncrypted::Unencrypted(w) => w.flush(),
|
MaybeEncrypted::Unencrypted(w) => w.flush(),
|
||||||
MaybeEncrypted::Encrypted(w) => w.flush(),
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
MaybeEncrypted::Aes(w) => w.flush(),
|
||||||
|
MaybeEncrypted::ZipCrypto(w) => w.flush(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,24 +188,52 @@ mod sealed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
enum EncryptWith<'k> {
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
Aes {
|
||||||
|
mode: AesMode,
|
||||||
|
password: &'k str,
|
||||||
|
},
|
||||||
|
ZipCrypto(ZipCryptoKeys, PhantomData<&'k ()>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(fuzzing)]
|
||||||
|
impl<'a> arbitrary::Arbitrary<'a> for EncryptWith<'a> {
|
||||||
|
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
if bool::arbitrary(u)? {
|
||||||
|
return Ok(EncryptWith::Aes {
|
||||||
|
mode: AesMode::arbitrary(u)?,
|
||||||
|
password: u.arbitrary::<&str>()?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EncryptWith::ZipCrypto(
|
||||||
|
ZipCryptoKeys::arbitrary(u)?,
|
||||||
|
PhantomData,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata for a file to be written
|
/// Metadata for a file to be written
|
||||||
#[derive(Clone, Debug, Copy)]
|
#[derive(Clone, Debug, Copy)]
|
||||||
pub struct FileOptions<T: FileOptionExtension> {
|
pub struct FileOptions<'k, T: FileOptionExtension> {
|
||||||
pub(crate) compression_method: CompressionMethod,
|
pub(crate) compression_method: CompressionMethod,
|
||||||
pub(crate) compression_level: Option<i64>,
|
pub(crate) compression_level: Option<i64>,
|
||||||
pub(crate) last_modified_time: DateTime,
|
pub(crate) last_modified_time: DateTime,
|
||||||
pub(crate) permissions: Option<u32>,
|
pub(crate) permissions: Option<u32>,
|
||||||
pub(crate) large_file: bool,
|
pub(crate) large_file: bool,
|
||||||
encrypt_with: Option<ZipCryptoKeys>,
|
encrypt_with: Option<EncryptWith<'k>>,
|
||||||
extended_options: T,
|
extended_options: T,
|
||||||
alignment: u16,
|
alignment: u16,
|
||||||
#[cfg(feature = "deflate-zopfli")]
|
#[cfg(feature = "deflate-zopfli")]
|
||||||
pub(super) zopfli_buffer_size: Option<usize>,
|
pub(super) zopfli_buffer_size: Option<usize>,
|
||||||
}
|
}
|
||||||
/// Simple File Options. Can be copied and good for simple writing zip files
|
/// Simple File Options. Can be copied and good for simple writing zip files
|
||||||
pub type SimpleFileOptions = FileOptions<()>;
|
pub type SimpleFileOptions = FileOptions<'static, ()>;
|
||||||
/// Adds Extra Data and Central Extra Data. It does not implement copy.
|
/// Adds Extra Data and Central Extra Data. It does not implement copy.
|
||||||
pub type FullFileOptions = FileOptions<ExtendedFileOptions>;
|
pub type FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>;
|
||||||
/// The Extension for Extra Data and Central Extra Data
|
/// The Extension for Extra Data and Central Extra Data
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ExtendedFileOptions {
|
pub struct ExtendedFileOptions {
|
||||||
|
@ -203,15 +242,15 @@ pub struct ExtendedFileOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(fuzzing)]
|
#[cfg(fuzzing)]
|
||||||
impl arbitrary::Arbitrary<'_> for FileOptions<ExtendedFileOptions> {
|
impl<'a> arbitrary::Arbitrary<'a> for FileOptions<'a, ExtendedFileOptions> {
|
||||||
fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Self> {
|
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
let mut options = FullFileOptions {
|
let mut options = FullFileOptions {
|
||||||
compression_method: CompressionMethod::arbitrary(u)?,
|
compression_method: CompressionMethod::arbitrary(u)?,
|
||||||
compression_level: None,
|
compression_level: None,
|
||||||
last_modified_time: DateTime::arbitrary(u)?,
|
last_modified_time: DateTime::arbitrary(u)?,
|
||||||
permissions: Option::<u32>::arbitrary(u)?,
|
permissions: Option::<u32>::arbitrary(u)?,
|
||||||
large_file: bool::arbitrary(u)?,
|
large_file: bool::arbitrary(u)?,
|
||||||
encrypt_with: Option::<ZipCryptoKeys>::arbitrary(u)?,
|
encrypt_with: Option::<EncryptWith>::arbitrary(u)?,
|
||||||
alignment: u16::arbitrary(u)?,
|
alignment: u16::arbitrary(u)?,
|
||||||
#[cfg(feature = "deflate-zopfli")]
|
#[cfg(feature = "deflate-zopfli")]
|
||||||
zopfli_buffer_size: None,
|
zopfli_buffer_size: None,
|
||||||
|
@ -253,7 +292,7 @@ impl arbitrary::Arbitrary<'_> for FileOptions<ExtendedFileOptions> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FileOptionExtension> FileOptions<T> {
|
impl<'k, T: FileOptionExtension> FileOptions<'k, T> {
|
||||||
/// Set the compression method for the new file
|
/// Set the compression method for the new file
|
||||||
///
|
///
|
||||||
/// The default is `CompressionMethod::Deflated` if it is enabled. If not,
|
/// The default is `CompressionMethod::Deflated` if it is enabled. If not,
|
||||||
|
@ -317,9 +356,24 @@ impl<T: FileOptionExtension> FileOptions<T> {
|
||||||
self.large_file = large;
|
self.large_file = large;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> Self {
|
|
||||||
self.encrypt_with = Some(ZipCryptoKeys::derive(password));
|
pub(crate) fn with_deprecated_encryption(self, password: &[u8]) -> FileOptions<'static, T> {
|
||||||
self
|
FileOptions {
|
||||||
|
encrypt_with: Some(EncryptWith::ZipCrypto(
|
||||||
|
ZipCryptoKeys::derive(password),
|
||||||
|
PhantomData,
|
||||||
|
)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the AES encryption parameters.
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
pub fn with_aes_encryption(self, mode: AesMode, password: &str) -> FileOptions<'_, T> {
|
||||||
|
FileOptions {
|
||||||
|
encrypt_with: Some(EncryptWith::Aes { mode, password }),
|
||||||
|
..self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the size of the buffer used to hold the next block that Zopfli will compress. The
|
/// Sets the size of the buffer used to hold the next block that Zopfli will compress. The
|
||||||
|
@ -344,7 +398,7 @@ impl<T: FileOptionExtension> FileOptions<T> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FileOptions<ExtendedFileOptions> {
|
impl<'k> FileOptions<'k, ExtendedFileOptions> {
|
||||||
/// Adds an extra data field.
|
/// Adds an extra data field.
|
||||||
pub fn add_extra_data(
|
pub fn add_extra_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -396,7 +450,7 @@ impl FileOptions<ExtendedFileOptions> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T: FileOptionExtension> Default for FileOptions<T> {
|
impl<'k, T: FileOptionExtension> Default for FileOptions<'k, T> {
|
||||||
/// Construct a new FileOptions object
|
/// Construct a new FileOptions object
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -700,16 +754,57 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
uncompressed_size: 0,
|
uncompressed_size: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut extra_field = options.extended_options.extra_data().cloned();
|
||||||
|
|
||||||
|
// Write AES encryption extra data.
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut aes_extra_data_start = 0;
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
if let Some(EncryptWith::Aes { .. }) = options.encrypt_with {
|
||||||
|
const AES_DUMMY_EXTRA_DATA: [u8; 11] = [
|
||||||
|
0x01, 0x99, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
let extra_data = extra_field.get_or_insert_with(Default::default);
|
||||||
|
let extra_data = match Arc::get_mut(extra_data) {
|
||||||
|
Some(exclusive) => exclusive,
|
||||||
|
None => {
|
||||||
|
let new = Arc::new(extra_data.to_vec());
|
||||||
|
Arc::get_mut(extra_field.insert(new)).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if extra_data.len() + AES_DUMMY_EXTRA_DATA.len() > u16::MAX as usize {
|
||||||
|
let _ = self.abort_file();
|
||||||
|
return Err(InvalidArchive("Extra data field is too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
aes_extra_data_start = extra_data.len() as u64;
|
||||||
|
|
||||||
|
// We write zero bytes for now since we need to update the data when finishing the
|
||||||
|
// file.
|
||||||
|
extra_data.write_all(&AES_DUMMY_EXTRA_DATA)?;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let header_start = self.inner.get_plain().stream_position()?;
|
let header_start = self.inner.get_plain().stream_position()?;
|
||||||
|
|
||||||
let permissions = options.permissions.unwrap_or(0o100644);
|
let permissions = options.permissions.unwrap_or(0o100644);
|
||||||
|
let (compression_method, aes_mode) = match options.encrypt_with {
|
||||||
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
Some(EncryptWith::Aes { mode, .. }) => (
|
||||||
|
CompressionMethod::Aes,
|
||||||
|
Some((mode, AesVendorVersion::Ae2, options.compression_method)),
|
||||||
|
),
|
||||||
|
_ => (options.compression_method, None),
|
||||||
|
};
|
||||||
let file = ZipFileData {
|
let file = ZipFileData {
|
||||||
system: System::Unix,
|
system: System::Unix,
|
||||||
version_made_by: DEFAULT_VERSION,
|
version_made_by: DEFAULT_VERSION,
|
||||||
encrypted: options.encrypt_with.is_some(),
|
encrypted: options.encrypt_with.is_some(),
|
||||||
using_data_descriptor: false,
|
using_data_descriptor: false,
|
||||||
compression_method: options.compression_method,
|
compression_method,
|
||||||
compression_level: options.compression_level,
|
compression_level: options.compression_level,
|
||||||
last_modified_time: options.last_modified_time,
|
last_modified_time: options.last_modified_time,
|
||||||
crc32: raw_values.crc32,
|
crc32: raw_values.crc32,
|
||||||
|
@ -717,15 +812,18 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
uncompressed_size: raw_values.uncompressed_size,
|
uncompressed_size: raw_values.uncompressed_size,
|
||||||
file_name: name.into(),
|
file_name: name.into(),
|
||||||
file_name_raw: vec![].into_boxed_slice(), // Never used for saving
|
file_name_raw: vec![].into_boxed_slice(), // Never used for saving
|
||||||
extra_field: options.extended_options.extra_data().cloned(),
|
extra_field,
|
||||||
central_extra_field: options.extended_options.central_extra_data().cloned(),
|
central_extra_field: options.extended_options.central_extra_data().cloned(),
|
||||||
file_comment: String::with_capacity(0).into_boxed_str(),
|
file_comment: String::with_capacity(0).into_boxed_str(),
|
||||||
header_start,
|
header_start,
|
||||||
|
extra_data_start: None,
|
||||||
data_start: OnceLock::new(),
|
data_start: OnceLock::new(),
|
||||||
central_header_start: 0,
|
central_header_start: 0,
|
||||||
external_attributes: permissions << 16,
|
external_attributes: permissions << 16,
|
||||||
large_file: options.large_file,
|
large_file: options.large_file,
|
||||||
aes_mode: None,
|
aes_mode,
|
||||||
|
aes_extra_data_start,
|
||||||
|
|
||||||
extra_fields: Vec::new(),
|
extra_fields: Vec::new(),
|
||||||
};
|
};
|
||||||
let index = self.insert_file_data(file)?;
|
let index = self.insert_file_data(file)?;
|
||||||
|
@ -777,6 +875,7 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
write_local_zip64_extra_field(writer, file)?;
|
write_local_zip64_extra_field(writer, file)?;
|
||||||
}
|
}
|
||||||
if let Some(extra_field) = &file.extra_field {
|
if let Some(extra_field) = &file.extra_field {
|
||||||
|
file.extra_data_start = Some(writer.stream_position()?);
|
||||||
writer.write_all(extra_field)?;
|
writer.write_all(extra_field)?;
|
||||||
}
|
}
|
||||||
let mut header_end = writer.stream_position()?;
|
let mut header_end = writer.stream_position()?;
|
||||||
|
@ -816,17 +915,29 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
debug_assert_eq!(header_end % align, 0);
|
debug_assert_eq!(header_end % align, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(keys) = options.encrypt_with {
|
match options.encrypt_with {
|
||||||
let mut zipwriter = crate::zipcrypto::ZipCryptoWriter {
|
#[cfg(feature = "aes-crypto")]
|
||||||
writer: mem::replace(&mut self.inner, Closed).unwrap(),
|
Some(EncryptWith::Aes { mode, password }) => {
|
||||||
buffer: vec![],
|
let aeswriter = AesWriter::new(
|
||||||
keys,
|
mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(),
|
||||||
};
|
mode,
|
||||||
let crypto_header = [0u8; 12];
|
password.as_bytes(),
|
||||||
|
)?;
|
||||||
|
self.inner = GenericZipWriter::Storer(MaybeEncrypted::Aes(aeswriter));
|
||||||
|
}
|
||||||
|
Some(EncryptWith::ZipCrypto(keys, ..)) => {
|
||||||
|
let mut zipwriter = crate::zipcrypto::ZipCryptoWriter {
|
||||||
|
writer: mem::replace(&mut self.inner, Closed).unwrap(),
|
||||||
|
buffer: vec![],
|
||||||
|
keys,
|
||||||
|
};
|
||||||
|
let crypto_header = [0u8; 12];
|
||||||
|
|
||||||
zipwriter.write_all(&crypto_header)?;
|
zipwriter.write_all(&crypto_header)?;
|
||||||
header_end = zipwriter.writer.stream_position()?;
|
header_end = zipwriter.writer.stream_position()?;
|
||||||
self.inner = Storer(MaybeEncrypted::Encrypted(zipwriter));
|
self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
self.stats.start = header_end;
|
self.stats.start = header_end;
|
||||||
debug_assert!(file.data_start.get().is_none());
|
debug_assert!(file.data_start.get().is_none());
|
||||||
|
@ -867,13 +978,28 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
Some((_, f)) => f,
|
Some((_, f)) => f,
|
||||||
};
|
};
|
||||||
file.crc32 = self.stats.hasher.clone().finalize();
|
|
||||||
file.uncompressed_size = self.stats.bytes_written;
|
file.uncompressed_size = self.stats.bytes_written;
|
||||||
|
|
||||||
let file_end = writer.stream_position()?;
|
let file_end = writer.stream_position()?;
|
||||||
debug_assert!(file_end >= self.stats.start);
|
debug_assert!(file_end >= self.stats.start);
|
||||||
file.compressed_size = file_end - self.stats.start;
|
file.compressed_size = file_end - self.stats.start;
|
||||||
|
|
||||||
|
file.crc32 = self.stats.hasher.clone().finalize();
|
||||||
|
if let Some(aes_mode) = &mut file.aes_mode {
|
||||||
|
// We prefer using AE-1 which provides an extra CRC check, but for small files we
|
||||||
|
// switch to AE-2 to prevent being able to use the CRC value to to reconstruct the
|
||||||
|
// unencrypted contents.
|
||||||
|
//
|
||||||
|
// C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq
|
||||||
|
aes_mode.1 = if self.stats.bytes_written < 20 {
|
||||||
|
file.crc32 = 0;
|
||||||
|
AesVendorVersion::Ae2
|
||||||
|
} else {
|
||||||
|
AesVendorVersion::Ae1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_aes_extra_data(writer, file)?;
|
||||||
update_local_file_header(writer, file)?;
|
update_local_file_header(writer, file)?;
|
||||||
writer.seek(SeekFrom::Start(file_end))?;
|
writer.seek(SeekFrom::Start(file_end))?;
|
||||||
}
|
}
|
||||||
|
@ -890,7 +1016,11 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
|
|
||||||
fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> {
|
fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> {
|
||||||
match mem::replace(&mut self.inner, Closed) {
|
match mem::replace(&mut self.inner, Closed) {
|
||||||
Storer(MaybeEncrypted::Encrypted(writer)) => {
|
#[cfg(feature = "aes-crypto")]
|
||||||
|
Storer(MaybeEncrypted::Aes(writer)) => {
|
||||||
|
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish()?));
|
||||||
|
}
|
||||||
|
Storer(MaybeEncrypted::ZipCrypto(writer)) => {
|
||||||
let crc32 = self.stats.hasher.clone().finalize();
|
let crc32 = self.stats.hasher.clone().finalize();
|
||||||
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
|
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1287,7 @@ impl<W: Write + Seek> ZipWriter<W> {
|
||||||
}
|
}
|
||||||
*options.permissions.as_mut().unwrap() |= 0o40000;
|
*options.permissions.as_mut().unwrap() |= 0o40000;
|
||||||
options.compression_method = Stored;
|
options.compression_method = Stored;
|
||||||
|
options.encrypt_with = None;
|
||||||
|
|
||||||
let name_as_string = name.into();
|
let name_as_string = name.into();
|
||||||
// Append a slash to the filename if it does not end with it.
|
// Append a slash to the filename if it does not end with it.
|
||||||
|
@ -1469,7 +1600,7 @@ impl<W: Write + Seek> GenericZipWriter<W> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
CompressionMethod::AES => Err(ZipError::UnsupportedArchive(
|
CompressionMethod::AES => Err(ZipError::UnsupportedArchive(
|
||||||
"AES compression is not supported for writing",
|
"AES encryption is enabled through FileOptions::with_aes_encryption",
|
||||||
)),
|
)),
|
||||||
#[cfg(feature = "zstd")]
|
#[cfg(feature = "zstd")]
|
||||||
CompressionMethod::Zstd => {
|
CompressionMethod::Zstd => {
|
||||||
|
@ -1607,6 +1738,50 @@ fn clamp_opt<T: Ord + Copy, U: Ord + Copy + TryFrom<T>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_aes_extra_data<W: Write + io::Seek>(
|
||||||
|
writer: &mut W,
|
||||||
|
file: &mut ZipFileData,
|
||||||
|
) -> ZipResult<()> {
|
||||||
|
let Some((aes_mode, version, compression_method)) = file.aes_mode else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let extra_data_start = file.extra_data_start.unwrap();
|
||||||
|
|
||||||
|
writer.seek(io::SeekFrom::Start(
|
||||||
|
extra_data_start + file.aes_extra_data_start,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
// Extra field header ID.
|
||||||
|
buf.write_u16_le(0x9901)?;
|
||||||
|
// Data size.
|
||||||
|
buf.write_u16_le(7)?;
|
||||||
|
// Integer version number.
|
||||||
|
buf.write_u16_le(version as u16)?;
|
||||||
|
// Vendor ID.
|
||||||
|
buf.write_all(b"AE")?;
|
||||||
|
// AES encryption strength.
|
||||||
|
buf.write_all(&[aes_mode as u8])?;
|
||||||
|
// Real compression method.
|
||||||
|
#[allow(deprecated)]
|
||||||
|
buf.write_u16_le(compression_method.to_u16())?;
|
||||||
|
|
||||||
|
writer.write_all(&buf)?;
|
||||||
|
|
||||||
|
let aes_extra_data_start = file.aes_extra_data_start as usize;
|
||||||
|
let extra_field = Arc::get_mut(file.extra_field.as_mut().unwrap()).unwrap();
|
||||||
|
extra_field
|
||||||
|
.splice(
|
||||||
|
aes_extra_data_start..(aes_extra_data_start + buf.len()),
|
||||||
|
buf,
|
||||||
|
)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
||||||
const CRC32_OFFSET: u64 = 14;
|
const CRC32_OFFSET: u64 = 14;
|
||||||
writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
|
writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
|
||||||
|
|
|
@ -7,6 +7,8 @@ use std::fmt::{Debug, Formatter};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::num::Wrapping;
|
use std::num::Wrapping;
|
||||||
|
|
||||||
|
use crate::result::ZipError;
|
||||||
|
|
||||||
/// A container to hold the current key state
|
/// A container to hold the current key state
|
||||||
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
|
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
|
||||||
#[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
#[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
@ -110,7 +112,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
|
||||||
pub fn validate(
|
pub fn validate(
|
||||||
mut self,
|
mut self,
|
||||||
validator: ZipCryptoValidator,
|
validator: ZipCryptoValidator,
|
||||||
) -> Result<Option<ZipCryptoReaderValid<R>>, std::io::Error> {
|
) -> Result<ZipCryptoReaderValid<R>, ZipError> {
|
||||||
// ZipCrypto prefixes a file with a 12 byte header
|
// ZipCrypto prefixes a file with a 12 byte header
|
||||||
let mut header_buf = [0u8; 12];
|
let mut header_buf = [0u8; 12];
|
||||||
self.file.read_exact(&mut header_buf)?;
|
self.file.read_exact(&mut header_buf)?;
|
||||||
|
@ -125,7 +127,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
|
||||||
// We also use 1 byte CRC.
|
// We also use 1 byte CRC.
|
||||||
|
|
||||||
if (crc32_plaintext >> 24) as u8 != header_buf[11] {
|
if (crc32_plaintext >> 24) as u8 != header_buf[11] {
|
||||||
return Ok(None); // Wrong password
|
return Err(ZipError::InvalidPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => {
|
ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => {
|
||||||
|
@ -137,12 +139,12 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
|
||||||
// We check only 1 byte.
|
// We check only 1 byte.
|
||||||
|
|
||||||
if (last_mod_time >> 8) as u8 != header_buf[11] {
|
if (last_mod_time >> 8) as u8 != header_buf[11] {
|
||||||
return Ok(None); // Wrong password
|
return Err(ZipError::InvalidPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(ZipCryptoReaderValid { reader: self }))
|
Ok(ZipCryptoReaderValid { reader: self })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#![cfg(feature = "aes-crypto")]
|
#![cfg(feature = "aes-crypto")]
|
||||||
|
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read, Write};
|
||||||
use zip::ZipArchive;
|
use zip::{result::ZipError, write::SimpleFileOptions, AesMode, CompressionMethod, ZipArchive};
|
||||||
|
|
||||||
const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
|
const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
|
||||||
|
|
||||||
|
@ -74,3 +74,81 @@ fn aes128_encrypted_file() {
|
||||||
.expect("couldn't read encrypted file");
|
.expect("couldn't read encrypted file");
|
||||||
assert_eq!(SECRET_CONTENT, content);
|
assert_eq!(SECRET_CONTENT, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes128_stored_roundtrip() {
|
||||||
|
let cursor = {
|
||||||
|
let mut zip = zip::ZipWriter::new(io::Cursor::new(Vec::new()));
|
||||||
|
|
||||||
|
zip.start_file(
|
||||||
|
"test.txt",
|
||||||
|
SimpleFileOptions::default().with_aes_encryption(AesMode::Aes128, "some password"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
zip.write_all(SECRET_CONTENT.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
zip.finish().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(cursor).expect("couldn't open test zip file");
|
||||||
|
test_extract_encrypted_file(&mut archive, "test.txt", "some password", "other password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes256_deflated_roundtrip() {
|
||||||
|
let cursor = {
|
||||||
|
let mut zip = zip::ZipWriter::new(io::Cursor::new(Vec::new()));
|
||||||
|
|
||||||
|
zip.start_file(
|
||||||
|
"test.txt",
|
||||||
|
SimpleFileOptions::default()
|
||||||
|
.compression_method(CompressionMethod::Deflated)
|
||||||
|
.with_aes_encryption(AesMode::Aes256, "some password"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
zip.write_all(SECRET_CONTENT.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
zip.finish().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(cursor).expect("couldn't open test zip file");
|
||||||
|
test_extract_encrypted_file(&mut archive, "test.txt", "some password", "other password");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_extract_encrypted_file<R: io::Read + io::Seek>(
|
||||||
|
archive: &mut ZipArchive<R>,
|
||||||
|
file_name: &str,
|
||||||
|
correct_password: &str,
|
||||||
|
incorrect_password: &str,
|
||||||
|
) {
|
||||||
|
{
|
||||||
|
let file = archive.by_name(file_name).map(|_| ());
|
||||||
|
match file {
|
||||||
|
Err(ZipError::UnsupportedArchive("Password required to decrypt file")) => {}
|
||||||
|
Err(err) => {
|
||||||
|
panic!("Failed to read file for unknown reason: {err:?}");
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
panic!("Was able to successfully read encrypted file without password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
match archive.by_name_decrypt(file_name, incorrect_password.as_bytes()) {
|
||||||
|
Err(ZipError::InvalidPassword) => {}
|
||||||
|
Err(err) => panic!("Expected invalid password error, got: {err:?}"),
|
||||||
|
Ok(_) => panic!("Expected invalid password, got decrypted file"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut content = String::new();
|
||||||
|
archive
|
||||||
|
.by_name_decrypt(file_name, correct_password.as_bytes())
|
||||||
|
.expect("couldn't read encrypted file")
|
||||||
|
.read_to_string(&mut content)
|
||||||
|
.expect("couldn't read encrypted file");
|
||||||
|
assert_eq!(SECRET_CONTENT, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue