use std::io;
use std::num::NonZeroUsize;
use std::ops::Range;
use std::thread;
use der::oid::ObjectIdentifier;
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
use crate::archive::{ApkLayout, ReadAt, ZipError, ZipLimit};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DigestAlgorithm {
Sha1,
Sha256,
Sha384,
Sha512,
}
impl DigestAlgorithm {
const ALL: [Self; 4] = [Self::Sha1, Self::Sha256, Self::Sha384, Self::Sha512];
pub(crate) fn hasher(self) -> EntryHasher {
match self {
Self::Sha1 => EntryHasher::Sha1(Sha1::new()),
Self::Sha256 => EntryHasher::Sha256(Sha256::new()),
Self::Sha384 => EntryHasher::Sha384(Sha384::new()),
Self::Sha512 => EntryHasher::Sha512(Sha512::new()),
}
}
pub fn digest(self, data: &[u8]) -> Vec<u8> {
let mut hasher = self.hasher();
hasher.update(data);
hasher.finish()
}
pub(crate) fn jar_name(self) -> &'static str {
match self {
Self::Sha1 => "SHA1",
Self::Sha256 => "SHA-256",
Self::Sha384 => "SHA-384",
Self::Sha512 => "SHA-512",
}
}
pub(crate) fn from_jar_name(name: &str) -> Option<Self> {
match name.to_ascii_uppercase().as_str() {
"SHA-1" => Some(Self::Sha1),
name => Self::ALL.into_iter().find(|a| a.jar_name() == name),
}
}
pub(crate) fn oid(self) -> ObjectIdentifier {
ObjectIdentifier::new_unwrap(match self {
Self::Sha1 => "1.3.14.3.2.26",
Self::Sha256 => "2.16.840.1.101.3.4.2.1",
Self::Sha384 => "2.16.840.1.101.3.4.2.2",
Self::Sha512 => "2.16.840.1.101.3.4.2.3",
})
}
pub(crate) fn from_oid(oid: &ObjectIdentifier) -> Option<Self> {
Self::ALL.into_iter().find(|a| a.oid() == *oid)
}
}
#[derive(Clone)]
pub(crate) enum EntryHasher {
Sha1(Sha1),
Sha256(Sha256),
Sha384(Sha384),
Sha512(Sha512),
}
impl EntryHasher {
pub fn update(&mut self, data: &[u8]) {
match self {
Self::Sha1(h) => h.update(data),
Self::Sha256(h) => h.update(data),
Self::Sha384(h) => h.update(data),
Self::Sha512(h) => h.update(data),
}
}
pub fn finish(self) -> Vec<u8> {
match self {
Self::Sha1(h) => h.finalize().to_vec(),
Self::Sha256(h) => h.finalize().to_vec(),
Self::Sha384(h) => h.finalize().to_vec(),
Self::Sha512(h) => h.finalize().to_vec(),
}
}
}
impl io::Write for EntryHasher {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.update(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(crate) enum ContentDigest {
ChunkedSha256,
ChunkedSha512,
}
impl ContentDigest {
const fn output_len(self) -> usize {
match self {
Self::ChunkedSha256 => 32,
Self::ChunkedSha512 => 64,
}
}
fn chunk(self, data: &[u8], out: &mut Vec<u8>) {
fn hash<D: Digest>(data: &[u8], out: &mut Vec<u8>) {
let mut hasher = D::new();
hasher.update([0xa5]);
hasher.update((data.len() as u32).to_le_bytes());
hasher.update(data);
out.extend_from_slice(&hasher.finalize());
}
match self {
Self::ChunkedSha256 => hash::<Sha256>(data, out),
Self::ChunkedSha512 => hash::<Sha512>(data, out),
}
}
fn top(self, count: u32, chunks: &[u8]) -> Vec<u8> {
fn hash<D: Digest>(count: u32, chunks: &[u8]) -> Vec<u8> {
let mut hasher = D::new();
hasher.update([0x5a]);
hasher.update(count.to_le_bytes());
hasher.update(chunks);
hasher.finalize().to_vec()
}
match self {
Self::ChunkedSha256 => hash::<Sha256>(count, chunks),
Self::ChunkedSha512 => hash::<Sha512>(count, chunks),
}
}
}
const CHUNK_SIZE: u64 = 1 << 20;
#[derive(Clone, Copy)]
enum Chunk {
File { offset: u64, len: usize },
Eocd,
}
pub(crate) fn content_digests(
source: &(impl ReadAt + ?Sized),
layout: &ApkLayout,
algorithms: &[ContentDigest],
) -> Result<Vec<(ContentDigest, Vec<u8>)>, ZipError> {
let eocd = layout.eocd_with_central_directory_at(layout.entries_end)?;
let mut chunks = Vec::new();
for section in [0..layout.entries_end, layout.central_directory.clone()] {
split(section, &mut chunks);
}
chunks.push(Chunk::Eocd);
let count = u32::try_from(chunks.len()).map_err(|_| ZipError::LimitExceeded { limit: ZipLimit::Size })?;
let workers = thread::available_parallelism().map_or(1, NonZeroUsize::get).min(chunks.len().div_ceil(4)).max(1);
let per_worker = chunks.len().div_ceil(workers);
let digest_range = |chunks: &[Chunk]| -> io::Result<Vec<Vec<u8>>> {
let mut buffer = vec![0u8; CHUNK_SIZE as usize];
let mut outputs: Vec<Vec<u8>> =
algorithms.iter().map(|a| Vec::with_capacity(chunks.len() * a.output_len())).collect();
for chunk in chunks {
let data = match *chunk {
Chunk::File { offset, len } => {
source.read_exact_at(&mut buffer[..len], offset)?;
&buffer[..len]
}
Chunk::Eocd => &eocd[..],
};
for (algorithm, output) in algorithms.iter().zip(&mut outputs) {
algorithm.chunk(data, output);
}
}
Ok(outputs)
};
let parts: Vec<Vec<Vec<u8>>> = if workers == 1 {
vec![digest_range(&chunks)?]
} else {
thread::scope(|scope| {
let handles: Vec<_> = chunks.chunks(per_worker).map(|range| scope.spawn(|| digest_range(range))).collect();
handles.into_iter().map(|h| h.join().expect("digest worker panicked")).collect::<io::Result<_>>()
})?
};
Ok(algorithms
.iter()
.enumerate()
.map(|(i, &algorithm)| {
let concatenated: Vec<u8> = parts.iter().flat_map(|part| part[i].iter().copied()).collect();
(algorithm, algorithm.top(count, &concatenated))
})
.collect())
}
fn split(section: Range<u64>, chunks: &mut Vec<Chunk>) {
let mut offset = section.start;
while offset < section.end {
let len = (section.end - offset).min(CHUNK_SIZE);
chunks.push(Chunk::File { offset, len: len as usize });
offset += len;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive::{ApkWriter, EntryOptions};
use crate::types::ArchivePath;
fn reference<D: Digest>(bytes: &[u8], layout: &ApkLayout) -> Vec<u8> {
let mut eocd = bytes[layout.eocd_offset as usize..].to_vec();
eocd[16..20].copy_from_slice(&(layout.entries_end as u32).to_le_bytes());
let sections = [
&bytes[..layout.entries_end as usize],
&bytes[layout.central_directory.start as usize..layout.central_directory.end as usize],
&eocd[..],
];
let mut digests = Vec::new();
let mut count = 0u32;
for chunk in sections.iter().flat_map(|s| s.chunks(1024 * 1024)) {
let mut hash = D::new();
hash.update([0xa5]);
hash.update((chunk.len() as u32).to_le_bytes());
hash.update(chunk);
digests.extend_from_slice(&hash.finalize());
count += 1;
}
let mut hash = D::new();
hash.update([0x5a]);
hash.update(count.to_le_bytes());
hash.update(digests);
hash.finalize().to_vec()
}
#[test]
fn parallel_digest_matches_the_specification() {
let mut writer = ApkWriter::new(io::Cursor::new(Vec::new()));
let noise: Vec<u8> = (0..5_500_000u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8).collect();
writer.add(&ArchivePath::try_new("assets/noise.bin").unwrap(), &noise, EntryOptions::STORED).unwrap();
let bytes = writer.finish().unwrap().into_inner();
let layout = ApkLayout::read(&mut io::Cursor::new(&bytes)).unwrap();
let digests =
content_digests(&bytes, &layout, &[ContentDigest::ChunkedSha256, ContentDigest::ChunkedSha512]).unwrap();
assert_eq!(digests[0], (ContentDigest::ChunkedSha256, reference::<Sha256>(&bytes, &layout)));
assert_eq!(digests[1], (ContentDigest::ChunkedSha512, reference::<Sha512>(&bytes, &layout)));
}
#[test]
fn chunk_boundaries_match_the_reference_for_both_digests() {
let archive = |len| {
let mut writer = ApkWriter::new(io::Cursor::new(Vec::new()));
writer.add(&ArchivePath::manifest(), &vec![0x5b; len], EntryOptions::STORED).unwrap();
writer.finish().unwrap().into_inner()
};
let prefix = ApkLayout::read(&mut io::Cursor::new(archive(0))).unwrap().entries_end as usize;
for section_len in [prefix, prefix + 1, (1 << 20) - 1, 1 << 20, (1 << 20) + 1] {
let bytes = archive(section_len - prefix);
let layout = ApkLayout::read(&mut io::Cursor::new(&bytes)).unwrap();
assert_eq!(layout.entries_end as usize, section_len);
let digests =
content_digests(&bytes, &layout, &[ContentDigest::ChunkedSha256, ContentDigest::ChunkedSha512])
.unwrap();
assert_eq!(digests[0].1, reference::<Sha256>(&bytes, &layout), "SHA-256, {section_len}");
assert_eq!(digests[1].1, reference::<Sha512>(&bytes, &layout), "SHA-512, {section_len}");
}
}
}