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