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 private int mCount = 0; 65 SignatureOutputStream(OutputStream out, Signature sig)66 public SignatureOutputStream(OutputStream out, Signature sig) { 67 super(out); 68 mSignature = sig; 69 } 70 71 @Override write(int b)72 public void write(int b) throws IOException { 73 try { 74 mSignature.update((byte) b); 75 } catch (SignatureException e) { 76 throw new IOException("SignatureException: " + e); 77 } 78 super.write(b); 79 mCount++; 80 } 81 82 @Override write(byte[] b, int off, int len)83 public void write(byte[] b, int off, int len) throws IOException { 84 try { 85 mSignature.update(b, off, len); 86 } catch (SignatureException e) { 87 throw new IOException("SignatureException: " + e); 88 } 89 super.write(b, off, len); 90 mCount += len; 91 } 92 size()93 public int size() { 94 return mCount; 95 } 96 } 97 98 private JarOutputStream mOutputJar; 99 private PrivateKey mKey; 100 private X509Certificate mCertificate; 101 private Manifest mManifest; 102 private BASE64Encoder mBase64Encoder; 103 private MessageDigest mMessageDigest; 104 105 private byte[] mBuffer = new byte[4096]; 106 107 /** 108 * Classes which implement this interface provides a method to check whether a file should 109 * be added to a Jar file. 110 */ 111 public interface IZipEntryFilter { 112 113 /** 114 * An exception thrown during packaging of a zip file into APK file. 115 * This is typically thrown by implementations of 116 * {@link IZipEntryFilter#checkEntry(String)}. 117 */ 118 public static class ZipAbortException extends Exception { 119 private static final long serialVersionUID = 1L; 120 ZipAbortException()121 public ZipAbortException() { 122 super(); 123 } 124 ZipAbortException(String format, Object... args)125 public ZipAbortException(String format, Object... args) { 126 super(String.format(format, args)); 127 } 128 ZipAbortException(Throwable cause, String format, Object... args)129 public ZipAbortException(Throwable cause, String format, Object... args) { 130 super(String.format(format, args), cause); 131 } 132 ZipAbortException(Throwable cause)133 public ZipAbortException(Throwable cause) { 134 super(cause); 135 } 136 } 137 138 139 /** 140 * Checks a file for inclusion in a Jar archive. 141 * @param archivePath the archive file path of the entry 142 * @return <code>true</code> if the file should be included. 143 * @throws ZipAbortException if writing the file should be aborted. 144 */ checkEntry(String archivePath)145 public boolean checkEntry(String archivePath) throws ZipAbortException; 146 } 147 148 /** 149 * Creates a {@link SignedJarBuilder} with a given output stream, and signing information. 150 * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then 151 * the archive will not be signed. 152 * @param out the {@link OutputStream} where to write the Jar archive. 153 * @param key the {@link PrivateKey} used to sign the archive, or <code>null</code>. 154 * @param certificate the {@link X509Certificate} used to sign the archive, or 155 * <code>null</code>. 156 * @throws IOException 157 * @throws NoSuchAlgorithmException 158 */ SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)159 public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate) 160 throws IOException, NoSuchAlgorithmException { 161 mOutputJar = new JarOutputStream(out); 162 mOutputJar.setLevel(9); 163 mKey = key; 164 mCertificate = certificate; 165 166 if (mKey != null && mCertificate != null) { 167 mManifest = new Manifest(); 168 Attributes main = mManifest.getMainAttributes(); 169 main.putValue("Manifest-Version", "1.0"); 170 main.putValue("Created-By", "1.0 (Android)"); 171 172 mBase64Encoder = new BASE64Encoder(); 173 mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); 174 } 175 } 176 177 /** 178 * Writes a new {@link File} into the archive. 179 * @param inputFile the {@link File} to write. 180 * @param jarPath the filepath inside the archive. 181 * @throws IOException 182 */ writeFile(File inputFile, String jarPath)183 public void writeFile(File inputFile, String jarPath) throws IOException { 184 // Get an input stream on the file. 185 FileInputStream fis = new FileInputStream(inputFile); 186 try { 187 188 // create the zip entry 189 JarEntry entry = new JarEntry(jarPath); 190 entry.setTime(inputFile.lastModified()); 191 192 writeEntry(fis, entry); 193 } finally { 194 // close the file stream used to read the file 195 fis.close(); 196 } 197 } 198 199 /** 200 * Copies the content of a Jar/Zip archive into the receiver archive. 201 * <p/>An optional {@link IZipEntryFilter} allows to selectively choose which files 202 * to copy over. 203 * @param input the {@link InputStream} for the Jar/Zip to copy. 204 * @param filter the filter or <code>null</code> 205 * @throws IOException 206 * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write 207 * must be aborted. 208 */ writeZip(InputStream input, IZipEntryFilter filter)209 public void writeZip(InputStream input, IZipEntryFilter filter) 210 throws IOException, ZipAbortException { 211 ZipInputStream zis = new ZipInputStream(input); 212 213 try { 214 // loop on the entries of the intermediary package and put them in the final package. 215 ZipEntry entry; 216 while ((entry = zis.getNextEntry()) != null) { 217 String name = entry.getName(); 218 219 // do not take directories or anything inside a potential META-INF folder. 220 if (entry.isDirectory() || name.startsWith("META-INF/")) { 221 continue; 222 } 223 224 // if we have a filter, we check the entry against it 225 if (filter != null && filter.checkEntry(name) == false) { 226 continue; 227 } 228 229 JarEntry newEntry; 230 231 // Preserve the STORED method of the input entry. 232 if (entry.getMethod() == JarEntry.STORED) { 233 newEntry = new JarEntry(entry); 234 } else { 235 // Create a new entry so that the compressed len is recomputed. 236 newEntry = new JarEntry(name); 237 } 238 239 writeEntry(zis, newEntry); 240 241 zis.closeEntry(); 242 } 243 } finally { 244 zis.close(); 245 } 246 } 247 248 /** 249 * Closes the Jar archive by creating the manifest, and signing the archive. 250 * @throws IOException 251 * @throws GeneralSecurityException 252 */ close()253 public void close() throws IOException, GeneralSecurityException { 254 if (mManifest != null) { 255 // write the manifest to the jar file 256 mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); 257 mManifest.write(mOutputJar); 258 259 // CERT.SF 260 Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm()); 261 signature.initSign(mKey); 262 mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF")); 263 writeSignatureFile(new SignatureOutputStream(mOutputJar, signature)); 264 265 // CERT.* 266 mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm())); 267 writeSignatureBlock(signature, mCertificate, mKey); 268 } 269 270 mOutputJar.close(); 271 } 272 273 /** 274 * Adds an entry to the output jar, and write its content from the {@link InputStream} 275 * @param input The input stream from where to write the entry content. 276 * @param entry the entry to write in the jar. 277 * @throws IOException 278 */ writeEntry(InputStream input, JarEntry entry)279 private void writeEntry(InputStream input, JarEntry entry) throws IOException { 280 // add the entry to the jar archive 281 mOutputJar.putNextEntry(entry); 282 283 // read the content of the entry from the input stream, and write it into the archive. 284 int count; 285 while ((count = input.read(mBuffer)) != -1) { 286 mOutputJar.write(mBuffer, 0, count); 287 288 // update the digest 289 if (mMessageDigest != null) { 290 mMessageDigest.update(mBuffer, 0, count); 291 } 292 } 293 294 // close the entry for this file 295 mOutputJar.closeEntry(); 296 297 if (mManifest != null) { 298 // update the manifest for this entry. 299 Attributes attr = mManifest.getAttributes(entry.getName()); 300 if (attr == null) { 301 attr = new Attributes(); 302 mManifest.getEntries().put(entry.getName(), attr); 303 } 304 attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest())); 305 } 306 } 307 308 /** Writes a .SF file with a digest to the manifest. */ writeSignatureFile(SignatureOutputStream out)309 private void writeSignatureFile(SignatureOutputStream out) 310 throws IOException, GeneralSecurityException { 311 Manifest sf = new Manifest(); 312 Attributes main = sf.getMainAttributes(); 313 main.putValue("Signature-Version", "1.0"); 314 main.putValue("Created-By", "1.0 (Android)"); 315 316 BASE64Encoder base64 = new BASE64Encoder(); 317 MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM); 318 PrintStream print = new PrintStream( 319 new DigestOutputStream(new ByteArrayOutputStream(), md), 320 true, "UTF-8"); 321 322 // Digest of the entire manifest 323 mManifest.write(print); 324 print.flush(); 325 main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest())); 326 327 Map<String, Attributes> entries = mManifest.getEntries(); 328 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 329 // Digest of the manifest stanza for this entry. 330 print.print("Name: " + entry.getKey() + "\r\n"); 331 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 332 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 333 } 334 print.print("\r\n"); 335 print.flush(); 336 337 Attributes sfAttr = new Attributes(); 338 sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest())); 339 sf.getEntries().put(entry.getKey(), sfAttr); 340 } 341 342 sf.write(out); 343 344 // A bug in the java.util.jar implementation of Android platforms 345 // up to version 1.6 will cause a spurious IOException to be thrown 346 // if the length of the signature file is a multiple of 1024 bytes. 347 // As a workaround, add an extra CRLF in this case. 348 if ((out.size() % 1024) == 0) { 349 out.write('\r'); 350 out.write('\n'); 351 } 352 } 353 354 /** Write the certificate file with a digital signature. */ writeSignatureBlock(Signature signature, X509Certificate publicKey, PrivateKey privateKey)355 private void writeSignatureBlock(Signature signature, X509Certificate publicKey, 356 PrivateKey privateKey) 357 throws IOException, GeneralSecurityException { 358 SignerInfo signerInfo = new SignerInfo( 359 new X500Name(publicKey.getIssuerX500Principal().getName()), 360 publicKey.getSerialNumber(), 361 AlgorithmId.get(DIGEST_ALGORITHM), 362 AlgorithmId.get(privateKey.getAlgorithm()), 363 signature.sign()); 364 365 PKCS7 pkcs7 = new PKCS7( 366 new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) }, 367 new ContentInfo(ContentInfo.DATA_OID, null), 368 new X509Certificate[] { publicKey }, 369 new SignerInfo[] { signerInfo }); 370 371 pkcs7.encodeSignedData(mOutputJar); 372 } 373 } 374