diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
commit | 55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch) | |
tree | ecd18b995aea8eeeb8b3823266280d41245bf0f7 /jarutils/src/com/android | |
parent | 82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff) | |
download | sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'jarutils/src/com/android')
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); + } +} |