/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sdklib.internal.build;
import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
import sun.misc.BASE64Encoder;
import sun.security.pkcs.ContentInfo;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;
import sun.security.x509.AlgorithmId;
import sun.security.x509.X500Name;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* A Jar file builder with signature support.
*/
public class SignedJarBuilder {
private static final String DIGEST_ALGORITHM = "SHA1";
private static final String DIGEST_ATTR = "SHA1-Digest";
private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest";
/** Write to another stream and also feed it to the Signature object. */
private static class SignatureOutputStream extends FilterOutputStream {
private Signature mSignature;
private int mCount = 0;
public SignatureOutputStream(OutputStream out, Signature sig) {
super(out);
mSignature = sig;
}
@Override
public void write(int b) throws IOException {
try {
mSignature.update((byte) b);
} catch (SignatureException e) {
throw new IOException("SignatureException: " + e);
}
super.write(b);
mCount++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
try {
mSignature.update(b, off, len);
} catch (SignatureException e) {
throw new IOException("SignatureException: " + e);
}
super.write(b, off, len);
mCount += len;
}
public int size() {
return mCount;
}
}
private JarOutputStream mOutputJar;
private PrivateKey mKey;
private X509Certificate mCertificate;
private Manifest mManifest;
private BASE64Encoder mBase64Encoder;
private MessageDigest mMessageDigest;
private byte[] mBuffer = new byte[4096];
/**
* Classes which implement this interface provides a method to check whether a file should
* be added to a Jar file.
*/
public interface IZipEntryFilter {
/**
* An exception thrown during packaging of a zip file into APK file.
* This is typically thrown by implementations of
* {@link IZipEntryFilter#checkEntry(String)}.
*/
public static class ZipAbortException extends Exception {
private static final long serialVersionUID = 1L;
public ZipAbortException() {
super();
}
public ZipAbortException(String format, Object... args) {
super(String.format(format, args));
}
public ZipAbortException(Throwable cause, String format, Object... args) {
super(String.format(format, args), cause);
}
public ZipAbortException(Throwable cause) {
super(cause);
}
}
/**
* Checks a file for inclusion in a Jar archive.
* @param archivePath the archive file path of the entry
* @return true
if the file should be included.
* @throws ZipAbortException if writing the file should be aborted.
*/
public boolean checkEntry(String archivePath) throws ZipAbortException;
}
/**
* Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
*
key
or certificate
is null
then
* the archive will not be signed.
* @param out the {@link OutputStream} where to write the Jar archive.
* @param key the {@link PrivateKey} used to sign the archive, or null
.
* @param certificate the {@link X509Certificate} used to sign the archive, or
* null
.
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
throws IOException, NoSuchAlgorithmException {
mOutputJar = new JarOutputStream(out);
mOutputJar.setLevel(9);
mKey = key;
mCertificate = certificate;
if (mKey != null && mCertificate != null) {
mManifest = new Manifest();
Attributes main = mManifest.getMainAttributes();
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android)");
mBase64Encoder = new BASE64Encoder();
mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
}
}
/**
* Writes a new {@link File} into the archive.
* @param inputFile the {@link File} to write.
* @param jarPath the filepath inside the archive.
* @throws IOException
*/
public void writeFile(File inputFile, String jarPath) throws IOException {
// Get an input stream on the file.
FileInputStream fis = new FileInputStream(inputFile);
try {
// create the zip entry
JarEntry entry = new JarEntry(jarPath);
entry.setTime(inputFile.lastModified());
writeEntry(fis, entry);
} finally {
// close the file stream used to read the file
fis.close();
}
}
/**
* Copies the content of a Jar/Zip archive into the receiver archive.
* An optional {@link IZipEntryFilter} allows to selectively choose which files
* to copy over.
* @param input the {@link InputStream} for the Jar/Zip to copy.
* @param filter the filter or null
* @throws IOException
* @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write
* must be aborted.
*/
public void writeZip(InputStream input, IZipEntryFilter filter)
throws IOException, ZipAbortException {
ZipInputStream zis = new ZipInputStream(input);
try {
// loop on the entries of the intermediary package and put them in the final package.
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
// do not take directories or anything inside a potential META-INF folder.
if (entry.isDirectory() || name.startsWith("META-INF/")) {
continue;
}
// if we have a filter, we check the entry against it
if (filter != null && filter.checkEntry(name) == false) {
continue;
}
JarEntry newEntry;
// Preserve the STORED method of the input entry.
if (entry.getMethod() == JarEntry.STORED) {
newEntry = new JarEntry(entry);
} else {
// Create a new entry so that the compressed len is recomputed.
newEntry = new JarEntry(name);
}
writeEntry(zis, newEntry);
zis.closeEntry();
}
} finally {
zis.close();
}
}
/**
* Closes the Jar archive by creating the manifest, and signing the archive.
* @throws IOException
* @throws GeneralSecurityException
*/
public void close() throws IOException, GeneralSecurityException {
if (mManifest != null) {
// write the manifest to the jar file
mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
mManifest.write(mOutputJar);
// CERT.SF
Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm());
signature.initSign(mKey);
mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
writeSignatureFile(new SignatureOutputStream(mOutputJar, signature));
// CERT.*
mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
writeSignatureBlock(signature, mCertificate, mKey);
}
mOutputJar.close();
mOutputJar = null;
}
/**
* Clean up of the builder for interrupted workflow.
* This does nothing if {@link #close()} was called successfully.
*/
public void cleanUp() {
if (mOutputJar != null) {
try {
mOutputJar.close();
} catch (IOException e) {
// pass
}
}
}
/**
* Adds an entry to the output jar, and write its content from the {@link InputStream}
* @param input The input stream from where to write the entry content.
* @param entry the entry to write in the jar.
* @throws IOException
*/
private void writeEntry(InputStream input, JarEntry entry) throws IOException {
// add the entry to the jar archive
mOutputJar.putNextEntry(entry);
// read the content of the entry from the input stream, and write it into the archive.
int count;
while ((count = input.read(mBuffer)) != -1) {
mOutputJar.write(mBuffer, 0, count);
// update the digest
if (mMessageDigest != null) {
mMessageDigest.update(mBuffer, 0, count);
}
}
// close the entry for this file
mOutputJar.closeEntry();
if (mManifest != null) {
// update the manifest for this entry.
Attributes attr = mManifest.getAttributes(entry.getName());
if (attr == null) {
attr = new Attributes();
mManifest.getEntries().put(entry.getName(), attr);
}
attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
}
}
/** Writes a .SF file with a digest to the manifest. */
private void writeSignatureFile(SignatureOutputStream out)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android)");
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
mManifest.write(print);
print.flush();
main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
Map