1 /*
2  * Copyright (C) 2020 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.apk;
18 
19 import com.android.apksig.internal.util.Pair;
20 import com.android.apksig.internal.zip.ZipUtils;
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.IOException;
26 import java.nio.ByteBuffer;
27 import java.nio.ByteOrder;
28 import java.security.MessageDigest;
29 import java.security.NoSuchAlgorithmException;
30 
31 /**
32  * Lightweight version of the ApkUtils for clients that only require a subset of the utility
33  * functionality.
34  */
35 public class ApkUtilsLite {
ApkUtilsLite()36     private ApkUtilsLite() {}
37 
38     /**
39      * Finds the main ZIP sections of the provided APK.
40      *
41      * @throws IOException if an I/O error occurred while reading the APK
42      * @throws ZipFormatException if the APK is malformed
43      */
findZipSections(DataSource apk)44     public static ZipSections findZipSections(DataSource apk)
45             throws IOException, ZipFormatException {
46         Pair<ByteBuffer, Long> eocdAndOffsetInFile =
47                 ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
48         if (eocdAndOffsetInFile == null) {
49             throw new ZipFormatException("ZIP End of Central Directory record not found");
50         }
51 
52         ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
53         long eocdOffset = eocdAndOffsetInFile.getSecond();
54         eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
55         long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
56         if (cdStartOffset > eocdOffset) {
57             throw new ZipFormatException(
58                     "ZIP Central Directory start offset out of range: " + cdStartOffset
59                             + ". ZIP End of Central Directory offset: " + eocdOffset);
60         }
61 
62         long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
63         long cdEndOffset = cdStartOffset + cdSizeBytes;
64         if (cdEndOffset > eocdOffset) {
65             throw new ZipFormatException(
66                     "ZIP Central Directory overlaps with End of Central Directory"
67                             + ". CD end: " + cdEndOffset
68                             + ", EoCD start: " + eocdOffset);
69         }
70 
71         int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
72 
73         return new ZipSections(
74                 cdStartOffset,
75                 cdSizeBytes,
76                 cdRecordCount,
77                 eocdOffset,
78                 eocdBuf);
79     }
80 
81     // See https://source.android.com/security/apksigning/v2.html
82     private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
83     private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
84     private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
85 
86     /**
87      * Returns the APK Signing Block of the provided APK.
88      *
89      * @throws IOException if an I/O error occurs
90      * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
91      *
92      * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
93      * </a>
94      */
findApkSigningBlock(DataSource apk, ZipSections zipSections)95     public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
96             throws IOException, ApkSigningBlockNotFoundException {
97         // FORMAT (see https://source.android.com/security/apksigning/v2.html):
98         // OFFSET       DATA TYPE  DESCRIPTION
99         // * @+0  bytes uint64:    size in bytes (excluding this field)
100         // * @+8  bytes payload
101         // * @-24 bytes uint64:    size in bytes (same as the one above)
102         // * @-16 bytes uint128:   magic
103 
104         long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
105         long centralDirEndOffset =
106                 centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
107         long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
108         if (centralDirEndOffset != eocdStartOffset) {
109             throw new ApkSigningBlockNotFoundException(
110                     "ZIP Central Directory is not immediately followed by End of Central Directory"
111                             + ". CD end: " + centralDirEndOffset
112                             + ", EoCD start: " + eocdStartOffset);
113         }
114 
115         if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
116             throw new ApkSigningBlockNotFoundException(
117                     "APK too small for APK Signing Block. ZIP Central Directory offset: "
118                             + centralDirStartOffset);
119         }
120         // Read the magic and offset in file from the footer section of the block:
121         // * uint64:   size of block
122         // * 16 bytes: magic
123         ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
124         footer.order(ByteOrder.LITTLE_ENDIAN);
125         if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
126                 || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
127             throw new ApkSigningBlockNotFoundException(
128                     "No APK Signing Block before ZIP Central Directory");
129         }
130         // Read and compare size fields
131         long apkSigBlockSizeInFooter = footer.getLong(0);
132         if ((apkSigBlockSizeInFooter < footer.capacity())
133                 || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
134             throw new ApkSigningBlockNotFoundException(
135                     "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
136         }
137         int totalSize = (int) (apkSigBlockSizeInFooter + 8);
138         long apkSigBlockOffset = centralDirStartOffset - totalSize;
139         if (apkSigBlockOffset < 0) {
140             throw new ApkSigningBlockNotFoundException(
141                     "APK Signing Block offset out of range: " + apkSigBlockOffset);
142         }
143         ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
144         apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
145         long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
146         if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
147             throw new ApkSigningBlockNotFoundException(
148                     "APK Signing Block sizes in header and footer do not match: "
149                             + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
150         }
151         return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
152     }
153 
154     /**
155      * Information about the location of the APK Signing Block inside an APK.
156      */
157     public static class ApkSigningBlock {
158         private final long mStartOffsetInApk;
159         private final DataSource mContents;
160 
161         /**
162          * Constructs a new {@code ApkSigningBlock}.
163          *
164          * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
165          *        Signing Block inside the APK file
166          * @param contents contents of the APK Signing Block
167          */
ApkSigningBlock(long startOffsetInApk, DataSource contents)168         public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
169             mStartOffsetInApk = startOffsetInApk;
170             mContents = contents;
171         }
172 
173         /**
174          * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
175          */
getStartOffset()176         public long getStartOffset() {
177             return mStartOffsetInApk;
178         }
179 
180         /**
181          * Returns the data source which provides the full contents of the APK Signing Block,
182          * including its footer.
183          */
getContents()184         public DataSource getContents() {
185             return mContents;
186         }
187     }
188 
computeSha256DigestBytes(byte[] data)189     public static byte[] computeSha256DigestBytes(byte[] data) {
190         MessageDigest messageDigest;
191         try {
192             messageDigest = MessageDigest.getInstance("SHA-256");
193         } catch (NoSuchAlgorithmException e) {
194             throw new IllegalStateException("SHA-256 is not found", e);
195         }
196         messageDigest.update(data);
197         return messageDigest.digest();
198     }
199 }
200