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