1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.apksig.internal.zip;
18 
19 import com.android.apksig.internal.util.Pair;
20 import com.android.apksig.util.DataSource;
21 import java.io.ByteArrayOutputStream;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.ByteOrder;
25 import java.util.zip.CRC32;
26 import java.util.zip.Deflater;
27 
28 /**
29  * Assorted ZIP format helpers.
30  *
31  * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
32  * order of these buffers is little-endian.
33  */
34 public abstract class ZipUtils {
ZipUtils()35     private ZipUtils() {}
36 
37     public static final short COMPRESSION_METHOD_STORED = 0;
38     public static final short COMPRESSION_METHOD_DEFLATED = 8;
39 
40     public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
41     public static final short GP_FLAG_EFS = 0x0800;
42 
43     private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
44     private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
45     private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
46     private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
47     private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
48     private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
49 
50     private static final int UINT16_MAX_VALUE = 0xffff;
51 
52     /**
53      * Sets the offset of the start of the ZIP Central Directory in the archive.
54      *
55      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
56      */
setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)57     public static void setZipEocdCentralDirectoryOffset(
58             ByteBuffer zipEndOfCentralDirectory, long offset) {
59         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
60         setUnsignedInt32(
61                 zipEndOfCentralDirectory,
62                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
63                 offset);
64     }
65 
66     /**
67      * Returns the offset of the start of the ZIP Central Directory in the archive.
68      *
69      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
70      */
getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)71     public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
72         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
73         return getUnsignedInt32(
74                 zipEndOfCentralDirectory,
75                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
76     }
77 
78     /**
79      * Returns the size (in bytes) of the ZIP Central Directory.
80      *
81      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
82      */
getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)83     public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
84         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
85         return getUnsignedInt32(
86                 zipEndOfCentralDirectory,
87                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
88     }
89 
90     /**
91      * Returns the total number of records in ZIP Central Directory.
92      *
93      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
94      */
getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)95     public static int getZipEocdCentralDirectoryTotalRecordCount(
96             ByteBuffer zipEndOfCentralDirectory) {
97         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
98         return getUnsignedInt16(
99                 zipEndOfCentralDirectory,
100                 zipEndOfCentralDirectory.position()
101                         + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
102     }
103 
104     /**
105      * Returns the ZIP End of Central Directory record of the provided ZIP file.
106      *
107      * @return contents of the ZIP End of Central Directory record and the record's offset in the
108      *         file or {@code null} if the file does not contain the record.
109      *
110      * @throws IOException if an I/O error occurs while reading the file.
111      */
findZipEndOfCentralDirectoryRecord(DataSource zip)112     public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
113             throws IOException {
114         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
115         // The record can be identified by its 4-byte signature/magic which is located at the very
116         // beginning of the record. A complication is that the record is variable-length because of
117         // the comment field.
118         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
119         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
120         // the candidate record's comment length is such that the remainder of the record takes up
121         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
122         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
123 
124         long fileSize = zip.size();
125         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
126             return null;
127         }
128 
129         // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
130         // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
131         // reading more data.
132         Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
133         if (result != null) {
134             return result;
135         }
136 
137         // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
138         // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
139         // the comment length field is an unsigned 16-bit number.
140         return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
141     }
142 
143     /**
144      * Returns the ZIP End of Central Directory record of the provided ZIP file.
145      *
146      * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
147      *        value is from 0 to 65535 inclusive. The smaller the value, the faster this method
148      *        locates the record, provided its comment field is no longer than this value.
149      *
150      * @return contents of the ZIP End of Central Directory record and the record's offset in the
151      *         file or {@code null} if the file does not contain the record.
152      *
153      * @throws IOException if an I/O error occurs while reading the file.
154      */
findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)155     private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
156             DataSource zip, int maxCommentSize) throws IOException {
157         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
158         // The record can be identified by its 4-byte signature/magic which is located at the very
159         // beginning of the record. A complication is that the record is variable-length because of
160         // the comment field.
161         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
162         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
163         // the candidate record's comment length is such that the remainder of the record takes up
164         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
165         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
166 
167         if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
168             throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
169         }
170 
171         long fileSize = zip.size();
172         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
173             // No space for EoCD record in the file.
174             return null;
175         }
176         // Lower maxCommentSize if the file is too small.
177         maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
178 
179         int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
180         long bufOffsetInFile = fileSize - maxEocdSize;
181         ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
182         buf.order(ByteOrder.LITTLE_ENDIAN);
183         int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
184         if (eocdOffsetInBuf == -1) {
185             // No EoCD record found in the buffer
186             return null;
187         }
188         // EoCD found
189         buf.position(eocdOffsetInBuf);
190         ByteBuffer eocd = buf.slice();
191         eocd.order(ByteOrder.LITTLE_ENDIAN);
192         return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
193     }
194 
195     /**
196      * Returns the position at which ZIP End of Central Directory record starts in the provided
197      * buffer or {@code -1} if the record is not present.
198      *
199      * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
200      */
findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)201     private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
202         assertByteOrderLittleEndian(zipContents);
203 
204         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
205         // The record can be identified by its 4-byte signature/magic which is located at the very
206         // beginning of the record. A complication is that the record is variable-length because of
207         // the comment field.
208         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
209         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
210         // the candidate record's comment length is such that the remainder of the record takes up
211         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
212         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
213 
214         int archiveSize = zipContents.capacity();
215         if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
216             return -1;
217         }
218         int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
219         int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
220         for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
221                 expectedCommentLength++) {
222             int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
223             if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
224                 int actualCommentLength =
225                         getUnsignedInt16(
226                                 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
227                 if (actualCommentLength == expectedCommentLength) {
228                     return eocdStartPos;
229                 }
230             }
231         }
232 
233         return -1;
234     }
235 
assertByteOrderLittleEndian(ByteBuffer buffer)236     static void assertByteOrderLittleEndian(ByteBuffer buffer) {
237         if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
238             throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
239         }
240     }
241 
getUnsignedInt16(ByteBuffer buffer, int offset)242     public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
243         return buffer.getShort(offset) & 0xffff;
244     }
245 
getUnsignedInt16(ByteBuffer buffer)246     public static int getUnsignedInt16(ByteBuffer buffer) {
247         return buffer.getShort() & 0xffff;
248     }
249 
setUnsignedInt16(ByteBuffer buffer, int offset, int value)250     static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
251         if ((value < 0) || (value > 0xffff)) {
252             throw new IllegalArgumentException("uint16 value of out range: " + value);
253         }
254         buffer.putShort(offset, (short) value);
255     }
256 
setUnsignedInt32(ByteBuffer buffer, int offset, long value)257     static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
258         if ((value < 0) || (value > 0xffffffffL)) {
259             throw new IllegalArgumentException("uint32 value of out range: " + value);
260         }
261         buffer.putInt(offset, (int) value);
262     }
263 
putUnsignedInt16(ByteBuffer buffer, int value)264     public static void putUnsignedInt16(ByteBuffer buffer, int value) {
265         if ((value < 0) || (value > 0xffff)) {
266             throw new IllegalArgumentException("uint16 value of out range: " + value);
267         }
268         buffer.putShort((short) value);
269     }
270 
getUnsignedInt32(ByteBuffer buffer, int offset)271     static long getUnsignedInt32(ByteBuffer buffer, int offset) {
272         return buffer.getInt(offset) & 0xffffffffL;
273     }
274 
getUnsignedInt32(ByteBuffer buffer)275     static long getUnsignedInt32(ByteBuffer buffer) {
276         return buffer.getInt() & 0xffffffffL;
277     }
278 
putUnsignedInt32(ByteBuffer buffer, long value)279     static void putUnsignedInt32(ByteBuffer buffer, long value) {
280         if ((value < 0) || (value > 0xffffffffL)) {
281             throw new IllegalArgumentException("uint32 value of out range: " + value);
282         }
283         buffer.putInt((int) value);
284     }
285 
deflate(ByteBuffer input)286     public static DeflateResult deflate(ByteBuffer input) {
287         byte[] inputBuf;
288         int inputOffset;
289         int inputLength = input.remaining();
290         if (input.hasArray()) {
291             inputBuf = input.array();
292             inputOffset = input.arrayOffset() + input.position();
293             input.position(input.limit());
294         } else {
295             inputBuf = new byte[inputLength];
296             inputOffset = 0;
297             input.get(inputBuf);
298         }
299         CRC32 crc32 = new CRC32();
300         crc32.update(inputBuf, inputOffset, inputLength);
301         long crc32Value = crc32.getValue();
302         ByteArrayOutputStream out = new ByteArrayOutputStream();
303         Deflater deflater = new Deflater(9, true);
304         deflater.setInput(inputBuf, inputOffset, inputLength);
305         deflater.finish();
306         byte[] buf = new byte[65536];
307         while (!deflater.finished()) {
308             int chunkSize = deflater.deflate(buf);
309             out.write(buf, 0, chunkSize);
310         }
311         return new DeflateResult(inputLength, crc32Value, out.toByteArray());
312     }
313 
314     public static class DeflateResult {
315         public final int inputSizeBytes;
316         public final long inputCrc32;
317         public final byte[] output;
318 
DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)319         public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
320             this.inputSizeBytes = inputSizeBytes;
321             this.inputCrc32 = inputCrc32;
322             this.output = output;
323         }
324     }
325 }