2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.sdklib.internal.build;
19 import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
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;
28 import java.io.ByteArrayOutputStream;
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;
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;
54 * A Jar file builder with signature support.
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";
61 /** Write to another stream and also feed it to the Signature object. */
62 private static class SignatureOutputStream extends FilterOutputStream {
63 private Signature mSignature;
65 public SignatureOutputStream(OutputStream out, Signature sig) {
71 public void write(int b) throws IOException {
73 mSignature.update((byte) b);
74 } catch (SignatureException e) {
75 throw new IOException("SignatureException: " + e);
81 public void write(byte[] b, int off, int len) throws IOException {
83 mSignature.update(b, off, len);
84 } catch (SignatureException e) {
85 throw new IOException("SignatureException: " + e);
87 super.write(b, off, len);
91 private JarOutputStream mOutputJar;
92 private PrivateKey mKey;
93 private X509Certificate mCertificate;
94 private Manifest mManifest;
95 private BASE64Encoder mBase64Encoder;
96 private MessageDigest mMessageDigest;
98 private byte[] mBuffer = new byte[4096];
101 * Classes which implement this interface provides a method to check whether a file should
102 * be added to a Jar file.
104 public interface IZipEntryFilter {
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)}.
111 public static class ZipAbortException extends Exception {
112 private static final long serialVersionUID = 1L;
114 public ZipAbortException() {
118 public ZipAbortException(String format, Object... args) {
119 super(String.format(format, args));
122 public ZipAbortException(Throwable cause, String format, Object... args) {
123 super(String.format(format, args), cause);
126 public ZipAbortException(Throwable cause) {
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.
138 public boolean checkEntry(String archivePath) throws ZipAbortException;
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
149 * @throws IOException
150 * @throws NoSuchAlgorithmException
152 public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
153 throws IOException, NoSuchAlgorithmException {
154 mOutputJar = new JarOutputStream(out);
155 mOutputJar.setLevel(9);
157 mCertificate = certificate;
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)");
165 mBase64Encoder = new BASE64Encoder();
166 mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
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
176 public void writeFile(File inputFile, String jarPath) throws IOException {
177 // Get an input stream on the file.
178 FileInputStream fis = new FileInputStream(inputFile);
181 // create the zip entry
182 JarEntry entry = new JarEntry(jarPath);
183 entry.setTime(inputFile.lastModified());
185 writeEntry(fis, entry);
187 // close the file stream used to read the file
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
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
202 public void writeZip(InputStream input, IZipEntryFilter filter)
203 throws IOException, ZipAbortException {
204 ZipInputStream zis = new ZipInputStream(input);
207 // loop on the entries of the intermediary package and put them in the final package.
209 while ((entry = zis.getNextEntry()) != null) {
210 String name = entry.getName();
212 // do not take directories or anything inside a potential META-INF folder.
213 if (entry.isDirectory() || name.startsWith("META-INF/")) {
217 // if we have a filter, we check the entry against it
218 if (filter != null && filter.checkEntry(name) == false) {
224 // Preserve the STORED method of the input entry.
225 if (entry.getMethod() == JarEntry.STORED) {
226 newEntry = new JarEntry(entry);
228 // Create a new entry so that the compressed len is recomputed.
229 newEntry = new JarEntry(name);
232 writeEntry(zis, newEntry);
242 * Closes the Jar archive by creating the manifest, and signing the archive.
243 * @throws IOException
244 * @throws GeneralSecurityException
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);
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));
259 mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
260 writeSignatureBlock(signature, mCertificate, mKey);
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
272 private void writeEntry(InputStream input, JarEntry entry) throws IOException {
273 // add the entry to the jar archive
274 mOutputJar.putNextEntry(entry);
276 // read the content of the entry from the input stream, and write it into the archive.
278 while ((count = input.read(mBuffer)) != -1) {
279 mOutputJar.write(mBuffer, 0, count);
282 if (mMessageDigest != null) {
283 mMessageDigest.update(mBuffer, 0, count);
287 // close the entry for this file
288 mOutputJar.closeEntry();
290 if (mManifest != null) {
291 // update the manifest for this entry.
292 Attributes attr = mManifest.getAttributes(entry.getName());
294 attr = new Attributes();
295 mManifest.getEntries().put(entry.getName(), attr);
297 attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
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)");
309 BASE64Encoder base64 = new BASE64Encoder();
310 MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
311 PrintStream print = new PrintStream(
312 new DigestOutputStream(new ByteArrayOutputStream(), md),
315 // Digest of the entire manifest
316 mManifest.write(print);
318 main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
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");
330 Attributes sfAttr = new Attributes();
331 sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
332 sf.getEntries().put(entry.getKey(), sfAttr);
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()),
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 });
355 pkcs7.encodeSignedData(mOutputJar);