SignApk.java revision 29706d155a7516e36b63c5201d3e294de6589814
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
17package com.android.signapk;
18
19import org.bouncycastle.asn1.ASN1InputStream;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.DEROutputStream;
22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
23import org.bouncycastle.cert.jcajce.JcaCertStore;
24import org.bouncycastle.cms.CMSException;
25import org.bouncycastle.cms.CMSProcessableByteArray;
26import org.bouncycastle.cms.CMSSignedData;
27import org.bouncycastle.cms.CMSSignedDataGenerator;
28import org.bouncycastle.cms.CMSTypedData;
29import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
30import org.bouncycastle.jce.provider.BouncyCastleProvider;
31import org.bouncycastle.operator.ContentSigner;
32import org.bouncycastle.operator.OperatorCreationException;
33import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
34import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
35import org.bouncycastle.util.encoders.Base64;
36
37import java.io.BufferedReader;
38import java.io.BufferedOutputStream;
39import java.io.ByteArrayOutputStream;
40import java.io.DataInputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileOutputStream;
44import java.io.FilterOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.io.InputStreamReader;
48import java.io.OutputStream;
49import java.io.PrintStream;
50import java.security.DigestOutputStream;
51import java.security.GeneralSecurityException;
52import java.security.Key;
53import java.security.KeyFactory;
54import java.security.MessageDigest;
55import java.security.PrivateKey;
56import java.security.Provider;
57import java.security.Security;
58import java.security.cert.CertificateEncodingException;
59import java.security.cert.CertificateFactory;
60import java.security.cert.X509Certificate;
61import java.security.spec.InvalidKeySpecException;
62import java.security.spec.KeySpec;
63import java.security.spec.PKCS8EncodedKeySpec;
64import java.util.ArrayList;
65import java.util.Collections;
66import java.util.Enumeration;
67import java.util.Map;
68import java.util.TreeMap;
69import java.util.jar.Attributes;
70import java.util.jar.JarEntry;
71import java.util.jar.JarFile;
72import java.util.jar.JarOutputStream;
73import java.util.jar.Manifest;
74import java.util.regex.Pattern;
75import javax.crypto.Cipher;
76import javax.crypto.EncryptedPrivateKeyInfo;
77import javax.crypto.SecretKeyFactory;
78import javax.crypto.spec.PBEKeySpec;
79
80/**
81 * Command line tool to sign JAR files (including APKs and OTA updates) in
82 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
83 */
84class SignApk {
85    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
86    private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
87    private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
88    private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA";
89
90    private static final String OTACERT_NAME = "META-INF/com/android/otacert";
91
92    private static Provider sBouncyCastleProvider;
93
94    // Files matching this pattern are not copied to the output.
95    private static Pattern stripPattern =
96        Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
97                        Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
98
99    private static X509Certificate readPublicKey(File file)
100        throws IOException, GeneralSecurityException {
101        FileInputStream input = new FileInputStream(file);
102        try {
103            CertificateFactory cf = CertificateFactory.getInstance("X.509");
104            return (X509Certificate) cf.generateCertificate(input);
105        } finally {
106            input.close();
107        }
108    }
109
110    /**
111     * Reads the password from stdin and returns it as a string.
112     *
113     * @param keyFile The file containing the private key.  Used to prompt the user.
114     */
115    private static String readPassword(File keyFile) {
116        // TODO: use Console.readPassword() when it's available.
117        System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
118        System.out.flush();
119        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
120        try {
121            return stdin.readLine();
122        } catch (IOException ex) {
123            return null;
124        }
125    }
126
127    /**
128     * Decrypt an encrypted PKCS 8 format private key.
129     *
130     * Based on ghstark's post on Aug 6, 2006 at
131     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
132     *
133     * @param encryptedPrivateKey The raw data of the private key
134     * @param keyFile The file containing the private key
135     */
136    private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
137        throws GeneralSecurityException {
138        EncryptedPrivateKeyInfo epkInfo;
139        try {
140            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
141        } catch (IOException ex) {
142            // Probably not an encrypted key.
143            return null;
144        }
145
146        char[] password = readPassword(keyFile).toCharArray();
147
148        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
149        Key key = skFactory.generateSecret(new PBEKeySpec(password));
150
151        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
152        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
153
154        try {
155            return epkInfo.getKeySpec(cipher);
156        } catch (InvalidKeySpecException ex) {
157            System.err.println("signapk: Password for " + keyFile + " may be bad.");
158            throw ex;
159        }
160    }
161
162    /** Read a PKCS 8 format private key. */
163    private static PrivateKey readPrivateKey(File file)
164        throws IOException, GeneralSecurityException {
165        DataInputStream input = new DataInputStream(new FileInputStream(file));
166        try {
167            byte[] bytes = new byte[(int) file.length()];
168            input.read(bytes);
169
170            KeySpec spec = decryptPrivateKey(bytes, file);
171            if (spec == null) {
172                spec = new PKCS8EncodedKeySpec(bytes);
173            }
174
175            try {
176                return KeyFactory.getInstance("RSA").generatePrivate(spec);
177            } catch (InvalidKeySpecException ex) {
178                return KeyFactory.getInstance("DSA").generatePrivate(spec);
179            }
180        } finally {
181            input.close();
182        }
183    }
184
185    /** Add the SHA1 of every file to the manifest, creating it if necessary. */
186    private static Manifest addDigestsToManifest(JarFile jar)
187        throws IOException, GeneralSecurityException {
188        Manifest input = jar.getManifest();
189        Manifest output = new Manifest();
190        Attributes main = output.getMainAttributes();
191        if (input != null) {
192            main.putAll(input.getMainAttributes());
193        } else {
194            main.putValue("Manifest-Version", "1.0");
195            main.putValue("Created-By", "1.0 (Android SignApk)");
196        }
197
198        MessageDigest md = MessageDigest.getInstance("SHA1");
199        byte[] buffer = new byte[4096];
200        int num;
201
202        // We sort the input entries by name, and add them to the
203        // output manifest in sorted order.  We expect that the output
204        // map will be deterministic.
205
206        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
207
208        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
209            JarEntry entry = e.nextElement();
210            byName.put(entry.getName(), entry);
211        }
212
213        for (JarEntry entry: byName.values()) {
214            String name = entry.getName();
215            if (!entry.isDirectory() &&
216                (stripPattern == null || !stripPattern.matcher(name).matches())) {
217                InputStream data = jar.getInputStream(entry);
218                while ((num = data.read(buffer)) > 0) {
219                    md.update(buffer, 0, num);
220                }
221
222                Attributes attr = null;
223                if (input != null) attr = input.getAttributes(name);
224                attr = attr != null ? new Attributes(attr) : new Attributes();
225                attr.putValue("SHA1-Digest",
226                              new String(Base64.encode(md.digest()), "ASCII"));
227                output.getEntries().put(name, attr);
228            }
229        }
230
231        return output;
232    }
233
234    /**
235     * Add a copy of the public key to the archive; this should
236     * exactly match one of the files in
237     * /system/etc/security/otacerts.zip on the device.  (The same
238     * cert can be extracted from the CERT.RSA file but this is much
239     * easier to get at.)
240     */
241    private static void addOtacert(JarOutputStream outputJar,
242                                   File publicKeyFile,
243                                   long timestamp,
244                                   Manifest manifest)
245        throws IOException, GeneralSecurityException {
246        MessageDigest md = MessageDigest.getInstance("SHA1");
247
248        JarEntry je = new JarEntry(OTACERT_NAME);
249        je.setTime(timestamp);
250        outputJar.putNextEntry(je);
251        FileInputStream input = new FileInputStream(publicKeyFile);
252        byte[] b = new byte[4096];
253        int read;
254        while ((read = input.read(b)) != -1) {
255            outputJar.write(b, 0, read);
256            md.update(b, 0, read);
257        }
258        input.close();
259
260        Attributes attr = new Attributes();
261        attr.putValue("SHA1-Digest",
262                      new String(Base64.encode(md.digest()), "ASCII"));
263        manifest.getEntries().put(OTACERT_NAME, attr);
264    }
265
266
267    /** Write to another stream and track how many bytes have been
268     *  written.
269     */
270    private static class CountOutputStream extends FilterOutputStream {
271        private int mCount;
272
273        public CountOutputStream(OutputStream out) {
274            super(out);
275            mCount = 0;
276        }
277
278        @Override
279        public void write(int b) throws IOException {
280            super.write(b);
281            mCount++;
282        }
283
284        @Override
285        public void write(byte[] b, int off, int len) throws IOException {
286            super.write(b, off, len);
287            mCount += len;
288        }
289
290        public int size() {
291            return mCount;
292        }
293    }
294
295    /** Write a .SF file with a digest of the specified manifest. */
296    private static void writeSignatureFile(Manifest manifest, OutputStream out)
297        throws IOException, GeneralSecurityException {
298        Manifest sf = new Manifest();
299        Attributes main = sf.getMainAttributes();
300        main.putValue("Signature-Version", "1.0");
301        main.putValue("Created-By", "1.0 (Android SignApk)");
302
303        MessageDigest md = MessageDigest.getInstance("SHA1");
304        PrintStream print = new PrintStream(
305            new DigestOutputStream(new ByteArrayOutputStream(), md),
306            true, "UTF-8");
307
308        // Digest of the entire manifest
309        manifest.write(print);
310        print.flush();
311        main.putValue("SHA1-Digest-Manifest",
312                      new String(Base64.encode(md.digest()), "ASCII"));
313
314        Map<String, Attributes> entries = manifest.getEntries();
315        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
316            // Digest of the manifest stanza for this entry.
317            print.print("Name: " + entry.getKey() + "\r\n");
318            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
319                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
320            }
321            print.print("\r\n");
322            print.flush();
323
324            Attributes sfAttr = new Attributes();
325            sfAttr.putValue("SHA1-Digest",
326                            new String(Base64.encode(md.digest()), "ASCII"));
327            sf.getEntries().put(entry.getKey(), sfAttr);
328        }
329
330        CountOutputStream cout = new CountOutputStream(out);
331        sf.write(cout);
332
333        // A bug in the java.util.jar implementation of Android platforms
334        // up to version 1.6 will cause a spurious IOException to be thrown
335        // if the length of the signature file is a multiple of 1024 bytes.
336        // As a workaround, add an extra CRLF in this case.
337        if ((cout.size() % 1024) == 0) {
338            cout.write('\r');
339            cout.write('\n');
340        }
341    }
342
343    /** Sign data and write the digital signature to 'out'. */
344    private static void writeSignatureBlock(
345        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
346        OutputStream out)
347        throws IOException,
348               CertificateEncodingException,
349               OperatorCreationException,
350               CMSException {
351        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
352        certList.add(publicKey);
353        JcaCertStore certs = new JcaCertStore(certList);
354
355        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
356        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
357            .setProvider(sBouncyCastleProvider)
358            .build(privateKey);
359        gen.addSignerInfoGenerator(
360            new JcaSignerInfoGeneratorBuilder(
361                new JcaDigestCalculatorProviderBuilder()
362                .setProvider(sBouncyCastleProvider)
363                .build())
364            .setDirectSignature(true)
365            .build(sha1Signer, publicKey));
366        gen.addCertificates(certs);
367        CMSSignedData sigData = gen.generate(data, false);
368
369        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
370        DEROutputStream dos = new DEROutputStream(out);
371        dos.writeObject(asn1.readObject());
372    }
373
374    /**
375     * Copy all the files in a manifest from input to output.  We set
376     * the modification times in the output to a fixed time, so as to
377     * reduce variation in the output file and make incremental OTAs
378     * more efficient.
379     */
380    private static void copyFiles(Manifest manifest,
381                                  JarFile in, JarOutputStream out, long timestamp) throws IOException {
382        byte[] buffer = new byte[4096];
383        int num;
384
385        Map<String, Attributes> entries = manifest.getEntries();
386        ArrayList<String> names = new ArrayList<String>(entries.keySet());
387        Collections.sort(names);
388        for (String name : names) {
389            JarEntry inEntry = in.getJarEntry(name);
390            JarEntry outEntry = null;
391            if (inEntry.getMethod() == JarEntry.STORED) {
392                // Preserve the STORED method of the input entry.
393                outEntry = new JarEntry(inEntry);
394            } else {
395                // Create a new entry so that the compressed len is recomputed.
396                outEntry = new JarEntry(name);
397            }
398            outEntry.setTime(timestamp);
399            out.putNextEntry(outEntry);
400
401            InputStream data = in.getInputStream(inEntry);
402            while ((num = data.read(buffer)) > 0) {
403                out.write(buffer, 0, num);
404            }
405            out.flush();
406        }
407    }
408
409    private static class WholeFileSignerOutputStream extends FilterOutputStream {
410        private boolean closing = false;
411        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
412        private OutputStream tee;
413
414        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
415            super(out);
416            this.tee = tee;
417        }
418
419        public void notifyClosing() {
420            closing = true;
421        }
422
423        public void finish() throws IOException {
424            closing = false;
425
426            byte[] data = footer.toByteArray();
427            if (data.length < 2)
428                throw new IOException("Less than two bytes written to footer");
429            write(data, 0, data.length - 2);
430        }
431
432        public byte[] getTail() {
433            return footer.toByteArray();
434        }
435
436        @Override
437        public void write(byte[] b) throws IOException {
438            write(b, 0, b.length);
439        }
440
441        @Override
442        public void write(byte[] b, int off, int len) throws IOException {
443            if (closing) {
444                // if the jar is about to close, save the footer that will be written
445                footer.write(b, off, len);
446            }
447            else {
448                // write to both output streams. out is the CMSTypedData signer and tee is the file.
449                out.write(b, off, len);
450                tee.write(b, off, len);
451            }
452        }
453
454        @Override
455        public void write(int b) throws IOException {
456            if (closing) {
457                // if the jar is about to close, save the footer that will be written
458                footer.write(b);
459            }
460            else {
461                // write to both output streams. out is the CMSTypedData signer and tee is the file.
462                out.write(b);
463                tee.write(b);
464            }
465        }
466    }
467
468    private static class CMSSigner implements CMSTypedData {
469        private JarFile inputJar;
470        private File publicKeyFile;
471        private X509Certificate publicKey;
472        private PrivateKey privateKey;
473        private String outputFile;
474        private OutputStream outputStream;
475        private final ASN1ObjectIdentifier type;
476        private WholeFileSignerOutputStream signer;
477
478        public CMSSigner(JarFile inputJar, File publicKeyFile,
479                         X509Certificate publicKey, PrivateKey privateKey,
480                         OutputStream outputStream) {
481            this.inputJar = inputJar;
482            this.publicKeyFile = publicKeyFile;
483            this.publicKey = publicKey;
484            this.privateKey = privateKey;
485            this.outputStream = outputStream;
486            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
487        }
488
489        public Object getContent() {
490            throw new UnsupportedOperationException();
491        }
492
493        public ASN1ObjectIdentifier getContentType() {
494            return type;
495        }
496
497        public void write(OutputStream out) throws IOException {
498            try {
499                signer = new WholeFileSignerOutputStream(out, outputStream);
500                JarOutputStream outputJar = new JarOutputStream(signer);
501
502                Manifest manifest = addDigestsToManifest(inputJar);
503                signFile(manifest, inputJar,
504                         new X509Certificate[]{ publicKey },
505                         new PrivateKey[]{ privateKey },
506                         outputJar);
507                // Assume the certificate is valid for at least an hour.
508                long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
509                addOtacert(outputJar, publicKeyFile, timestamp, manifest);
510
511                signer.notifyClosing();
512                outputJar.close();
513                signer.finish();
514            }
515            catch (Exception e) {
516                throw new IOException(e);
517            }
518        }
519
520        public void writeSignatureBlock(ByteArrayOutputStream temp)
521            throws IOException,
522                   CertificateEncodingException,
523                   OperatorCreationException,
524                   CMSException {
525            SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
526        }
527
528        public WholeFileSignerOutputStream getSigner() {
529            return signer;
530        }
531    }
532
533    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
534                                      X509Certificate publicKey, PrivateKey privateKey,
535                                      OutputStream outputStream) throws Exception {
536        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
537                                         publicKey, privateKey, outputStream);
538
539        ByteArrayOutputStream temp = new ByteArrayOutputStream();
540
541        // put a readable message and a null char at the start of the
542        // archive comment, so that tools that display the comment
543        // (hopefully) show something sensible.
544        // TODO: anything more useful we can put in this message?
545        byte[] message = "signed by SignApk".getBytes("UTF-8");
546        temp.write(message);
547        temp.write(0);
548
549        cmsOut.writeSignatureBlock(temp);
550
551        byte[] zipData = cmsOut.getSigner().getTail();
552
553        // For a zip with no archive comment, the
554        // end-of-central-directory record will be 22 bytes long, so
555        // we expect to find the EOCD marker 22 bytes from the end.
556        if (zipData[zipData.length-22] != 0x50 ||
557            zipData[zipData.length-21] != 0x4b ||
558            zipData[zipData.length-20] != 0x05 ||
559            zipData[zipData.length-19] != 0x06) {
560            throw new IllegalArgumentException("zip data already has an archive comment");
561        }
562
563        int total_size = temp.size() + 6;
564        if (total_size > 0xffff) {
565            throw new IllegalArgumentException("signature is too big for ZIP file comment");
566        }
567        // signature starts this many bytes from the end of the file
568        int signature_start = total_size - message.length - 1;
569        temp.write(signature_start & 0xff);
570        temp.write((signature_start >> 8) & 0xff);
571        // Why the 0xff bytes?  In a zip file with no archive comment,
572        // bytes [-6:-2] of the file are the little-endian offset from
573        // the start of the file to the central directory.  So for the
574        // two high bytes to be 0xff 0xff, the archive would have to
575        // be nearly 4GB in size.  So it's unlikely that a real
576        // commentless archive would have 0xffs here, and lets us tell
577        // an old signed archive from a new one.
578        temp.write(0xff);
579        temp.write(0xff);
580        temp.write(total_size & 0xff);
581        temp.write((total_size >> 8) & 0xff);
582        temp.flush();
583
584        // Signature verification checks that the EOCD header is the
585        // last such sequence in the file (to avoid minzip finding a
586        // fake EOCD appended after the signature in its scan).  The
587        // odds of producing this sequence by chance are very low, but
588        // let's catch it here if it does.
589        byte[] b = temp.toByteArray();
590        for (int i = 0; i < b.length-3; ++i) {
591            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
592                throw new IllegalArgumentException("found spurious EOCD header at " + i);
593            }
594        }
595
596        outputStream.write(total_size & 0xff);
597        outputStream.write((total_size >> 8) & 0xff);
598        temp.writeTo(outputStream);
599    }
600
601    private static void signFile(Manifest manifest, JarFile inputJar,
602                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
603                                 JarOutputStream outputJar)
604        throws Exception {
605        // Assume the certificate is valid for at least an hour.
606        long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
607
608        JarEntry je;
609
610        // Everything else
611        copyFiles(manifest, inputJar, outputJar, timestamp);
612
613        // MANIFEST.MF
614        je = new JarEntry(JarFile.MANIFEST_NAME);
615        je.setTime(timestamp);
616        outputJar.putNextEntry(je);
617        manifest.write(outputJar);
618
619        int numKeys = publicKey.length;
620        for (int k = 0; k < numKeys; ++k) {
621            // CERT.SF / CERT#.SF
622            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
623                              (String.format(CERT_SF_MULTI_NAME, k)));
624            je.setTime(timestamp);
625            outputJar.putNextEntry(je);
626            ByteArrayOutputStream baos = new ByteArrayOutputStream();
627            writeSignatureFile(manifest, baos);
628            byte[] signedData = baos.toByteArray();
629            outputJar.write(signedData);
630
631            // CERT.RSA / CERT#.RSA
632            je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
633                              (String.format(CERT_RSA_MULTI_NAME, k)));
634            je.setTime(timestamp);
635            outputJar.putNextEntry(je);
636            writeSignatureBlock(new CMSProcessableByteArray(signedData),
637                                publicKey[k], privateKey[k], outputJar);
638        }
639    }
640
641    private static void usage() {
642        System.err.println("Usage: signapk [-w] " +
643                           "publickey.x509[.pem] privatekey.pk8 " +
644                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
645                           "input.jar output.jar");
646        System.exit(2);
647    }
648
649    public static void main(String[] args) {
650        if (args.length < 4) usage();
651
652        sBouncyCastleProvider = new BouncyCastleProvider();
653        Security.addProvider(sBouncyCastleProvider);
654
655        boolean signWholeFile = false;
656        int argstart = 0;
657        if (args[0].equals("-w")) {
658            signWholeFile = true;
659            argstart = 1;
660        }
661
662        if ((args.length - argstart) % 2 == 1) usage();
663        int numKeys = ((args.length - argstart) / 2) - 1;
664        if (signWholeFile && numKeys > 1) {
665            System.err.println("Only one key may be used with -w.");
666            System.exit(2);
667        }
668
669        String inputFilename = args[args.length-2];
670        String outputFilename = args[args.length-1];
671
672        JarFile inputJar = null;
673        FileOutputStream outputFile = null;
674
675        try {
676            File firstPublicKeyFile = new File(args[argstart+0]);
677
678            X509Certificate[] publicKey = new X509Certificate[numKeys];
679            for (int i = 0; i < numKeys; ++i) {
680                int argNum = argstart + i*2;
681                publicKey[i] = readPublicKey(new File(args[argNum]));
682            }
683
684            // Set the ZIP file timestamp to the starting valid time
685            // of the 0th certificate plus one hour (to match what
686            // we've historically done).
687            long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
688
689            PrivateKey[] privateKey = new PrivateKey[numKeys];
690            for (int i = 0; i < numKeys; ++i) {
691                int argNum = argstart + i*2 + 1;
692                privateKey[i] = readPrivateKey(new File(args[argNum]));
693            }
694            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
695
696            outputFile = new FileOutputStream(outputFilename);
697
698
699            if (signWholeFile) {
700                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
701                                      publicKey[0], privateKey[0], outputFile);
702            } else {
703                JarOutputStream outputJar = new JarOutputStream(outputFile);
704
705                // For signing .apks, use the maximum compression to make
706                // them as small as possible (since they live forever on
707                // the system partition).  For OTA packages, use the
708                // default compression level, which is much much faster
709                // and produces output that is only a tiny bit larger
710                // (~0.1% on full OTA packages I tested).
711                outputJar.setLevel(9);
712
713                signFile(addDigestsToManifest(inputJar), inputJar,
714                         publicKey, privateKey, outputJar);
715                outputJar.close();
716            }
717        } catch (Exception e) {
718            e.printStackTrace();
719            System.exit(1);
720        } finally {
721            try {
722                if (inputJar != null) inputJar.close();
723                if (outputFile != null) outputFile.close();
724            } catch (IOException e) {
725                e.printStackTrace();
726                System.exit(1);
727            }
728        }
729    }
730}
731