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.ByteBufferSink;
20 import com.android.apksig.util.DataSink;
21 import com.android.apksig.util.DataSource;
22 import com.android.apksig.zip.ZipFormatException;
23 import java.io.Closeable;
24 import java.io.IOException;
25 import java.nio.ByteBuffer;
26 import java.nio.ByteOrder;
27 import java.nio.charset.StandardCharsets;
28 import java.util.zip.DataFormatException;
29 import java.util.zip.Inflater;
30 
31 /**
32  * ZIP Local File record.
33  *
34  * <p>The record consists of the Local File Header, file data, and (if present) Data Descriptor.
35  */
36 public class LocalFileRecord {
37     private static final int RECORD_SIGNATURE = 0x04034b50;
38     private static final int HEADER_SIZE_BYTES = 30;
39 
40     private static final int GP_FLAGS_OFFSET = 6;
41     private static final int CRC32_OFFSET = 14;
42     private static final int COMPRESSED_SIZE_OFFSET = 18;
43     private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
44     private static final int NAME_LENGTH_OFFSET = 26;
45     private static final int EXTRA_LENGTH_OFFSET = 28;
46     private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
47 
48     private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12;
49     private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
50 
51     private final String mName;
52     private final int mNameSizeBytes;
53     private final ByteBuffer mExtra;
54 
55     private final long mStartOffsetInArchive;
56     private final long mSize;
57 
58     private final int mDataStartOffset;
59     private final long mDataSize;
60     private final boolean mDataCompressed;
61     private final long mUncompressedDataSize;
62 
LocalFileRecord( String name, int nameSizeBytes, ByteBuffer extra, long startOffsetInArchive, long size, int dataStartOffset, long dataSize, boolean dataCompressed, long uncompressedDataSize)63     private LocalFileRecord(
64             String name,
65             int nameSizeBytes,
66             ByteBuffer extra,
67             long startOffsetInArchive,
68             long size,
69             int dataStartOffset,
70             long dataSize,
71             boolean dataCompressed,
72             long uncompressedDataSize) {
73         mName = name;
74         mNameSizeBytes = nameSizeBytes;
75         mExtra = extra;
76         mStartOffsetInArchive = startOffsetInArchive;
77         mSize = size;
78         mDataStartOffset = dataStartOffset;
79         mDataSize = dataSize;
80         mDataCompressed = dataCompressed;
81         mUncompressedDataSize = uncompressedDataSize;
82     }
83 
getName()84     public String getName() {
85         return mName;
86     }
87 
getExtra()88     public ByteBuffer getExtra() {
89         return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra;
90     }
91 
getExtraFieldStartOffsetInsideRecord()92     public int getExtraFieldStartOffsetInsideRecord() {
93         return HEADER_SIZE_BYTES + mNameSizeBytes;
94     }
95 
getStartOffsetInArchive()96     public long getStartOffsetInArchive() {
97         return mStartOffsetInArchive;
98     }
99 
getDataStartOffsetInRecord()100     public int getDataStartOffsetInRecord() {
101         return mDataStartOffset;
102     }
103 
104     /**
105      * Returns the size (in bytes) of this record.
106      */
getSize()107     public long getSize() {
108         return mSize;
109     }
110 
111     /**
112      * Returns {@code true} if this record's file data is stored in compressed form.
113      */
isDataCompressed()114     public boolean isDataCompressed() {
115         return mDataCompressed;
116     }
117 
118     /**
119      * Returns the Local File record starting at the current position of the provided buffer
120      * and advances the buffer's position immediately past the end of the record. The record
121      * consists of the Local File Header, data, and (if present) Data Descriptor.
122      */
getRecord( DataSource apk, CentralDirectoryRecord cdRecord, long cdStartOffset)123     public static LocalFileRecord getRecord(
124             DataSource apk,
125             CentralDirectoryRecord cdRecord,
126             long cdStartOffset) throws ZipFormatException, IOException {
127         return getRecord(
128                 apk,
129                 cdRecord,
130                 cdStartOffset,
131                 true, // obtain extra field contents
132                 true // include Data Descriptor (if present)
133                 );
134     }
135 
136     /**
137      * Returns the Local File record starting at the current position of the provided buffer
138      * and advances the buffer's position immediately past the end of the record. The record
139      * consists of the Local File Header, data, and (if present) Data Descriptor.
140      */
getRecord( DataSource apk, CentralDirectoryRecord cdRecord, long cdStartOffset, boolean extraFieldContentsNeeded, boolean dataDescriptorIncluded)141     private static LocalFileRecord getRecord(
142             DataSource apk,
143             CentralDirectoryRecord cdRecord,
144             long cdStartOffset,
145             boolean extraFieldContentsNeeded,
146             boolean dataDescriptorIncluded) throws ZipFormatException, IOException {
147         // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
148         // exhibited when reading an APK for the purposes of verifying its signatures.
149 
150         String entryName = cdRecord.getName();
151         int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes();
152         int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes;
153         long headerStartOffset = cdRecord.getLocalFileHeaderOffset();
154         long headerEndOffset = headerStartOffset + headerSizeWithName;
155         if (headerEndOffset > cdStartOffset) {
156             throw new ZipFormatException(
157                     "Local File Header of " + entryName + " extends beyond start of Central"
158                             + " Directory. LFH end: " + headerEndOffset
159                             + ", CD start: " + cdStartOffset);
160         }
161         ByteBuffer header;
162         try {
163             header = apk.getByteBuffer(headerStartOffset, headerSizeWithName);
164         } catch (IOException e) {
165             throw new IOException("Failed to read Local File Header of " + entryName, e);
166         }
167         header.order(ByteOrder.LITTLE_ENDIAN);
168 
169         int recordSignature = header.getInt();
170         if (recordSignature != RECORD_SIGNATURE) {
171             throw new ZipFormatException(
172                     "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
173                             + Long.toHexString(recordSignature & 0xffffffffL));
174         }
175         short gpFlags = header.getShort(GP_FLAGS_OFFSET);
176         boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
177         boolean cdDataDescriptorUsed =
178                 (cdRecord.getGpFlags() & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
179         if (dataDescriptorUsed != cdDataDescriptorUsed) {
180             throw new ZipFormatException(
181                     "Data Descriptor presence mismatch between Local File Header and Central"
182                             + " Directory for entry " + entryName
183                             + ". LFH: " + dataDescriptorUsed + ", CD: " + cdDataDescriptorUsed);
184         }
185         long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32();
186         long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize();
187         long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize();
188         if (!dataDescriptorUsed) {
189             long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
190             if (crc32 != uncompressedDataCrc32FromCdRecord) {
191                 throw new ZipFormatException(
192                         "CRC-32 mismatch between Local File Header and Central Directory for entry "
193                                 + entryName + ". LFH: " + crc32
194                                 + ", CD: " + uncompressedDataCrc32FromCdRecord);
195             }
196             long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
197             if (compressedSize != compressedDataSizeFromCdRecord) {
198                 throw new ZipFormatException(
199                         "Compressed size mismatch between Local File Header and Central Directory"
200                                 + " for entry " + entryName + ". LFH: " + compressedSize
201                                 + ", CD: " + compressedDataSizeFromCdRecord);
202             }
203             long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
204             if (uncompressedSize != uncompressedDataSizeFromCdRecord) {
205                 throw new ZipFormatException(
206                         "Uncompressed size mismatch between Local File Header and Central Directory"
207                                 + " for entry " + entryName + ". LFH: " + uncompressedSize
208                                 + ", CD: " + uncompressedDataSizeFromCdRecord);
209             }
210         }
211         int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
212         if (nameLength > cdRecordEntryNameSizeBytes) {
213             throw new ZipFormatException(
214                     "Name mismatch between Local File Header and Central Directory for entry"
215                             + entryName + ". LFH: " + nameLength
216                             + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes");
217         }
218         String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
219         if (!entryName.equals(name)) {
220             throw new ZipFormatException(
221                     "Name mismatch between Local File Header and Central Directory. LFH: \""
222                             + name + "\", CD: \"" + entryName + "\"");
223         }
224         int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
225         long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength;
226         long dataSize;
227         boolean compressed =
228                 (cdRecord.getCompressionMethod() != ZipUtils.COMPRESSION_METHOD_STORED);
229         if (compressed) {
230             dataSize = compressedDataSizeFromCdRecord;
231         } else {
232             dataSize = uncompressedDataSizeFromCdRecord;
233         }
234         long dataEndOffset = dataStartOffset + dataSize;
235         if (dataEndOffset > cdStartOffset) {
236             throw new ZipFormatException(
237                     "Local File Header data of " + entryName + " overlaps with Central Directory"
238                             + ". LFH data start: " + dataStartOffset
239                             + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset);
240         }
241 
242         ByteBuffer extra = EMPTY_BYTE_BUFFER;
243         if ((extraFieldContentsNeeded) && (extraLength > 0)) {
244             extra = apk.getByteBuffer(
245                     headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength);
246         }
247 
248         long recordEndOffset = dataEndOffset;
249         // Include the Data Descriptor (if requested and present) into the record.
250         if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) {
251             // The record's data is supposed to be followed by the Data Descriptor. Unfortunately,
252             // the descriptor's size is not known in advance because the spec lets the signature
253             // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell
254             // how long the Data Descriptor record is. Most parsers (including Android) check
255             // whether the first four bytes look like Data Descriptor record signature and, if so,
256             // assume that it is indeed the record's signature. However, this is the wrong
257             // conclusion if the record's CRC-32 (next field after the signature) has the same value
258             // as the signature. In any case, we're doing what Android is doing.
259             long dataDescriptorEndOffset =
260                     dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE;
261             if (dataDescriptorEndOffset > cdStartOffset) {
262                 throw new ZipFormatException(
263                         "Data Descriptor of " + entryName + " overlaps with Central Directory"
264                                 + ". Data Descriptor end: " + dataEndOffset
265                                 + ", CD start: " + cdStartOffset);
266             }
267             ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4);
268             dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN);
269             if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) {
270                 dataDescriptorEndOffset += 4;
271                 if (dataDescriptorEndOffset > cdStartOffset) {
272                     throw new ZipFormatException(
273                             "Data Descriptor of " + entryName + " overlaps with Central Directory"
274                                     + ". Data Descriptor end: " + dataEndOffset
275                                     + ", CD start: " + cdStartOffset);
276                 }
277             }
278             recordEndOffset = dataDescriptorEndOffset;
279         }
280 
281         long recordSize = recordEndOffset - headerStartOffset;
282         int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength;
283 
284         return new LocalFileRecord(
285                 entryName,
286                 cdRecordEntryNameSizeBytes,
287                 extra,
288                 headerStartOffset,
289                 recordSize,
290                 dataStartOffsetInRecord,
291                 dataSize,
292                 compressed,
293                 uncompressedDataSizeFromCdRecord);
294     }
295 
296     /**
297      * Outputs this record and returns returns the number of bytes output.
298      */
outputRecord(DataSource sourceApk, DataSink output)299     public long outputRecord(DataSource sourceApk, DataSink output) throws IOException {
300         long size = getSize();
301         sourceApk.feed(getStartOffsetInArchive(), size, output);
302         return size;
303     }
304 
305     /**
306      * Outputs this record, replacing its extra field with the provided one, and returns returns the
307      * number of bytes output.
308      */
outputRecordWithModifiedExtra( DataSource sourceApk, ByteBuffer extra, DataSink output)309     public long outputRecordWithModifiedExtra(
310             DataSource sourceApk,
311             ByteBuffer extra,
312             DataSink output) throws IOException {
313         long recordStartOffsetInSource = getStartOffsetInArchive();
314         int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord();
315         int extraSizeBytes = extra.remaining();
316         int headerSize = extraStartOffsetInRecord + extraSizeBytes;
317         ByteBuffer header = ByteBuffer.allocate(headerSize);
318         header.order(ByteOrder.LITTLE_ENDIAN);
319         sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header);
320         header.put(extra.slice());
321         header.flip();
322         ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes);
323 
324         long outputByteCount = header.remaining();
325         output.consume(header);
326         long remainingRecordSize = getSize() - mDataStartOffset;
327         sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output);
328         outputByteCount += remainingRecordSize;
329         return outputByteCount;
330     }
331 
332     /**
333      * Outputs the specified Local File Header record with its data and returns the number of bytes
334      * output.
335      */
outputRecordWithDeflateCompressedData( String name, int lastModifiedTime, int lastModifiedDate, byte[] compressedData, long crc32, long uncompressedSize, DataSink output)336     public static long outputRecordWithDeflateCompressedData(
337             String name,
338             int lastModifiedTime,
339             int lastModifiedDate,
340             byte[] compressedData,
341             long crc32,
342             long uncompressedSize,
343             DataSink output) throws IOException {
344         byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
345         int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
346         ByteBuffer result = ByteBuffer.allocate(recordSize);
347         result.order(ByteOrder.LITTLE_ENDIAN);
348         result.putInt(RECORD_SIGNATURE);
349         ZipUtils.putUnsignedInt16(result,  0x14); // Minimum version needed to extract
350         result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name
351         result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
352         ZipUtils.putUnsignedInt16(result, lastModifiedTime);
353         ZipUtils.putUnsignedInt16(result, lastModifiedDate);
354         ZipUtils.putUnsignedInt32(result, crc32);
355         ZipUtils.putUnsignedInt32(result, compressedData.length);
356         ZipUtils.putUnsignedInt32(result, uncompressedSize);
357         ZipUtils.putUnsignedInt16(result, nameBytes.length);
358         ZipUtils.putUnsignedInt16(result, 0); // Extra field length
359         result.put(nameBytes);
360         if (result.hasRemaining()) {
361             throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
362         }
363         result.flip();
364 
365         long outputByteCount = result.remaining();
366         output.consume(result);
367         outputByteCount += compressedData.length;
368         output.consume(compressedData, 0, compressedData.length);
369         return outputByteCount;
370     }
371 
372     private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0);
373 
374     /**
375      * Sends uncompressed data of this record into the the provided data sink.
376      */
outputUncompressedData( DataSource lfhSection, DataSink sink)377     public void outputUncompressedData(
378             DataSource lfhSection,
379             DataSink sink) throws IOException, ZipFormatException {
380         long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset;
381         try {
382             if (mDataCompressed) {
383                 try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
384                     lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter);
385                     long actualUncompressedSize = inflateAdapter.getOutputByteCount();
386                     if (actualUncompressedSize != mUncompressedDataSize) {
387                         throw new ZipFormatException(
388                                 "Unexpected size of uncompressed data of " + mName
389                                         + ". Expected: " + mUncompressedDataSize + " bytes"
390                                         + ", actual: " + actualUncompressedSize + " bytes");
391                     }
392                 } catch (IOException e) {
393                     if (e.getCause() instanceof DataFormatException) {
394                         throw new ZipFormatException("Data of entry " + mName + " malformed", e);
395                     }
396                     throw e;
397                 }
398             } else {
399                 lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink);
400                 // No need to check whether output size is as expected because DataSource.feed is
401                 // guaranteed to output exactly the number of bytes requested.
402             }
403         } catch (IOException e) {
404             throw new IOException(
405                     "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed")
406                         + " entry " + mName,
407                     e);
408         }
409         // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
410         // thus don't check either.
411     }
412 
413     /**
414      * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the
415      * provided data sink.
416      */
outputUncompressedData( DataSource source, CentralDirectoryRecord cdRecord, long cdStartOffsetInArchive, DataSink sink)417     public static void outputUncompressedData(
418             DataSource source,
419             CentralDirectoryRecord cdRecord,
420             long cdStartOffsetInArchive,
421             DataSink sink) throws ZipFormatException, IOException {
422         // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
423         // exhibited when reading an APK for the purposes of verifying its signatures.
424         // When verifying an APK, Android doesn't care reading the extra field or the Data
425         // Descriptor.
426         LocalFileRecord lfhRecord =
427                 getRecord(
428                         source,
429                         cdRecord,
430                         cdStartOffsetInArchive,
431                         false, // don't care about the extra field
432                         false // don't read the Data Descriptor
433                         );
434         lfhRecord.outputUncompressedData(source, sink);
435     }
436 
437     /**
438      * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
439      */
getUncompressedData( DataSource source, CentralDirectoryRecord cdRecord, long cdStartOffsetInArchive)440     public static byte[] getUncompressedData(
441             DataSource source,
442             CentralDirectoryRecord cdRecord,
443             long cdStartOffsetInArchive) throws ZipFormatException, IOException {
444         if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
445             throw new IOException(
446                     cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
447         }
448         byte[] result = null;
449         try {
450             result = new byte[(int) cdRecord.getUncompressedSize()];
451         } catch (OutOfMemoryError e) {
452             throw new IOException(
453                     cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e);
454         }
455         ByteBuffer resultBuf = ByteBuffer.wrap(result);
456         ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
457         outputUncompressedData(
458                 source,
459                 cdRecord,
460                 cdStartOffsetInArchive,
461                 resultSink);
462         return result;
463     }
464 
465     /**
466      * {@link DataSink} which inflates received data and outputs the deflated data into the provided
467      * delegate sink.
468      */
469     private static class InflateSinkAdapter implements DataSink, Closeable {
470         private final DataSink mDelegate;
471 
472         private Inflater mInflater = new Inflater(true);
473         private byte[] mOutputBuffer;
474         private byte[] mInputBuffer;
475         private long mOutputByteCount;
476         private boolean mClosed;
477 
InflateSinkAdapter(DataSink delegate)478         private InflateSinkAdapter(DataSink delegate) {
479             mDelegate = delegate;
480         }
481 
482         @Override
consume(byte[] buf, int offset, int length)483         public void consume(byte[] buf, int offset, int length) throws IOException {
484             checkNotClosed();
485             mInflater.setInput(buf, offset, length);
486             if (mOutputBuffer == null) {
487                 mOutputBuffer = new byte[65536];
488             }
489             while (!mInflater.finished()) {
490                 int outputChunkSize;
491                 try {
492                     outputChunkSize = mInflater.inflate(mOutputBuffer);
493                 } catch (DataFormatException e) {
494                     throw new IOException("Failed to inflate data", e);
495                 }
496                 if (outputChunkSize == 0) {
497                     return;
498                 }
499                 mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
500                 mOutputByteCount += outputChunkSize;
501             }
502         }
503 
504         @Override
consume(ByteBuffer buf)505         public void consume(ByteBuffer buf) throws IOException {
506             checkNotClosed();
507             if (buf.hasArray()) {
508                 consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
509                 buf.position(buf.limit());
510             } else {
511                 if (mInputBuffer == null) {
512                     mInputBuffer = new byte[65536];
513                 }
514                 while (buf.hasRemaining()) {
515                     int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
516                     buf.get(mInputBuffer, 0, chunkSize);
517                     consume(mInputBuffer, 0, chunkSize);
518                 }
519             }
520         }
521 
getOutputByteCount()522         public long getOutputByteCount() {
523             return mOutputByteCount;
524         }
525 
526         @Override
close()527         public void close() throws IOException {
528             mClosed = true;
529             mInputBuffer = null;
530             mOutputBuffer = null;
531             if (mInflater != null) {
532                 mInflater.end();
533                 mInflater = null;
534             }
535         }
536 
checkNotClosed()537         private void checkNotClosed() {
538             if (mClosed) {
539                 throw new IllegalStateException("Closed");
540             }
541         }
542     }
543 }
544