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