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 65 public SignatureOutputStream(OutputStream out, Signature sig) { 66 super(out); 67 mSignature = sig; 68 } 69 70 @Override 71 public void write(int b) throws IOException { 72 try { 73 mSignature.update((byte) b); 74 } catch (SignatureException e) { 75 throw new IOException("SignatureException: " + e); 76 } 77 super.write(b); 78 } 79 80 @Override 81 public void write(byte[] b, int off, int len) throws IOException { 82 try { 83 mSignature.update(b, off, len); 84 } catch (SignatureException e) { 85 throw new IOException("SignatureException: " + e); 86 } 87 super.write(b, off, len); 88 } 89 } 90 91 private JarOutputStream mOutputJar; 92 private PrivateKey mKey; 93 private X509Certificate mCertificate; 94 private Manifest mManifest; 95 private BASE64Encoder mBase64Encoder; 96 private MessageDigest mMessageDigest; 97 98 private byte[] mBuffer = new byte[4096]; 99 100 /** 101 * Classes which implement this interface provides a method to check whether a file should 102 * be added to a Jar file. 103 */ 104 public interface IZipEntryFilter { 105 106 /** 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)}. 110 */ 111 public static class ZipAbortException extends Exception { 112 private static final long serialVersionUID = 1L; 113 114 public ZipAbortException() { 115 super(); 116 } 117 118 public ZipAbortException(String format, Object... args) { 119 super(String.format(format, args)); 120 } 121 122 public ZipAbortException(Throwable cause, String format, Object... args) { 123 super(String.format(format, args), cause); 124 } 125 126 public ZipAbortException(Throwable cause) { 127 super(cause); 128 } 129 } 130 131 132 /** 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. 137 */ 138 public boolean checkEntry(String archivePath) throws ZipAbortException; 139 } 140 141 /** 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 148 * <code>null</code>. 149 * @throws IOException 150 * @throws NoSuchAlgorithmException 151 */ 152 public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate) 153 throws IOException, NoSuchAlgorithmException { 154 mOutputJar = new JarOutputStream(out); 155 mOutputJar.setLevel(9); 156 mKey = key; 157 mCertificate = certificate; 158 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)"); 164 165 mBase64Encoder = new BASE64Encoder(); 166 mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); 167 } 168 } 169 170 /** 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 175 */ 176 public void writeFile(File inputFile, String jarPath) throws IOException { 177 // Get an input stream on the file. 178 FileInputStream fis = new FileInputStream(inputFile); 179 try { 180 181 // create the zip entry 182 JarEntry entry = new JarEntry(jarPath); 183 entry.setTime(inputFile.lastModified()); 184 185 writeEntry(fis, entry); 186 } finally { 187 // close the file stream used to read the file 188 fis.close(); 189 } 190 } 191 192 /** 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 195 * to copy over. 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 200 * must be aborted. 201 */ 202 public void writeZip(InputStream input, IZipEntryFilter filter) 203 throws IOException, ZipAbortException { 204 ZipInputStream zis = new ZipInputStream(input); 205 206 try { 207 // loop on the entries of the intermediary package and put them in the final package. 208 ZipEntry entry; 209 while ((entry = zis.getNextEntry()) != null) { 210 String name = entry.getName(); 211 212 // do not take directories or anything inside a potential META-INF folder. 213 if (entry.isDirectory() || name.startsWith("META-INF/")) { 214 continue; 215 } 216 217 // if we have a filter, we check the entry against it 218 if (filter != null && filter.checkEntry(name) == false) { 219 continue; 220 } 221 222 JarEntry newEntry; 223 224 // Preserve the STORED method of the input entry. 225 if (entry.getMethod() == JarEntry.STORED) { 226 newEntry = new JarEntry(entry); 227 } else { 228 // Create a new entry so that the compressed len is recomputed. 229 newEntry = new JarEntry(name); 230 } 231 232 writeEntry(zis, newEntry); 233 234 zis.closeEntry(); 235 } 236 } finally { 237 zis.close(); 238 } 239 } 240 241 /** 242 * Closes the Jar archive by creating the manifest, and signing the archive. 243 * @throws IOException 244 * @throws GeneralSecurityException 245 */ 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); 251 252 // CERT.SF 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)); 257 258 // CERT.* 259 mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm())); 260 writeSignatureBlock(signature, mCertificate, mKey); 261 } 262 263 mOutputJar.close(); 264 } 265 266 /** 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 271 */ 272 private void writeEntry(InputStream input, JarEntry entry) throws IOException { 273 // add the entry to the jar archive 274 mOutputJar.putNextEntry(entry); 275 276 // read the content of the entry from the input stream, and write it into the archive. 277 int count; 278 while ((count = input.read(mBuffer)) != -1) { 279 mOutputJar.write(mBuffer, 0, count); 280 281 // update the digest 282 if (mMessageDigest != null) { 283 mMessageDigest.update(mBuffer, 0, count); 284 } 285 } 286 287 // close the entry for this file 288 mOutputJar.closeEntry(); 289 290 if (mManifest != null) { 291 // update the manifest for this entry. 292 Attributes attr = mManifest.getAttributes(entry.getName()); 293 if (attr == null) { 294 attr = new Attributes(); 295 mManifest.getEntries().put(entry.getName(), attr); 296 } 297 attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest())); 298 } 299 } 300 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)"); 308 309 BASE64Encoder base64 = new BASE64Encoder(); 310 MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM); 311 PrintStream print = new PrintStream( 312 new DigestOutputStream(new ByteArrayOutputStream(), md), 313 true, "UTF-8"); 314 315 // Digest of the entire manifest 316 mManifest.write(print); 317 print.flush(); 318 main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest())); 319 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"); 326 } 327 print.print("\r\n"); 328 print.flush(); 329 330 Attributes sfAttr = new Attributes(); 331 sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest())); 332 sf.getEntries().put(entry.getKey(), sfAttr); 333 } 334 335 sf.write(out); 336 } 337 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()), 347 signature.sign()); 348 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 }); 354 355 pkcs7.encodeSignedData(mOutputJar); 356 } 357 } 358