#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::{Cursor, Read};
use zip_core::{ZipArchive, ZipCoreError};
const FIXTURE: &[u8] = include_bytes!("data/securezip-strong-aes256-pw.zip");
const EXPECTED: &[u8] = include_bytes!("data/securezip-strong-aes256-pw.expected");
const SIGNED: &[u8] = include_bytes!("data/securezip-strong-signed.zip");
const PASSWORD: &[u8] = b"Sr0ninPass!";
fn decrypt(zip: &[u8], name: &str, password: &[u8]) -> Result<Vec<u8>, ZipCoreError> {
let mut ar = ZipArchive::new(Cursor::new(zip.to_vec()))?;
let mut f = ar.by_name_decrypt(name, password)?;
let mut out = Vec::new();
f.read_to_end(&mut out).map_err(|e| {
match e
.into_inner()
.and_then(|b| b.downcast::<ZipCoreError>().ok())
{
Some(inner) => *inner,
None => ZipCoreError::WrongPassword(name.to_string()),
}
})?;
Ok(out)
}
#[test]
fn decrypts_fixture_byte_for_byte_vs_oracle() {
let out = decrypt(FIXTURE, "secret.txt", PASSWORD).expect("must decrypt");
assert_eq!(
out, EXPECTED,
"decrypted bytes must equal the 7zz -so ground truth"
);
}
#[test]
fn wrong_password_refuses_loud() {
let err = decrypt(FIXTURE, "secret.txt", b"WrongPass!").unwrap_err();
assert!(
matches!(err, ZipCoreError::WrongPassword(_)),
"a wrong password must fail loud (WrongPassword), got {err:?}"
);
}
#[test]
fn certificate_variant_stays_refused() {
let mut ar = ZipArchive::new(Cursor::new(SIGNED.to_vec())).unwrap();
let name = ar.file_names().next().unwrap().to_string();
let res = ar.by_name_decrypt(&name, PASSWORD).map(|_| ());
assert!(
matches!(res, Err(ZipCoreError::UnsupportedEncryption { .. })),
"certificate strong encryption must stay refused, got {res:?}"
);
}
#[test]
fn oracle_7zz_matches_expected() {
use std::process::Command;
let manifest = env!("CARGO_MANIFEST_DIR");
let path = format!("{manifest}/tests/data/securezip-strong-aes256-pw.zip");
let out = match Command::new("7zz")
.args(["x", "-so", "-p", "Sr0ninPass!", &path])
.output()
{
Ok(o) if o.status.success() => o.stdout,
_ => {
eprintln!("skipping: 7zz not available");
return;
}
};
assert_eq!(
out, EXPECTED,
"committed .expected must equal live 7zz output"
);
}
mod mint {
use aes::cipher::generic_array::GenericArray;
use cbc::cipher::{BlockEncryptMut, KeyIvInit};
use sha1::{Digest, Sha1};
fn sha1_of(parts: &[&[u8]]) -> [u8; 20] {
let mut h = Sha1::new();
for p in parts {
h.update(p);
}
h.finalize().into()
}
fn dk_half(d: &[u8; 20], c: u8) -> [u8; 20] {
let mut b = [c; 64];
for (x, y) in b.iter_mut().zip(d.iter()) {
*x ^= *y;
}
sha1_of(&[&b])
}
fn derive_key(d: &[u8; 20], ks: usize) -> Vec<u8> {
let mut k = Vec::new();
k.extend_from_slice(&dk_half(d, 0x36));
k.extend_from_slice(&dk_half(d, 0x5C));
k.truncate(ks);
k
}
fn cbc_encrypt(key: &[u8], iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
let mut out = data.to_vec();
macro_rules! run {
($t:ty) => {{
let mut e = cbc::Encryptor::<$t>::new_from_slices(key, iv).unwrap();
for c in out.chunks_exact_mut(16) {
e.encrypt_block_mut(GenericArray::from_mut_slice(c));
}
}};
}
match key.len() {
16 => run!(aes::Aes128),
24 => run!(aes::Aes192),
_ => run!(aes::Aes256),
}
out
}
fn crc32(d: &[u8]) -> u32 {
let mut h = crc32fast::Hasher::new();
h.update(d);
h.finalize()
}
fn deflate(d: &[u8]) -> Vec<u8> {
use std::io::Write;
let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(d).unwrap();
e.finish().unwrap()
}
pub fn entry(
method: u16,
key_size: usize,
iv0: bool,
plaintext: &[u8],
password: &[u8],
) -> (Vec<u8>, u32, u32) {
let compressed = if method == 0 {
plaintext.to_vec()
} else {
deflate(plaintext)
};
let crc = crc32(plaintext);
let mut iv = [0u8; 16];
let iv_used;
if iv0 {
iv[0..4].copy_from_slice(&crc.to_le_bytes());
iv[4..12].copy_from_slice(&(plaintext.len() as u64).to_le_bytes());
iv_used = 12;
} else {
iv = [0x24u8; 16];
iv_used = 16;
}
let master = derive_key(&sha1_of(&[password]), key_size);
let rd = [0xABu8; 16];
let mut erd_plain = rd.to_vec();
erd_plain.extend_from_slice(&[0x10u8; 16]);
let erd = cbc_encrypt(&master, &iv, &erd_plain);
let file_key = derive_key(&sha1_of(&[&iv[..iv_used], &rd]), key_size);
let v = [0xCDu8; 12];
let mut vdata_plain = v.to_vec();
vdata_plain.extend_from_slice(&crc32(&v).to_le_bytes());
let vdata = cbc_encrypt(&file_key, &iv, &vdata_plain);
let mut file_iv = [0u8; 16];
file_iv.copy_from_slice(&vdata[vdata.len() - 16..]);
let pad = (16 - compressed.len() % 16) % 16;
let mut padded = compressed.clone();
padded.resize(padded.len() + pad, 0);
let file_cipher = cbc_encrypt(&file_key, &file_iv, &padded);
let alg_id = 0x660Eu16 + ((key_size - 16) / 8) as u16;
let mut rem = Vec::new();
rem.extend_from_slice(&3u16.to_le_bytes()); rem.extend_from_slice(&alg_id.to_le_bytes()); rem.extend_from_slice(&((key_size * 8) as u16).to_le_bytes()); rem.extend_from_slice(&1u16.to_le_bytes()); rem.extend_from_slice(&(erd.len() as u16).to_le_bytes()); rem.extend_from_slice(&erd);
rem.extend_from_slice(&0u32.to_le_bytes()); rem.extend_from_slice(&(vdata.len() as u16).to_le_bytes()); rem.extend_from_slice(&vdata);
let mut data = Vec::new();
data.extend_from_slice(&(if iv0 { 0u16 } else { 16u16 }).to_le_bytes()); if !iv0 {
data.extend_from_slice(&iv);
}
data.extend_from_slice(&(rem.len() as u32).to_le_bytes()); data.extend_from_slice(&rem);
data.extend_from_slice(&file_cipher);
(data, crc, plaintext.len() as u32)
}
pub fn zip_around(method: u16, crc: u32, uncompressed: u32, data: &[u8]) -> Vec<u8> {
let name = b"secret.txt";
let flags = 0x0041u16;
let mut o = Vec::new();
o.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
o.extend_from_slice(&51u16.to_le_bytes());
o.extend_from_slice(&flags.to_le_bytes());
o.extend_from_slice(&method.to_le_bytes());
o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(&crc.to_le_bytes());
o.extend_from_slice(&(data.len() as u32).to_le_bytes());
o.extend_from_slice(&uncompressed.to_le_bytes());
o.extend_from_slice(&(name.len() as u16).to_le_bytes());
o.extend_from_slice(&0u16.to_le_bytes());
o.extend_from_slice(name);
o.extend_from_slice(data);
let cd = o.len();
o.extend_from_slice(&[0x50, 0x4b, 0x01, 0x02]);
o.extend_from_slice(&51u16.to_le_bytes());
o.extend_from_slice(&51u16.to_le_bytes());
o.extend_from_slice(&flags.to_le_bytes());
o.extend_from_slice(&method.to_le_bytes());
o.extend_from_slice(&0u32.to_le_bytes());
o.extend_from_slice(&crc.to_le_bytes());
o.extend_from_slice(&(data.len() as u32).to_le_bytes());
o.extend_from_slice(&uncompressed.to_le_bytes());
o.extend_from_slice(&(name.len() as u16).to_le_bytes());
o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&0u16.to_le_bytes()); o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(name);
let cd_size = o.len() - cd;
o.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(&1u16.to_le_bytes());
o.extend_from_slice(&1u16.to_le_bytes());
o.extend_from_slice(&(cd_size as u32).to_le_bytes());
o.extend_from_slice(&(cd as u32).to_le_bytes());
o.extend_from_slice(&0u16.to_le_bytes());
o
}
pub fn zip(
method: u16,
key_size: usize,
iv0: bool,
plaintext: &[u8],
password: &[u8],
) -> Vec<u8> {
let (data, crc, uncompressed) = entry(method, key_size, iv0, plaintext, password);
zip_around(method, crc, uncompressed, &data)
}
}
const MINT_PW: &[u8] = b"m1nt-Pass!";
#[test]
fn roundtrip_all_key_sizes_and_methods() {
let pt = b"the quick brown fox jumps over the lazy dog, 0123456789 padding..".to_vec();
for key_size in [16usize, 24, 32] {
for method in [0u16, 8] {
let zip = mint::zip(method, key_size, false, &pt, MINT_PW);
let out = decrypt(&zip, "secret.txt", MINT_PW)
.unwrap_or_else(|e| panic!("ks={key_size} method={method}: {e:?}"));
assert_eq!(out, pt, "ks={key_size} method={method}");
}
}
}
#[test]
fn roundtrip_ivsize_zero_derives_iv() {
let pt = b"IVSize=0 derives the IV from crc and uncompressed size".to_vec();
let zip = mint::zip(0, 32, true, &pt, MINT_PW);
assert_eq!(decrypt(&zip, "secret.txt", MINT_PW).unwrap(), pt);
}
#[test]
fn minted_wrong_password_refuses() {
let zip = mint::zip(0, 32, false, b"secret payload!!", MINT_PW);
assert!(matches!(
decrypt(&zip, "secret.txt", b"not-the-pw"),
Err(ZipCoreError::WrongPassword(_))
));
}
#[test]
fn corrupt_validation_blob_fails_crc_gate() {
let pt = b"payload for the vdata gate".to_vec();
let (mut data, crc, uncompressed) = mint::entry(0, 32, false, &pt, MINT_PW);
data[70] ^= 0xFF;
let zip = mint::zip_around(0, crc, uncompressed, &data);
assert!(matches!(
decrypt(&zip, "secret.txt", MINT_PW),
Err(ZipCoreError::WrongPassword(_))
));
}
#[test]
fn malformed_headers_refuse_loud() {
let pt = b"sixteen bytes...".to_vec();
let base = || mint::entry(0, 32, false, &pt, MINT_PW);
let cases: &[(usize, u8, &str)] = &[
(0, 8, "IVSize neither 0 nor 16"),
(18, 8, "Size (rem) below 16"),
(22, 2, "Format != 3"),
(24, 0x00, "AlgID below AES range"), (26, 0xFF, "BitLen mismatch"),
(28, 0x00, "no password/cert flag"), (28, 0x02, "certificate flag"), (29, 0x40, "3DES-ERD flag"), (30, 0x00, "ErdSize below one block"), (64, 1, "Reserved1 nonzero"),
(68, 0x20, "VSize inconsistent"), ];
for &(off, val, label) in cases {
let (mut data, crc, uncompressed) = base();
data[off] = val;
let zip = mint::zip_around(0, crc, uncompressed, &data);
assert!(
matches!(
decrypt(&zip, "secret.txt", MINT_PW),
Err(ZipCoreError::UnsupportedEncryption { .. })
),
"case '{label}' must refuse loud"
);
}
}
#[test]
fn algid_high_and_erd_overflow_refuse() {
let pt = b"sixteen bytes...".to_vec();
let (mut d1, c1, u1) = mint::entry(0, 32, false, &pt, MINT_PW);
d1[24] = 0x20;
d1[25] = 0x66;
let z1 = mint::zip_around(0, c1, u1, &d1);
assert!(matches!(
decrypt(&z1, "secret.txt", MINT_PW),
Err(ZipCoreError::UnsupportedEncryption { .. })
));
let (mut d2, c2, u2) = mint::entry(0, 32, false, &pt, MINT_PW);
d2[30] = 0x11;
d2[31] = 0x01;
let z2 = mint::zip_around(0, c2, u2, &d2);
assert!(matches!(
decrypt(&z2, "secret.txt", MINT_PW),
Err(ZipCoreError::UnsupportedEncryption { .. })
));
}