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