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