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