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