aboutsummaryrefslogtreecommitdiffstats
path: root/jarutils/src/com/android
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
commit55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch)
treeecd18b995aea8eeeb8b3823266280d41245bf0f7 /jarutils/src/com/android
parent82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff)
downloadsdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'jarutils/src/com/android')
-rw-r--r--jarutils/src/com/android/jarutils/DebugKeyProvider.java202
-rw-r--r--jarutils/src/com/android/jarutils/JavaResourceFilter.java96
-rw-r--r--jarutils/src/com/android/jarutils/KeystoreHelper.java228
-rw-r--r--jarutils/src/com/android/jarutils/SignedJarBuilder.java324
4 files changed, 850 insertions, 0 deletions
diff --git a/jarutils/src/com/android/jarutils/DebugKeyProvider.java b/jarutils/src/com/android/jarutils/DebugKeyProvider.java
new file mode 100644
index 0000000..6dc32ba
--- /dev/null
+++ b/jarutils/src/com/android/jarutils/DebugKeyProvider.java
@@ -0,0 +1,202 @@
+/*
+ * 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.jarutils;
+
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableEntryException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+
+/**
+ * A provider of a dummy key to sign Android application for debugging purpose.
+ * <p/>This provider uses a custom keystore to create and store a key with a known password.
+ */
+public class DebugKeyProvider {
+
+ public interface IKeyGenOutput {
+ public void out(String message);
+ public void err(String message);
+ }
+
+ private static final String PASSWORD_STRING = "android";
+ private static final char[] PASSWORD_CHAR = PASSWORD_STRING.toCharArray();
+ private static final String DEBUG_ALIAS = "AndroidDebugKey";
+
+ // Certificate CN value. This is a hard-coded value for the debug key.
+ // Android Market checks against this value in order to refuse applications signed with
+ // debug keys.
+ private static final String CERTIFICATE_DESC = "CN=Android Debug,O=Android,C=US";
+
+ private KeyStore.PrivateKeyEntry mEntry;
+
+ public static class KeytoolException extends Exception {
+ /** default serial uid */
+ private static final long serialVersionUID = 1L;
+ private String mJavaHome = null;
+ private String mCommandLine = null;
+
+ KeytoolException(String message) {
+ super(message);
+ }
+
+ KeytoolException(String message, String javaHome, String commandLine) {
+ super(message);
+
+ mJavaHome = javaHome;
+ mCommandLine = commandLine;
+ }
+
+ public String getJavaHome() {
+ return mJavaHome;
+ }
+
+ public String getCommandLine() {
+ return mCommandLine;
+ }
+ }
+
+ /**
+ * Creates a provider using a keystore at the given location.
+ * <p/>The keystore, and a new random android debug key are created if they do not yet exist.
+ * <p/>Password for the store/key is <code>android</code>, and the key alias is
+ * <code>AndroidDebugKey</code>.
+ * @param osKeyStorePath the OS path to the keystore, or <code>null</code> if the default one
+ * is to be used.
+ * @param storeType an optional keystore type, or <code>null</code> if the default is to
+ * be used.
+ * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr
+ * of the keytool process call.
+ * @throws KeytoolException If the creation of the debug key failed.
+ * @throws AndroidLocationException
+ */
+ public DebugKeyProvider(String osKeyStorePath, String storeType, IKeyGenOutput output)
+ throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
+ UnrecoverableEntryException, IOException, KeytoolException, AndroidLocationException {
+
+ if (osKeyStorePath == null) {
+ osKeyStorePath = getDefaultKeyStoreOsPath();
+ }
+
+ if (loadKeyEntry(osKeyStorePath, storeType) == false) {
+ // create the store with the key
+ createNewStore(osKeyStorePath, storeType, output);
+ }
+ }
+
+ /**
+ * Returns the OS path to the default debug keystore.
+ *
+ * @return The OS path to the default debug keystore.
+ * @throws KeytoolException
+ * @throws AndroidLocationException
+ */
+ public static String getDefaultKeyStoreOsPath()
+ throws KeytoolException, AndroidLocationException {
+ String folder = AndroidLocation.getFolder();
+ if (folder == null) {
+ throw new KeytoolException("Failed to get HOME directory!\n");
+ }
+ String osKeyStorePath = folder + "debug.keystore";
+
+ return osKeyStorePath;
+ }
+
+ /**
+ * Returns the debug {@link PrivateKey} to use to sign applications for debug purpose.
+ * @return the private key or <code>null</code> if its creation failed.
+ */
+ public PrivateKey getDebugKey() throws KeyStoreException, NoSuchAlgorithmException,
+ UnrecoverableKeyException, UnrecoverableEntryException {
+ if (mEntry != null) {
+ return mEntry.getPrivateKey();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the debug {@link Certificate} to use to sign applications for debug purpose.
+ * @return the certificate or <code>null</code> if its creation failed.
+ */
+ public Certificate getCertificate() throws KeyStoreException, NoSuchAlgorithmException,
+ UnrecoverableKeyException, UnrecoverableEntryException {
+ if (mEntry != null) {
+ return mEntry.getCertificate();
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads the debug key from the keystore.
+ * @param osKeyStorePath the OS path to the keystore.
+ * @param storeType an optional keystore type, or <code>null</code> if the default is to
+ * be used.
+ * @return <code>true</code> if success, <code>false</code> if the keystore does not exist.
+ */
+ private boolean loadKeyEntry(String osKeyStorePath, String storeType) throws KeyStoreException,
+ NoSuchAlgorithmException, CertificateException, IOException,
+ UnrecoverableEntryException {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(
+ storeType != null ? storeType : KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(osKeyStorePath);
+ keyStore.load(fis, PASSWORD_CHAR);
+ fis.close();
+ mEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ DEBUG_ALIAS, new KeyStore.PasswordProtection(PASSWORD_CHAR));
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a new store
+ * @param osKeyStorePath the location of the store
+ * @param storeType an optional keystore type, or <code>null</code> if the default is to
+ * be used.
+ * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr
+ * of the keytool process call.
+ * @throws KeyStoreException
+ * @throws NoSuchAlgorithmException
+ * @throws CertificateException
+ * @throws UnrecoverableEntryException
+ * @throws IOException
+ * @throws KeytoolException
+ */
+ private void createNewStore(String osKeyStorePath, String storeType, IKeyGenOutput output)
+ throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
+ UnrecoverableEntryException, IOException, KeytoolException {
+
+ if (KeystoreHelper.createNewStore(osKeyStorePath, storeType, PASSWORD_STRING, DEBUG_ALIAS,
+ PASSWORD_STRING, CERTIFICATE_DESC, 1 /* validity*/, output)) {
+ loadKeyEntry(osKeyStorePath, storeType);
+ }
+ }
+}
diff --git a/jarutils/src/com/android/jarutils/JavaResourceFilter.java b/jarutils/src/com/android/jarutils/JavaResourceFilter.java
new file mode 100644
index 0000000..d9f8da6
--- /dev/null
+++ b/jarutils/src/com/android/jarutils/JavaResourceFilter.java
@@ -0,0 +1,96 @@
+/*
+ * 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.jarutils;
+
+import com.android.jarutils.SignedJarBuilder.IZipEntryFilter;
+
+/**
+ * A basic implementation of {@link IZipEntryFilter} to filter out anything that is not a
+ * java resource.
+ */
+public class JavaResourceFilter implements IZipEntryFilter {
+
+ public boolean checkEntry(String name) {
+ // split the path into segments.
+ String[] segments = name.split("/");
+
+ // empty path? skip to next entry.
+ if (segments.length == 0) {
+ return false;
+ }
+
+ // Check each folders to make sure they should be included.
+ // Folders like CVS, .svn, etc.. should already have been excluded from the
+ // jar file, but we need to exclude some other folder (like /META-INF) so
+ // we check anyway.
+ for (int i = 0 ; i < segments.length - 1; i++) {
+ if (checkFolderForPackaging(segments[i]) == false) {
+ return false;
+ }
+ }
+
+ // get the file name from the path
+ String fileName = segments[segments.length-1];
+
+ return checkFileForPackaging(fileName);
+ }
+
+ /**
+ * Checks whether a folder and its content is valid for packaging into the .apk as
+ * standard Java resource.
+ * @param folderName the name of the folder.
+ */
+ public static boolean checkFolderForPackaging(String folderName) {
+ return folderName.equals("CVS") == false &&
+ folderName.equals(".svn") == false &&
+ folderName.equals("SCCS") == false &&
+ folderName.equals("META-INF") == false &&
+ folderName.startsWith("_") == false;
+ }
+
+ /**
+ * Checks a file to make sure it should be packaged as standard resources.
+ * @param fileName the name of the file (including extension)
+ * @return true if the file should be packaged as standard java resources.
+ */
+ public static boolean checkFileForPackaging(String fileName) {
+ String[] fileSegments = fileName.split("\\.");
+ String fileExt = "";
+ if (fileSegments.length > 1) {
+ fileExt = fileSegments[fileSegments.length-1];
+ }
+
+ return checkFileForPackaging(fileName, fileExt);
+ }
+
+ /**
+ * Checks a file to make sure it should be packaged as standard resources.
+ * @param fileName the name of the file (including extension)
+ * @param extension the extension of the file (excluding '.')
+ * @return true if the file should be packaged as standard java resources.
+ */
+ public static boolean checkFileForPackaging(String fileName, String extension) {
+ return "aidl".equalsIgnoreCase(extension) == false &&
+ "java".equalsIgnoreCase(extension) == false &&
+ "class".equalsIgnoreCase(extension) == false &&
+ "package.html".equalsIgnoreCase(fileName) == false &&
+ "overview.html".equalsIgnoreCase(fileName) == false &&
+ ".cvsignore".equalsIgnoreCase(fileName) == false &&
+ ".DS_Store".equals(fileName) == false &&
+ fileName.charAt(fileName.length()-1) != '~';
+ }
+}
diff --git a/jarutils/src/com/android/jarutils/KeystoreHelper.java b/jarutils/src/com/android/jarutils/KeystoreHelper.java
new file mode 100644
index 0000000..c694684
--- /dev/null
+++ b/jarutils/src/com/android/jarutils/KeystoreHelper.java
@@ -0,0 +1,228 @@
+/*
+ * 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.jarutils;
+
+import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+
+/**
+ * A Helper to create new keystore/key.
+ */
+public final class KeystoreHelper {
+
+ /**
+ * Creates a new store
+ * @param osKeyStorePath the location of the store
+ * @param storeType an optional keystore type, or <code>null</code> if the default is to
+ * be used.
+ * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr
+ * of the keytool process call.
+ * @throws KeyStoreException
+ * @throws NoSuchAlgorithmException
+ * @throws CertificateException
+ * @throws UnrecoverableEntryException
+ * @throws IOException
+ * @throws KeytoolException
+ */
+ public static boolean createNewStore(
+ String osKeyStorePath,
+ String storeType,
+ String storePassword,
+ String alias,
+ String keyPassword,
+ String description,
+ int validityYears,
+ IKeyGenOutput output)
+ throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
+ UnrecoverableEntryException, IOException, KeytoolException {
+
+ // get the executable name of keytool depending on the platform.
+ String os = System.getProperty("os.name");
+
+ String keytoolCommand;
+ if (os.startsWith("Windows")) {
+ keytoolCommand = "keytool.exe";
+ } else {
+ keytoolCommand = "keytool";
+ }
+
+ String javaHome = System.getProperty("java.home");
+
+ if (javaHome != null && javaHome.length() > 0) {
+ keytoolCommand = javaHome + File.separator + "bin" + File.separator + keytoolCommand;
+ }
+
+ // create the command line to call key tool to build the key with no user input.
+ ArrayList<String> commandList = new ArrayList<String>();
+ commandList.add(keytoolCommand);
+ commandList.add("-genkey");
+ commandList.add("-alias");
+ commandList.add(alias);
+ commandList.add("-keyalg");
+ commandList.add("RSA");
+ commandList.add("-dname");
+ commandList.add(description);
+ commandList.add("-validity");
+ commandList.add(Integer.toString(validityYears * 365));
+ commandList.add("-keypass");
+ commandList.add(keyPassword);
+ commandList.add("-keystore");
+ commandList.add(osKeyStorePath);
+ commandList.add("-storepass");
+ commandList.add(storePassword);
+ if (storeType != null) {
+ commandList.add("-storetype");
+ commandList.add(storeType);
+ }
+
+ String[] commandArray = commandList.toArray(new String[commandList.size()]);
+
+ // launch the command line process
+ int result = 0;
+ try {
+ result = grabProcessOutput(Runtime.getRuntime().exec(commandArray), output);
+ } catch (Exception e) {
+ // create the command line as one string
+ StringBuilder builder = new StringBuilder();
+ boolean firstArg = true;
+ for (String arg : commandArray) {
+ boolean hasSpace = arg.indexOf(' ') != -1;
+
+ if (firstArg == true) {
+ firstArg = false;
+ } else {
+ builder.append(' ');
+ }
+
+ if (hasSpace) {
+ builder.append('"');
+ }
+
+ builder.append(arg);
+
+ if (hasSpace) {
+ builder.append('"');
+ }
+ }
+
+ throw new KeytoolException("Failed to create key: " + e.getMessage(),
+ javaHome, builder.toString());
+ }
+
+ if (result != 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the stderr/stdout outputs of a process and return when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the ouput from
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ private static int grabProcessOutput(final Process process, final IKeyGenOutput output) {
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ Thread t1 = new Thread("") {
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ if (output != null) {
+ output.err(line);
+ } else {
+ System.err.println(line);
+ }
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ Thread t2 = new Thread("") {
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ if (output != null) {
+ output.out(line);
+ } else {
+ System.out.println(line);
+ }
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ t1.start();
+ t2.start();
+
+ // it looks like on windows process#waitFor() can return
+ // before the thread have filled the arrays, so we wait for both threads and the
+ // process itself.
+ try {
+ t1.join();
+ } catch (InterruptedException e) {
+ }
+ try {
+ t2.join();
+ } catch (InterruptedException e) {
+ }
+
+ // get the return code from the process
+ try {
+ return process.waitFor();
+ } catch (InterruptedException e) {
+ // since we're waiting for the output thread above, we should never actually wait
+ // on the process to end, since it'll be done by the time we call waitFor()
+ return 0;
+ }
+ }
+}
diff --git a/jarutils/src/com/android/jarutils/SignedJarBuilder.java b/jarutils/src/com/android/jarutils/SignedJarBuilder.java
new file mode 100644
index 0000000..335ab7d
--- /dev/null
+++ b/jarutils/src/com/android/jarutils/SignedJarBuilder.java
@@ -0,0 +1,324 @@
+/*
+ * 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.jarutils;
+
+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;
+
+ 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);
+ }
+
+ @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);
+ }
+ }
+
+ 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 {
+ /**
+ * Checks a file for inclusion in a Jar archive.
+ * @param name the archive file path of the entry
+ * @return <code>true</code> if the file should be included.
+ */
+ public boolean checkEntry(String name);
+ }
+
+ /**
+ * Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
+ * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> 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 <code>null</code>.
+ * @param certificate the {@link X509Certificate} used to sign the archive, or
+ * <code>null</code>.
+ * @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.
+ * <p/>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 <code>null</code>
+ * @throws IOException
+ */
+ public void writeZip(InputStream input, IZipEntryFilter filter) throws IOException {
+ 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();
+ }
+
+ /**
+ * 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(OutputStream 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<String, Attributes> entries = mManifest.getEntries();
+ for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
+ // Digest of the manifest stanza for this entry.
+ print.print("Name: " + entry.getKey() + "\r\n");
+ for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
+ print.print(att.getKey() + ": " + att.getValue() + "\r\n");
+ }
+ print.print("\r\n");
+ print.flush();
+
+ Attributes sfAttr = new Attributes();
+ sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
+ sf.getEntries().put(entry.getKey(), sfAttr);
+ }
+
+ sf.write(out);
+ }
+
+ /** Write the certificate file with a digital signature. */
+ private void writeSignatureBlock(Signature signature, X509Certificate publicKey,
+ PrivateKey privateKey)
+ throws IOException, GeneralSecurityException {
+ SignerInfo signerInfo = new SignerInfo(
+ new X500Name(publicKey.getIssuerX500Principal().getName()),
+ publicKey.getSerialNumber(),
+ AlgorithmId.get(DIGEST_ALGORITHM),
+ AlgorithmId.get(privateKey.getAlgorithm()),
+ signature.sign());
+
+ PKCS7 pkcs7 = new PKCS7(
+ new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) },
+ new ContentInfo(ContentInfo.DATA_OID, null),
+ new X509Certificate[] { publicKey },
+ new SignerInfo[] { signerInfo });
+
+ pkcs7.encodeSignedData(mOutputJar);
+ }
+}