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