1use std::io;
5use std::num::NonZeroUsize;
6use std::ops::Range;
7use std::thread;
8
9use der::oid::ObjectIdentifier;
10use sha1::Sha1;
11use sha2::{Digest, Sha256, Sha384, Sha512};
12
13use crate::archive::{ApkLayout, ReadAt, ZipError, ZipLimit};
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum DigestAlgorithm {
18 Sha1,
19 Sha256,
20 Sha384,
21 Sha512,
22}
23
24impl DigestAlgorithm {
25 const ALL: [Self; 4] = [Self::Sha1, Self::Sha256, Self::Sha384, Self::Sha512];
26
27 pub(crate) fn hasher(self) -> EntryHasher {
28 match self {
29 Self::Sha1 => EntryHasher::Sha1(Sha1::new()),
30 Self::Sha256 => EntryHasher::Sha256(Sha256::new()),
31 Self::Sha384 => EntryHasher::Sha384(Sha384::new()),
32 Self::Sha512 => EntryHasher::Sha512(Sha512::new()),
33 }
34 }
35
36 pub fn digest(self, data: &[u8]) -> Vec<u8> {
37 let mut hasher = self.hasher();
38 hasher.update(data);
39 hasher.finish()
40 }
41
42 pub(crate) fn jar_name(self) -> &'static str {
44 match self {
45 Self::Sha1 => "SHA1",
46 Self::Sha256 => "SHA-256",
47 Self::Sha384 => "SHA-384",
48 Self::Sha512 => "SHA-512",
49 }
50 }
51
52 pub(crate) fn from_jar_name(name: &str) -> Option<Self> {
53 match name.to_ascii_uppercase().as_str() {
54 "SHA-1" => Some(Self::Sha1),
55 name => Self::ALL.into_iter().find(|a| a.jar_name() == name),
56 }
57 }
58
59 pub(crate) fn oid(self) -> ObjectIdentifier {
60 ObjectIdentifier::new_unwrap(match self {
61 Self::Sha1 => "1.3.14.3.2.26",
62 Self::Sha256 => "2.16.840.1.101.3.4.2.1",
63 Self::Sha384 => "2.16.840.1.101.3.4.2.2",
64 Self::Sha512 => "2.16.840.1.101.3.4.2.3",
65 })
66 }
67
68 pub(crate) fn from_oid(oid: &ObjectIdentifier) -> Option<Self> {
69 Self::ALL.into_iter().find(|a| a.oid() == *oid)
70 }
71}
72
73#[derive(Clone)]
75pub(crate) enum EntryHasher {
76 Sha1(Sha1),
77 Sha256(Sha256),
78 Sha384(Sha384),
79 Sha512(Sha512),
80}
81
82impl EntryHasher {
83 pub fn update(&mut self, data: &[u8]) {
84 match self {
85 Self::Sha1(h) => h.update(data),
86 Self::Sha256(h) => h.update(data),
87 Self::Sha384(h) => h.update(data),
88 Self::Sha512(h) => h.update(data),
89 }
90 }
91
92 pub fn finish(self) -> Vec<u8> {
93 match self {
94 Self::Sha1(h) => h.finalize().to_vec(),
95 Self::Sha256(h) => h.finalize().to_vec(),
96 Self::Sha384(h) => h.finalize().to_vec(),
97 Self::Sha512(h) => h.finalize().to_vec(),
98 }
99 }
100}
101
102impl io::Write for EntryHasher {
103 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
104 self.update(buf);
105 Ok(buf.len())
106 }
107
108 fn flush(&mut self) -> io::Result<()> {
109 Ok(())
110 }
111}
112
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub(crate) enum ContentDigest {
116 ChunkedSha256,
117 ChunkedSha512,
118}
119
120impl ContentDigest {
121 const fn output_len(self) -> usize {
122 match self {
123 Self::ChunkedSha256 => 32,
124 Self::ChunkedSha512 => 64,
125 }
126 }
127
128 fn chunk(self, data: &[u8], out: &mut Vec<u8>) {
129 fn hash<D: Digest>(data: &[u8], out: &mut Vec<u8>) {
130 let mut hasher = D::new();
131 hasher.update([0xa5]);
132 hasher.update((data.len() as u32).to_le_bytes());
133 hasher.update(data);
134 out.extend_from_slice(&hasher.finalize());
135 }
136 match self {
137 Self::ChunkedSha256 => hash::<Sha256>(data, out),
138 Self::ChunkedSha512 => hash::<Sha512>(data, out),
139 }
140 }
141
142 fn top(self, count: u32, chunks: &[u8]) -> Vec<u8> {
143 fn hash<D: Digest>(count: u32, chunks: &[u8]) -> Vec<u8> {
144 let mut hasher = D::new();
145 hasher.update([0x5a]);
146 hasher.update(count.to_le_bytes());
147 hasher.update(chunks);
148 hasher.finalize().to_vec()
149 }
150 match self {
151 Self::ChunkedSha256 => hash::<Sha256>(count, chunks),
152 Self::ChunkedSha512 => hash::<Sha512>(count, chunks),
153 }
154 }
155}
156
157const CHUNK_SIZE: u64 = 1 << 20;
158
159#[derive(Clone, Copy)]
161enum Chunk {
162 File { offset: u64, len: usize },
163 Eocd,
164}
165
166pub(crate) fn content_digests(
171 source: &(impl ReadAt + ?Sized),
172 layout: &ApkLayout,
173 algorithms: &[ContentDigest],
174) -> Result<Vec<(ContentDigest, Vec<u8>)>, ZipError> {
175 let eocd = layout.eocd_with_central_directory_at(layout.entries_end)?;
176 let mut chunks = Vec::new();
177 for section in [0..layout.entries_end, layout.central_directory.clone()] {
178 split(section, &mut chunks);
179 }
180 chunks.push(Chunk::Eocd);
181 let count = u32::try_from(chunks.len()).map_err(|_| ZipError::LimitExceeded { limit: ZipLimit::Size })?;
182
183 let workers = thread::available_parallelism().map_or(1, NonZeroUsize::get).min(chunks.len().div_ceil(4)).max(1);
184 let per_worker = chunks.len().div_ceil(workers);
185 let digest_range = |chunks: &[Chunk]| -> io::Result<Vec<Vec<u8>>> {
186 let mut buffer = vec![0u8; CHUNK_SIZE as usize];
187 let mut outputs: Vec<Vec<u8>> =
188 algorithms.iter().map(|a| Vec::with_capacity(chunks.len() * a.output_len())).collect();
189 for chunk in chunks {
190 let data = match *chunk {
191 Chunk::File { offset, len } => {
192 source.read_exact_at(&mut buffer[..len], offset)?;
193 &buffer[..len]
194 }
195 Chunk::Eocd => &eocd[..],
196 };
197 for (algorithm, output) in algorithms.iter().zip(&mut outputs) {
198 algorithm.chunk(data, output);
199 }
200 }
201 Ok(outputs)
202 };
203 let parts: Vec<Vec<Vec<u8>>> = if workers == 1 {
204 vec![digest_range(&chunks)?]
205 } else {
206 thread::scope(|scope| {
207 let handles: Vec<_> = chunks.chunks(per_worker).map(|range| scope.spawn(|| digest_range(range))).collect();
208 handles.into_iter().map(|h| h.join().expect("digest worker panicked")).collect::<io::Result<_>>()
209 })?
210 };
211 Ok(algorithms
212 .iter()
213 .enumerate()
214 .map(|(i, &algorithm)| {
215 let concatenated: Vec<u8> = parts.iter().flat_map(|part| part[i].iter().copied()).collect();
216 (algorithm, algorithm.top(count, &concatenated))
217 })
218 .collect())
219}
220
221fn split(section: Range<u64>, chunks: &mut Vec<Chunk>) {
222 let mut offset = section.start;
223 while offset < section.end {
224 let len = (section.end - offset).min(CHUNK_SIZE);
225 chunks.push(Chunk::File { offset, len: len as usize });
226 offset += len;
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::archive::{ApkWriter, EntryOptions};
234 use crate::types::ArchivePath;
235
236 fn reference<D: Digest>(bytes: &[u8], layout: &ApkLayout) -> Vec<u8> {
238 let mut eocd = bytes[layout.eocd_offset as usize..].to_vec();
239 eocd[16..20].copy_from_slice(&(layout.entries_end as u32).to_le_bytes());
240 let sections = [
241 &bytes[..layout.entries_end as usize],
242 &bytes[layout.central_directory.start as usize..layout.central_directory.end as usize],
243 &eocd[..],
244 ];
245 let mut digests = Vec::new();
246 let mut count = 0u32;
247 for chunk in sections.iter().flat_map(|s| s.chunks(1024 * 1024)) {
248 let mut hash = D::new();
249 hash.update([0xa5]);
250 hash.update((chunk.len() as u32).to_le_bytes());
251 hash.update(chunk);
252 digests.extend_from_slice(&hash.finalize());
253 count += 1;
254 }
255 let mut hash = D::new();
256 hash.update([0x5a]);
257 hash.update(count.to_le_bytes());
258 hash.update(digests);
259 hash.finalize().to_vec()
260 }
261
262 #[test]
263 fn parallel_digest_matches_the_specification() {
264 let mut writer = ApkWriter::new(io::Cursor::new(Vec::new()));
265 let noise: Vec<u8> = (0..5_500_000u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8).collect();
266 writer.add(&ArchivePath::try_new("assets/noise.bin").unwrap(), &noise, EntryOptions::STORED).unwrap();
267 let bytes = writer.finish().unwrap().into_inner();
268 let layout = ApkLayout::read(&mut io::Cursor::new(&bytes)).unwrap();
269 let digests =
270 content_digests(&bytes, &layout, &[ContentDigest::ChunkedSha256, ContentDigest::ChunkedSha512]).unwrap();
271 assert_eq!(digests[0], (ContentDigest::ChunkedSha256, reference::<Sha256>(&bytes, &layout)));
272 assert_eq!(digests[1], (ContentDigest::ChunkedSha512, reference::<Sha512>(&bytes, &layout)));
273 }
274
275 #[test]
276 fn chunk_boundaries_match_the_reference_for_both_digests() {
277 let archive = |len| {
278 let mut writer = ApkWriter::new(io::Cursor::new(Vec::new()));
279 writer.add(&ArchivePath::manifest(), &vec![0x5b; len], EntryOptions::STORED).unwrap();
280 writer.finish().unwrap().into_inner()
281 };
282 let prefix = ApkLayout::read(&mut io::Cursor::new(archive(0))).unwrap().entries_end as usize;
283 for section_len in [prefix, prefix + 1, (1 << 20) - 1, 1 << 20, (1 << 20) + 1] {
284 let bytes = archive(section_len - prefix);
285 let layout = ApkLayout::read(&mut io::Cursor::new(&bytes)).unwrap();
286 assert_eq!(layout.entries_end as usize, section_len);
287 let digests =
288 content_digests(&bytes, &layout, &[ContentDigest::ChunkedSha256, ContentDigest::ChunkedSha512])
289 .unwrap();
290 assert_eq!(digests[0].1, reference::<Sha256>(&bytes, &layout), "SHA-256, {section_len}");
291 assert_eq!(digests[1].1, reference::<Sha512>(&bytes, &layout), "SHA-512, {section_len}");
292 }
293 }
294}