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