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      */
getDigestAlgorithm(X509Certificate cert, int minSdkVersion)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. */
getSignatureAlgorithm(X509Certificate cert, int minSdkVersion)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 
readPublicKey(File file)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      */
readPassword(File keyFile)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      */
decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)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. */
readPrivateKey(File file)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      */
addDigestsToManifest(JarFile jar, int hashes)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      */
addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest, int hash)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 
CountOutputStream(OutputStream out)402         public CountOutputStream(OutputStream out) {
403             super(out);
404             mCount = 0;
405         }
406 
407         @Override
write(int b)408         public void write(int b) throws IOException {
409             super.write(b);
410             mCount++;
411         }
412 
413         @Override
write(byte[] b, int off, int len)414         public void write(byte[] b, int off, int len) throws IOException {
415             super.write(b, off, len);
416             mCount += len;
417         }
418 
size()419         public int size() {
420             return mCount;
421         }
422     }
423 
424     /** Write a .SF file with a digest of the specified manifest. */
writeSignatureFile(Manifest manifest, OutputStream out, int hash, boolean additionallySignedUsingAnApkSignatureScheme)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'. */
writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int minSdkVersion, OutputStream 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      */
copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp, int defaultAlignment)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      */
getStoredEntryDataAlignment(String entryName, int defaultAlignment)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 
WholeFileSignerOutputStream(OutputStream out, OutputStream tee)630         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
631             super(out);
632             this.tee = tee;
633         }
634 
notifyClosing()635         public void notifyClosing() {
636             closing = true;
637         }
638 
finish()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 
getTail()648         public byte[] getTail() {
649             return footer.toByteArray();
650         }
651 
652         @Override
write(byte[] b)653         public void write(byte[] b) throws IOException {
654             write(b, 0, b.length);
655         }
656 
657         @Override
write(byte[] b, int off, int len)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
write(int b)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 
CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, long timestamp, int minSdkVersion, OutputStream outputStream)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
getContent()713         public Object getContent() {
714             return this;
715         }
716 
717         @Override
getContentType()718         public ASN1ObjectIdentifier getContentType() {
719             return type;
720         }
721 
722         @Override
write(OutputStream out)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 
writeSignatureBlock(ByteArrayOutputStream temp)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 
getSigner()759         public WholeFileSignerOutputStream getSigner() {
760             return signer;
761         }
762     }
763 
signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, long timestamp, int minSdkVersion, OutputStream outputStream)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 
signFile(Manifest manifest, X509Certificate[] publicKey, PrivateKey[] privateKey, long timestamp, int minSdkVersion, boolean additionallySignedUsingAnApkSignatureScheme, JarOutputStream outputJar)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      */
loadProviderIfNecessary(String providerClassName)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      */
createV2SignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)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 
getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm)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 
usage()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 
main(String[] args)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