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.apk.ApkFormatException; 20 import com.android.apksig.internal.util.Pair; 21 import com.android.apksig.util.DataSource; 22 import com.android.apksig.zip.ZipFormatException; 23 import com.android.apksig.zip.ZipSections; 24 25 import java.io.ByteArrayOutputStream; 26 import java.io.IOException; 27 import java.nio.ByteBuffer; 28 import java.nio.ByteOrder; 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.zip.CRC32; 32 import java.util.zip.Deflater; 33 34 /** 35 * Assorted ZIP format helpers. 36 * 37 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 38 * order of these buffers is little-endian. 39 */ 40 public abstract class ZipUtils { ZipUtils()41 private ZipUtils() {} 42 43 public static final short COMPRESSION_METHOD_STORED = 0; 44 public static final short COMPRESSION_METHOD_DEFLATED = 8; 45 46 public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; 47 public static final short GP_FLAG_EFS = 0x0800; 48 49 private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 50 private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 51 private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; 52 private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 53 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 54 private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 55 56 public static final int ZIP64_RECORD_ID = 0x1; 57 public static final String ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME = "uncompressedSize"; 58 public static final String ZIP64_COMPRESSED_SIZE_FIELD_NAME = "compressedSize"; 59 public static final String ZIP64_LFH_OFFSET_FIELD_NAME = "localFileHeaderOffset"; 60 61 public static final int UINT16_MAX_VALUE = 0xffff; 62 public static final long UINT32_MAX_VALUE = 0xffffffffL; 63 64 /** 65 * Sets the offset of the start of the ZIP Central Directory in the archive. 66 * 67 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 68 */ setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)69 public static void setZipEocdCentralDirectoryOffset( 70 ByteBuffer zipEndOfCentralDirectory, long offset) { 71 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 72 setUnsignedInt32( 73 zipEndOfCentralDirectory, 74 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, 75 offset); 76 } 77 78 /** 79 * Sets the length of EOCD comment. 80 * 81 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 82 */ updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory)83 public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { 84 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 85 int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE; 86 setUnsignedInt16( 87 zipEndOfCentralDirectory, 88 zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET, 89 commentLen); 90 } 91 92 /** 93 * Returns the offset of the start of the ZIP Central Directory in the archive. 94 * 95 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 96 */ getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)97 public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 98 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 99 return getUnsignedInt32( 100 zipEndOfCentralDirectory, 101 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 102 } 103 104 /** 105 * Returns the size (in bytes) of the ZIP Central Directory. 106 * 107 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 108 */ getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)109 public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 110 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 111 return getUnsignedInt32( 112 zipEndOfCentralDirectory, 113 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 114 } 115 116 /** 117 * Returns the total number of records in ZIP Central Directory. 118 * 119 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 120 */ getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)121 public static int getZipEocdCentralDirectoryTotalRecordCount( 122 ByteBuffer zipEndOfCentralDirectory) { 123 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 124 return getUnsignedInt16( 125 zipEndOfCentralDirectory, 126 zipEndOfCentralDirectory.position() 127 + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); 128 } 129 130 /** 131 * Returns the ZIP End of Central Directory record of the provided ZIP file. 132 * 133 * @return contents of the ZIP End of Central Directory record and the record's offset in the 134 * file or {@code null} if the file does not contain the record. 135 * 136 * @throws IOException if an I/O error occurs while reading the file. 137 */ findZipEndOfCentralDirectoryRecord(DataSource zip)138 public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip) 139 throws IOException { 140 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 141 // The record can be identified by its 4-byte signature/magic which is located at the very 142 // beginning of the record. A complication is that the record is variable-length because of 143 // the comment field. 144 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 145 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 146 // the candidate record's comment length is such that the remainder of the record takes up 147 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 148 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 149 150 long fileSize = zip.size(); 151 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 152 return null; 153 } 154 155 // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 156 // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 157 // reading more data. 158 Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); 159 if (result != null) { 160 return result; 161 } 162 163 // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 164 // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 165 // the comment length field is an unsigned 16-bit number. 166 return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 167 } 168 169 /** 170 * Returns the ZIP End of Central Directory record of the provided ZIP file. 171 * 172 * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 173 * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 174 * locates the record, provided its comment field is no longer than this value. 175 * 176 * @return contents of the ZIP End of Central Directory record and the record's offset in the 177 * file or {@code null} if the file does not contain the record. 178 * 179 * @throws IOException if an I/O error occurs while reading the file. 180 */ findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)181 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( 182 DataSource zip, int maxCommentSize) throws IOException { 183 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 184 // The record can be identified by its 4-byte signature/magic which is located at the very 185 // beginning of the record. A complication is that the record is variable-length because of 186 // the comment field. 187 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 188 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 189 // the candidate record's comment length is such that the remainder of the record takes up 190 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 191 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 192 193 if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 194 throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 195 } 196 197 long fileSize = zip.size(); 198 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 199 // No space for EoCD record in the file. 200 return null; 201 } 202 // Lower maxCommentSize if the file is too small. 203 maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 204 205 int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 206 long bufOffsetInFile = fileSize - maxEocdSize; 207 ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); 208 buf.order(ByteOrder.LITTLE_ENDIAN); 209 int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 210 if (eocdOffsetInBuf == -1) { 211 // No EoCD record found in the buffer 212 return null; 213 } 214 // EoCD found 215 buf.position(eocdOffsetInBuf); 216 ByteBuffer eocd = buf.slice(); 217 eocd.order(ByteOrder.LITTLE_ENDIAN); 218 return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); 219 } 220 221 /** 222 * Returns the position at which ZIP End of Central Directory record starts in the provided 223 * buffer or {@code -1} if the record is not present. 224 * 225 * <p>NOTE: Byte order of {@code zipContents} must be little-endian. 226 */ findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)227 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 228 assertByteOrderLittleEndian(zipContents); 229 230 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 231 // The record can be identified by its 4-byte signature/magic which is located at the very 232 // beginning of the record. A complication is that the record is variable-length because of 233 // the comment field. 234 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 235 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 236 // the candidate record's comment length is such that the remainder of the record takes up 237 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 238 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 239 240 int archiveSize = zipContents.capacity(); 241 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 242 return -1; 243 } 244 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 245 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 246 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 247 expectedCommentLength++) { 248 int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 249 if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 250 int actualCommentLength = 251 getUnsignedInt16( 252 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 253 if (actualCommentLength == expectedCommentLength) { 254 return eocdStartPos; 255 } 256 } 257 } 258 259 return -1; 260 } 261 262 /** 263 * Parses the provided extra field for the ZIP64 block and sets the fields in the provided 264 * {@code zip64Fields} that were affected by the 32-bit limit. 265 * 266 * <p>Since the ZIP64 block only includes those fields that exceed the limit, the specified 267 * {@code zip64Fields} is used to determine which fields should be read and updated from the 268 * ZIP64 block. 269 */ parseExtraField(ByteBuffer extra, Zip64Fields zip64Fields)270 static void parseExtraField(ByteBuffer extra, Zip64Fields zip64Fields) 271 throws ZipFormatException { 272 extra.order(ByteOrder.LITTLE_ENDIAN); 273 // Each record within the extra field must contain at least a UINT16 headerId and size 274 // FORMAT: 275 // * uint16: headerId 276 // * uint16: size 277 // * Payload of the specified size 278 while (extra.remaining() > 4) { 279 int headerId = getUnsignedInt16(extra); 280 int extraRecordSize = getUnsignedInt16(extra); 281 if (extraRecordSize > extra.remaining()) { 282 throw new ZipFormatException( 283 "Extra field record with ID " 284 + Long.toHexString(headerId) 285 + " exceeds size of field; size of block: " 286 + extraRecordSize 287 + ", remaining extra buffer: " 288 + extra.remaining()); 289 } 290 if (headerId == ZIP64_RECORD_ID) { 291 // Each field in the ZIP64 record only exists if the corresponding field in the 292 // local file header / central directory with the UINT32 max value; the fields must 293 // always be in the order uncompressedSize, compressedSize, and 294 // localFileHeaderOffset, where applicable. 295 // ZIP64 FORMAT: 296 // * uint64: uncompressed size (if the base uncompressed value is 0xffffffff) 297 // * uint64: compressed size (if the base compressed value is 0xffffffff) 298 // * uint64: local file header offset (if the base LFH offset value is 0xffffffff) 299 if (zip64Fields.uncompressedSize == UINT32_MAX_VALUE) { 300 if (extraRecordSize >= 8) { 301 zip64Fields.uncompressedSize = extra.getLong(); 302 extraRecordSize -= 8; 303 } else { 304 throw new ZipFormatException( 305 "Expected an uncompressed size value in the ZIP64 record, " 306 + "remaining size of record: " 307 + extraRecordSize); 308 } 309 } 310 if (zip64Fields.compressedSize == UINT32_MAX_VALUE) { 311 if (extraRecordSize >= 8) { 312 zip64Fields.compressedSize = extra.getLong(); 313 extraRecordSize -= 8; 314 } else { 315 throw new ZipFormatException( 316 "Expected a compressed size value in the ZIP64 record, " 317 + "remaining size of record: " 318 + extraRecordSize); 319 } 320 } 321 if (zip64Fields.localFileHeaderOffset == UINT32_MAX_VALUE) { 322 if (extraRecordSize >= 8) { 323 zip64Fields.localFileHeaderOffset = extra.getLong(); 324 } else { 325 throw new ZipFormatException( 326 "Expected a LFH offset in the ZIP64 record, " 327 + "remaining size of record: " 328 + extraRecordSize); 329 } 330 } 331 // Once the ZIP64 record is found, no further parsing is required. 332 break; 333 } else { 334 // Skip over the unexpected record and check subsequent records. 335 extra.position(extra.position() + extraRecordSize); 336 } 337 } 338 } 339 340 /** 341 * Checks whether the provided {@code headerValue} from the LFH / CD Record exceeds the 32-bit 342 * limit and must be obtained from the Zip64 record; if so, then the specified {@code 343 * zip64Value} is verified and returned to the caller. 344 */ checkAndReturnZip64Value( long headerValue, long zip64Value, String name, String fieldName)345 static long checkAndReturnZip64Value( 346 long headerValue, long zip64Value, String name, String fieldName) 347 throws ZipFormatException { 348 // If the value in the header does not indicate that the value exceeds the 32-bit 349 // limitation and must be in the Zip64 record, then return the provided value. 350 if (headerValue != UINT32_MAX_VALUE) { 351 return headerValue; 352 } 353 if (zip64Value == UINT32_MAX_VALUE) { 354 throw new ZipFormatException( 355 "Unable to obtain ZIP64 " + fieldName + " field for record: " + name); 356 } 357 return zip64Value; 358 } 359 assertByteOrderLittleEndian(ByteBuffer buffer)360 static void assertByteOrderLittleEndian(ByteBuffer buffer) { 361 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 362 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 363 } 364 } 365 getUnsignedInt16(ByteBuffer buffer, int offset)366 public static int getUnsignedInt16(ByteBuffer buffer, int offset) { 367 return buffer.getShort(offset) & 0xffff; 368 } 369 getUnsignedInt16(ByteBuffer buffer)370 public static int getUnsignedInt16(ByteBuffer buffer) { 371 return buffer.getShort() & 0xffff; 372 } 373 parseZipCentralDirectory( DataSource apk, ZipSections apkSections)374 public static List<CentralDirectoryRecord> parseZipCentralDirectory( 375 DataSource apk, 376 ZipSections apkSections) 377 throws IOException, ApkFormatException { 378 // Read the ZIP Central Directory 379 long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); 380 if (cdSizeBytes > Integer.MAX_VALUE) { 381 throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); 382 } 383 long cdOffset = apkSections.getZipCentralDirectoryOffset(); 384 ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); 385 cd.order(ByteOrder.LITTLE_ENDIAN); 386 387 // Parse the ZIP Central Directory 388 int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); 389 List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); 390 for (int i = 0; i < expectedCdRecordCount; i++) { 391 CentralDirectoryRecord cdRecord; 392 int offsetInsideCd = cd.position(); 393 try { 394 cdRecord = CentralDirectoryRecord.getRecord(cd); 395 } catch (ZipFormatException e) { 396 throw new ApkFormatException( 397 "Malformed ZIP Central Directory record #" + (i + 1) 398 + " at file offset " + (cdOffset + offsetInsideCd), 399 e); 400 } 401 String entryName = cdRecord.getName(); 402 if (entryName.endsWith("/")) { 403 // Ignore directory entries 404 continue; 405 } 406 cdRecords.add(cdRecord); 407 } 408 // There may be more data in Central Directory, but we don't warn or throw because Android 409 // ignores unused CD data. 410 411 return cdRecords; 412 } 413 setUnsignedInt16(ByteBuffer buffer, int offset, int value)414 static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { 415 if ((value < 0) || (value > 0xffff)) { 416 throw new IllegalArgumentException("uint16 value of out range: " + value); 417 } 418 buffer.putShort(offset, (short) value); 419 } 420 setUnsignedInt32(ByteBuffer buffer, int offset, long value)421 static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 422 if ((value < 0) || (value > 0xffffffffL)) { 423 throw new IllegalArgumentException("uint32 value of out range: " + value); 424 } 425 buffer.putInt(offset, (int) value); 426 } 427 putUnsignedInt16(ByteBuffer buffer, int value)428 public static void putUnsignedInt16(ByteBuffer buffer, int value) { 429 if ((value < 0) || (value > 0xffff)) { 430 throw new IllegalArgumentException("uint16 value of out range: " + value); 431 } 432 buffer.putShort((short) value); 433 } 434 getUnsignedInt32(ByteBuffer buffer, int offset)435 static long getUnsignedInt32(ByteBuffer buffer, int offset) { 436 return buffer.getInt(offset) & 0xffffffffL; 437 } 438 getUnsignedInt32(ByteBuffer buffer)439 static long getUnsignedInt32(ByteBuffer buffer) { 440 return buffer.getInt() & 0xffffffffL; 441 } 442 putUnsignedInt32(ByteBuffer buffer, long value)443 static void putUnsignedInt32(ByteBuffer buffer, long value) { 444 if ((value < 0) || (value > 0xffffffffL)) { 445 throw new IllegalArgumentException("uint32 value of out range: " + value); 446 } 447 buffer.putInt((int) value); 448 } 449 deflate(ByteBuffer input)450 public static DeflateResult deflate(ByteBuffer input) { 451 byte[] inputBuf; 452 int inputOffset; 453 int inputLength = input.remaining(); 454 if (input.hasArray()) { 455 inputBuf = input.array(); 456 inputOffset = input.arrayOffset() + input.position(); 457 input.position(input.limit()); 458 } else { 459 inputBuf = new byte[inputLength]; 460 inputOffset = 0; 461 input.get(inputBuf); 462 } 463 CRC32 crc32 = new CRC32(); 464 crc32.update(inputBuf, inputOffset, inputLength); 465 long crc32Value = crc32.getValue(); 466 ByteArrayOutputStream out = new ByteArrayOutputStream(); 467 Deflater deflater = new Deflater(9, true); 468 deflater.setInput(inputBuf, inputOffset, inputLength); 469 deflater.finish(); 470 byte[] buf = new byte[65536]; 471 while (!deflater.finished()) { 472 int chunkSize = deflater.deflate(buf); 473 out.write(buf, 0, chunkSize); 474 } 475 return new DeflateResult(inputLength, crc32Value, out.toByteArray()); 476 } 477 478 public static class DeflateResult { 479 public final int inputSizeBytes; 480 public final long inputCrc32; 481 public final byte[] output; 482 DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)483 public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { 484 this.inputSizeBytes = inputSizeBytes; 485 this.inputCrc32 = inputCrc32; 486 this.output = output; 487 } 488 } 489 490 /** 491 * Class containing the file header / central directory fields that can be affected by the 32- 492 * bit limit. In the case that any of these fields exceed this limit, the value will be set to 493 * 0xffffffff, and the value can be found in the extra field. This class can be used with {@link 494 * #parseExtraField(ByteBuffer, Zip64Fields)} to obtain the corresponding values for each 495 * affected field. 496 */ 497 static class Zip64Fields { 498 public long uncompressedSize; 499 public long compressedSize; 500 public long localFileHeaderOffset; 501 Zip64Fields(long uncompressedSize, long compressedSize)502 Zip64Fields(long uncompressedSize, long compressedSize) { 503 this(uncompressedSize, compressedSize, -1); 504 } 505 Zip64Fields(long uncompressedSize, long compressedSize, long localFileHeaderOffset)506 Zip64Fields(long uncompressedSize, long compressedSize, long localFileHeaderOffset) { 507 this.uncompressedSize = uncompressedSize; 508 this.compressedSize = compressedSize; 509 this.localFileHeaderOffset = localFileHeaderOffset; 510 } 511 } 512 }