Home | History | Annotate | Download | only in build
      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.sdklib.internal.build;
     18 
     19 import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
     20 
     21 import sun.misc.BASE64Encoder;
     22 import sun.security.pkcs.ContentInfo;
     23 import sun.security.pkcs.PKCS7;
     24 import sun.security.pkcs.SignerInfo;
     25 import sun.security.x509.AlgorithmId;
     26 import sun.security.x509.X500Name;
     27 
     28 import java.io.ByteArrayOutputStream;
     29 import java.io.File;
     30 import java.io.FileInputStream;
     31 import java.io.FilterOutputStream;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.io.OutputStream;
     35 import java.io.PrintStream;
     36 import java.security.DigestOutputStream;
     37 import java.security.GeneralSecurityException;
     38 import java.security.MessageDigest;
     39 import java.security.NoSuchAlgorithmException;
     40 import java.security.PrivateKey;
     41 import java.security.Signature;
     42 import java.security.SignatureException;
     43 import java.security.cert.X509Certificate;
     44 import java.util.Map;
     45 import java.util.jar.Attributes;
     46 import java.util.jar.JarEntry;
     47 import java.util.jar.JarFile;
     48 import java.util.jar.JarOutputStream;
     49 import java.util.jar.Manifest;
     50 import java.util.zip.ZipEntry;
     51 import java.util.zip.ZipInputStream;
     52 
     53 /**
     54  * A Jar file builder with signature support.
     55  */
     56 public class SignedJarBuilder {
     57     private static final String DIGEST_ALGORITHM = "SHA1";
     58     private static final String DIGEST_ATTR = "SHA1-Digest";
     59     private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest";
     60 
     61     /** Write to another stream and also feed it to the Signature object. */
     62     private static class SignatureOutputStream extends FilterOutputStream {
     63         private Signature mSignature;
     64 
     65         public SignatureOutputStream(OutputStream out, Signature sig) {
     66             super(out);
     67             mSignature = sig;
     68         }
     69 
     70         @Override
     71         public void write(int b) throws IOException {
     72             try {
     73                 mSignature.update((byte) b);
     74             } catch (SignatureException e) {
     75                 throw new IOException("SignatureException: " + e);
     76             }
     77             super.write(b);
     78         }
     79 
     80         @Override
     81         public void write(byte[] b, int off, int len) throws IOException {
     82             try {
     83                 mSignature.update(b, off, len);
     84             } catch (SignatureException e) {
     85                 throw new IOException("SignatureException: " + e);
     86             }
     87             super.write(b, off, len);
     88         }
     89     }
     90 
     91     private JarOutputStream mOutputJar;
     92     private PrivateKey mKey;
     93     private X509Certificate mCertificate;
     94     private Manifest mManifest;
     95     private BASE64Encoder mBase64Encoder;
     96     private MessageDigest mMessageDigest;
     97 
     98     private byte[] mBuffer = new byte[4096];
     99 
    100     /**
    101      * Classes which implement this interface provides a method to check whether a file should
    102      * be added to a Jar file.
    103      */
    104     public interface IZipEntryFilter {
    105 
    106         /**
    107          * An exception thrown during packaging of a zip file into APK file.
    108          * This is typically thrown by implementations of
    109          * {@link IZipEntryFilter#checkEntry(String)}.
    110          */
    111         public static class ZipAbortException extends Exception {
    112             private static final long serialVersionUID = 1L;
    113 
    114             public ZipAbortException() {
    115                 super();
    116             }
    117 
    118             public ZipAbortException(String format, Object... args) {
    119                 super(String.format(format, args));
    120             }
    121 
    122             public ZipAbortException(Throwable cause, String format, Object... args) {
    123                 super(String.format(format, args), cause);
    124             }
    125 
    126             public ZipAbortException(Throwable cause) {
    127                 super(cause);
    128             }
    129         }
    130 
    131 
    132         /**
    133          * Checks a file for inclusion in a Jar archive.
    134          * @param archivePath the archive file path of the entry
    135          * @return <code>true</code> if the file should be included.
    136          * @throws ZipAbortException if writing the file should be aborted.
    137          */
    138         public boolean checkEntry(String archivePath) throws ZipAbortException;
    139     }
    140 
    141     /**
    142      * Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
    143      * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then
    144      * the archive will not be signed.
    145      * @param out the {@link OutputStream} where to write the Jar archive.
    146      * @param key the {@link PrivateKey} used to sign the archive, or <code>null</code>.
    147      * @param certificate the {@link X509Certificate} used to sign the archive, or
    148      * <code>null</code>.
    149      * @throws IOException
    150      * @throws NoSuchAlgorithmException
    151      */
    152     public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
    153             throws IOException, NoSuchAlgorithmException {
    154         mOutputJar = new JarOutputStream(out);
    155         mOutputJar.setLevel(9);
    156         mKey = key;
    157         mCertificate = certificate;
    158 
    159         if (mKey != null && mCertificate != null) {
    160             mManifest = new Manifest();
    161             Attributes main = mManifest.getMainAttributes();
    162             main.putValue("Manifest-Version", "1.0");
    163             main.putValue("Created-By", "1.0 (Android)");
    164 
    165             mBase64Encoder = new BASE64Encoder();
    166             mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
    167         }
    168     }
    169 
    170     /**
    171      * Writes a new {@link File} into the archive.
    172      * @param inputFile the {@link File} to write.
    173      * @param jarPath the filepath inside the archive.
    174      * @throws IOException
    175      */
    176     public void writeFile(File inputFile, String jarPath) throws IOException {
    177         // Get an input stream on the file.
    178         FileInputStream fis = new FileInputStream(inputFile);
    179         try {
    180 
    181             // create the zip entry
    182             JarEntry entry = new JarEntry(jarPath);
    183             entry.setTime(inputFile.lastModified());
    184 
    185             writeEntry(fis, entry);
    186         } finally {
    187             // close the file stream used to read the file
    188             fis.close();
    189         }
    190     }
    191 
    192     /**
    193      * Copies the content of a Jar/Zip archive into the receiver archive.
    194      * <p/>An optional {@link IZipEntryFilter} allows to selectively choose which files
    195      * to copy over.
    196      * @param input the {@link InputStream} for the Jar/Zip to copy.
    197      * @param filter the filter or <code>null</code>
    198      * @throws IOException
    199      * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write
    200      *                           must be aborted.
    201      */
    202     public void writeZip(InputStream input, IZipEntryFilter filter)
    203             throws IOException, ZipAbortException {
    204         ZipInputStream zis = new ZipInputStream(input);
    205 
    206         try {
    207             // loop on the entries of the intermediary package and put them in the final package.
    208             ZipEntry entry;
    209             while ((entry = zis.getNextEntry()) != null) {
    210                 String name = entry.getName();
    211 
    212                 // do not take directories or anything inside a potential META-INF folder.
    213                 if (entry.isDirectory() || name.startsWith("META-INF/")) {
    214                     continue;
    215                 }
    216 
    217                 // if we have a filter, we check the entry against it
    218                 if (filter != null && filter.checkEntry(name) == false) {
    219                     continue;
    220                 }
    221 
    222                 JarEntry newEntry;
    223 
    224                 // Preserve the STORED method of the input entry.
    225                 if (entry.getMethod() == JarEntry.STORED) {
    226                     newEntry = new JarEntry(entry);
    227                 } else {
    228                     // Create a new entry so that the compressed len is recomputed.
    229                     newEntry = new JarEntry(name);
    230                 }
    231 
    232                 writeEntry(zis, newEntry);
    233 
    234                 zis.closeEntry();
    235             }
    236         } finally {
    237             zis.close();
    238         }
    239     }
    240 
    241     /**
    242      * Closes the Jar archive by creating the manifest, and signing the archive.
    243      * @throws IOException
    244      * @throws GeneralSecurityException
    245      */
    246     public void close() throws IOException, GeneralSecurityException {
    247         if (mManifest != null) {
    248             // write the manifest to the jar file
    249             mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
    250             mManifest.write(mOutputJar);
    251 
    252             // CERT.SF
    253             Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm());
    254             signature.initSign(mKey);
    255             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
    256             writeSignatureFile(new SignatureOutputStream(mOutputJar, signature));
    257 
    258             // CERT.*
    259             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
    260             writeSignatureBlock(signature, mCertificate, mKey);
    261         }
    262 
    263         mOutputJar.close();
    264     }
    265 
    266     /**
    267      * Adds an entry to the output jar, and write its content from the {@link InputStream}
    268      * @param input The input stream from where to write the entry content.
    269      * @param entry the entry to write in the jar.
    270      * @throws IOException
    271      */
    272     private void writeEntry(InputStream input, JarEntry entry) throws IOException {
    273         // add the entry to the jar archive
    274         mOutputJar.putNextEntry(entry);
    275 
    276         // read the content of the entry from the input stream, and write it into the archive.
    277         int count;
    278         while ((count = input.read(mBuffer)) != -1) {
    279             mOutputJar.write(mBuffer, 0, count);
    280 
    281             // update the digest
    282             if (mMessageDigest != null) {
    283                 mMessageDigest.update(mBuffer, 0, count);
    284             }
    285         }
    286 
    287         // close the entry for this file
    288         mOutputJar.closeEntry();
    289 
    290         if (mManifest != null) {
    291             // update the manifest for this entry.
    292             Attributes attr = mManifest.getAttributes(entry.getName());
    293             if (attr == null) {
    294                 attr = new Attributes();
    295                 mManifest.getEntries().put(entry.getName(), attr);
    296             }
    297             attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
    298         }
    299     }
    300 
    301     /** Writes a .SF file with a digest to the manifest. */
    302     private void writeSignatureFile(OutputStream out)
    303             throws IOException, GeneralSecurityException {
    304         Manifest sf = new Manifest();
    305         Attributes main = sf.getMainAttributes();
    306         main.putValue("Signature-Version", "1.0");
    307         main.putValue("Created-By", "1.0 (Android)");
    308 
    309         BASE64Encoder base64 = new BASE64Encoder();
    310         MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
    311         PrintStream print = new PrintStream(
    312                 new DigestOutputStream(new ByteArrayOutputStream(), md),
    313                 true, "UTF-8");
    314 
    315         // Digest of the entire manifest
    316         mManifest.write(print);
    317         print.flush();
    318         main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
    319 
    320         Map<String, Attributes> entries = mManifest.getEntries();
    321         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
    322             // Digest of the manifest stanza for this entry.
    323             print.print("Name: " + entry.getKey() + "\r\n");
    324             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
    325                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
    326             }
    327             print.print("\r\n");
    328             print.flush();
    329 
    330             Attributes sfAttr = new Attributes();
    331             sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
    332             sf.getEntries().put(entry.getKey(), sfAttr);
    333         }
    334 
    335         sf.write(out);
    336     }
    337 
    338     /** Write the certificate file with a digital signature. */
    339     private void writeSignatureBlock(Signature signature, X509Certificate publicKey,
    340             PrivateKey privateKey)
    341             throws IOException, GeneralSecurityException {
    342         SignerInfo signerInfo = new SignerInfo(
    343                 new X500Name(publicKey.getIssuerX500Principal().getName()),
    344                 publicKey.getSerialNumber(),
    345                 AlgorithmId.get(DIGEST_ALGORITHM),
    346                 AlgorithmId.get(privateKey.getAlgorithm()),
    347                 signature.sign());
    348 
    349         PKCS7 pkcs7 = new PKCS7(
    350                 new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) },
    351                 new ContentInfo(ContentInfo.DATA_OID, null),
    352                 new X509Certificate[] { publicKey },
    353                 new SignerInfo[] { signerInfo });
    354 
    355         pkcs7.encodeSignedData(mOutputJar);
    356     }
    357 }
    358