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