1 /* 2 * Copyright (C) 2017 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 android.util.apk; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 22 import java.io.IOException; 23 import java.io.RandomAccessFile; 24 import java.nio.ByteBuffer; 25 import java.nio.ByteOrder; 26 import java.security.DigestException; 27 import java.security.MessageDigest; 28 import java.security.NoSuchAlgorithmException; 29 import java.util.ArrayList; 30 31 /** 32 * VerityBuilder builds the verity Merkle tree and other metadata. The generated tree format can 33 * be stored on disk for fs-verity setup and used by kernel. The builder support standard 34 * fs-verity, and Android specific apk-verity that requires additional kernel patches. 35 * 36 * <p>Unlike a regular Merkle tree of fs-verity, the apk-verity tree does not cover the file content 37 * fully, and has to skip APK Signing Block with some special treatment for the "Central Directory 38 * offset" field of ZIP End of Central Directory. 39 * 40 * @hide 41 */ 42 public abstract class VerityBuilder { VerityBuilder()43 private VerityBuilder() {} 44 45 private static final int CHUNK_SIZE_BYTES = 4096; // Typical Linux block size 46 private static final int DIGEST_SIZE_BYTES = 32; // SHA-256 size 47 private static final int FSVERITY_HEADER_SIZE_BYTES = 64; 48 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE = 4; 49 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 50 private static final String JCA_DIGEST_ALGORITHM = "SHA-256"; 51 private static final byte[] DEFAULT_SALT = new byte[8]; 52 53 /** Result generated by the builder. */ 54 public static class VerityResult { 55 /** Raw fs-verity metadata and Merkle tree ready to be deployed on disk. */ 56 public final ByteBuffer verityData; 57 58 /** Size of the Merkle tree in {@code verityData}. */ 59 public final int merkleTreeSize; 60 61 /** Root hash of the Merkle tree. */ 62 public final byte[] rootHash; 63 VerityResult(ByteBuffer verityData, int merkleTreeSize, byte[] rootHash)64 private VerityResult(ByteBuffer verityData, int merkleTreeSize, byte[] rootHash) { 65 this.verityData = verityData; 66 this.merkleTreeSize = merkleTreeSize; 67 this.rootHash = rootHash; 68 } 69 } 70 71 /** 72 * Generates the 4k, SHA-256 based Merkle tree for the given APK and stores in the {@link 73 * ByteBuffer} created by the {@link ByteBufferFactory}. The Merkle tree does not cover Signing 74 * Block specificed in {@code signatureInfo}. The output is suitable to be used as the on-disk 75 * format for fs-verity to use (with elide and patch extensions). 76 * 77 * @return VerityResult containing a buffer with the generated Merkle tree stored at the 78 * front, the tree size, and the calculated root hash. 79 */ 80 @NonNull generateApkVerityTree(@onNull RandomAccessFile apk, @Nullable SignatureInfo signatureInfo, @NonNull ByteBufferFactory bufferFactory)81 public static VerityResult generateApkVerityTree(@NonNull RandomAccessFile apk, 82 @Nullable SignatureInfo signatureInfo, @NonNull ByteBufferFactory bufferFactory) 83 throws IOException, SecurityException, NoSuchAlgorithmException, DigestException { 84 return generateVerityTreeInternal(apk, bufferFactory, signatureInfo); 85 } 86 87 @NonNull generateVerityTreeInternal(@onNull RandomAccessFile apk, @NonNull ByteBufferFactory bufferFactory, @Nullable SignatureInfo signatureInfo)88 private static VerityResult generateVerityTreeInternal(@NonNull RandomAccessFile apk, 89 @NonNull ByteBufferFactory bufferFactory, @Nullable SignatureInfo signatureInfo) 90 throws IOException, SecurityException, NoSuchAlgorithmException, DigestException { 91 long signingBlockSize = 92 signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset; 93 long dataSize = apk.length() - signingBlockSize; 94 int[] levelOffset = calculateVerityLevelOffset(dataSize); 95 int merkleTreeSize = levelOffset[levelOffset.length - 1]; 96 97 ByteBuffer output = bufferFactory.create( 98 merkleTreeSize 99 + CHUNK_SIZE_BYTES); // maximum size of apk-verity metadata 100 output.order(ByteOrder.LITTLE_ENDIAN); 101 ByteBuffer tree = slice(output, 0, merkleTreeSize); 102 byte[] apkRootHash = generateVerityTreeInternal(apk, signatureInfo, DEFAULT_SALT, 103 levelOffset, tree); 104 return new VerityResult(output, merkleTreeSize, apkRootHash); 105 } 106 generateApkVerityFooter(@onNull RandomAccessFile apk, @NonNull SignatureInfo signatureInfo, @NonNull ByteBuffer footerOutput)107 static void generateApkVerityFooter(@NonNull RandomAccessFile apk, 108 @NonNull SignatureInfo signatureInfo, @NonNull ByteBuffer footerOutput) 109 throws IOException { 110 footerOutput.order(ByteOrder.LITTLE_ENDIAN); 111 generateApkVerityHeader(footerOutput, apk.length(), DEFAULT_SALT); 112 long signingBlockSize = 113 signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset; 114 generateApkVerityExtensions(footerOutput, signatureInfo.apkSigningBlockOffset, 115 signingBlockSize, signatureInfo.eocdOffset); 116 } 117 118 /** 119 * Generates the fs-verity hash tree. It is the actual verity tree format on disk, as is 120 * re-generated on device. 121 * 122 * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the 123 * input file. If the total size is larger than 4 KB, take this level as input and repeat the 124 * same procedure, until the level is within 4 KB. If salt is given, it will apply to each 125 * digestion before the actual data. 126 * 127 * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt. 128 * 129 * @return the root hash of the generated hash tree. 130 */ generateFsVerityRootHash(@onNull String apkPath, byte[] salt, @NonNull ByteBufferFactory bufferFactory)131 public static byte[] generateFsVerityRootHash(@NonNull String apkPath, byte[] salt, 132 @NonNull ByteBufferFactory bufferFactory) 133 throws IOException, NoSuchAlgorithmException, DigestException { 134 try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { 135 int[] levelOffset = calculateVerityLevelOffset(apk.length()); 136 int merkleTreeSize = levelOffset[levelOffset.length - 1]; 137 138 ByteBuffer output = bufferFactory.create( 139 merkleTreeSize 140 + CHUNK_SIZE_BYTES); // maximum size of apk-verity metadata 141 output.order(ByteOrder.LITTLE_ENDIAN); 142 ByteBuffer tree = slice(output, 0, merkleTreeSize); 143 return generateFsVerityTreeInternal(apk, salt, levelOffset, tree); 144 } 145 } 146 /** 147 * Calculates the apk-verity root hash for integrity measurement. This needs to be consistent 148 * to what kernel returns. 149 */ 150 @NonNull generateApkVerityRootHash(@onNull RandomAccessFile apk, @NonNull ByteBuffer apkDigest, @NonNull SignatureInfo signatureInfo)151 static byte[] generateApkVerityRootHash(@NonNull RandomAccessFile apk, 152 @NonNull ByteBuffer apkDigest, @NonNull SignatureInfo signatureInfo) 153 throws NoSuchAlgorithmException, DigestException, IOException { 154 assertSigningBlockAlignedAndHasFullPages(signatureInfo); 155 156 ByteBuffer footer = ByteBuffer.allocate(CHUNK_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN); 157 generateApkVerityFooter(apk, signatureInfo, footer); 158 footer.flip(); 159 160 MessageDigest md = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM); 161 md.update(footer); 162 md.update(apkDigest); 163 return md.digest(); 164 } 165 166 /** 167 * Generates the apk-verity header and hash tree to be used by kernel for the given apk. This 168 * method does not check whether the root hash exists in the Signing Block or not. 169 * 170 * <p>The output is stored in the {@link ByteBuffer} created by the given {@link 171 * ByteBufferFactory}. 172 * 173 * @return the root hash of the generated hash tree. 174 */ 175 @NonNull generateApkVerity(@onNull String apkPath, @NonNull ByteBufferFactory bufferFactory, @NonNull SignatureInfo signatureInfo)176 static byte[] generateApkVerity(@NonNull String apkPath, 177 @NonNull ByteBufferFactory bufferFactory, @NonNull SignatureInfo signatureInfo) 178 throws IOException, SignatureNotFoundException, SecurityException, DigestException, 179 NoSuchAlgorithmException { 180 try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { 181 VerityResult result = generateVerityTreeInternal(apk, bufferFactory, signatureInfo); 182 ByteBuffer footer = slice(result.verityData, result.merkleTreeSize, 183 result.verityData.limit()); 184 generateApkVerityFooter(apk, signatureInfo, footer); 185 // Put the reverse offset to apk-verity header at the end. 186 footer.putInt(footer.position() + 4); 187 result.verityData.limit(result.merkleTreeSize + footer.position()); 188 return result.rootHash; 189 } 190 } 191 192 /** 193 * A helper class to consume and digest data by block continuously, and write into a buffer. 194 */ 195 private static class BufferedDigester implements DataDigester { 196 /** Amount of the data to digest in each cycle before writting out the digest. */ 197 private static final int BUFFER_SIZE = CHUNK_SIZE_BYTES; 198 199 /** 200 * Amount of data the {@link MessageDigest} has consumed since the last reset. This must be 201 * always less than BUFFER_SIZE since {@link MessageDigest} is reset whenever it has 202 * consumed BUFFER_SIZE of data. 203 */ 204 private int mBytesDigestedSinceReset; 205 206 /** The final output {@link ByteBuffer} to write the digest to sequentially. */ 207 private final ByteBuffer mOutput; 208 209 private final MessageDigest mMd; 210 private final byte[] mDigestBuffer = new byte[DIGEST_SIZE_BYTES]; 211 private final byte[] mSalt; 212 BufferedDigester(@ullable byte[] salt, @NonNull ByteBuffer output)213 private BufferedDigester(@Nullable byte[] salt, @NonNull ByteBuffer output) 214 throws NoSuchAlgorithmException { 215 mSalt = salt; 216 mOutput = output.slice(); 217 mMd = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM); 218 if (mSalt != null) { 219 mMd.update(mSalt); 220 } 221 mBytesDigestedSinceReset = 0; 222 } 223 224 /** 225 * Consumes and digests data up to BUFFER_SIZE (may continue from the previous remaining), 226 * then writes the final digest to the output buffer. Repeat until all data are consumed. 227 * If the last consumption is not enough for BUFFER_SIZE, the state will stay and future 228 * consumption will continuous from there. 229 */ 230 @Override consume(ByteBuffer buffer)231 public void consume(ByteBuffer buffer) throws DigestException { 232 int offset = buffer.position(); 233 int remaining = buffer.remaining(); 234 while (remaining > 0) { 235 int allowance = (int) Math.min(remaining, BUFFER_SIZE - mBytesDigestedSinceReset); 236 // Optimization: set the buffer limit to avoid allocating a new ByteBuffer object. 237 buffer.limit(buffer.position() + allowance); 238 mMd.update(buffer); 239 offset += allowance; 240 remaining -= allowance; 241 mBytesDigestedSinceReset += allowance; 242 243 if (mBytesDigestedSinceReset == BUFFER_SIZE) { 244 mMd.digest(mDigestBuffer, 0, mDigestBuffer.length); 245 mOutput.put(mDigestBuffer); 246 // After digest, MessageDigest resets automatically, so no need to reset again. 247 if (mSalt != null) { 248 mMd.update(mSalt); 249 } 250 mBytesDigestedSinceReset = 0; 251 } 252 } 253 } 254 assertEmptyBuffer()255 public void assertEmptyBuffer() throws DigestException { 256 if (mBytesDigestedSinceReset != 0) { 257 throw new IllegalStateException("Buffer is not empty: " + mBytesDigestedSinceReset); 258 } 259 } 260 fillUpLastOutputChunk()261 private void fillUpLastOutputChunk() { 262 int lastBlockSize = (int) (mOutput.position() % BUFFER_SIZE); 263 if (lastBlockSize == 0) { 264 return; 265 } 266 mOutput.put(ByteBuffer.allocate(BUFFER_SIZE - lastBlockSize)); 267 } 268 } 269 270 /** 271 * Digest the source by chunk in the given range. If the last chunk is not a full chunk, 272 * digest the remaining. 273 */ consumeByChunk(DataDigester digester, DataSource source, int chunkSize)274 private static void consumeByChunk(DataDigester digester, DataSource source, int chunkSize) 275 throws IOException, DigestException { 276 long inputRemaining = source.size(); 277 long inputOffset = 0; 278 while (inputRemaining > 0) { 279 int size = (int) Math.min(inputRemaining, chunkSize); 280 source.feedIntoDataDigester(digester, inputOffset, size); 281 inputOffset += size; 282 inputRemaining -= size; 283 } 284 } 285 286 // Rationale: 1) 1 MB should fit in memory space on all devices. 2) It is not too granular 287 // thus the syscall overhead is not too big. 288 private static final int MMAP_REGION_SIZE_BYTES = 1024 * 1024; 289 generateFsVerityDigestAtLeafLevel(RandomAccessFile file, @Nullable byte[] salt, ByteBuffer output)290 private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file, 291 @Nullable byte[] salt, ByteBuffer output) 292 throws IOException, NoSuchAlgorithmException, DigestException { 293 BufferedDigester digester = new BufferedDigester(salt, output); 294 295 // 1. Digest the whole file by chunks. 296 consumeByChunk(digester, 297 DataSource.create(file.getFD(), 0, file.length()), 298 MMAP_REGION_SIZE_BYTES); 299 300 // 2. Pad 0s up to the nearest 4096-byte block before hashing. 301 int lastIncompleteChunkSize = (int) (file.length() % CHUNK_SIZE_BYTES); 302 if (lastIncompleteChunkSize != 0) { 303 digester.consume(ByteBuffer.allocate(CHUNK_SIZE_BYTES - lastIncompleteChunkSize)); 304 } 305 digester.assertEmptyBuffer(); 306 307 // 3. Fill up the rest of buffer with 0s. 308 digester.fillUpLastOutputChunk(); 309 } 310 generateApkVerityDigestAtLeafLevel(RandomAccessFile apk, SignatureInfo signatureInfo, byte[] salt, ByteBuffer output)311 private static void generateApkVerityDigestAtLeafLevel(RandomAccessFile apk, 312 SignatureInfo signatureInfo, byte[] salt, ByteBuffer output) 313 throws IOException, NoSuchAlgorithmException, DigestException { 314 BufferedDigester digester = new BufferedDigester(salt, output); 315 316 // 1. Digest from the beginning of the file, until APK Signing Block is reached. 317 consumeByChunk(digester, 318 DataSource.create(apk.getFD(), 0, signatureInfo.apkSigningBlockOffset), 319 MMAP_REGION_SIZE_BYTES); 320 321 // 2. Skip APK Signing Block and continue digesting, until the Central Directory offset 322 // field in EoCD is reached. 323 long eocdCdOffsetFieldPosition = 324 signatureInfo.eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET; 325 consumeByChunk(digester, 326 DataSource.create(apk.getFD(), signatureInfo.centralDirOffset, 327 eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset), 328 MMAP_REGION_SIZE_BYTES); 329 330 // 3. Consume offset of Signing Block as an alternative EoCD. 331 ByteBuffer alternativeCentralDirOffset = ByteBuffer.allocate( 332 ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE).order(ByteOrder.LITTLE_ENDIAN); 333 alternativeCentralDirOffset.putInt(Math.toIntExact(signatureInfo.apkSigningBlockOffset)); 334 alternativeCentralDirOffset.flip(); 335 digester.consume(alternativeCentralDirOffset); 336 337 // 4. Read from end of the Central Directory offset field in EoCD to the end of the file. 338 long offsetAfterEocdCdOffsetField = 339 eocdCdOffsetFieldPosition + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; 340 consumeByChunk(digester, 341 DataSource.create(apk.getFD(), offsetAfterEocdCdOffsetField, 342 apk.length() - offsetAfterEocdCdOffsetField), 343 MMAP_REGION_SIZE_BYTES); 344 345 // 5. Pad 0s up to the nearest 4096-byte block before hashing. 346 int lastIncompleteChunkSize = (int) (apk.length() % CHUNK_SIZE_BYTES); 347 if (lastIncompleteChunkSize != 0) { 348 digester.consume(ByteBuffer.allocate(CHUNK_SIZE_BYTES - lastIncompleteChunkSize)); 349 } 350 digester.assertEmptyBuffer(); 351 352 // 6. Fill up the rest of buffer with 0s. 353 digester.fillUpLastOutputChunk(); 354 } 355 356 @NonNull generateFsVerityTreeInternal(@onNull RandomAccessFile apk, @Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output)357 private static byte[] generateFsVerityTreeInternal(@NonNull RandomAccessFile apk, 358 @Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output) 359 throws IOException, NoSuchAlgorithmException, DigestException { 360 // 1. Digest the apk to generate the leaf level hashes. 361 generateFsVerityDigestAtLeafLevel(apk, salt, 362 slice(output, levelOffset[levelOffset.length - 2], 363 levelOffset[levelOffset.length - 1])); 364 365 // 2. Digest the lower level hashes bottom up. 366 for (int level = levelOffset.length - 3; level >= 0; level--) { 367 ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]); 368 ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]); 369 370 DataSource source = new ByteBufferDataSource(inputBuffer); 371 BufferedDigester digester = new BufferedDigester(salt, outputBuffer); 372 consumeByChunk(digester, source, CHUNK_SIZE_BYTES); 373 digester.assertEmptyBuffer(); 374 digester.fillUpLastOutputChunk(); 375 } 376 377 // 3. Digest the first block (i.e. first level) to generate the root hash. 378 byte[] rootHash = new byte[DIGEST_SIZE_BYTES]; 379 BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash)); 380 digester.consume(slice(output, 0, CHUNK_SIZE_BYTES)); 381 digester.assertEmptyBuffer(); 382 return rootHash; 383 } 384 385 @NonNull generateVerityTreeInternal(@onNull RandomAccessFile apk, @Nullable SignatureInfo signatureInfo, @Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output)386 private static byte[] generateVerityTreeInternal(@NonNull RandomAccessFile apk, 387 @Nullable SignatureInfo signatureInfo, @Nullable byte[] salt, 388 @NonNull int[] levelOffset, @NonNull ByteBuffer output) 389 throws IOException, NoSuchAlgorithmException, DigestException { 390 // 1. Digest the apk to generate the leaf level hashes. 391 assertSigningBlockAlignedAndHasFullPages(signatureInfo); 392 generateApkVerityDigestAtLeafLevel(apk, signatureInfo, salt, slice(output, 393 levelOffset[levelOffset.length - 2], levelOffset[levelOffset.length - 1])); 394 395 // 2. Digest the lower level hashes bottom up. 396 for (int level = levelOffset.length - 3; level >= 0; level--) { 397 ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]); 398 ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]); 399 400 DataSource source = new ByteBufferDataSource(inputBuffer); 401 BufferedDigester digester = new BufferedDigester(salt, outputBuffer); 402 consumeByChunk(digester, source, CHUNK_SIZE_BYTES); 403 digester.assertEmptyBuffer(); 404 digester.fillUpLastOutputChunk(); 405 } 406 407 // 3. Digest the first block (i.e. first level) to generate the root hash. 408 byte[] rootHash = new byte[DIGEST_SIZE_BYTES]; 409 BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash)); 410 digester.consume(slice(output, 0, CHUNK_SIZE_BYTES)); 411 digester.assertEmptyBuffer(); 412 return rootHash; 413 } 414 generateApkVerityHeader(ByteBuffer buffer, long fileSize, byte[] salt)415 private static ByteBuffer generateApkVerityHeader(ByteBuffer buffer, long fileSize, 416 byte[] salt) { 417 if (salt.length != 8) { 418 throw new IllegalArgumentException("salt is not 8 bytes long"); 419 } 420 421 // TODO(b/30972906): update the reference when there is a better one in public. 422 buffer.put("TrueBrew".getBytes()); // magic 423 424 buffer.put((byte) 1); // major version 425 buffer.put((byte) 0); // minor version 426 buffer.put((byte) 12); // log2(block-size): log2(4096) 427 buffer.put((byte) 7); // log2(leaves-per-node): log2(4096 / 32) 428 429 buffer.putShort((short) 1); // meta algorithm, SHA256 == 1 430 buffer.putShort((short) 1); // data algorithm, SHA256 == 1 431 432 buffer.putInt(0); // flags 433 buffer.putInt(0); // reserved 434 435 buffer.putLong(fileSize); // original file size 436 437 buffer.put((byte) 2); // authenticated extension count 438 buffer.put((byte) 0); // unauthenticated extension count 439 buffer.put(salt); // salt (8 bytes) 440 skip(buffer, 22); // reserved 441 442 return buffer; 443 } 444 generateApkVerityExtensions(ByteBuffer buffer, long signingBlockOffset, long signingBlockSize, long eocdOffset)445 private static ByteBuffer generateApkVerityExtensions(ByteBuffer buffer, 446 long signingBlockOffset, long signingBlockSize, long eocdOffset) { 447 // Snapshot of the experimental fs-verity structs (different from upstream). 448 // 449 // struct fsverity_extension_elide { 450 // __le64 offset; 451 // __le64 length; 452 // } 453 // 454 // struct fsverity_extension_patch { 455 // __le64 offset; 456 // u8 databytes[]; 457 // }; 458 459 final int kSizeOfFsverityExtensionHeader = 8; 460 final int kExtensionSizeAlignment = 8; 461 462 { 463 // struct fsverity_extension #1 464 final int kSizeOfFsverityElidedExtension = 16; 465 466 // First field is total size of extension, padded to 64-bit alignment 467 buffer.putInt(kSizeOfFsverityExtensionHeader + kSizeOfFsverityElidedExtension); 468 buffer.putShort((short) 1); // ID of elide extension 469 skip(buffer, 2); // reserved 470 471 // struct fsverity_extension_elide 472 buffer.putLong(signingBlockOffset); 473 buffer.putLong(signingBlockSize); 474 } 475 476 { 477 // struct fsverity_extension #2 478 final int kTotalSize = kSizeOfFsverityExtensionHeader 479 + 8 // offset size 480 + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; 481 482 buffer.putInt(kTotalSize); // Total size of extension, padded to 64-bit alignment 483 buffer.putShort((short) 2); // ID of patch extension 484 skip(buffer, 2); // reserved 485 486 // struct fsverity_extension_patch 487 buffer.putLong(eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); // offset 488 buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes 489 490 // The extension needs to be 0-padded at the end, since the length may not be multiple 491 // of 8. 492 int kPadding = kExtensionSizeAlignment - kTotalSize % kExtensionSizeAlignment; 493 if (kPadding == kExtensionSizeAlignment) { 494 kPadding = 0; 495 } 496 skip(buffer, kPadding); // padding 497 } 498 499 return buffer; 500 } 501 502 /** 503 * Returns an array of summed area table of level size in the verity tree. In other words, the 504 * returned array is offset of each level in the verity tree file format, plus an additional 505 * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size 506 * is level + 1. Thus, the returned array is guarantee to have at least 2 elements. 507 */ calculateVerityLevelOffset(long fileSize)508 private static int[] calculateVerityLevelOffset(long fileSize) { 509 ArrayList<Long> levelSize = new ArrayList<>(); 510 while (true) { 511 long levelDigestSize = divideRoundup(fileSize, CHUNK_SIZE_BYTES) * DIGEST_SIZE_BYTES; 512 long chunksSize = CHUNK_SIZE_BYTES * divideRoundup(levelDigestSize, CHUNK_SIZE_BYTES); 513 levelSize.add(chunksSize); 514 if (levelDigestSize <= CHUNK_SIZE_BYTES) { 515 break; 516 } 517 fileSize = levelDigestSize; 518 } 519 520 // Reverse and convert to summed area table. 521 int[] levelOffset = new int[levelSize.size() + 1]; 522 levelOffset[0] = 0; 523 for (int i = 0; i < levelSize.size(); i++) { 524 // We don't support verity tree if it is larger then Integer.MAX_VALUE. 525 levelOffset[i + 1] = levelOffset[i] 526 + Math.toIntExact(levelSize.get(levelSize.size() - i - 1)); 527 } 528 return levelOffset; 529 } 530 assertSigningBlockAlignedAndHasFullPages( @onNull SignatureInfo signatureInfo)531 private static void assertSigningBlockAlignedAndHasFullPages( 532 @NonNull SignatureInfo signatureInfo) { 533 if (signatureInfo.apkSigningBlockOffset % CHUNK_SIZE_BYTES != 0) { 534 throw new IllegalArgumentException( 535 "APK Signing Block does not start at the page boundary: " 536 + signatureInfo.apkSigningBlockOffset); 537 } 538 539 if ((signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset) 540 % CHUNK_SIZE_BYTES != 0) { 541 throw new IllegalArgumentException( 542 "Size of APK Signing Block is not a multiple of 4096: " 543 + (signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset)); 544 } 545 } 546 547 /** Returns a slice of the buffer which shares content with the provided buffer. */ slice(ByteBuffer buffer, int begin, int end)548 private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { 549 ByteBuffer b = buffer.duplicate(); 550 b.position(0); // to ensure position <= limit invariant. 551 b.limit(end); 552 b.position(begin); 553 return b.slice(); 554 } 555 556 /** Skip the {@code ByteBuffer} position by {@code bytes}. */ skip(ByteBuffer buffer, int bytes)557 private static void skip(ByteBuffer buffer, int bytes) { 558 buffer.position(buffer.position() + bytes); 559 } 560 561 /** Divides a number and round up to the closest integer. */ divideRoundup(long dividend, long divisor)562 private static long divideRoundup(long dividend, long divisor) { 563 return (dividend + divisor - 1) / divisor; 564 } 565 } 566