use std::{
io::{self, Read as _},
sync::Arc,
};
use bzip2::read::BzDecoder;
use flate2::read::ZlibDecoder;
use crate::{
error::Error,
extract::slice::SliceReader,
header::CompressMethod,
installer::EncryptionMode,
records::dataentry::{DataEntry, DataFlag},
};
pub(crate) enum EncryptionContext<'a> {
Modern {
key: &'a [u8; 32],
base_nonce: &'a [u8; 24],
#[allow(dead_code)]
mode: EncryptionMode,
},
Legacy {
password: &'a str,
use_sha1: bool,
unicode: bool,
},
}
const CHUNK_MAGIC: [u8; 4] = *b"zlb\x1a";
pub(crate) fn decompress_chunk(
slice: &SliceReader<'_>,
data: &DataEntry,
compression: CompressMethod,
encryption: Option<&EncryptionContext<'_>>,
) -> Result<Arc<[u8]>, Error> {
if data.first_slice != data.last_slice {
return Err(Error::MultiSliceChunk {
first: data.first_slice,
last: data.last_slice,
});
}
let chunk_is_encrypted = data.flags.contains(&DataFlag::ChunkEncrypted);
if chunk_is_encrypted && encryption.is_none() {
return Err(Error::Encrypted);
}
let salt_overhead: u64 =
if chunk_is_encrypted && matches!(encryption, Some(EncryptionContext::Legacy { .. })) {
8
} else {
0
};
let magic_len: u64 = 4;
let total = magic_len
.checked_add(salt_overhead)
.and_then(|n| n.checked_add(data.chunk_compressed_size))
.ok_or(Error::Overflow {
what: "chunk total bytes",
})?;
let region = slice.read_at(data.first_slice, data.start_offset, total)?;
let (magic, after_magic) = match region {
[m0, m1, m2, m3, rest @ ..] => ([*m0, *m1, *m2, *m3], rest),
_ => {
return Err(Error::Truncated {
what: "chunk header",
});
}
};
if magic != CHUNK_MAGIC {
return Err(Error::BadChunkMagic { got: magic });
}
let owned_body: Vec<u8>;
let compressed: &[u8] = if chunk_is_encrypted {
let ctx = encryption.ok_or(Error::Encrypted)?;
match ctx {
EncryptionContext::Modern {
key, base_nonce, ..
} => {
let nonce = crate::crypto::xchacha20::chunk_nonce(
base_nonce,
u64::from(data.start_offset),
i32::try_from(data.first_slice).unwrap_or(0),
);
let mut buf = after_magic.to_vec();
crate::crypto::xchacha20::apply_keystream(key, &nonce, &mut buf);
owned_body = buf;
&owned_body
}
EncryptionContext::Legacy {
password,
use_sha1,
unicode,
} => {
let (salt_arr, body) = match after_magic {
[s0, s1, s2, s3, s4, s5, s6, s7, rest @ ..] => {
([*s0, *s1, *s2, *s3, *s4, *s5, *s6, *s7], rest)
}
_ => {
return Err(Error::Truncated {
what: "ARC4 chunk salt",
});
}
};
let key = crate::crypto::kdflegacy::arc4_chunk_key(
password, &salt_arr, *use_sha1, *unicode,
);
let mut buf = body.to_vec();
let mut cipher = crate::crypto::arc4::Rc4::new(&key);
cipher.discard(1000);
cipher.apply(&mut buf);
owned_body = buf;
&owned_body
}
}
} else {
after_magic
};
let mut out = Vec::<u8>::new();
match compression {
CompressMethod::Stored => {
out.extend_from_slice(compressed);
}
CompressMethod::Zlib => {
let mut dec = ZlibDecoder::new(compressed);
dec.read_to_end(&mut out).map_err(|e| Error::Decompress {
stream: "chunk Zlib",
source: e,
})?;
}
CompressMethod::Bzip2 => {
let mut dec = BzDecoder::new(compressed);
dec.read_to_end(&mut out).map_err(|e| Error::Decompress {
stream: "chunk BZip2",
source: e,
})?;
}
CompressMethod::Lzma1 => {
decompress_lzma1(compressed, &mut out)?;
}
CompressMethod::Lzma2 => {
let [_inno_prop, stream @ ..] = compressed else {
return Err(Error::Truncated {
what: "LZMA2 prop byte",
});
};
let mut input = io::BufReader::new(stream);
lzma_rs::lzma2_decompress(&mut input, &mut out).map_err(|e| Error::Decompress {
stream: "chunk LZMA2",
source: io::Error::other(e.to_string()),
})?;
}
}
Ok(Arc::<[u8]>::from(out))
}
fn decompress_lzma1(compressed: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
let mut input = io::BufReader::new(compressed);
let opts = lzma_rs::decompress::Options {
unpacked_size: lzma_rs::decompress::UnpackedSize::UseProvided(None),
memlimit: None,
allow_incomplete: false,
};
lzma_rs::lzma_decompress_with_options(&mut input, out, &opts).map_err(|e| {
Error::Decompress {
stream: "chunk LZMA1",
source: io::Error::other(e.to_string()),
}
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::{
crypto::{arc4::Rc4, kdflegacy::arc4_chunk_key},
records::dataentry::{DataChecksum, DataEntry, DataFlag},
};
#[test]
fn legacy_arc4_round_trip_synthetic() {
let plaintext = b"Inno test payload v1\n".to_vec();
let mut compressed_lzma_alone: Vec<u8> = Vec::new();
let mut input = std::io::Cursor::new(plaintext.clone());
lzma_rs::lzma_compress(&mut input, &mut compressed_lzma_alone).unwrap();
let mut inno_lzma1 = Vec::with_capacity(compressed_lzma_alone.len() - 8);
inno_lzma1.extend_from_slice(&compressed_lzma_alone[..5]);
inno_lzma1.extend_from_slice(&compressed_lzma_alone[13..]);
let salt = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
let password = "test123";
let key = arc4_chunk_key(
password, &salt, true,
true,
);
let mut encrypted = inno_lzma1.clone();
let mut enc = Rc4::new(&key);
enc.discard(1000);
enc.apply(&mut encrypted);
let mut input_buf = vec![0u8; 0x100];
let setup1_offset = input_buf.len() as u64;
let chunk_start_in_setup1 = 0x42_u32;
input_buf.resize(input_buf.len() + chunk_start_in_setup1 as usize, 0u8);
input_buf.extend_from_slice(b"zlb\x1a");
input_buf.extend_from_slice(&salt);
input_buf.extend_from_slice(&encrypted);
let slice = SliceReader::embedded(&input_buf, setup1_offset).unwrap();
let mut flags = HashSet::new();
flags.insert(DataFlag::ChunkEncrypted);
flags.insert(DataFlag::ChunkCompressed);
let data = DataEntry {
first_slice: 0,
last_slice: 0,
start_offset: chunk_start_in_setup1,
chunk_sub_offset: 0,
original_size: plaintext.len() as u64,
chunk_compressed_size: encrypted.len() as u64,
checksum: DataChecksum::Sha256([0u8; 32]),
timestamp_seconds: 0,
timestamp_nanos: 0,
file_version: 0,
flags,
flags_raw: vec![0],
sign_mode: crate::records::dataentry::SignMode::NoSetting,
sign_mode_raw: 0,
};
let ctx = EncryptionContext::Legacy {
password,
use_sha1: true,
unicode: true,
};
let bytes = decompress_chunk(&slice, &data, CompressMethod::Lzma1, Some(&ctx)).unwrap();
assert_eq!(bytes.as_ref(), plaintext.as_slice());
}
}