SignApk.java revision 29706d155a7516e36b63c5201d3e294de6589814
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 17package com.android.signapk; 18 19import org.bouncycastle.asn1.ASN1InputStream; 20import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21import org.bouncycastle.asn1.DEROutputStream; 22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23import org.bouncycastle.cert.jcajce.JcaCertStore; 24import org.bouncycastle.cms.CMSException; 25import org.bouncycastle.cms.CMSProcessableByteArray; 26import org.bouncycastle.cms.CMSSignedData; 27import org.bouncycastle.cms.CMSSignedDataGenerator; 28import org.bouncycastle.cms.CMSTypedData; 29import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30import org.bouncycastle.jce.provider.BouncyCastleProvider; 31import org.bouncycastle.operator.ContentSigner; 32import org.bouncycastle.operator.OperatorCreationException; 33import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35import org.bouncycastle.util.encoders.Base64; 36 37import java.io.BufferedReader; 38import java.io.BufferedOutputStream; 39import java.io.ByteArrayOutputStream; 40import java.io.DataInputStream; 41import java.io.File; 42import java.io.FileInputStream; 43import java.io.FileOutputStream; 44import java.io.FilterOutputStream; 45import java.io.IOException; 46import java.io.InputStream; 47import java.io.InputStreamReader; 48import java.io.OutputStream; 49import java.io.PrintStream; 50import java.security.DigestOutputStream; 51import java.security.GeneralSecurityException; 52import java.security.Key; 53import java.security.KeyFactory; 54import java.security.MessageDigest; 55import java.security.PrivateKey; 56import java.security.Provider; 57import java.security.Security; 58import java.security.cert.CertificateEncodingException; 59import java.security.cert.CertificateFactory; 60import java.security.cert.X509Certificate; 61import java.security.spec.InvalidKeySpecException; 62import java.security.spec.KeySpec; 63import java.security.spec.PKCS8EncodedKeySpec; 64import java.util.ArrayList; 65import java.util.Collections; 66import java.util.Enumeration; 67import java.util.Map; 68import java.util.TreeMap; 69import java.util.jar.Attributes; 70import java.util.jar.JarEntry; 71import java.util.jar.JarFile; 72import java.util.jar.JarOutputStream; 73import java.util.jar.Manifest; 74import java.util.regex.Pattern; 75import javax.crypto.Cipher; 76import javax.crypto.EncryptedPrivateKeyInfo; 77import javax.crypto.SecretKeyFactory; 78import javax.crypto.spec.PBEKeySpec; 79 80/** 81 * Command line tool to sign JAR files (including APKs and OTA updates) in 82 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. 83 */ 84class SignApk { 85 private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 86 private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; 87 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF"; 88 private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA"; 89 90 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 91 92 private static Provider sBouncyCastleProvider; 93 94 // Files matching this pattern are not copied to the output. 95 private static Pattern stripPattern = 96 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" + 97 Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 98 99 private static X509Certificate readPublicKey(File file) 100 throws IOException, GeneralSecurityException { 101 FileInputStream input = new FileInputStream(file); 102 try { 103 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 104 return (X509Certificate) cf.generateCertificate(input); 105 } finally { 106 input.close(); 107 } 108 } 109 110 /** 111 * Reads the password from stdin and returns it as a string. 112 * 113 * @param keyFile The file containing the private key. Used to prompt the user. 114 */ 115 private static String readPassword(File keyFile) { 116 // TODO: use Console.readPassword() when it's available. 117 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 118 System.out.flush(); 119 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 120 try { 121 return stdin.readLine(); 122 } catch (IOException ex) { 123 return null; 124 } 125 } 126 127 /** 128 * Decrypt an encrypted PKCS 8 format private key. 129 * 130 * Based on ghstark's post on Aug 6, 2006 at 131 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 132 * 133 * @param encryptedPrivateKey The raw data of the private key 134 * @param keyFile The file containing the private key 135 */ 136 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 137 throws GeneralSecurityException { 138 EncryptedPrivateKeyInfo epkInfo; 139 try { 140 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 141 } catch (IOException ex) { 142 // Probably not an encrypted key. 143 return null; 144 } 145 146 char[] password = readPassword(keyFile).toCharArray(); 147 148 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 149 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 150 151 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 152 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 153 154 try { 155 return epkInfo.getKeySpec(cipher); 156 } catch (InvalidKeySpecException ex) { 157 System.err.println("signapk: Password for " + keyFile + " may be bad."); 158 throw ex; 159 } 160 } 161 162 /** Read a PKCS 8 format private key. */ 163 private static PrivateKey readPrivateKey(File file) 164 throws IOException, GeneralSecurityException { 165 DataInputStream input = new DataInputStream(new FileInputStream(file)); 166 try { 167 byte[] bytes = new byte[(int) file.length()]; 168 input.read(bytes); 169 170 KeySpec spec = decryptPrivateKey(bytes, file); 171 if (spec == null) { 172 spec = new PKCS8EncodedKeySpec(bytes); 173 } 174 175 try { 176 return KeyFactory.getInstance("RSA").generatePrivate(spec); 177 } catch (InvalidKeySpecException ex) { 178 return KeyFactory.getInstance("DSA").generatePrivate(spec); 179 } 180 } finally { 181 input.close(); 182 } 183 } 184 185 /** Add the SHA1 of every file to the manifest, creating it if necessary. */ 186 private static Manifest addDigestsToManifest(JarFile jar) 187 throws IOException, GeneralSecurityException { 188 Manifest input = jar.getManifest(); 189 Manifest output = new Manifest(); 190 Attributes main = output.getMainAttributes(); 191 if (input != null) { 192 main.putAll(input.getMainAttributes()); 193 } else { 194 main.putValue("Manifest-Version", "1.0"); 195 main.putValue("Created-By", "1.0 (Android SignApk)"); 196 } 197 198 MessageDigest md = MessageDigest.getInstance("SHA1"); 199 byte[] buffer = new byte[4096]; 200 int num; 201 202 // We sort the input entries by name, and add them to the 203 // output manifest in sorted order. We expect that the output 204 // map will be deterministic. 205 206 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); 207 208 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { 209 JarEntry entry = e.nextElement(); 210 byName.put(entry.getName(), entry); 211 } 212 213 for (JarEntry entry: byName.values()) { 214 String name = entry.getName(); 215 if (!entry.isDirectory() && 216 (stripPattern == null || !stripPattern.matcher(name).matches())) { 217 InputStream data = jar.getInputStream(entry); 218 while ((num = data.read(buffer)) > 0) { 219 md.update(buffer, 0, num); 220 } 221 222 Attributes attr = null; 223 if (input != null) attr = input.getAttributes(name); 224 attr = attr != null ? new Attributes(attr) : new Attributes(); 225 attr.putValue("SHA1-Digest", 226 new String(Base64.encode(md.digest()), "ASCII")); 227 output.getEntries().put(name, attr); 228 } 229 } 230 231 return output; 232 } 233 234 /** 235 * Add a copy of the public key to the archive; this should 236 * exactly match one of the files in 237 * /system/etc/security/otacerts.zip on the device. (The same 238 * cert can be extracted from the CERT.RSA file but this is much 239 * easier to get at.) 240 */ 241 private static void addOtacert(JarOutputStream outputJar, 242 File publicKeyFile, 243 long timestamp, 244 Manifest manifest) 245 throws IOException, GeneralSecurityException { 246 MessageDigest md = MessageDigest.getInstance("SHA1"); 247 248 JarEntry je = new JarEntry(OTACERT_NAME); 249 je.setTime(timestamp); 250 outputJar.putNextEntry(je); 251 FileInputStream input = new FileInputStream(publicKeyFile); 252 byte[] b = new byte[4096]; 253 int read; 254 while ((read = input.read(b)) != -1) { 255 outputJar.write(b, 0, read); 256 md.update(b, 0, read); 257 } 258 input.close(); 259 260 Attributes attr = new Attributes(); 261 attr.putValue("SHA1-Digest", 262 new String(Base64.encode(md.digest()), "ASCII")); 263 manifest.getEntries().put(OTACERT_NAME, attr); 264 } 265 266 267 /** Write to another stream and track how many bytes have been 268 * written. 269 */ 270 private static class CountOutputStream extends FilterOutputStream { 271 private int mCount; 272 273 public CountOutputStream(OutputStream out) { 274 super(out); 275 mCount = 0; 276 } 277 278 @Override 279 public void write(int b) throws IOException { 280 super.write(b); 281 mCount++; 282 } 283 284 @Override 285 public void write(byte[] b, int off, int len) throws IOException { 286 super.write(b, off, len); 287 mCount += len; 288 } 289 290 public int size() { 291 return mCount; 292 } 293 } 294 295 /** Write a .SF file with a digest of the specified manifest. */ 296 private static void writeSignatureFile(Manifest manifest, OutputStream out) 297 throws IOException, GeneralSecurityException { 298 Manifest sf = new Manifest(); 299 Attributes main = sf.getMainAttributes(); 300 main.putValue("Signature-Version", "1.0"); 301 main.putValue("Created-By", "1.0 (Android SignApk)"); 302 303 MessageDigest md = MessageDigest.getInstance("SHA1"); 304 PrintStream print = new PrintStream( 305 new DigestOutputStream(new ByteArrayOutputStream(), md), 306 true, "UTF-8"); 307 308 // Digest of the entire manifest 309 manifest.write(print); 310 print.flush(); 311 main.putValue("SHA1-Digest-Manifest", 312 new String(Base64.encode(md.digest()), "ASCII")); 313 314 Map<String, Attributes> entries = manifest.getEntries(); 315 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 316 // Digest of the manifest stanza for this entry. 317 print.print("Name: " + entry.getKey() + "\r\n"); 318 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 319 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 320 } 321 print.print("\r\n"); 322 print.flush(); 323 324 Attributes sfAttr = new Attributes(); 325 sfAttr.putValue("SHA1-Digest", 326 new String(Base64.encode(md.digest()), "ASCII")); 327 sf.getEntries().put(entry.getKey(), sfAttr); 328 } 329 330 CountOutputStream cout = new CountOutputStream(out); 331 sf.write(cout); 332 333 // A bug in the java.util.jar implementation of Android platforms 334 // up to version 1.6 will cause a spurious IOException to be thrown 335 // if the length of the signature file is a multiple of 1024 bytes. 336 // As a workaround, add an extra CRLF in this case. 337 if ((cout.size() % 1024) == 0) { 338 cout.write('\r'); 339 cout.write('\n'); 340 } 341 } 342 343 /** Sign data and write the digital signature to 'out'. */ 344 private static void writeSignatureBlock( 345 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, 346 OutputStream out) 347 throws IOException, 348 CertificateEncodingException, 349 OperatorCreationException, 350 CMSException { 351 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 352 certList.add(publicKey); 353 JcaCertStore certs = new JcaCertStore(certList); 354 355 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 356 ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA") 357 .setProvider(sBouncyCastleProvider) 358 .build(privateKey); 359 gen.addSignerInfoGenerator( 360 new JcaSignerInfoGeneratorBuilder( 361 new JcaDigestCalculatorProviderBuilder() 362 .setProvider(sBouncyCastleProvider) 363 .build()) 364 .setDirectSignature(true) 365 .build(sha1Signer, publicKey)); 366 gen.addCertificates(certs); 367 CMSSignedData sigData = gen.generate(data, false); 368 369 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded()); 370 DEROutputStream dos = new DEROutputStream(out); 371 dos.writeObject(asn1.readObject()); 372 } 373 374 /** 375 * Copy all the files in a manifest from input to output. We set 376 * the modification times in the output to a fixed time, so as to 377 * reduce variation in the output file and make incremental OTAs 378 * more efficient. 379 */ 380 private static void copyFiles(Manifest manifest, 381 JarFile in, JarOutputStream out, long timestamp) throws IOException { 382 byte[] buffer = new byte[4096]; 383 int num; 384 385 Map<String, Attributes> entries = manifest.getEntries(); 386 ArrayList<String> names = new ArrayList<String>(entries.keySet()); 387 Collections.sort(names); 388 for (String name : names) { 389 JarEntry inEntry = in.getJarEntry(name); 390 JarEntry outEntry = null; 391 if (inEntry.getMethod() == JarEntry.STORED) { 392 // Preserve the STORED method of the input entry. 393 outEntry = new JarEntry(inEntry); 394 } else { 395 // Create a new entry so that the compressed len is recomputed. 396 outEntry = new JarEntry(name); 397 } 398 outEntry.setTime(timestamp); 399 out.putNextEntry(outEntry); 400 401 InputStream data = in.getInputStream(inEntry); 402 while ((num = data.read(buffer)) > 0) { 403 out.write(buffer, 0, num); 404 } 405 out.flush(); 406 } 407 } 408 409 private static class WholeFileSignerOutputStream extends FilterOutputStream { 410 private boolean closing = false; 411 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 412 private OutputStream tee; 413 414 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 415 super(out); 416 this.tee = tee; 417 } 418 419 public void notifyClosing() { 420 closing = true; 421 } 422 423 public void finish() throws IOException { 424 closing = false; 425 426 byte[] data = footer.toByteArray(); 427 if (data.length < 2) 428 throw new IOException("Less than two bytes written to footer"); 429 write(data, 0, data.length - 2); 430 } 431 432 public byte[] getTail() { 433 return footer.toByteArray(); 434 } 435 436 @Override 437 public void write(byte[] b) throws IOException { 438 write(b, 0, b.length); 439 } 440 441 @Override 442 public void write(byte[] b, int off, int len) throws IOException { 443 if (closing) { 444 // if the jar is about to close, save the footer that will be written 445 footer.write(b, off, len); 446 } 447 else { 448 // write to both output streams. out is the CMSTypedData signer and tee is the file. 449 out.write(b, off, len); 450 tee.write(b, off, len); 451 } 452 } 453 454 @Override 455 public void write(int b) throws IOException { 456 if (closing) { 457 // if the jar is about to close, save the footer that will be written 458 footer.write(b); 459 } 460 else { 461 // write to both output streams. out is the CMSTypedData signer and tee is the file. 462 out.write(b); 463 tee.write(b); 464 } 465 } 466 } 467 468 private static class CMSSigner implements CMSTypedData { 469 private JarFile inputJar; 470 private File publicKeyFile; 471 private X509Certificate publicKey; 472 private PrivateKey privateKey; 473 private String outputFile; 474 private OutputStream outputStream; 475 private final ASN1ObjectIdentifier type; 476 private WholeFileSignerOutputStream signer; 477 478 public CMSSigner(JarFile inputJar, File publicKeyFile, 479 X509Certificate publicKey, PrivateKey privateKey, 480 OutputStream outputStream) { 481 this.inputJar = inputJar; 482 this.publicKeyFile = publicKeyFile; 483 this.publicKey = publicKey; 484 this.privateKey = privateKey; 485 this.outputStream = outputStream; 486 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 487 } 488 489 public Object getContent() { 490 throw new UnsupportedOperationException(); 491 } 492 493 public ASN1ObjectIdentifier getContentType() { 494 return type; 495 } 496 497 public void write(OutputStream out) throws IOException { 498 try { 499 signer = new WholeFileSignerOutputStream(out, outputStream); 500 JarOutputStream outputJar = new JarOutputStream(signer); 501 502 Manifest manifest = addDigestsToManifest(inputJar); 503 signFile(manifest, inputJar, 504 new X509Certificate[]{ publicKey }, 505 new PrivateKey[]{ privateKey }, 506 outputJar); 507 // Assume the certificate is valid for at least an hour. 508 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 509 addOtacert(outputJar, publicKeyFile, timestamp, manifest); 510 511 signer.notifyClosing(); 512 outputJar.close(); 513 signer.finish(); 514 } 515 catch (Exception e) { 516 throw new IOException(e); 517 } 518 } 519 520 public void writeSignatureBlock(ByteArrayOutputStream temp) 521 throws IOException, 522 CertificateEncodingException, 523 OperatorCreationException, 524 CMSException { 525 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp); 526 } 527 528 public WholeFileSignerOutputStream getSigner() { 529 return signer; 530 } 531 } 532 533 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 534 X509Certificate publicKey, PrivateKey privateKey, 535 OutputStream outputStream) throws Exception { 536 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 537 publicKey, privateKey, outputStream); 538 539 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 540 541 // put a readable message and a null char at the start of the 542 // archive comment, so that tools that display the comment 543 // (hopefully) show something sensible. 544 // TODO: anything more useful we can put in this message? 545 byte[] message = "signed by SignApk".getBytes("UTF-8"); 546 temp.write(message); 547 temp.write(0); 548 549 cmsOut.writeSignatureBlock(temp); 550 551 byte[] zipData = cmsOut.getSigner().getTail(); 552 553 // For a zip with no archive comment, the 554 // end-of-central-directory record will be 22 bytes long, so 555 // we expect to find the EOCD marker 22 bytes from the end. 556 if (zipData[zipData.length-22] != 0x50 || 557 zipData[zipData.length-21] != 0x4b || 558 zipData[zipData.length-20] != 0x05 || 559 zipData[zipData.length-19] != 0x06) { 560 throw new IllegalArgumentException("zip data already has an archive comment"); 561 } 562 563 int total_size = temp.size() + 6; 564 if (total_size > 0xffff) { 565 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 566 } 567 // signature starts this many bytes from the end of the file 568 int signature_start = total_size - message.length - 1; 569 temp.write(signature_start & 0xff); 570 temp.write((signature_start >> 8) & 0xff); 571 // Why the 0xff bytes? In a zip file with no archive comment, 572 // bytes [-6:-2] of the file are the little-endian offset from 573 // the start of the file to the central directory. So for the 574 // two high bytes to be 0xff 0xff, the archive would have to 575 // be nearly 4GB in size. So it's unlikely that a real 576 // commentless archive would have 0xffs here, and lets us tell 577 // an old signed archive from a new one. 578 temp.write(0xff); 579 temp.write(0xff); 580 temp.write(total_size & 0xff); 581 temp.write((total_size >> 8) & 0xff); 582 temp.flush(); 583 584 // Signature verification checks that the EOCD header is the 585 // last such sequence in the file (to avoid minzip finding a 586 // fake EOCD appended after the signature in its scan). The 587 // odds of producing this sequence by chance are very low, but 588 // let's catch it here if it does. 589 byte[] b = temp.toByteArray(); 590 for (int i = 0; i < b.length-3; ++i) { 591 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 592 throw new IllegalArgumentException("found spurious EOCD header at " + i); 593 } 594 } 595 596 outputStream.write(total_size & 0xff); 597 outputStream.write((total_size >> 8) & 0xff); 598 temp.writeTo(outputStream); 599 } 600 601 private static void signFile(Manifest manifest, JarFile inputJar, 602 X509Certificate[] publicKey, PrivateKey[] privateKey, 603 JarOutputStream outputJar) 604 throws Exception { 605 // Assume the certificate is valid for at least an hour. 606 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 607 608 JarEntry je; 609 610 // Everything else 611 copyFiles(manifest, inputJar, outputJar, timestamp); 612 613 // MANIFEST.MF 614 je = new JarEntry(JarFile.MANIFEST_NAME); 615 je.setTime(timestamp); 616 outputJar.putNextEntry(je); 617 manifest.write(outputJar); 618 619 int numKeys = publicKey.length; 620 for (int k = 0; k < numKeys; ++k) { 621 // CERT.SF / CERT#.SF 622 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : 623 (String.format(CERT_SF_MULTI_NAME, k))); 624 je.setTime(timestamp); 625 outputJar.putNextEntry(je); 626 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 627 writeSignatureFile(manifest, baos); 628 byte[] signedData = baos.toByteArray(); 629 outputJar.write(signedData); 630 631 // CERT.RSA / CERT#.RSA 632 je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME : 633 (String.format(CERT_RSA_MULTI_NAME, k))); 634 je.setTime(timestamp); 635 outputJar.putNextEntry(je); 636 writeSignatureBlock(new CMSProcessableByteArray(signedData), 637 publicKey[k], privateKey[k], outputJar); 638 } 639 } 640 641 private static void usage() { 642 System.err.println("Usage: signapk [-w] " + 643 "publickey.x509[.pem] privatekey.pk8 " + 644 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 645 "input.jar output.jar"); 646 System.exit(2); 647 } 648 649 public static void main(String[] args) { 650 if (args.length < 4) usage(); 651 652 sBouncyCastleProvider = new BouncyCastleProvider(); 653 Security.addProvider(sBouncyCastleProvider); 654 655 boolean signWholeFile = false; 656 int argstart = 0; 657 if (args[0].equals("-w")) { 658 signWholeFile = true; 659 argstart = 1; 660 } 661 662 if ((args.length - argstart) % 2 == 1) usage(); 663 int numKeys = ((args.length - argstart) / 2) - 1; 664 if (signWholeFile && numKeys > 1) { 665 System.err.println("Only one key may be used with -w."); 666 System.exit(2); 667 } 668 669 String inputFilename = args[args.length-2]; 670 String outputFilename = args[args.length-1]; 671 672 JarFile inputJar = null; 673 FileOutputStream outputFile = null; 674 675 try { 676 File firstPublicKeyFile = new File(args[argstart+0]); 677 678 X509Certificate[] publicKey = new X509Certificate[numKeys]; 679 for (int i = 0; i < numKeys; ++i) { 680 int argNum = argstart + i*2; 681 publicKey[i] = readPublicKey(new File(args[argNum])); 682 } 683 684 // Set the ZIP file timestamp to the starting valid time 685 // of the 0th certificate plus one hour (to match what 686 // we've historically done). 687 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 688 689 PrivateKey[] privateKey = new PrivateKey[numKeys]; 690 for (int i = 0; i < numKeys; ++i) { 691 int argNum = argstart + i*2 + 1; 692 privateKey[i] = readPrivateKey(new File(args[argNum])); 693 } 694 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 695 696 outputFile = new FileOutputStream(outputFilename); 697 698 699 if (signWholeFile) { 700 SignApk.signWholeFile(inputJar, firstPublicKeyFile, 701 publicKey[0], privateKey[0], outputFile); 702 } else { 703 JarOutputStream outputJar = new JarOutputStream(outputFile); 704 705 // For signing .apks, use the maximum compression to make 706 // them as small as possible (since they live forever on 707 // the system partition). For OTA packages, use the 708 // default compression level, which is much much faster 709 // and produces output that is only a tiny bit larger 710 // (~0.1% on full OTA packages I tested). 711 outputJar.setLevel(9); 712 713 signFile(addDigestsToManifest(inputJar), inputJar, 714 publicKey, privateKey, outputJar); 715 outputJar.close(); 716 } 717 } catch (Exception e) { 718 e.printStackTrace(); 719 System.exit(1); 720 } finally { 721 try { 722 if (inputJar != null) inputJar.close(); 723 if (outputFile != null) outputFile.close(); 724 } catch (IOException e) { 725 e.printStackTrace(); 726 System.exit(1); 727 } 728 } 729 } 730} 731