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