• 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 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 4096;
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)904     private static void loadProviderIfNecessary(String providerClassName) {
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 = null;
924         for (Constructor<?> c : klass.getConstructors()) {
925             if (c.getParameterTypes().length == 0) {
926                 constructor = c;
927                 break;
928             }
929         }
930         if (constructor == null) {
931             System.err.println("No zero-arg constructor found for " + providerClassName);
932             System.exit(1);
933             return;
934         }
935 
936         final Object o;
937         try {
938             o = constructor.newInstance();
939         } catch (Exception e) {
940             e.printStackTrace();
941             System.exit(1);
942             return;
943         }
944         if (!(o instanceof Provider)) {
945             System.err.println("Not a Provider class: " + providerClassName);
946             System.exit(1);
947         }
948 
949         Security.insertProviderAt((Provider) o, 1);
950     }
951 
createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)952     private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
953             PrivateKey[] privateKeys, X509Certificate[] certificates) {
954         if (privateKeys.length != certificates.length) {
955             throw new IllegalArgumentException(
956                     "The number of private keys must match the number of certificates: "
957                             + privateKeys.length + " vs" + certificates.length);
958         }
959         List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
960         String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
961         for (int i = 0; i < privateKeys.length; i++) {
962             String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
963             DefaultApkSignerEngine.SignerConfig signerConfig =
964                     new DefaultApkSignerEngine.SignerConfig.Builder(
965                             signerName,
966                             privateKeys[i],
967                             Collections.singletonList(certificates[i]))
968                             .build();
969             signerConfigs.add(signerConfig);
970         }
971         return signerConfigs;
972     }
973 
974     private static class ZipSections {
975         ByteBuffer beforeCentralDir;
976         ByteBuffer centralDir;
977         ByteBuffer eocd;
978     }
979 
findMainZipSections(ByteBuffer apk)980     private static ZipSections findMainZipSections(ByteBuffer apk)
981             throws IOException, ZipFormatException {
982         apk.slice();
983         ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
984         long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
985         long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
986         long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
987         long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
988         if (centralDirEndOffset != eocdStartOffset) {
989             throw new ZipFormatException(
990                     "ZIP Central Directory is not immediately followed by End of Central Directory"
991                             + ". CD end: " + centralDirEndOffset
992                             + ", EoCD start: " + eocdStartOffset);
993         }
994         apk.position(0);
995         apk.limit((int) centralDirStartOffset);
996         ByteBuffer beforeCentralDir = apk.slice();
997 
998         apk.position((int) centralDirStartOffset);
999         apk.limit((int) centralDirEndOffset);
1000         ByteBuffer centralDir = apk.slice();
1001 
1002         apk.position((int) eocdStartOffset);
1003         apk.limit(apk.capacity());
1004         ByteBuffer eocd = apk.slice();
1005 
1006         apk.position(0);
1007         apk.limit(apk.capacity());
1008 
1009         ZipSections result = new ZipSections();
1010         result.beforeCentralDir = beforeCentralDir;
1011         result.centralDir = centralDir;
1012         result.eocd = eocd;
1013         return result;
1014     }
1015 
1016     /**
1017      * Returns the API Level corresponding to the APK's minSdkVersion.
1018      *
1019      * @throws MinSdkVersionException if the API Level cannot be determined from the APK.
1020      */
getMinSdkVersion(JarFile apk)1021     private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException {
1022         JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml");
1023         if (manifestEntry == null) {
1024             throw new MinSdkVersionException("No AndroidManifest.xml in APK");
1025         }
1026         byte[] manifestBytes;
1027         try {
1028             try (InputStream manifestIn = apk.getInputStream(manifestEntry)) {
1029                 manifestBytes = toByteArray(manifestIn);
1030             }
1031         } catch (IOException e) {
1032             throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e);
1033         }
1034         return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes));
1035     }
1036 
toByteArray(InputStream in)1037     private static byte[] toByteArray(InputStream in) throws IOException {
1038         ByteArrayOutputStream result = new ByteArrayOutputStream();
1039         byte[] buf = new byte[65536];
1040         int chunkSize;
1041         while ((chunkSize = in.read(buf)) != -1) {
1042             result.write(buf, 0, chunkSize);
1043         }
1044         return result.toByteArray();
1045     }
1046 
usage()1047     private static void usage() {
1048         System.err.println("Usage: signapk [-w] " +
1049                            "[-a <alignment>] " +
1050                            "[--align-file-size] " +
1051                            "[-providerClass <className>] " +
1052                            "[-loadPrivateKeysFromKeyStore <keyStoreName>]" +
1053                            "[-keyStorePin <pin>]" +
1054                            "[--min-sdk-version <n>] " +
1055                            "[--disable-v2] " +
1056                            "[--enable-v4] " +
1057                            "publickey.x509[.pem] privatekey.pk8 " +
1058                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
1059                            "input.jar output.jar [output-v4-file]");
1060         System.exit(2);
1061     }
1062 
main(String[] args)1063     public static void main(String[] args) {
1064         if (args.length < 4) usage();
1065 
1066         // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
1067         // the standard or Bouncy Castle ones.
1068         Security.insertProviderAt(new OpenSSLProvider(), 1);
1069         // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
1070         // DSA which may still be needed.
1071         // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
1072         Security.addProvider(new BouncyCastleProvider());
1073 
1074         boolean signWholeFile = false;
1075         String providerClass = null;
1076         String keyStoreName = null;
1077         String keyStorePin = null;
1078         int alignment = 4;
1079         boolean alignFileSize = false;
1080         Integer minSdkVersionOverride = null;
1081         boolean signUsingApkSignatureSchemeV2 = true;
1082         boolean signUsingApkSignatureSchemeV4 = false;
1083         SigningCertificateLineage certLineage = null;
1084         Integer rotationMinSdkVersion = null;
1085 
1086         int argstart = 0;
1087         while (argstart < args.length && args[argstart].startsWith("-")) {
1088             if ("-w".equals(args[argstart])) {
1089                 signWholeFile = true;
1090                 ++argstart;
1091             } else if ("-providerClass".equals(args[argstart])) {
1092                 if (argstart + 1 >= args.length) {
1093                     usage();
1094                 }
1095                 providerClass = args[++argstart];
1096                 ++argstart;
1097             } else if ("-loadPrivateKeysFromKeyStore".equals(args[argstart])) {
1098                 if (argstart + 1 >= args.length) {
1099                     usage();
1100                 }
1101                 keyStoreName = args[++argstart];
1102                 ++argstart;
1103             } else if ("-keyStorePin".equals(args[argstart])) {
1104                 if (argstart + 1 >= args.length) {
1105                     usage();
1106                 }
1107                 keyStorePin = args[++argstart];
1108                 ++argstart;
1109             } else if ("-a".equals(args[argstart])) {
1110                 alignment = Integer.parseInt(args[++argstart]);
1111                 ++argstart;
1112             } else if ("--align-file-size".equals(args[argstart])) {
1113                 alignFileSize = true;
1114                 ++argstart;
1115             } else if ("--min-sdk-version".equals(args[argstart])) {
1116                 String minSdkVersionString = args[++argstart];
1117                 try {
1118                     minSdkVersionOverride = Integer.parseInt(minSdkVersionString);
1119                 } catch (NumberFormatException e) {
1120                     throw new IllegalArgumentException(
1121                             "--min-sdk-version must be a decimal number: " + minSdkVersionString);
1122                 }
1123                 ++argstart;
1124             } else if ("--disable-v2".equals(args[argstart])) {
1125                 signUsingApkSignatureSchemeV2 = false;
1126                 ++argstart;
1127             } else if ("--enable-v4".equals(args[argstart])) {
1128                 signUsingApkSignatureSchemeV4 = true;
1129                 ++argstart;
1130             } else if ("--lineage".equals(args[argstart])) {
1131                 File lineageFile = new File(args[++argstart]);
1132                 try {
1133                     certLineage = SigningCertificateLineage.readFromFile(lineageFile);
1134                 } catch (Exception e) {
1135                     throw new IllegalArgumentException(
1136                             "Error reading lineage file: " + e.getMessage());
1137                 }
1138                 ++argstart;
1139             } else if ("--rotation-min-sdk-version".equals(args[argstart])) {
1140                 String rotationMinSdkVersionString = args[++argstart];
1141                 try {
1142                     rotationMinSdkVersion = Integer.parseInt(rotationMinSdkVersionString);
1143                 } catch (NumberFormatException e) {
1144                     throw new IllegalArgumentException(
1145                             "--rotation-min-sdk-version must be a decimal number: " + rotationMinSdkVersionString);
1146                 }
1147                 ++argstart;
1148             } else {
1149                 usage();
1150             }
1151         }
1152 
1153         int numArgsExcludeV4FilePath;
1154         if (signUsingApkSignatureSchemeV4) {
1155             numArgsExcludeV4FilePath = args.length - 1;
1156         } else {
1157             numArgsExcludeV4FilePath = args.length;
1158         }
1159         if ((numArgsExcludeV4FilePath - argstart) % 2 == 1) usage();
1160         int numKeys = ((numArgsExcludeV4FilePath - argstart) / 2) - 1;
1161         if (signWholeFile && numKeys > 1) {
1162             System.err.println("Only one key may be used with -w.");
1163             System.exit(2);
1164         }
1165 
1166         loadProviderIfNecessary(providerClass);
1167 
1168         String inputFilename = args[numArgsExcludeV4FilePath - 2];
1169         String outputFilename = args[numArgsExcludeV4FilePath - 1];
1170         String outputV4Filename = "";
1171         if (signUsingApkSignatureSchemeV4) {
1172             outputV4Filename = args[args.length - 1];
1173         }
1174 
1175         JarFile inputJar = null;
1176         FileOutputStream outputFile = null;
1177 
1178         try {
1179             File firstPublicKeyFile = new File(args[argstart+0]);
1180 
1181             X509Certificate[] publicKey = new X509Certificate[numKeys];
1182             try {
1183                 for (int i = 0; i < numKeys; ++i) {
1184                     int argNum = argstart + i*2;
1185                     publicKey[i] = readPublicKey(new File(args[argNum]));
1186                 }
1187             } catch (IllegalArgumentException e) {
1188                 System.err.println(e);
1189                 System.exit(1);
1190             }
1191 
1192             // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
1193             long timestamp = 1230768000000L;
1194             // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
1195             // timestamp using the current timezone. We thus adjust the milliseconds since epoch
1196             // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
1197             timestamp -= TimeZone.getDefault().getOffset(timestamp);
1198             KeyStore keyStore = null;
1199             if (keyStoreName != null) {
1200                 keyStore = createKeyStore(keyStoreName, keyStorePin);
1201             }
1202             PrivateKey[] privateKey = new PrivateKey[numKeys];
1203             for (int i = 0; i < numKeys; ++i) {
1204                 int argNum = argstart + i*2 + 1;
1205                 if (keyStore == null) {
1206                     privateKey[i] = readPrivateKey(new File(args[argNum]));
1207                 } else {
1208                     final String keyAlias = args[argNum];
1209                     privateKey[i] = loadPrivateKeyFromKeyStore(keyStore, keyAlias);
1210                 }
1211             }
1212             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
1213 
1214             outputFile = new FileOutputStream(outputFilename);
1215 
1216             // NOTE: Signing currently recompresses any compressed entries using Deflate (default
1217             // compression level for OTA update files and maximum compession level for APKs).
1218             if (signWholeFile) {
1219                 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
1220                 signWholeFile(inputJar, firstPublicKeyFile,
1221                         publicKey[0], privateKey[0], digestAlgorithm,
1222                         timestamp,
1223                         outputFile);
1224             } else {
1225                 // Determine the value to use as minSdkVersion of the APK being signed
1226                 int minSdkVersion;
1227                 if (minSdkVersionOverride != null) {
1228                     minSdkVersion = minSdkVersionOverride;
1229                 } else {
1230                     try {
1231                         minSdkVersion = getMinSdkVersion(inputJar);
1232                     } catch (MinSdkVersionException e) {
1233                         throw new IllegalArgumentException(
1234                                 "Cannot detect minSdkVersion. Use --min-sdk-version to override",
1235                                 e);
1236                     }
1237                 }
1238 
1239                 DefaultApkSignerEngine.Builder builder = new DefaultApkSignerEngine.Builder(
1240                     createSignerConfigs(privateKey, publicKey), minSdkVersion)
1241                     .setV1SigningEnabled(true)
1242                     .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
1243                     .setOtherSignersSignaturesPreserved(false)
1244                     .setCreatedBy("1.0 (Android SignApk)");
1245 
1246                 if (certLineage != null) {
1247                    builder = builder.setSigningCertificateLineage(certLineage);
1248                 }
1249 
1250                 if (rotationMinSdkVersion != null) {
1251                    builder = builder.setMinSdkVersionForRotation(rotationMinSdkVersion);
1252                 }
1253 
1254                 try (ApkSignerEngine apkSigner = builder.build()) {
1255                     // We don't preserve the input APK's APK Signing Block (which contains v2
1256                     // signatures)
1257                     apkSigner.inputApkSigningBlock(null);
1258 
1259                     // Build the output APK in memory, by copying input APK's ZIP entries across
1260                     // and then signing the output APK.
1261                     ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
1262                     CountingOutputStream outputJarCounter =
1263                             new CountingOutputStream(v1SignedApkBuf);
1264                     JarOutputStream outputJar = new JarOutputStream(outputJarCounter);
1265                     // Use maximum compression for compressed entries because the APK lives forever
1266                     // on the system partition.
1267                     outputJar.setLevel(9);
1268                     copyFiles(inputJar, null, apkSigner, outputJar,
1269                               outputJarCounter, timestamp, alignment);
1270                     ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
1271                             apkSigner.outputJarEntries();
1272                     if (addV1SignatureRequest != null) {
1273                         addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
1274                         addV1SignatureRequest.done();
1275                     }
1276                     outputJar.close();
1277                     ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
1278                     v1SignedApkBuf.reset();
1279                     ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
1280 
1281                     ZipSections zipSections = findMainZipSections(v1SignedApk);
1282 
1283                     ByteBuffer eocd = ByteBuffer.allocate(zipSections.eocd.remaining());
1284                     eocd.put(zipSections.eocd);
1285                     eocd.flip();
1286                     eocd.order(ByteOrder.LITTLE_ENDIAN);
1287                     // This loop is supposed to be iterated twice at most.
1288                     // The second pass is to align the file size after amending EOCD comments
1289                     // with assumption that re-generated signing block would be the same size.
1290                     while (true) {
1291                         ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest =
1292                                 apkSigner.outputZipSections2(
1293                                         DataSources.asDataSource(zipSections.beforeCentralDir),
1294                                         DataSources.asDataSource(zipSections.centralDir),
1295                                         DataSources.asDataSource(eocd));
1296                         if (addV2SignatureRequest == null) break;
1297 
1298                         // Need to insert the returned APK Signing Block before ZIP Central
1299                         // Directory.
1300                         int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock();
1301                         byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
1302                         // Because the APK Signing Block is inserted before the Central Directory,
1303                         // we need to adjust accordingly the offset of Central Directory inside the
1304                         // ZIP End of Central Directory (EoCD) record.
1305                         ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
1306                         modifiedEocd.put(eocd);
1307                         modifiedEocd.flip();
1308                         modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
1309                         ApkUtils.setZipEocdCentralDirectoryOffset(
1310                                 modifiedEocd,
1311                                 zipSections.beforeCentralDir.remaining() + padding +
1312                                 apkSigningBlock.length);
1313                         outputChunks =
1314                                 new ByteBuffer[] {
1315                                         zipSections.beforeCentralDir,
1316                                         ByteBuffer.allocate(padding),
1317                                         ByteBuffer.wrap(apkSigningBlock),
1318                                         zipSections.centralDir,
1319                                         modifiedEocd};
1320                         addV2SignatureRequest.done();
1321 
1322                         // Exit the loop if we don't need to align the file size
1323                         if (!alignFileSize || alignment < 2) {
1324                             break;
1325                         }
1326 
1327                         // Calculate the file size
1328                         eocd = modifiedEocd;
1329                         int fileSize = 0;
1330                         for (ByteBuffer buf : outputChunks) {
1331                             fileSize += buf.remaining();
1332                         }
1333                         // Exit the loop because the file size is aligned.
1334                         if (fileSize % alignment == 0) {
1335                             break;
1336                         }
1337                         // Pad EOCD comment to align the file size.
1338                         int commentLen = alignment - fileSize % alignment;
1339                         modifiedEocd = ByteBuffer.allocate(eocd.remaining() + commentLen);
1340                         modifiedEocd.put(eocd);
1341                         modifiedEocd.rewind();
1342                         modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
1343                         ApkUtils.updateZipEocdCommentLen(modifiedEocd);
1344                         // Since V2 signing block should cover modified EOCD,
1345                         // re-iterate the loop with modified EOCD.
1346                         eocd = modifiedEocd;
1347                     }
1348 
1349                     // This assumes outputChunks are array-backed. To avoid this assumption, the
1350                     // code could be rewritten to use FileChannel.
1351                     for (ByteBuffer outputChunk : outputChunks) {
1352                         outputFile.write(
1353                                 outputChunk.array(),
1354                                 outputChunk.arrayOffset() + outputChunk.position(),
1355                                 outputChunk.remaining());
1356                         outputChunk.position(outputChunk.limit());
1357                     }
1358 
1359                     outputFile.close();
1360                     outputFile = null;
1361                     apkSigner.outputDone();
1362 
1363                     if (signUsingApkSignatureSchemeV4) {
1364                         final DataSource outputApkIn = DataSources.asDataSource(
1365                                 new RandomAccessFile(new File(outputFilename), "r"));
1366                         final File outputV4File =  new File(outputV4Filename);
1367                         apkSigner.signV4(outputApkIn, outputV4File, false /* ignore failures */);
1368                     }
1369                 }
1370 
1371                 return;
1372             }
1373         } catch (Exception e) {
1374             e.printStackTrace();
1375             System.exit(1);
1376         } finally {
1377             try {
1378                 if (inputJar != null) inputJar.close();
1379                 if (outputFile != null) outputFile.close();
1380             } catch (IOException e) {
1381                 e.printStackTrace();
1382                 System.exit(1);
1383             }
1384         }
1385     }
1386 }
1387