前言
项目中的手机客户端需要根据下载用户的不同附带不同的信息,所以就需要在WEB服务器端动态生成APK,于是有了下面的经历。
历程
1.一开始的想法就是在APK本身就是个ZIP压缩包嘛,我把他解压缩,在里面中放一个配置文件,然后下载的时候,WEB服务器根据用户信息的不同动态替换这个配置文件,最后重新压缩成apk,然后APP运行的时候,读取这个配置文件就可以了。代码写好了,生成的安装包放手机一安装,安装包解析失败!
2.于是各种找资料,好多博客里面也都写Java动态往APK中添加配置文件,修改配置文件,可年代已经久远,加上本人手机安卓9.0了,估计是不行。于是找到一个往ZIP的comment区添加信息的博客,是一种新思路。于是复制代码实现了这个功能,往comments区添加信息,读取信息都可以,但安装的时候还是失败了。博主写的签名时只勾选V1的操作也试了,还是不行,还是不行。原文链接:https://www.jianshu.com/p/013f953d4508
3.感觉高版本对安装包的解析太严格了,于是又找资料,幸运的发现了美团的github项目:https://github.com/Meituan-Dianping/walle,美团的多渠道打包工具,提供了一个walle-cli-all.jar
包,在这个项目下找不到,在另一个项目中找到了这包,先没写代码,用这个包直接运行试了下,添加了一些渠道信息和额外信息,可以运行,放到手机上,安装成功了,有点激动,终于找到个可用的办法了!
代码
代码不是我原创的,是美团的github项目上下下来的,下面粘贴处我用到的核心代码。
往APK中写入信息:
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.FileOutputStream; 4 import java.io.IOException; 5 import java.io.RandomAccessFile; 6 import java.nio.ByteBuffer; 7 import java.nio.ByteOrder; 8 import java.nio.channels.FileChannel; 9 import java.util.HashMap; 10 import java.util.Map; 11 import java.util.Set; 12 import java.util.UUID; 13 14 15 public final class PayloadWriter { 16 private PayloadWriter() { 17 super(); 18 } 19 20 /** 21 * put (id, String) into apk, update if id exists 22 * @param apkFile apk file 23 * @param id id 24 * @param string string content 25 * @throws IOException 26 * @throws SignatureNotFoundException 27 */ 28 public static void put(final File apkFile, final int id, final String string) throws IOException, SignatureNotFoundException { 29 put(apkFile, id, string, false); 30 } 31 /** 32 * put (id, String) into apk, update if id exists 33 * @param apkFile apk file 34 * @param id id 35 * @param string string 36 * @param lowMemory if need low memory operation, maybe a little slower 37 * @throws IOException 38 * @throws SignatureNotFoundException 39 */ 40 public static void put(final File apkFile, final int id, final String string, final boolean lowMemory) throws IOException, SignatureNotFoundException { 41 final byte[] bytes = string.getBytes(ApkUtil.DEFAULT_CHARSET); 42 final ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); 43 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 44 byteBuffer.put(bytes, 0, bytes.length); 45 byteBuffer.flip(); 46 put(apkFile, id, byteBuffer, lowMemory); 47 } 48 /** 49 * put (id, buffer) into apk, update if id exists 50 * 51 * @param apkFile apk file 52 * @param id id 53 * @param buffer buffer 54 * @throws IOException 55 * @throws SignatureNotFoundException 56 */ 57 public static void put(final File apkFile, final int id, final ByteBuffer buffer) throws IOException, SignatureNotFoundException { 58 put(apkFile, id, buffer, false); 59 } 60 61 /** 62 * put (id, buffer) into apk, update if id exists 63 * @param apkFile apk file 64 * @param id id 65 * @param buffer buffer 66 * @param lowMemory if need low memory operation, maybe a little slower 67 * @throws IOException 68 * @throws SignatureNotFoundException 69 */ 70 public static void put(final File apkFile, final int id, final ByteBuffer buffer, final boolean lowMemory) throws IOException, SignatureNotFoundException { 71 final Map<Integer, ByteBuffer> idValues = new HashMap<Integer, ByteBuffer>(); 72 idValues.put(id, buffer); 73 putAll(apkFile, idValues, lowMemory); 74 } 75 /** 76 * put new idValues into apk, update if id exists 77 * 78 * @param apkFile apk file 79 * @param idValues id value. NOTE: use unknown IDs. DO NOT use ID that have already been used. See <a href='https://source.android.com/security/apksigning/v2.html'>APK Signature Scheme v2</a> 80 * @throws IOException 81 * @throws SignatureNotFoundException 82 */ 83 public static void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues) throws IOException, SignatureNotFoundException { 84 putAll(apkFile, idValues, false); 85 } 86 /** 87 * put new idValues into apk, update if id exists 88 * 89 * @param apkFile apk file 90 * @param idValues id value. NOTE: use unknown IDs. DO NOT use ID that have already been used. See <a href='https://source.android.com/security/apksigning/v2.html'>APK Signature Scheme v2</a> 91 * @param lowMemory if need low memory operation, maybe a little slower 92 * @throws IOException 93 * @throws SignatureNotFoundException 94 */ 95 public static void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues, final boolean lowMemory) throws IOException, SignatureNotFoundException { 96 handleApkSigningBlock(apkFile, new ApkSigningBlockHandler() { 97 @Override 98 public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) { 99 if (idValues != null && !idValues.isEmpty()) { 100 originIdValues.putAll(idValues); 101 } 102 final ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); 103 final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet(); 104 for (Map.Entry<Integer, ByteBuffer> entry : entrySet) { 105 final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue()); 106 apkSigningBlock.addPayload(payload); 107 } 108 return apkSigningBlock; 109 } 110 }, lowMemory); 111 } 112 /** 113 * remove content by id 114 * 115 * @param apkFile apk file 116 * @param id id 117 * @throws IOException 118 * @throws SignatureNotFoundException 119 */ 120 public static void remove(final File apkFile, final int id) throws IOException, SignatureNotFoundException { 121 remove(apkFile, id, false); 122 } 123 /** 124 * remove content by id 125 * 126 * @param apkFile apk file 127 * @param id id 128 * @param lowMemory if need low memory operation, maybe a little slower 129 * @throws IOException 130 * @throws SignatureNotFoundException 131 */ 132 public static void remove(final File apkFile, final int id, final boolean lowMemory) throws IOException, SignatureNotFoundException { 133 PayloadWriter.handleApkSigningBlock(apkFile, new PayloadWriter.ApkSigningBlockHandler() { 134 @Override 135 public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) { 136 final ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); 137 final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet(); 138 for (Map.Entry<Integer, ByteBuffer> entry : entrySet) { 139 if (entry.getKey() != id) { 140 final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue()); 141 apkSigningBlock.addPayload(payload); 142 } 143 } 144 return apkSigningBlock; 145 } 146 }, lowMemory); 147 } 148 149 interface ApkSigningBlockHandler { 150 ApkSigningBlock handle(Map<Integer, ByteBuffer> originIdValues); 151 } 152 153 static void handleApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler, final boolean lowMemory) throws IOException, SignatureNotFoundException { 154 RandomAccessFile fIn = null; 155 FileChannel fileChannel = null; 156 try { 157 fIn = new RandomAccessFile(apkFile, "rw"); 158 fileChannel = fIn.getChannel(); 159 final long commentLength = ApkUtil.getCommentLength(fileChannel); 160 final long centralDirStartOffset = ApkUtil.findCentralDirStartOffset(fileChannel, commentLength); 161 // Find the APK Signing Block. The block immediately precedes the Central Directory. 162 final Pair<ByteBuffer, Long> apkSigningBlockAndOffset = ApkUtil.findApkSigningBlock(fileChannel, centralDirStartOffset); 163 final ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst(); 164 final long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); 165 166 final Map<Integer, ByteBuffer> originIdValues = ApkUtil.findIdValues(apkSigningBlock2); 167 // Find the APK Signature Scheme v2 Block inside the APK Signing Block. 168 final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(ApkUtil.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); 169 170 if (apkSignatureSchemeV2Block == null) { 171 throw new IOException( 172 "No APK Signature Scheme v2 block in APK Signing Block"); 173 } 174 175 final boolean needPadding = originIdValues.remove(ApkUtil.VERITY_PADDING_BLOCK_ID) != null; 176 final ApkSigningBlock apkSigningBlock = handler.handle(originIdValues); 177 // replace VERITY_PADDING_BLOCK with new one 178 if (needPadding) { 179 // uint64: size (excluding this field) 180 // repeated ID-value pairs: 181 // uint64: size (excluding this field) 182 // uint32: ID 183 // (size - 4) bytes: value 184 // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes) 185 // uint64: size (same as the one above) 186 // uint128: magic 187 188 int blocksSize = 0; 189 for (ApkSigningPayload payload : apkSigningBlock.getPayloads()) { 190 blocksSize += payload.getTotalSize(); 191 } 192 193 int resultSize = 8 + blocksSize + 8 + 16; // size(uint64) + pairs size + size(uint64) + magic(uint128) 194 if (resultSize % ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { 195 int padding = ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - 12 // size(uint64) + id(uint32) 196 - (resultSize % ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 197 if (padding < 0) { 198 padding += ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; 199 } 200 final ByteBuffer dummy = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN); 201 apkSigningBlock.addPayload(new ApkSigningPayload(ApkUtil.VERITY_PADDING_BLOCK_ID,dummy)); 202 } 203 } 204 205 if (apkSigningBlockOffset != 0 && centralDirStartOffset != 0) { 206 207 // read CentralDir 208 fIn.seek(centralDirStartOffset); 209 210 byte[] centralDirBytes = null; 211 File tempCentralBytesFile = null; 212 // read CentralDir 213 if (lowMemory) { 214 tempCentralBytesFile = new File(apkFile.getParent(), UUID.randomUUID().toString()); 215 FileOutputStream outStream = null; 216 try { 217 outStream = new FileOutputStream(tempCentralBytesFile); 218 final byte[] buffer = new byte[1024]; 219 220 int len; 221 while ((len = fIn.read(buffer)) > 0){ 222 outStream.write(buffer, 0, len); 223 } 224 } finally { 225 if (outStream != null) { 226 outStream.close(); 227 } 228 } 229 } else { 230 centralDirBytes = new byte[(int) (fileChannel.size() - centralDirStartOffset)]; 231 fIn.read(centralDirBytes); 232 } 233 234 //update apk sign 235 fileChannel.position(apkSigningBlockOffset); 236 final long length = apkSigningBlock.writeApkSigningBlock(fIn); 237 238 // update CentralDir 239 if (lowMemory) { 240 FileInputStream inputStream = null; 241 try { 242 inputStream = new FileInputStream(tempCentralBytesFile); 243 final byte[] buffer = new byte[1024]; 244 245 int len; 246 while ((len = inputStream.read(buffer)) > 0){ 247 fIn.write(buffer, 0, len); 248 } 249 } finally { 250 if (inputStream != null) { 251 inputStream.close(); 252 } 253 tempCentralBytesFile.delete(); 254 } 255 } else { 256 // store CentralDir 257 fIn.write(centralDirBytes); 258 } 259 // update length 260 fIn.setLength(fIn.getFilePointer()); 261 262 // update CentralDir Offset 263 264 // End of central directory record (EOCD) 265 // Offset Bytes Description[23] 266 // 0 4 End of central directory signature = 0x06054b50 267 // 4 2 Number of this disk 268 // 6 2 Disk where central directory starts 269 // 8 2 Number of central directory records on this disk 270 // 10 2 Total number of central directory records 271 // 12 4 Size of central directory (bytes) 272 // 16 4 Offset of start of central directory, relative to start of archive 273 // 20 2 Comment length (n) 274 // 22 n Comment 275 276 fIn.seek(fileChannel.size() - commentLength - 6); 277 // 6 = 2(Comment length) + 4 (Offset of start of central directory, relative to start of archive) 278 final ByteBuffer temp = ByteBuffer.allocate(4); 279 temp.order(ByteOrder.LITTLE_ENDIAN); 280 temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset))); 281 // 8 = size of block in bytes (excluding this field) (uint64) 282 temp.flip(); 283 fIn.write(temp.array()); 284 285 } 286 } finally { 287 if (fileChannel != null) { 288 fileChannel.close(); 289 } 290 if (fIn != null) { 291 fIn.close(); 292 } 293 } 294 } 295 296 public static void main(String[] args) { 297 try { 298 put(new File("C:\Users\Plum\Downloads\app.apk"), ApkUtil.APK_CHANNEL_BLOCK_ID,"附加信息"); 299 } catch (IOException | SignatureNotFoundException e) { 300 e.printStackTrace(); 301 } 302 } 303 }
从APK中读取信息:
1 import java.io.File; 2 import java.io.IOException; 3 import java.io.RandomAccessFile; 4 import java.io.UnsupportedEncodingException; 5 import java.nio.ByteBuffer; 6 import java.nio.channels.FileChannel; 7 import java.util.Arrays; 8 import java.util.Map; 9 10 public final class PayloadReader { 11 private PayloadReader() { 12 super(); 13 } 14 15 /** 16 * get string (UTF-8) by id 17 * 18 * @param apkFile apk file 19 * @return null if not found 20 */ 21 public static String getString(final File apkFile, final int id) { 22 final byte[] bytes = PayloadReader.get(apkFile, id); 23 if (bytes == null) { 24 return null; 25 } 26 try { 27 return new String(bytes, ApkUtil.DEFAULT_CHARSET); 28 } catch (UnsupportedEncodingException e) { 29 e.printStackTrace(); 30 } 31 return null; 32 } 33 34 /** 35 * get bytes by id <br/> 36 * 37 * @param apkFile apk file 38 * @param id id 39 * @return bytes 40 */ 41 public static byte[] get(final File apkFile, final int id) { 42 final Map<Integer, ByteBuffer> idValues = getAll(apkFile); 43 if (idValues == null) { 44 return null; 45 } 46 final ByteBuffer byteBuffer = idValues.get(id); 47 if (byteBuffer == null) { 48 return null; 49 } 50 return getBytes(byteBuffer); 51 } 52 53 /** 54 * get data from byteBuffer 55 * 56 * @param byteBuffer buffer 57 * @return useful data 58 */ 59 private static byte[] getBytes(final ByteBuffer byteBuffer) { 60 final byte[] array = byteBuffer.array(); 61 final int arrayOffset = byteBuffer.arrayOffset(); 62 return Arrays.copyOfRange(array, arrayOffset + byteBuffer.position(), 63 arrayOffset + byteBuffer.limit()); 64 } 65 66 /** 67 * get all custom (id, buffer) <br/> 68 * Note: get final from byteBuffer, please use {@link PayloadReader#getBytes getBytes} 69 * 70 * @param apkFile apk file 71 * @return all custom (id, buffer) 72 */ 73 private static Map<Integer, ByteBuffer> getAll(final File apkFile) { 74 Map<Integer, ByteBuffer> idValues = null; 75 try { 76 RandomAccessFile randomAccessFile = null; 77 FileChannel fileChannel = null; 78 try { 79 randomAccessFile = new RandomAccessFile(apkFile, "r"); 80 fileChannel = randomAccessFile.getChannel(); 81 final ByteBuffer apkSigningBlock2 = ApkUtil.findApkSigningBlock(fileChannel).getFirst(); 82 idValues = ApkUtil.findIdValues(apkSigningBlock2); 83 } catch (IOException ignore) { 84 } finally { 85 try { 86 if (fileChannel != null) { 87 fileChannel.close(); 88 } 89 } catch (IOException ignore) { 90 } 91 try { 92 if (randomAccessFile != null) { 93 randomAccessFile.close(); 94 } 95 } catch (IOException ignore) { 96 } 97 } 98 } catch (SignatureNotFoundException ignore) { 99 } 100 101 return idValues; 102 } 103 104 105 }
下面是一些辅助类:
1 /** 2 * Pair of two elements. 3 */ 4 final class Pair<A, B> { 5 private final A mFirst; 6 private final B mSecond; 7 8 private Pair(final A first, final B second) { 9 mFirst = first; 10 mSecond = second; 11 } 12 13 public static <A, B> Pair<A, B> of(final A first, final B second) { 14 return new Pair<A, B>(first, second); 15 } 16 17 public A getFirst() { 18 return mFirst; 19 } 20 21 public B getSecond() { 22 return mSecond; 23 } 24 25 @Override 26 public int hashCode() { 27 final int prime = 31; 28 int result = 1; 29 result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); 30 result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); 31 return result; 32 } 33 34 @Override 35 public boolean equals(final Object obj) { 36 if (this == obj) { 37 return true; 38 } 39 if (obj == null) { 40 return false; 41 } 42 if (getClass() != obj.getClass()) { 43 return false; 44 } 45 @SuppressWarnings("rawtypes") 46 final Pair other = (Pair) obj; 47 if (mFirst == null) { 48 if (other.mFirst != null) { 49 return false; 50 } 51 } else if (!mFirst.equals(other.mFirst)) { 52 return false; 53 } 54 if (mSecond == null) { 55 if (other.mSecond != null) { 56 return false; 57 } 58 } else if (!mSecond.equals(other.mSecond)) { 59 return false; 60 } 61 return true; 62 } 63 }
public class SignatureNotFoundException extends Exception { private static final long serialVersionUID = 1L; public SignatureNotFoundException(final String message) { super(message); } public SignatureNotFoundException(final String message, final Throwable cause) { super(message, cause); } }
1 import java.io.DataOutput; 2 import java.io.IOException; 3 import java.nio.ByteBuffer; 4 import java.nio.ByteOrder; 5 import java.util.ArrayList; 6 import java.util.List; 7 8 /** 9 * https://source.android.com/security/apksigning/v2.html 10 * https://en.wikipedia.org/wiki/Zip_(file_format) 11 */ 12 class ApkSigningBlock { 13 // The format of the APK Signing Block is as follows (all numeric fields are little-endian): 14 15 // .size of block in bytes (excluding this field) (uint64) 16 // .Sequence of uint64-length-prefixed ID-value pairs: 17 // *ID (uint32) 18 // *value (variable-length: length of the pair - 4 bytes) 19 // .size of block in bytes—same as the very first field (uint64) 20 // .magic “APK Sig Block 42” (16 bytes) 21 22 // FORMAT: 23 // OFFSET DATA TYPE DESCRIPTION 24 // * @+0 bytes uint64: size in bytes (excluding this field) 25 // * @+8 bytes payload 26 // * @-24 bytes uint64: size in bytes (same as the one above) 27 // * @-16 bytes uint128: magic 28 29 // payload 有 8字节的大小,4字节的ID,还有payload的内容组成 30 31 private final List<ApkSigningPayload> payloads; 32 33 ApkSigningBlock() { 34 super(); 35 36 payloads = new ArrayList<ApkSigningPayload>(); 37 } 38 39 public final List<ApkSigningPayload> getPayloads() { 40 return payloads; 41 } 42 43 public void addPayload(final ApkSigningPayload payload) { 44 payloads.add(payload); 45 } 46 47 public long writeApkSigningBlock(final DataOutput dataOutput) throws IOException { 48 long length = 24; // 24 = 8(size of block in bytes—same as the very first field (uint64)) + 16 (magic “APK Sig Block 42” (16 bytes)) 49 for (int index = 0; index < payloads.size(); ++index) { 50 final ApkSigningPayload payload = payloads.get(index); 51 final byte[] bytes = payload.getByteBuffer(); 52 length += 12 + bytes.length; // 12 = 8(uint64-length-prefixed) + 4 (ID (uint32)) 53 } 54 55 ByteBuffer byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 56 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 57 byteBuffer.putLong(length); 58 byteBuffer.flip(); 59 dataOutput.write(byteBuffer.array()); 60 61 for (int index = 0; index < payloads.size(); ++index) { 62 final ApkSigningPayload payload = payloads.get(index); 63 final byte[] bytes = payload.getByteBuffer(); 64 65 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 66 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 67 byteBuffer.putLong(bytes.length + (8 - 4)); // Long.BYTES - Integer.BYTES 68 byteBuffer.flip(); 69 dataOutput.write(byteBuffer.array()); 70 71 byteBuffer = ByteBuffer.allocate(4); // Integer.BYTES 72 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 73 byteBuffer.putInt(payload.getId()); 74 byteBuffer.flip(); 75 dataOutput.write(byteBuffer.array()); 76 77 dataOutput.write(bytes); 78 } 79 80 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 81 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 82 byteBuffer.putLong(length); 83 byteBuffer.flip(); 84 dataOutput.write(byteBuffer.array()); 85 86 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 87 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 88 byteBuffer.putLong(ApkUtil.APK_SIG_BLOCK_MAGIC_LO); 89 byteBuffer.flip(); 90 dataOutput.write(byteBuffer.array()); 91 92 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES 93 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 94 byteBuffer.putLong(ApkUtil.APK_SIG_BLOCK_MAGIC_HI); 95 byteBuffer.flip(); 96 dataOutput.write(byteBuffer.array()); 97 98 return length; 99 } 100 }
1 import java.nio.ByteBuffer; 2 import java.nio.ByteOrder; 3 import java.util.Arrays; 4 5 class ApkSigningPayload { 6 private final int id; 7 private final ByteBuffer buffer; 8 private final int totalSize; 9 10 ApkSigningPayload(final int id, final ByteBuffer buffer) { 11 super(); 12 this.id = id; 13 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 14 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 15 } 16 this.buffer = buffer; 17 // assume buffer is not consumed 18 this.totalSize = 8 + 4 + buffer.remaining(); // size + id + value 19 } 20 21 public int getId() { 22 return id; 23 } 24 25 public byte[] getByteBuffer() { 26 final byte[] array = buffer.array(); 27 final int arrayOffset = buffer.arrayOffset(); 28 return Arrays.copyOfRange(array, arrayOffset + buffer.position(), 29 arrayOffset + buffer.limit()); 30 } 31 32 /** 33 * Total bytes of this block 34 */ 35 public int getTotalSize() { 36 return totalSize; 37 } 38 }
1 import java.io.IOException; 2 import java.nio.BufferUnderflowException; 3 import java.nio.ByteBuffer; 4 import java.nio.ByteOrder; 5 import java.nio.channels.FileChannel; 6 import java.util.LinkedHashMap; 7 import java.util.Map; 8 9 final class ApkUtil { 10 private ApkUtil() { 11 super(); 12 } 13 14 /** 15 * APK Signing Block Magic Code: magic “APK Sig Block 42” (16 bytes) 16 * "APK Sig Block 42" : 41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 17 */ 18 public static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; // LITTLE_ENDIAN, High 19 public static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; // LITTLE_ENDIAN, Low 20 private static final int APK_SIG_BLOCK_MIN_SIZE = 32; 21 22 /* 23 The v2 signature of the APK is stored as an ID-value pair with ID 0x7109871a 24 (https://source.android.com/security/apksigning/v2.html#apk-signing-block) 25 */ 26 public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; 27 28 /** 29 * The padding in APK SIG BLOCK (V3 scheme introduced) 30 * See https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java 31 */ 32 public static final int VERITY_PADDING_BLOCK_ID = 0x42726577; 33 34 public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; 35 36 37 // Our Channel Block ID 38 public static final int APK_CHANNEL_BLOCK_ID = 0x71777777; 39 40 public static final String DEFAULT_CHARSET = "UTF-8"; 41 42 private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 43 private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 44 private static final int UINT16_MAX_VALUE = 0xffff; 45 private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 46 47 public static long getCommentLength(final FileChannel fileChannel) throws IOException { 48 // End of central directory record (EOCD) 49 // Offset Bytes Description[23] 50 // 0 4 End of central directory signature = 0x06054b50 51 // 4 2 Number of this disk 52 // 6 2 Disk where central directory starts 53 // 8 2 Number of central directory records on this disk 54 // 10 2 Total number of central directory records 55 // 12 4 Size of central directory (bytes) 56 // 16 4 Offset of start of central directory, relative to start of archive 57 // 20 2 Comment length (n) 58 // 22 n Comment 59 // For a zip with no archive comment, the 60 // end-of-central-directory record will be 22 bytes long, so 61 // we expect to find the EOCD marker 22 bytes from the end. 62 63 64 final long archiveSize = fileChannel.size(); 65 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 66 throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record"); 67 } 68 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 69 // The record can be identified by its 4-byte signature/magic which is located at the very 70 // beginning of the record. A complication is that the record is variable-length because of 71 // the comment field. 72 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 73 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 74 // the candidate record's comment length is such that the remainder of the record takes up 75 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 76 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 77 final long maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 78 final long eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 79 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 80 expectedCommentLength++) { 81 final long eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 82 83 final ByteBuffer byteBuffer = ByteBuffer.allocate(4); 84 fileChannel.position(eocdStartPos); 85 fileChannel.read(byteBuffer); 86 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 87 88 if (byteBuffer.getInt(0) == ZIP_EOCD_REC_SIG) { 89 final ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2); 90 fileChannel.position(eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 91 fileChannel.read(commentLengthByteBuffer); 92 commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN); 93 94 final int actualCommentLength = commentLengthByteBuffer.getShort(0); 95 if (actualCommentLength == expectedCommentLength) { 96 return actualCommentLength; 97 } 98 } 99 } 100 throw new IOException("ZIP End of Central Directory (EOCD) record not found"); 101 } 102 103 public static long findCentralDirStartOffset(final FileChannel fileChannel) throws IOException { 104 return findCentralDirStartOffset(fileChannel, getCommentLength(fileChannel)); 105 } 106 107 public static long findCentralDirStartOffset(final FileChannel fileChannel, final long commentLength) throws IOException { 108 // End of central directory record (EOCD) 109 // Offset Bytes Description[23] 110 // 0 4 End of central directory signature = 0x06054b50 111 // 4 2 Number of this disk 112 // 6 2 Disk where central directory starts 113 // 8 2 Number of central directory records on this disk 114 // 10 2 Total number of central directory records 115 // 12 4 Size of central directory (bytes) 116 // 16 4 Offset of start of central directory, relative to start of archive 117 // 20 2 Comment length (n) 118 // 22 n Comment 119 // For a zip with no archive comment, the 120 // end-of-central-directory record will be 22 bytes long, so 121 // we expect to find the EOCD marker 22 bytes from the end. 122 123 final ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4); 124 zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN); 125 fileChannel.position(fileChannel.size() - commentLength - 6); // 6 = 2 (Comment length) + 4 (Offset of start of central directory, relative to start of archive) 126 fileChannel.read(zipCentralDirectoryStart); 127 final long centralDirStartOffset = zipCentralDirectoryStart.getInt(0); 128 return centralDirStartOffset; 129 } 130 131 public static Pair<ByteBuffer, Long> findApkSigningBlock( 132 final FileChannel fileChannel) throws IOException, SignatureNotFoundException { 133 final long centralDirOffset = findCentralDirStartOffset(fileChannel); 134 return findApkSigningBlock(fileChannel, centralDirOffset); 135 } 136 137 public static Pair<ByteBuffer, Long> findApkSigningBlock( 138 final FileChannel fileChannel, final long centralDirOffset) throws IOException, SignatureNotFoundException { 139 140 // Find the APK Signing Block. The block immediately precedes the Central Directory. 141 142 // FORMAT: 143 // OFFSET DATA TYPE DESCRIPTION 144 // * @+0 bytes uint64: size in bytes (excluding this field) 145 // * @+8 bytes payload 146 // * @-24 bytes uint64: size in bytes (same as the one above) 147 // * @-16 bytes uint128: magic 148 149 if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { 150 throw new SignatureNotFoundException( 151 "APK too small for APK Signing Block. ZIP Central Directory offset: " 152 + centralDirOffset); 153 } 154 // Read the magic and offset in file from the footer section of the block: 155 // * uint64: size of block 156 // * 16 bytes: magic 157 fileChannel.position(centralDirOffset - 24); 158 final ByteBuffer footer = ByteBuffer.allocate(24); 159 fileChannel.read(footer); 160 footer.order(ByteOrder.LITTLE_ENDIAN); 161 if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) 162 || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { 163 throw new SignatureNotFoundException( 164 "No APK Signing Block before ZIP Central Directory"); 165 } 166 // Read and compare size fields 167 final long apkSigBlockSizeInFooter = footer.getLong(0); 168 if ((apkSigBlockSizeInFooter < footer.capacity()) 169 || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { 170 throw new SignatureNotFoundException( 171 "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); 172 } 173 final int totalSize = (int) (apkSigBlockSizeInFooter + 8); 174 final long apkSigBlockOffset = centralDirOffset - totalSize; 175 if (apkSigBlockOffset < 0) { 176 throw new SignatureNotFoundException( 177 "APK Signing Block offset out of range: " + apkSigBlockOffset); 178 } 179 fileChannel.position(apkSigBlockOffset); 180 final ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); 181 fileChannel.read(apkSigBlock); 182 apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); 183 final long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); 184 if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { 185 throw new SignatureNotFoundException( 186 "APK Signing Block sizes in header and footer do not match: " 187 + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); 188 } 189 return Pair.of(apkSigBlock, apkSigBlockOffset); 190 } 191 192 public static Map<Integer, ByteBuffer> findIdValues(final ByteBuffer apkSigningBlock) throws SignatureNotFoundException { 193 checkByteOrderLittleEndian(apkSigningBlock); 194 // FORMAT: 195 // OFFSET DATA TYPE DESCRIPTION 196 // * @+0 bytes uint64: size in bytes (excluding this field) 197 // * @+8 bytes pairs 198 // * @-24 bytes uint64: size in bytes (same as the one above) 199 // * @-16 bytes uint128: magic 200 final ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); 201 202 final Map<Integer, ByteBuffer> idValues = new LinkedHashMap<Integer, ByteBuffer>(); // keep order 203 204 int entryCount = 0; 205 while (pairs.hasRemaining()) { 206 entryCount++; 207 if (pairs.remaining() < 8) { 208 throw new SignatureNotFoundException( 209 "Insufficient data to read size of APK Signing Block entry #" + entryCount); 210 } 211 final long lenLong = pairs.getLong(); 212 if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { 213 throw new SignatureNotFoundException( 214 "APK Signing Block entry #" + entryCount 215 + " size out of range: " + lenLong); 216 } 217 final int len = (int) lenLong; 218 final int nextEntryPos = pairs.position() + len; 219 if (len > pairs.remaining()) { 220 throw new SignatureNotFoundException( 221 "APK Signing Block entry #" + entryCount + " size out of range: " + len 222 + ", available: " + pairs.remaining()); 223 } 224 final int id = pairs.getInt(); 225 idValues.put(id, getByteBuffer(pairs, len - 4)); 226 227 pairs.position(nextEntryPos); 228 } 229 230 return idValues; 231 } 232 233 /** 234 * Returns new byte buffer whose content is a shared subsequence of this buffer's content 235 * between the specified start (inclusive) and end (exclusive) positions. As opposed to 236 * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source 237 * buffer's byte order. 238 */ 239 private static ByteBuffer sliceFromTo(final ByteBuffer source, final int start, final int end) { 240 if (start < 0) { 241 throw new IllegalArgumentException("start: " + start); 242 } 243 if (end < start) { 244 throw new IllegalArgumentException("end < start: " + end + " < " + start); 245 } 246 final int capacity = source.capacity(); 247 if (end > source.capacity()) { 248 throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); 249 } 250 final int originalLimit = source.limit(); 251 final int originalPosition = source.position(); 252 try { 253 source.position(0); 254 source.limit(end); 255 source.position(start); 256 final ByteBuffer result = source.slice(); 257 result.order(source.order()); 258 return result; 259 } finally { 260 source.position(0); 261 source.limit(originalLimit); 262 source.position(originalPosition); 263 } 264 } 265 266 /** 267 * Relative <em>get</em> method for reading {@code size} number of bytes from the current 268 * position of this buffer. 269 * <p> 270 * <p>This method reads the next {@code size} bytes at this buffer's current position, 271 * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to 272 * {@code size}, byte order set to this buffer's byte order; and then increments the position by 273 * {@code size}. 274 */ 275 private static ByteBuffer getByteBuffer(final ByteBuffer source, final int size) 276 throws BufferUnderflowException { 277 if (size < 0) { 278 throw new IllegalArgumentException("size: " + size); 279 } 280 final int originalLimit = source.limit(); 281 final int position = source.position(); 282 final int limit = position + size; 283 if ((limit < position) || (limit > originalLimit)) { 284 throw new BufferUnderflowException(); 285 } 286 source.limit(limit); 287 try { 288 final ByteBuffer result = source.slice(); 289 result.order(source.order()); 290 source.position(limit); 291 return result; 292 } finally { 293 source.limit(originalLimit); 294 } 295 } 296 297 private static void checkByteOrderLittleEndian(final ByteBuffer buffer) { 298 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 299 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 300 } 301 } 302 303 304 }
总结
可以看出,现在高版本安卓对安装包的校验很严格,所以,普通的替换配置文件决定不行的,一替换,就破坏签名了,是我太年轻。直接往安装包的comments区写信息,以前可行,但现在也不行了。美团的这个工具,我也没看的很透很懂,只是知道他的原理还是往comments区添加信息,只是添加过程中对安装包的签名信息做了一些处理。