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 = new byte[(int) cdRecord.getUncompressedSize()];
449         ByteBuffer resultBuf = ByteBuffer.wrap(result);
450         ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
451         outputUncompressedData(
452                 source,
453                 cdRecord,
454                 cdStartOffsetInArchive,
455                 resultSink);
456         return result;
457     }
458 
459     /**
460      * {@link DataSink} which inflates received data and outputs the deflated data into the provided
461      * delegate sink.
462      */
463     private static class InflateSinkAdapter implements DataSink, Closeable {
464         private final DataSink mDelegate;
465 
466         private Inflater mInflater = new Inflater(true);
467         private byte[] mOutputBuffer;
468         private byte[] mInputBuffer;
469         private long mOutputByteCount;
470         private boolean mClosed;
471 
InflateSinkAdapter(DataSink delegate)472         private InflateSinkAdapter(DataSink delegate) {
473             mDelegate = delegate;
474         }
475 
476         @Override
consume(byte[] buf, int offset, int length)477         public void consume(byte[] buf, int offset, int length) throws IOException {
478             checkNotClosed();
479             mInflater.setInput(buf, offset, length);
480             if (mOutputBuffer == null) {
481                 mOutputBuffer = new byte[65536];
482             }
483             while (!mInflater.finished()) {
484                 int outputChunkSize;
485                 try {
486                     outputChunkSize = mInflater.inflate(mOutputBuffer);
487                 } catch (DataFormatException e) {
488                     throw new IOException("Failed to inflate data", e);
489                 }
490                 if (outputChunkSize == 0) {
491                     return;
492                 }
493                 mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
494                 mOutputByteCount += outputChunkSize;
495             }
496         }
497 
498         @Override
consume(ByteBuffer buf)499         public void consume(ByteBuffer buf) throws IOException {
500             checkNotClosed();
501             if (buf.hasArray()) {
502                 consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
503                 buf.position(buf.limit());
504             } else {
505                 if (mInputBuffer == null) {
506                     mInputBuffer = new byte[65536];
507                 }
508                 while (buf.hasRemaining()) {
509                     int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
510                     buf.get(mInputBuffer, 0, chunkSize);
511                     consume(mInputBuffer, 0, chunkSize);
512                 }
513             }
514         }
515 
getOutputByteCount()516         public long getOutputByteCount() {
517             return mOutputByteCount;
518         }
519 
520         @Override
close()521         public void close() throws IOException {
522             mClosed = true;
523             mInputBuffer = null;
524             mOutputBuffer = null;
525             if (mInflater != null) {
526                 mInflater.end();
527                 mInflater = null;
528             }
529         }
530 
checkNotClosed()531         private void checkNotClosed() {
532             if (mClosed) {
533                 throw new IllegalStateException("Closed");
534             }
535         }
536     }
537 }
538