Skip to main content

apk/
digest.rs

1//! Digests: per-entry digests for JAR signing, and the chunked content
2//! digests of APK Signature Scheme v2.
3
4use 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/// A digest used by JAR (v1) signatures, weakest first.
16#[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    /// The name used in JAR manifest attributes, e.g. `SHA-256-Digest`.
43    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/// An incremental [`DigestAlgorithm`], which can be written to.
74#[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/// A chunked content digest of APK Signature Scheme v2 and v3.
114#[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/// One 1 MiB (or shorter, final) chunk of a digested section.
160#[derive(Clone, Copy)]
161enum Chunk {
162    File { offset: u64, len: usize },
163    Eocd,
164}
165
166/// Computes `algorithms` over the APK in `source`, as if its central directory
167/// started at `layout.entries_end` (that is, with any signing block removed).
168///
169/// Chunks are hashed in parallel; each worker reads its chunks positionally.
170pub(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    /// The v2 digest computed directly from the specification.
237    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}