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