1 /* 2 * Copyright (C) 2008 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.signapk; 18 19 import org.bouncycastle.asn1.ASN1InputStream; 20 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21 import org.bouncycastle.asn1.DEROutputStream; 22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24 import org.bouncycastle.cert.jcajce.JcaCertStore; 25 import org.bouncycastle.cms.CMSException; 26 import org.bouncycastle.cms.CMSSignedData; 27 import org.bouncycastle.cms.CMSSignedDataGenerator; 28 import org.bouncycastle.cms.CMSTypedData; 29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30 import org.bouncycastle.jce.provider.BouncyCastleProvider; 31 import org.bouncycastle.operator.ContentSigner; 32 import org.bouncycastle.operator.OperatorCreationException; 33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35 import org.conscrypt.OpenSSLProvider; 36 37 import com.android.apksig.ApkSignerEngine; 38 import com.android.apksig.DefaultApkSignerEngine; 39 import com.android.apksig.SigningCertificateLineage; 40 import com.android.apksig.Hints; 41 import com.android.apksig.apk.ApkUtils; 42 import com.android.apksig.apk.MinSdkVersionException; 43 import com.android.apksig.util.DataSink; 44 import com.android.apksig.util.DataSources; 45 import com.android.apksig.zip.ZipFormatException; 46 47 import java.io.Console; 48 import java.io.BufferedReader; 49 import java.io.ByteArrayInputStream; 50 import java.io.ByteArrayOutputStream; 51 import java.io.DataInputStream; 52 import java.io.File; 53 import java.io.FileInputStream; 54 import java.io.FileOutputStream; 55 import java.io.FilterOutputStream; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.io.InputStreamReader; 59 import java.io.OutputStream; 60 import java.lang.reflect.Constructor; 61 import java.nio.ByteBuffer; 62 import java.nio.ByteOrder; 63 import java.nio.charset.StandardCharsets; 64 import java.security.GeneralSecurityException; 65 import java.security.Key; 66 import java.security.KeyFactory; 67 import java.security.PrivateKey; 68 import java.security.Provider; 69 import java.security.Security; 70 import java.security.cert.CertificateEncodingException; 71 import java.security.cert.CertificateFactory; 72 import java.security.cert.X509Certificate; 73 import java.security.spec.InvalidKeySpecException; 74 import java.security.spec.PKCS8EncodedKeySpec; 75 import java.util.ArrayList; 76 import java.util.Collections; 77 import java.util.Enumeration; 78 import java.util.HashSet; 79 import java.util.List; 80 import java.util.Locale; 81 import java.util.TimeZone; 82 import java.util.jar.JarEntry; 83 import java.util.jar.JarFile; 84 import java.util.jar.JarOutputStream; 85 import java.util.regex.Pattern; 86 import java.util.zip.ZipEntry; 87 88 import javax.crypto.Cipher; 89 import javax.crypto.EncryptedPrivateKeyInfo; 90 import javax.crypto.SecretKeyFactory; 91 import javax.crypto.spec.PBEKeySpec; 92 93 /** 94 * HISTORICAL NOTE: 95 * 96 * Prior to the keylimepie release, SignApk ignored the signature 97 * algorithm specified in the certificate and always used SHA1withRSA. 98 * 99 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 100 * the signature algorithm in the certificate to select which to use 101 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 102 * 103 * Because there are old keys still in use whose certificate actually 104 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 105 * for compatibility with older releases. This can be changed by 106 * altering the getAlgorithm() function below. 107 */ 108 109 110 /** 111 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 112 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 113 * SHA-256 (see historical note). The tool can additionally sign APKs using 114 * APK Signature Scheme v2. 115 */ 116 class SignApk { 117 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 118 119 /** 120 * Extensible data block/field header ID used for storing information about alignment of 121 * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section 122 * 4.5 Extensible data fields. 123 */ 124 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; 125 126 /** 127 * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed 128 * entries. 129 */ 130 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; 131 132 // bitmasks for which hash algorithms we need the manifest to include. 133 private static final int USE_SHA1 = 1; 134 private static final int USE_SHA256 = 2; 135 136 /** 137 * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used 138 * for signing an OTA update package using the private key corresponding to the provided 139 * certificate. 140 */ getDigestAlgorithmForOta(X509Certificate cert)141 private static int getDigestAlgorithmForOta(X509Certificate cert) { 142 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 143 if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { 144 // see "HISTORICAL NOTE" above. 145 return USE_SHA1; 146 } else if (sigAlg.startsWith("SHA256WITH")) { 147 return USE_SHA256; 148 } else { 149 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 150 "\" in cert [" + cert.getSubjectDN()); 151 } 152 } 153 154 /** 155 * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA 156 * update package using the private key corresponding to the provided certificate and the 157 * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). 158 */ getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash)159 private static String getJcaSignatureAlgorithmForOta( 160 X509Certificate cert, int hash) { 161 String sigAlgDigestPrefix; 162 switch (hash) { 163 case USE_SHA1: 164 sigAlgDigestPrefix = "SHA1"; 165 break; 166 case USE_SHA256: 167 sigAlgDigestPrefix = "SHA256"; 168 break; 169 default: 170 throw new IllegalArgumentException("Unknown hash ID: " + hash); 171 } 172 173 String keyAlgorithm = cert.getPublicKey().getAlgorithm(); 174 if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 175 return sigAlgDigestPrefix + "withRSA"; 176 } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 177 return sigAlgDigestPrefix + "withECDSA"; 178 } else { 179 throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 180 } 181 } 182 readPublicKey(File file)183 private static X509Certificate readPublicKey(File file) 184 throws IOException, GeneralSecurityException { 185 FileInputStream input = new FileInputStream(file); 186 try { 187 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 188 return (X509Certificate) cf.generateCertificate(input); 189 } finally { 190 input.close(); 191 } 192 } 193 194 /** 195 * If a console doesn't exist, reads the password from stdin 196 * If a console exists, reads the password from console and returns it as a string. 197 * 198 * @param keyFile The file containing the private key. Used to prompt the user. 199 */ readPassword(File keyFile)200 private static String readPassword(File keyFile) { 201 Console console; 202 char[] pwd; 203 if ((console = System.console()) == null) { 204 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 205 System.out.flush(); 206 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 207 try { 208 return stdin.readLine(); 209 } catch (IOException ex) { 210 return null; 211 } 212 } else { 213 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) { 214 return String.valueOf(pwd); 215 } else { 216 return null; 217 } 218 } 219 } 220 221 /** 222 * Decrypt an encrypted PKCS#8 format private key. 223 * 224 * Based on ghstark's post on Aug 6, 2006 at 225 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 226 * 227 * @param encryptedPrivateKey The raw data of the private key 228 * @param keyFile The file containing the private key 229 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)230 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 231 throws GeneralSecurityException { 232 EncryptedPrivateKeyInfo epkInfo; 233 try { 234 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 235 } catch (IOException ex) { 236 // Probably not an encrypted key. 237 return null; 238 } 239 240 char[] password = readPassword(keyFile).toCharArray(); 241 242 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 243 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 244 245 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 246 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 247 248 try { 249 return epkInfo.getKeySpec(cipher); 250 } catch (InvalidKeySpecException ex) { 251 System.err.println("signapk: Password for " + keyFile + " may be bad."); 252 throw ex; 253 } 254 } 255 256 /** Read a PKCS#8 format private key. */ readPrivateKey(File file)257 private static PrivateKey readPrivateKey(File file) 258 throws IOException, GeneralSecurityException { 259 DataInputStream input = new DataInputStream(new FileInputStream(file)); 260 try { 261 byte[] bytes = new byte[(int) file.length()]; 262 input.read(bytes); 263 264 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 265 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); 266 if (spec == null) { 267 spec = new PKCS8EncodedKeySpec(bytes); 268 } 269 270 /* 271 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 272 * OID and use that to construct a KeyFactory. 273 */ 274 PrivateKeyInfo pki; 275 try (ASN1InputStream bIn = 276 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { 277 pki = PrivateKeyInfo.getInstance(bIn.readObject()); 278 } 279 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 280 281 return KeyFactory.getInstance(algOid).generatePrivate(spec); 282 } finally { 283 input.close(); 284 } 285 } 286 287 /** 288 * Add a copy of the public key to the archive; this should 289 * exactly match one of the files in 290 * /system/etc/security/otacerts.zip on the device. (The same 291 * cert can be extracted from the OTA update package's signature 292 * block but this is much easier to get at.) 293 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp)294 private static void addOtacert(JarOutputStream outputJar, 295 File publicKeyFile, 296 long timestamp) 297 throws IOException { 298 299 JarEntry je = new JarEntry(OTACERT_NAME); 300 je.setTime(timestamp); 301 outputJar.putNextEntry(je); 302 FileInputStream input = new FileInputStream(publicKeyFile); 303 byte[] b = new byte[4096]; 304 int read; 305 while ((read = input.read(b)) != -1) { 306 outputJar.write(b, 0, read); 307 } 308 input.close(); 309 } 310 311 312 /** Sign data and write the digital signature to 'out'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out)313 private static void writeSignatureBlock( 314 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, 315 OutputStream out) 316 throws IOException, 317 CertificateEncodingException, 318 OperatorCreationException, 319 CMSException { 320 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 321 certList.add(publicKey); 322 JcaCertStore certs = new JcaCertStore(certList); 323 324 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 325 ContentSigner signer = 326 new JcaContentSignerBuilder( 327 getJcaSignatureAlgorithmForOta(publicKey, hash)) 328 .build(privateKey); 329 gen.addSignerInfoGenerator( 330 new JcaSignerInfoGeneratorBuilder( 331 new JcaDigestCalculatorProviderBuilder() 332 .build()) 333 .setDirectSignature(true) 334 .build(signer, publicKey)); 335 gen.addCertificates(certs); 336 CMSSignedData sigData = gen.generate(data, false); 337 338 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 339 DEROutputStream dos = new DEROutputStream(out); 340 dos.writeObject(asn1.readObject()); 341 } 342 } 343 344 /** 345 * Adds ZIP entries which represent the v1 signature (JAR signature scheme). 346 */ addV1Signature( ApkSignerEngine apkSigner, ApkSignerEngine.OutputJarSignatureRequest v1Signature, JarOutputStream out, long timestamp)347 private static void addV1Signature( 348 ApkSignerEngine apkSigner, 349 ApkSignerEngine.OutputJarSignatureRequest v1Signature, 350 JarOutputStream out, 351 long timestamp) throws IOException { 352 for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry 353 : v1Signature.getAdditionalJarEntries()) { 354 String entryName = entry.getName(); 355 JarEntry outEntry = new JarEntry(entryName); 356 outEntry.setTime(timestamp); 357 out.putNextEntry(outEntry); 358 byte[] entryData = entry.getData(); 359 out.write(entryData); 360 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 361 apkSigner.outputJarEntry(entryName); 362 if (inspectEntryRequest != null) { 363 inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); 364 inspectEntryRequest.done(); 365 } 366 } 367 } 368 369 /** 370 * Copy all JAR entries from input to output. We set the modification times in the output to a 371 * fixed time, so as to reduce variation in the output file and make incremental OTAs more 372 * efficient. 373 */ copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, CountingOutputStream outCounter, long timestamp, int defaultAlignment)374 private static void copyFiles( 375 JarFile in, 376 Pattern ignoredFilenamePattern, 377 ApkSignerEngine apkSigner, 378 JarOutputStream out, 379 CountingOutputStream outCounter, 380 long timestamp, 381 int defaultAlignment) throws IOException { 382 byte[] buffer = new byte[4096]; 383 int num; 384 385 List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(in); 386 ArrayList<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); 387 388 ArrayList<String> names = new ArrayList<String>(); 389 for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) { 390 JarEntry entry = e.nextElement(); 391 if (entry.isDirectory()) { 392 continue; 393 } 394 String entryName = entry.getName(); 395 if ((ignoredFilenamePattern != null) 396 && (ignoredFilenamePattern.matcher(entryName).matches())) { 397 continue; 398 } 399 if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { 400 continue; // We regenerate it below. 401 } 402 names.add(entryName); 403 } 404 Collections.sort(names); 405 406 boolean firstEntry = true; 407 long offset = 0L; 408 409 // We do the copy in two passes -- first copying all the 410 // entries that are STORED, then copying all the entries that 411 // have any other compression flag (which in practice means 412 // DEFLATED). This groups all the stored entries together at 413 // the start of the file and makes it easier to do alignment 414 // on them (since only stored entries are aligned). 415 416 List<String> remainingNames = new ArrayList<>(names.size()); 417 for (String name : names) { 418 JarEntry inEntry = in.getJarEntry(name); 419 if (inEntry.getMethod() != JarEntry.STORED) { 420 // Defer outputting this entry until we're ready to output compressed entries. 421 remainingNames.add(name); 422 continue; 423 } 424 425 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 426 continue; 427 } 428 429 // Preserve the STORED method of the input entry. 430 JarEntry outEntry = new JarEntry(inEntry); 431 outEntry.setTime(timestamp); 432 // Discard comment and extra fields of this entry to 433 // simplify alignment logic below and for consistency with 434 // how compressed entries are handled later. 435 outEntry.setComment(null); 436 outEntry.setExtra(null); 437 438 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 439 // Alignment of the entry's data is achieved by adding a data block to the entry's Local 440 // File Header extra field. The data block contains information about the alignment 441 // value and the necessary padding bytes (0x00) to achieve the alignment. This works 442 // because the entry's data will be located immediately after the extra field. 443 // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format 444 // of the extra field. 445 446 // 'offset' is the offset into the file at which we expect the entry's data to begin. 447 // This is the value we need to make a multiple of 'alignment'. 448 offset += JarFile.LOCHDR + outEntry.getName().length(); 449 if (firstEntry) { 450 // The first entry in a jar file has an extra field of four bytes that you can't get 451 // rid of; any extra data you specify in the JarEntry is appended to these forced 452 // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. 453 // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 454 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. 455 offset += 4; 456 firstEntry = false; 457 } 458 int extraPaddingSizeBytes = 0; 459 if (alignment > 0) { 460 long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; 461 extraPaddingSizeBytes = 462 (alignment - (int) (paddingStartOffset % alignment)) % alignment; 463 } 464 byte[] extra = 465 new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; 466 ByteBuffer extraBuf = ByteBuffer.wrap(extra); 467 extraBuf.order(ByteOrder.LITTLE_ENDIAN); 468 extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID 469 extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size 470 extraBuf.putShort((short) alignment); 471 outEntry.setExtra(extra); 472 offset += extra.length; 473 474 long entryHeaderStart = outCounter.getWrittenBytes(); 475 out.putNextEntry(outEntry); 476 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 477 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 478 DataSink entryDataSink = 479 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 480 481 long entryDataStart = outCounter.getWrittenBytes(); 482 try (InputStream data = in.getInputStream(inEntry)) { 483 while ((num = data.read(buffer)) > 0) { 484 out.write(buffer, 0, num); 485 if (entryDataSink != null) { 486 entryDataSink.consume(buffer, 0, num); 487 } 488 offset += num; 489 } 490 } 491 out.closeEntry(); 492 out.flush(); 493 if (inspectEntryRequest != null) { 494 inspectEntryRequest.done(); 495 } 496 497 if (pinPatterns != null) { 498 boolean pinFileHeader = false; 499 for (Hints.PatternWithRange pinPattern : pinPatterns) { 500 if (!pinPattern.matcher(name).matches()) { 501 continue; 502 } 503 Hints.ByteRange dataRange = 504 new Hints.ByteRange( 505 entryDataStart, 506 outCounter.getWrittenBytes()); 507 Hints.ByteRange pinRange = 508 pinPattern.ClampToAbsoluteByteRange(dataRange); 509 if (pinRange != null) { 510 pinFileHeader = true; 511 pinByteRanges.add(pinRange); 512 } 513 } 514 if (pinFileHeader) { 515 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 516 entryDataStart)); 517 } 518 } 519 } 520 521 // Copy all the non-STORED entries. We don't attempt to 522 // maintain the 'offset' variable past this point; we don't do 523 // alignment on these entries. 524 525 for (String name : remainingNames) { 526 JarEntry inEntry = in.getJarEntry(name); 527 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 528 continue; 529 } 530 531 // Create a new entry so that the compressed len is recomputed. 532 JarEntry outEntry = new JarEntry(name); 533 outEntry.setTime(timestamp); 534 long entryHeaderStart = outCounter.getWrittenBytes(); 535 out.putNextEntry(outEntry); 536 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 537 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 538 DataSink entryDataSink = 539 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 540 541 long entryDataStart = outCounter.getWrittenBytes(); 542 InputStream data = in.getInputStream(inEntry); 543 while ((num = data.read(buffer)) > 0) { 544 out.write(buffer, 0, num); 545 if (entryDataSink != null) { 546 entryDataSink.consume(buffer, 0, num); 547 } 548 } 549 out.closeEntry(); 550 out.flush(); 551 if (inspectEntryRequest != null) { 552 inspectEntryRequest.done(); 553 } 554 555 if (pinPatterns != null) { 556 boolean pinFileHeader = false; 557 for (Hints.PatternWithRange pinPattern : pinPatterns) { 558 if (!pinPattern.matcher(name).matches()) { 559 continue; 560 } 561 Hints.ByteRange dataRange = 562 new Hints.ByteRange( 563 entryDataStart, 564 outCounter.getWrittenBytes()); 565 Hints.ByteRange pinRange = 566 pinPattern.ClampToAbsoluteByteRange(dataRange); 567 if (pinRange != null) { 568 pinFileHeader = true; 569 pinByteRanges.add(pinRange); 570 } 571 } 572 if (pinFileHeader) { 573 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 574 entryDataStart)); 575 } 576 } 577 } 578 579 if (pinByteRanges != null) { 580 // Cover central directory 581 pinByteRanges.add( 582 new Hints.ByteRange(outCounter.getWrittenBytes(), 583 Long.MAX_VALUE)); 584 addPinByteRanges(out, pinByteRanges, timestamp); 585 } 586 } 587 extractPinPatterns(JarFile in)588 private static List<Hints.PatternWithRange> extractPinPatterns(JarFile in) throws IOException { 589 ZipEntry pinMetaEntry = in.getEntry(Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME); 590 if (pinMetaEntry == null) { 591 return null; 592 } 593 InputStream pinMetaStream = in.getInputStream(pinMetaEntry); 594 byte[] patternBlob = new byte[(int) pinMetaEntry.getSize()]; 595 pinMetaStream.read(patternBlob); 596 return Hints.parsePinPatterns(patternBlob); 597 } 598 addPinByteRanges(JarOutputStream outputJar, ArrayList<Hints.ByteRange> pinByteRanges, long timestamp)599 private static void addPinByteRanges(JarOutputStream outputJar, 600 ArrayList<Hints.ByteRange> pinByteRanges, 601 long timestamp) throws IOException { 602 JarEntry je = new JarEntry(Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME); 603 je.setTime(timestamp); 604 outputJar.putNextEntry(je); 605 outputJar.write(Hints.encodeByteRangeList(pinByteRanges)); 606 } 607 shouldOutputApkEntry( ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)608 private static boolean shouldOutputApkEntry( 609 ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) 610 throws IOException { 611 if (apkSigner == null) { 612 return true; 613 } 614 615 ApkSignerEngine.InputJarEntryInstructions instructions = 616 apkSigner.inputJarEntry(inEntry.getName()); 617 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 618 instructions.getInspectJarEntryRequest(); 619 if (inspectEntryRequest != null) { 620 provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); 621 } 622 switch (instructions.getOutputPolicy()) { 623 case OUTPUT: 624 return true; 625 case SKIP: 626 case OUTPUT_BY_ENGINE: 627 return false; 628 default: 629 throw new RuntimeException( 630 "Unsupported output policy: " + instructions.getOutputPolicy()); 631 } 632 } 633 provideJarEntry( JarFile jarFile, JarEntry jarEntry, ApkSignerEngine.InspectJarEntryRequest request, byte[] tmpbuf)634 private static void provideJarEntry( 635 JarFile jarFile, 636 JarEntry jarEntry, 637 ApkSignerEngine.InspectJarEntryRequest request, 638 byte[] tmpbuf) throws IOException { 639 DataSink dataSink = request.getDataSink(); 640 try (InputStream in = jarFile.getInputStream(jarEntry)) { 641 int chunkSize; 642 while ((chunkSize = in.read(tmpbuf)) > 0) { 643 dataSink.consume(tmpbuf, 0, chunkSize); 644 } 645 request.done(); 646 } 647 } 648 649 /** 650 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 651 * relative to start of file or {@code 0} if alignment of this entry's data is not important. 652 */ getStoredEntryDataAlignment(String entryName, int defaultAlignment)653 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 654 if (defaultAlignment <= 0) { 655 return 0; 656 } 657 658 if (entryName.endsWith(".so")) { 659 // Align .so contents to memory page boundary to enable memory-mapped 660 // execution. 661 return 4096; 662 } else { 663 return defaultAlignment; 664 } 665 } 666 667 private static class WholeFileSignerOutputStream extends FilterOutputStream { 668 private boolean closing = false; 669 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 670 private OutputStream tee; 671 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)672 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 673 super(out); 674 this.tee = tee; 675 } 676 notifyClosing()677 public void notifyClosing() { 678 closing = true; 679 } 680 finish()681 public void finish() throws IOException { 682 closing = false; 683 684 byte[] data = footer.toByteArray(); 685 if (data.length < 2) 686 throw new IOException("Less than two bytes written to footer"); 687 write(data, 0, data.length - 2); 688 } 689 getTail()690 public byte[] getTail() { 691 return footer.toByteArray(); 692 } 693 694 @Override write(byte[] b)695 public void write(byte[] b) throws IOException { 696 write(b, 0, b.length); 697 } 698 699 @Override write(byte[] b, int off, int len)700 public void write(byte[] b, int off, int len) throws IOException { 701 if (closing) { 702 // if the jar is about to close, save the footer that will be written 703 footer.write(b, off, len); 704 } 705 else { 706 // write to both output streams. out is the CMSTypedData signer and tee is the file. 707 out.write(b, off, len); 708 tee.write(b, off, len); 709 } 710 } 711 712 @Override write(int b)713 public void write(int b) throws IOException { 714 if (closing) { 715 // if the jar is about to close, save the footer that will be written 716 footer.write(b); 717 } 718 else { 719 // write to both output streams. out is the CMSTypedData signer and tee is the file. 720 out.write(b); 721 tee.write(b); 722 } 723 } 724 } 725 726 private static class CMSSigner implements CMSTypedData { 727 private final JarFile inputJar; 728 private final File publicKeyFile; 729 private final X509Certificate publicKey; 730 private final PrivateKey privateKey; 731 private final int hash; 732 private final long timestamp; 733 private final OutputStream outputStream; 734 private final ASN1ObjectIdentifier type; 735 private WholeFileSignerOutputStream signer; 736 737 // Files matching this pattern are not copied to the output. 738 private static final Pattern STRIP_PATTERN = 739 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" 740 + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 741 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)742 public CMSSigner(JarFile inputJar, File publicKeyFile, 743 X509Certificate publicKey, PrivateKey privateKey, int hash, 744 long timestamp, OutputStream outputStream) { 745 this.inputJar = inputJar; 746 this.publicKeyFile = publicKeyFile; 747 this.publicKey = publicKey; 748 this.privateKey = privateKey; 749 this.hash = hash; 750 this.timestamp = timestamp; 751 this.outputStream = outputStream; 752 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 753 } 754 755 /** 756 * This should actually return byte[] or something similar, but nothing 757 * actually checks it currently. 758 */ 759 @Override getContent()760 public Object getContent() { 761 return this; 762 } 763 764 @Override getContentType()765 public ASN1ObjectIdentifier getContentType() { 766 return type; 767 } 768 769 @Override write(OutputStream out)770 public void write(OutputStream out) throws IOException { 771 try { 772 signer = new WholeFileSignerOutputStream(out, outputStream); 773 CountingOutputStream outputJarCounter = new CountingOutputStream(signer); 774 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 775 776 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, 777 outputJarCounter, timestamp, 0); 778 addOtacert(outputJar, publicKeyFile, timestamp); 779 780 signer.notifyClosing(); 781 outputJar.close(); 782 signer.finish(); 783 } 784 catch (Exception e) { 785 throw new IOException(e); 786 } 787 } 788 writeSignatureBlock(ByteArrayOutputStream temp)789 public void writeSignatureBlock(ByteArrayOutputStream temp) 790 throws IOException, 791 CertificateEncodingException, 792 OperatorCreationException, 793 CMSException { 794 SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); 795 } 796 getSigner()797 public WholeFileSignerOutputStream getSigner() { 798 return signer; 799 } 800 } 801 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)802 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 803 X509Certificate publicKey, PrivateKey privateKey, 804 int hash, long timestamp, 805 OutputStream outputStream) throws Exception { 806 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 807 publicKey, privateKey, hash, timestamp, outputStream); 808 809 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 810 811 // put a readable message and a null char at the start of the 812 // archive comment, so that tools that display the comment 813 // (hopefully) show something sensible. 814 // TODO: anything more useful we can put in this message? 815 byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8); 816 temp.write(message); 817 temp.write(0); 818 819 cmsOut.writeSignatureBlock(temp); 820 821 byte[] zipData = cmsOut.getSigner().getTail(); 822 823 // For a zip with no archive comment, the 824 // end-of-central-directory record will be 22 bytes long, so 825 // we expect to find the EOCD marker 22 bytes from the end. 826 if (zipData[zipData.length-22] != 0x50 || 827 zipData[zipData.length-21] != 0x4b || 828 zipData[zipData.length-20] != 0x05 || 829 zipData[zipData.length-19] != 0x06) { 830 throw new IllegalArgumentException("zip data already has an archive comment"); 831 } 832 833 int total_size = temp.size() + 6; 834 if (total_size > 0xffff) { 835 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 836 } 837 // signature starts this many bytes from the end of the file 838 int signature_start = total_size - message.length - 1; 839 temp.write(signature_start & 0xff); 840 temp.write((signature_start >> 8) & 0xff); 841 // Why the 0xff bytes? In a zip file with no archive comment, 842 // bytes [-6:-2] of the file are the little-endian offset from 843 // the start of the file to the central directory. So for the 844 // two high bytes to be 0xff 0xff, the archive would have to 845 // be nearly 4GB in size. So it's unlikely that a real 846 // commentless archive would have 0xffs here, and lets us tell 847 // an old signed archive from a new one. 848 temp.write(0xff); 849 temp.write(0xff); 850 temp.write(total_size & 0xff); 851 temp.write((total_size >> 8) & 0xff); 852 temp.flush(); 853 854 // Signature verification checks that the EOCD header is the 855 // last such sequence in the file (to avoid minzip finding a 856 // fake EOCD appended after the signature in its scan). The 857 // odds of producing this sequence by chance are very low, but 858 // let's catch it here if it does. 859 byte[] b = temp.toByteArray(); 860 for (int i = 0; i < b.length-3; ++i) { 861 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 862 throw new IllegalArgumentException("found spurious EOCD header at " + i); 863 } 864 } 865 866 outputStream.write(total_size & 0xff); 867 outputStream.write((total_size >> 8) & 0xff); 868 temp.writeTo(outputStream); 869 } 870 871 /** 872 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 873 * types that might be stored in PKCS#11-like storage. 874 */ loadProviderIfNecessary(String providerClassName)875 private static void loadProviderIfNecessary(String providerClassName) { 876 if (providerClassName == null) { 877 return; 878 } 879 880 final Class<?> klass; 881 try { 882 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 883 if (sysLoader != null) { 884 klass = sysLoader.loadClass(providerClassName); 885 } else { 886 klass = Class.forName(providerClassName); 887 } 888 } catch (ClassNotFoundException e) { 889 e.printStackTrace(); 890 System.exit(1); 891 return; 892 } 893 894 Constructor<?> constructor = null; 895 for (Constructor<?> c : klass.getConstructors()) { 896 if (c.getParameterTypes().length == 0) { 897 constructor = c; 898 break; 899 } 900 } 901 if (constructor == null) { 902 System.err.println("No zero-arg constructor found for " + providerClassName); 903 System.exit(1); 904 return; 905 } 906 907 final Object o; 908 try { 909 o = constructor.newInstance(); 910 } catch (Exception e) { 911 e.printStackTrace(); 912 System.exit(1); 913 return; 914 } 915 if (!(o instanceof Provider)) { 916 System.err.println("Not a Provider class: " + providerClassName); 917 System.exit(1); 918 } 919 920 Security.insertProviderAt((Provider) o, 1); 921 } 922 createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)923 private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs( 924 PrivateKey[] privateKeys, X509Certificate[] certificates) { 925 if (privateKeys.length != certificates.length) { 926 throw new IllegalArgumentException( 927 "The number of private keys must match the number of certificates: " 928 + privateKeys.length + " vs" + certificates.length); 929 } 930 List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>(); 931 String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; 932 for (int i = 0; i < privateKeys.length; i++) { 933 String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); 934 DefaultApkSignerEngine.SignerConfig signerConfig = 935 new DefaultApkSignerEngine.SignerConfig.Builder( 936 signerName, 937 privateKeys[i], 938 Collections.singletonList(certificates[i])) 939 .build(); 940 signerConfigs.add(signerConfig); 941 } 942 return signerConfigs; 943 } 944 945 private static class ZipSections { 946 ByteBuffer beforeCentralDir; 947 ByteBuffer centralDir; 948 ByteBuffer eocd; 949 } 950 findMainZipSections(ByteBuffer apk)951 private static ZipSections findMainZipSections(ByteBuffer apk) 952 throws IOException, ZipFormatException { 953 apk.slice(); 954 ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk)); 955 long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); 956 long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); 957 long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; 958 long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); 959 if (centralDirEndOffset != eocdStartOffset) { 960 throw new ZipFormatException( 961 "ZIP Central Directory is not immediately followed by End of Central Directory" 962 + ". CD end: " + centralDirEndOffset 963 + ", EoCD start: " + eocdStartOffset); 964 } 965 apk.position(0); 966 apk.limit((int) centralDirStartOffset); 967 ByteBuffer beforeCentralDir = apk.slice(); 968 969 apk.position((int) centralDirStartOffset); 970 apk.limit((int) centralDirEndOffset); 971 ByteBuffer centralDir = apk.slice(); 972 973 apk.position((int) eocdStartOffset); 974 apk.limit(apk.capacity()); 975 ByteBuffer eocd = apk.slice(); 976 977 apk.position(0); 978 apk.limit(apk.capacity()); 979 980 ZipSections result = new ZipSections(); 981 result.beforeCentralDir = beforeCentralDir; 982 result.centralDir = centralDir; 983 result.eocd = eocd; 984 return result; 985 } 986 987 /** 988 * Returns the API Level corresponding to the APK's minSdkVersion. 989 * 990 * @throws MinSdkVersionException if the API Level cannot be determined from the APK. 991 */ getMinSdkVersion(JarFile apk)992 private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException { 993 JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml"); 994 if (manifestEntry == null) { 995 throw new MinSdkVersionException("No AndroidManifest.xml in APK"); 996 } 997 byte[] manifestBytes; 998 try { 999 try (InputStream manifestIn = apk.getInputStream(manifestEntry)) { 1000 manifestBytes = toByteArray(manifestIn); 1001 } 1002 } catch (IOException e) { 1003 throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e); 1004 } 1005 return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes)); 1006 } 1007 toByteArray(InputStream in)1008 private static byte[] toByteArray(InputStream in) throws IOException { 1009 ByteArrayOutputStream result = new ByteArrayOutputStream(); 1010 byte[] buf = new byte[65536]; 1011 int chunkSize; 1012 while ((chunkSize = in.read(buf)) != -1) { 1013 result.write(buf, 0, chunkSize); 1014 } 1015 return result.toByteArray(); 1016 } 1017 usage()1018 private static void usage() { 1019 System.err.println("Usage: signapk [-w] " + 1020 "[-a <alignment>] " + 1021 "[-providerClass <className>] " + 1022 "[--min-sdk-version <n>] " + 1023 "[--disable-v2] " + 1024 "publickey.x509[.pem] privatekey.pk8 " + 1025 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 1026 "input.jar output.jar"); 1027 System.exit(2); 1028 } 1029 main(String[] args)1030 public static void main(String[] args) { 1031 if (args.length < 4) usage(); 1032 1033 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than 1034 // the standard or Bouncy Castle ones. 1035 Security.insertProviderAt(new OpenSSLProvider(), 1); 1036 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer 1037 // DSA which may still be needed. 1038 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. 1039 Security.addProvider(new BouncyCastleProvider()); 1040 1041 boolean signWholeFile = false; 1042 String providerClass = null; 1043 int alignment = 4; 1044 Integer minSdkVersionOverride = null; 1045 boolean signUsingApkSignatureSchemeV2 = true; 1046 SigningCertificateLineage certLineage = null; 1047 1048 int argstart = 0; 1049 while (argstart < args.length && args[argstart].startsWith("-")) { 1050 if ("-w".equals(args[argstart])) { 1051 signWholeFile = true; 1052 ++argstart; 1053 } else if ("-providerClass".equals(args[argstart])) { 1054 if (argstart + 1 >= args.length) { 1055 usage(); 1056 } 1057 providerClass = args[++argstart]; 1058 ++argstart; 1059 } else if ("-a".equals(args[argstart])) { 1060 alignment = Integer.parseInt(args[++argstart]); 1061 ++argstart; 1062 } else if ("--min-sdk-version".equals(args[argstart])) { 1063 String minSdkVersionString = args[++argstart]; 1064 try { 1065 minSdkVersionOverride = Integer.parseInt(minSdkVersionString); 1066 } catch (NumberFormatException e) { 1067 throw new IllegalArgumentException( 1068 "--min-sdk-version must be a decimal number: " + minSdkVersionString); 1069 } 1070 ++argstart; 1071 } else if ("--disable-v2".equals(args[argstart])) { 1072 signUsingApkSignatureSchemeV2 = false; 1073 ++argstart; 1074 } else if ("--lineage".equals(args[argstart])) { 1075 File lineageFile = new File(args[++argstart]); 1076 try { 1077 certLineage = SigningCertificateLineage.readFromFile(lineageFile); 1078 } catch (Exception e) { 1079 throw new IllegalArgumentException( 1080 "Error reading lineage file: " + e.getMessage()); 1081 } 1082 ++argstart; 1083 } else { 1084 usage(); 1085 } 1086 } 1087 1088 if ((args.length - argstart) % 2 == 1) usage(); 1089 int numKeys = ((args.length - argstart) / 2) - 1; 1090 if (signWholeFile && numKeys > 1) { 1091 System.err.println("Only one key may be used with -w."); 1092 System.exit(2); 1093 } 1094 1095 loadProviderIfNecessary(providerClass); 1096 1097 String inputFilename = args[args.length-2]; 1098 String outputFilename = args[args.length-1]; 1099 1100 JarFile inputJar = null; 1101 FileOutputStream outputFile = null; 1102 1103 try { 1104 File firstPublicKeyFile = new File(args[argstart+0]); 1105 1106 X509Certificate[] publicKey = new X509Certificate[numKeys]; 1107 try { 1108 for (int i = 0; i < numKeys; ++i) { 1109 int argNum = argstart + i*2; 1110 publicKey[i] = readPublicKey(new File(args[argNum])); 1111 } 1112 } catch (IllegalArgumentException e) { 1113 System.err.println(e); 1114 System.exit(1); 1115 } 1116 1117 // Set all ZIP file timestamps to Jan 1 2009 00:00:00. 1118 long timestamp = 1230768000000L; 1119 // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS 1120 // timestamp using the current timezone. We thus adjust the milliseconds since epoch 1121 // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. 1122 timestamp -= TimeZone.getDefault().getOffset(timestamp); 1123 1124 PrivateKey[] privateKey = new PrivateKey[numKeys]; 1125 for (int i = 0; i < numKeys; ++i) { 1126 int argNum = argstart + i*2 + 1; 1127 privateKey[i] = readPrivateKey(new File(args[argNum])); 1128 } 1129 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 1130 1131 outputFile = new FileOutputStream(outputFilename); 1132 1133 // NOTE: Signing currently recompresses any compressed entries using Deflate (default 1134 // compression level for OTA update files and maximum compession level for APKs). 1135 if (signWholeFile) { 1136 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); 1137 signWholeFile(inputJar, firstPublicKeyFile, 1138 publicKey[0], privateKey[0], digestAlgorithm, 1139 timestamp, 1140 outputFile); 1141 } else { 1142 // Determine the value to use as minSdkVersion of the APK being signed 1143 int minSdkVersion; 1144 if (minSdkVersionOverride != null) { 1145 minSdkVersion = minSdkVersionOverride; 1146 } else { 1147 try { 1148 minSdkVersion = getMinSdkVersion(inputJar); 1149 } catch (MinSdkVersionException e) { 1150 throw new IllegalArgumentException( 1151 "Cannot detect minSdkVersion. Use --min-sdk-version to override", 1152 e); 1153 } 1154 } 1155 1156 try (ApkSignerEngine apkSigner = 1157 new DefaultApkSignerEngine.Builder( 1158 createSignerConfigs(privateKey, publicKey), minSdkVersion) 1159 .setV1SigningEnabled(true) 1160 .setV2SigningEnabled(signUsingApkSignatureSchemeV2) 1161 .setOtherSignersSignaturesPreserved(false) 1162 .setCreatedBy("1.0 (Android SignApk)") 1163 .setSigningCertificateLineage(certLineage) 1164 .build()) { 1165 // We don't preserve the input APK's APK Signing Block (which contains v2 1166 // signatures) 1167 apkSigner.inputApkSigningBlock(null); 1168 1169 // Build the output APK in memory, by copying input APK's ZIP entries across 1170 // and then signing the output APK. 1171 ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); 1172 CountingOutputStream outputJarCounter = 1173 new CountingOutputStream(v1SignedApkBuf); 1174 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 1175 // Use maximum compression for compressed entries because the APK lives forever 1176 // on the system partition. 1177 outputJar.setLevel(9); 1178 copyFiles(inputJar, null, apkSigner, outputJar, 1179 outputJarCounter, timestamp, alignment); 1180 ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = 1181 apkSigner.outputJarEntries(); 1182 if (addV1SignatureRequest != null) { 1183 addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); 1184 addV1SignatureRequest.done(); 1185 } 1186 outputJar.close(); 1187 ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); 1188 v1SignedApkBuf.reset(); 1189 ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk}; 1190 1191 ZipSections zipSections = findMainZipSections(v1SignedApk); 1192 ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest = 1193 apkSigner.outputZipSections2( 1194 DataSources.asDataSource(zipSections.beforeCentralDir), 1195 DataSources.asDataSource(zipSections.centralDir), 1196 DataSources.asDataSource(zipSections.eocd)); 1197 if (addV2SignatureRequest != null) { 1198 // Need to insert the returned APK Signing Block before ZIP Central 1199 // Directory. 1200 int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock(); 1201 byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); 1202 // Because the APK Signing Block is inserted before the Central Directory, 1203 // we need to adjust accordingly the offset of Central Directory inside the 1204 // ZIP End of Central Directory (EoCD) record. 1205 ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining()); 1206 modifiedEocd.put(zipSections.eocd); 1207 modifiedEocd.flip(); 1208 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1209 ApkUtils.setZipEocdCentralDirectoryOffset( 1210 modifiedEocd, 1211 zipSections.beforeCentralDir.remaining() + padding + 1212 apkSigningBlock.length); 1213 outputChunks = 1214 new ByteBuffer[] { 1215 zipSections.beforeCentralDir, 1216 ByteBuffer.allocate(padding), 1217 ByteBuffer.wrap(apkSigningBlock), 1218 zipSections.centralDir, 1219 modifiedEocd}; 1220 addV2SignatureRequest.done(); 1221 } 1222 1223 // This assumes outputChunks are array-backed. To avoid this assumption, the 1224 // code could be rewritten to use FileChannel. 1225 for (ByteBuffer outputChunk : outputChunks) { 1226 outputFile.write( 1227 outputChunk.array(), 1228 outputChunk.arrayOffset() + outputChunk.position(), 1229 outputChunk.remaining()); 1230 outputChunk.position(outputChunk.limit()); 1231 } 1232 1233 outputFile.close(); 1234 outputFile = null; 1235 apkSigner.outputDone(); 1236 } 1237 1238 return; 1239 } 1240 } catch (Exception e) { 1241 e.printStackTrace(); 1242 System.exit(1); 1243 } finally { 1244 try { 1245 if (inputJar != null) inputJar.close(); 1246 if (outputFile != null) outputFile.close(); 1247 } catch (IOException e) { 1248 e.printStackTrace(); 1249 System.exit(1); 1250 } 1251 } 1252 } 1253 } 1254