1 /* 2 * Copyright (C) 2019 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.fastdeploy; 18 19 import android.util.Log; 20 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.RandomAccessFile; 24 import java.nio.ByteBuffer; 25 import java.nio.ByteOrder; 26 import java.nio.channels.FileChannel; 27 28 /** 29 * Extremely light-weight APK parser class. 30 * Aware of Central Directory, Local File Headers and Signature. 31 * No Zip64 support yet. 32 */ 33 public final class ApkArchive { 34 private static final String TAG = "ApkArchive"; 35 36 // Central Directory constants. 37 private static final int EOCD_SIGNATURE = 0x06054b50; 38 private static final int EOCD_MIN_SIZE = 22; 39 private static final long EOCD_MAX_SIZE = 65_535L + EOCD_MIN_SIZE; 40 41 private static final int CD_ENTRY_HEADER_SIZE_BYTES = 22; 42 private static final int CD_LOCAL_FILE_HEADER_SIZE_OFFSET = 12; 43 44 // Signature constants. 45 private static final int EOSIGNATURE_SIZE = 24; 46 47 public final static class Dump { 48 final byte[] cd; 49 final byte[] signature; 50 Dump(byte[] cd, byte[] signature)51 Dump(byte[] cd, byte[] signature) { 52 this.cd = cd; 53 this.signature = signature; 54 } 55 } 56 57 final static class Location { 58 final long offset; 59 final long size; 60 Location(long offset, long size)61 public Location(long offset, long size) { 62 this.offset = offset; 63 this.size = size; 64 } 65 } 66 67 private final RandomAccessFile mFile; 68 private final FileChannel mChannel; 69 ApkArchive(File apk)70 public ApkArchive(File apk) throws IOException { 71 mFile = new RandomAccessFile(apk, "r"); 72 mChannel = mFile.getChannel(); 73 } 74 75 /** 76 * Extract the APK metadata: content of Central Directory and Signature. 77 * 78 * @return raw content from APK representing CD and Signature data. 79 */ extractMetadata()80 public Dump extractMetadata() throws IOException { 81 Location cdLoc = getCDLocation(); 82 byte[] cd = readMetadata(cdLoc); 83 84 byte[] signature = null; 85 Location sigLoc = getSignatureLocation(cdLoc.offset); 86 if (sigLoc != null) { 87 signature = readMetadata(sigLoc); 88 long size = ByteBuffer.wrap(signature).order(ByteOrder.LITTLE_ENDIAN).getLong(); 89 if (sigLoc.size != size) { 90 Log.e(TAG, "Mismatching signature sizes: " + sigLoc.size + " != " + size); 91 signature = null; 92 } 93 } 94 95 return new Dump(cd, signature); 96 } 97 findEndOfCDRecord()98 private long findEndOfCDRecord() throws IOException { 99 final long fileSize = mChannel.size(); 100 int sizeToRead = Math.toIntExact(Math.min(fileSize, EOCD_MAX_SIZE)); 101 final long readOffset = fileSize - sizeToRead; 102 ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, readOffset, 103 sizeToRead).order(ByteOrder.LITTLE_ENDIAN); 104 105 buffer.position(sizeToRead - EOCD_MIN_SIZE); 106 while (true) { 107 int signature = buffer.getInt(); // Read 4 bytes. 108 if (signature == EOCD_SIGNATURE) { 109 return readOffset + buffer.position() - 4; 110 } 111 if (buffer.position() == 4) { 112 break; 113 } 114 buffer.position(buffer.position() - Integer.BYTES - 1); // Backtrack 5 bytes. 115 } 116 117 return -1L; 118 } 119 findCDRecord(ByteBuffer buf)120 private Location findCDRecord(ByteBuffer buf) { 121 if (buf.order() != ByteOrder.LITTLE_ENDIAN) { 122 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 123 } 124 if (buf.remaining() < CD_ENTRY_HEADER_SIZE_BYTES) { 125 throw new IllegalArgumentException( 126 "Input too short. Need at least " + CD_ENTRY_HEADER_SIZE_BYTES 127 + " bytes, available: " + buf.remaining() + "bytes."); 128 } 129 130 int originalPosition = buf.position(); 131 int recordSignature = buf.getInt(); 132 if (recordSignature != EOCD_SIGNATURE) { 133 throw new IllegalArgumentException( 134 "Not a Central Directory record. Signature: 0x" 135 + Long.toHexString(recordSignature & 0xffffffffL)); 136 } 137 138 buf.position(originalPosition + CD_LOCAL_FILE_HEADER_SIZE_OFFSET); 139 long size = buf.getInt() & 0xffffffffL; 140 long offset = buf.getInt() & 0xffffffffL; 141 return new Location(offset, size); 142 } 143 144 // Retrieve the location of the Central Directory Record. getCDLocation()145 Location getCDLocation() throws IOException { 146 long eocdRecord = findEndOfCDRecord(); 147 if (eocdRecord < 0) { 148 throw new IllegalArgumentException("Unable to find End of Central Directory record."); 149 } 150 151 Location location = findCDRecord(mChannel.map(FileChannel.MapMode.READ_ONLY, eocdRecord, 152 CD_ENTRY_HEADER_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN)); 153 if (location == null) { 154 throw new IllegalArgumentException("Unable to find Central Directory File Header."); 155 } 156 157 return location; 158 } 159 160 // Retrieve the location of the signature block starting from Central 161 // Directory Record or null if signature is not found. getSignatureLocation(long cdRecordOffset)162 Location getSignatureLocation(long cdRecordOffset) throws IOException { 163 long signatureOffset = cdRecordOffset - EOSIGNATURE_SIZE; 164 if (signatureOffset < 0) { 165 Log.e(TAG, "Unable to find Signature."); 166 return null; 167 } 168 169 ByteBuffer signature = mChannel.map(FileChannel.MapMode.READ_ONLY, signatureOffset, 170 EOSIGNATURE_SIZE).order(ByteOrder.LITTLE_ENDIAN); 171 172 long size = signature.getLong(); 173 174 byte[] sign = new byte[16]; 175 signature.get(sign); 176 String signAsString = new String(sign); 177 if (!"APK Sig Block 42".equals(signAsString)) { 178 Log.e(TAG, "Signature magic does not match: " + signAsString); 179 return null; 180 } 181 182 long offset = cdRecordOffset - size - 8; 183 184 return new Location(offset, size); 185 } 186 readMetadata(Location loc)187 private byte[] readMetadata(Location loc) throws IOException { 188 byte[] payload = new byte[(int) loc.size]; 189 ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, loc.offset, loc.size); 190 buffer.get(payload); 191 return payload; 192 } 193 } 194