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.signapk; 18 19 import org.bouncycastle.asn1.ASN1InputStream; 20 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21 import org.bouncycastle.asn1.DEROutputStream; 22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24 import org.bouncycastle.cert.jcajce.JcaCertStore; 25 import org.bouncycastle.cms.CMSException; 26 import org.bouncycastle.cms.CMSSignedData; 27 import org.bouncycastle.cms.CMSSignedDataGenerator; 28 import org.bouncycastle.cms.CMSTypedData; 29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30 import org.bouncycastle.jce.provider.BouncyCastleProvider; 31 import org.bouncycastle.operator.ContentSigner; 32 import org.bouncycastle.operator.OperatorCreationException; 33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35 import org.conscrypt.OpenSSLProvider; 36 37 import com.android.apksig.ApkSignerEngine; 38 import com.android.apksig.DefaultApkSignerEngine; 39 import com.android.apksig.SigningCertificateLineage; 40 import com.android.apksig.Hints; 41 import com.android.apksig.apk.ApkUtils; 42 import com.android.apksig.apk.MinSdkVersionException; 43 import com.android.apksig.util.DataSink; 44 import com.android.apksig.util.DataSource; 45 import com.android.apksig.util.DataSources; 46 import com.android.apksig.zip.ZipFormatException; 47 48 import java.io.Console; 49 import java.io.BufferedReader; 50 import java.io.ByteArrayInputStream; 51 import java.io.ByteArrayOutputStream; 52 import java.io.DataInputStream; 53 import java.io.File; 54 import java.io.FileInputStream; 55 import java.io.FileOutputStream; 56 import java.io.FilterOutputStream; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.InputStreamReader; 60 import java.io.OutputStream; 61 import java.io.RandomAccessFile; 62 import java.lang.reflect.Constructor; 63 import java.nio.ByteBuffer; 64 import java.nio.ByteOrder; 65 import java.nio.charset.StandardCharsets; 66 import java.security.GeneralSecurityException; 67 import java.security.NoSuchAlgorithmException; 68 import java.security.Key; 69 import java.security.KeyFactory; 70 import java.security.KeyStore; 71 import java.security.KeyStoreException; 72 import java.security.KeyStore.PrivateKeyEntry; 73 import java.security.PrivateKey; 74 import java.security.Provider; 75 import java.security.Security; 76 import java.security.UnrecoverableEntryException; 77 import java.security.UnrecoverableKeyException; 78 import java.security.cert.CertificateEncodingException; 79 import java.security.cert.CertificateException; 80 import java.security.cert.CertificateFactory; 81 import java.security.cert.X509Certificate; 82 import java.security.spec.InvalidKeySpecException; 83 import java.security.spec.PKCS8EncodedKeySpec; 84 import java.util.ArrayList; 85 import java.util.Collections; 86 import java.util.Enumeration; 87 import java.util.HashSet; 88 import java.util.List; 89 import java.util.Locale; 90 import java.util.TimeZone; 91 import java.util.jar.JarEntry; 92 import java.util.jar.JarFile; 93 import java.util.jar.JarOutputStream; 94 import java.util.regex.Pattern; 95 import java.util.zip.ZipEntry; 96 97 import javax.crypto.Cipher; 98 import javax.crypto.EncryptedPrivateKeyInfo; 99 import javax.crypto.SecretKeyFactory; 100 import javax.crypto.spec.PBEKeySpec; 101 102 /** 103 * HISTORICAL NOTE: 104 * 105 * Prior to the keylimepie release, SignApk ignored the signature 106 * algorithm specified in the certificate and always used SHA1withRSA. 107 * 108 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 109 * the signature algorithm in the certificate to select which to use 110 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 111 * 112 * Because there are old keys still in use whose certificate actually 113 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 114 * for compatibility with older releases. This can be changed by 115 * altering the getAlgorithm() function below. 116 */ 117 118 119 /** 120 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 121 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 122 * SHA-256 (see historical note). The tool can additionally sign APKs using 123 * APK Signature Scheme v2. 124 */ 125 class SignApk { 126 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 127 128 /** 129 * Extensible data block/field header ID used for storing information about alignment of 130 * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section 131 * 4.5 Extensible data fields. 132 */ 133 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; 134 135 /** 136 * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed 137 * entries. 138 */ 139 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; 140 141 // bitmasks for which hash algorithms we need the manifest to include. 142 private static final int USE_SHA1 = 1; 143 private static final int USE_SHA256 = 2; 144 145 /** 146 * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used 147 * for signing an OTA update package using the private key corresponding to the provided 148 * certificate. 149 */ getDigestAlgorithmForOta(X509Certificate cert)150 private static int getDigestAlgorithmForOta(X509Certificate cert) { 151 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 152 if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { 153 // see "HISTORICAL NOTE" above. 154 return USE_SHA1; 155 } else if (sigAlg.startsWith("SHA256WITH")) { 156 return USE_SHA256; 157 } else { 158 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 159 "\" in cert [" + cert.getSubjectDN()); 160 } 161 } 162 163 /** 164 * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA 165 * update package using the private key corresponding to the provided certificate and the 166 * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). 167 */ getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash)168 private static String getJcaSignatureAlgorithmForOta( 169 X509Certificate cert, int hash) { 170 String sigAlgDigestPrefix; 171 switch (hash) { 172 case USE_SHA1: 173 sigAlgDigestPrefix = "SHA1"; 174 break; 175 case USE_SHA256: 176 sigAlgDigestPrefix = "SHA256"; 177 break; 178 default: 179 throw new IllegalArgumentException("Unknown hash ID: " + hash); 180 } 181 182 String keyAlgorithm = cert.getPublicKey().getAlgorithm(); 183 if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 184 return sigAlgDigestPrefix + "withRSA"; 185 } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 186 return sigAlgDigestPrefix + "withECDSA"; 187 } else { 188 throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 189 } 190 } 191 readPublicKey(File file)192 private static X509Certificate readPublicKey(File file) 193 throws IOException, GeneralSecurityException { 194 FileInputStream input = new FileInputStream(file); 195 try { 196 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 197 return (X509Certificate) cf.generateCertificate(input); 198 } finally { 199 input.close(); 200 } 201 } 202 203 /** 204 * If a console doesn't exist, reads the password from stdin 205 * If a console exists, reads the password from console and returns it as a string. 206 * 207 * @param keyFileName Name of the file containing the private key. Used to prompt the user. 208 */ readPassword(String keyFileName)209 private static char[] readPassword(String keyFileName) { 210 Console console; 211 if ((console = System.console()) == null) { 212 System.out.print( 213 "Enter password for " + keyFileName + " (password will not be hidden): "); 214 System.out.flush(); 215 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 216 try { 217 String result = stdin.readLine(); 218 return result == null ? null : result.toCharArray(); 219 } catch (IOException ex) { 220 return null; 221 } 222 } else { 223 return console.readPassword("[%s]", "Enter password for " + keyFileName); 224 } 225 } 226 227 /** 228 * Decrypt an encrypted PKCS#8 format private key. 229 * 230 * Based on ghstark's post on Aug 6, 2006 at 231 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 232 * 233 * @param encryptedPrivateKey The raw data of the private key 234 * @param keyFile The file containing the private key 235 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)236 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 237 throws GeneralSecurityException { 238 EncryptedPrivateKeyInfo epkInfo; 239 try { 240 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 241 } catch (IOException ex) { 242 // Probably not an encrypted key. 243 return null; 244 } 245 246 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 247 Key key = skFactory.generateSecret(new PBEKeySpec(readPassword(keyFile.getPath()))); 248 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 249 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 250 251 try { 252 return epkInfo.getKeySpec(cipher); 253 } catch (InvalidKeySpecException ex) { 254 System.err.println("signapk: Password for " + keyFile + " may be bad."); 255 throw ex; 256 } 257 } 258 259 /** Read a PKCS#8 format private key. */ readPrivateKey(File file)260 private static PrivateKey readPrivateKey(File file) 261 throws IOException, GeneralSecurityException { 262 DataInputStream input = new DataInputStream(new FileInputStream(file)); 263 try { 264 byte[] bytes = new byte[(int) file.length()]; 265 input.read(bytes); 266 267 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 268 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); 269 if (spec == null) { 270 spec = new PKCS8EncodedKeySpec(bytes); 271 } 272 273 /* 274 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 275 * OID and use that to construct a KeyFactory. 276 */ 277 PrivateKeyInfo pki; 278 try (ASN1InputStream bIn = 279 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { 280 pki = PrivateKeyInfo.getInstance(bIn.readObject()); 281 } 282 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 283 284 return KeyFactory.getInstance(algOid).generatePrivate(spec); 285 } finally { 286 input.close(); 287 } 288 } 289 createKeyStore(String keyStoreName, String keyStorePin)290 private static KeyStore createKeyStore(String keyStoreName, String keyStorePin) throws 291 CertificateException, 292 IOException, 293 KeyStoreException, 294 NoSuchAlgorithmException { 295 KeyStore keyStore = KeyStore.getInstance(keyStoreName); 296 keyStore.load(null, keyStorePin == null ? null : keyStorePin.toCharArray()); 297 return keyStore; 298 } 299 300 /** Get a PKCS#11 private key from keyStore */ loadPrivateKeyFromKeyStore( final KeyStore keyStore, final String keyName)301 private static PrivateKey loadPrivateKeyFromKeyStore( 302 final KeyStore keyStore, final String keyName) 303 throws CertificateException, KeyStoreException, NoSuchAlgorithmException, 304 UnrecoverableKeyException, UnrecoverableEntryException { 305 final PrivateKeyEntry privateKeyEntry = (PrivateKeyEntry) keyStore.getEntry(keyName, null); 306 if (privateKeyEntry == null) { 307 throw new Error( 308 "Key " 309 + keyName 310 + " not found in the token provided by PKCS11 library!"); 311 } 312 return privateKeyEntry.getPrivateKey(); 313 } 314 315 /** 316 * Add a copy of the public key to the archive; this should 317 * exactly match one of the files in 318 * /system/etc/security/otacerts.zip on the device. (The same 319 * cert can be extracted from the OTA update package's signature 320 * block but this is much easier to get at.) 321 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp)322 private static void addOtacert(JarOutputStream outputJar, 323 File publicKeyFile, 324 long timestamp) 325 throws IOException { 326 327 JarEntry je = new JarEntry(OTACERT_NAME); 328 je.setTime(timestamp); 329 outputJar.putNextEntry(je); 330 FileInputStream input = new FileInputStream(publicKeyFile); 331 byte[] b = new byte[4096]; 332 int read; 333 while ((read = input.read(b)) != -1) { 334 outputJar.write(b, 0, read); 335 } 336 input.close(); 337 } 338 339 340 /** Sign data and write the digital signature to 'out'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out)341 private static void writeSignatureBlock( 342 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, 343 OutputStream out) 344 throws IOException, 345 CertificateEncodingException, 346 OperatorCreationException, 347 CMSException { 348 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 349 certList.add(publicKey); 350 JcaCertStore certs = new JcaCertStore(certList); 351 352 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 353 ContentSigner signer = 354 new JcaContentSignerBuilder( 355 getJcaSignatureAlgorithmForOta(publicKey, hash)) 356 .build(privateKey); 357 gen.addSignerInfoGenerator( 358 new JcaSignerInfoGeneratorBuilder( 359 new JcaDigestCalculatorProviderBuilder() 360 .build()) 361 .setDirectSignature(true) 362 .build(signer, publicKey)); 363 gen.addCertificates(certs); 364 CMSSignedData sigData = gen.generate(data, false); 365 366 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 367 DEROutputStream dos = new DEROutputStream(out); 368 dos.writeObject(asn1.readObject()); 369 } 370 } 371 372 /** 373 * Adds ZIP entries which represent the v1 signature (JAR signature scheme). 374 */ addV1Signature( ApkSignerEngine apkSigner, ApkSignerEngine.OutputJarSignatureRequest v1Signature, JarOutputStream out, long timestamp)375 private static void addV1Signature( 376 ApkSignerEngine apkSigner, 377 ApkSignerEngine.OutputJarSignatureRequest v1Signature, 378 JarOutputStream out, 379 long timestamp) throws IOException { 380 for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry 381 : v1Signature.getAdditionalJarEntries()) { 382 String entryName = entry.getName(); 383 JarEntry outEntry = new JarEntry(entryName); 384 outEntry.setTime(timestamp); 385 out.putNextEntry(outEntry); 386 byte[] entryData = entry.getData(); 387 out.write(entryData); 388 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 389 apkSigner.outputJarEntry(entryName); 390 if (inspectEntryRequest != null) { 391 inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); 392 inspectEntryRequest.done(); 393 } 394 } 395 } 396 397 /** 398 * Copy all JAR entries from input to output. We set the modification times in the output to a 399 * fixed time, so as to reduce variation in the output file and make incremental OTAs more 400 * efficient. 401 */ copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, CountingOutputStream outCounter, long timestamp, int defaultAlignment)402 private static void copyFiles( 403 JarFile in, 404 Pattern ignoredFilenamePattern, 405 ApkSignerEngine apkSigner, 406 JarOutputStream out, 407 CountingOutputStream outCounter, 408 long timestamp, 409 int defaultAlignment) throws IOException { 410 byte[] buffer = new byte[4096]; 411 int num; 412 413 List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(in); 414 ArrayList<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); 415 416 ArrayList<String> names = new ArrayList<String>(); 417 for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) { 418 JarEntry entry = e.nextElement(); 419 if (entry.isDirectory()) { 420 continue; 421 } 422 String entryName = entry.getName(); 423 if ((ignoredFilenamePattern != null) 424 && (ignoredFilenamePattern.matcher(entryName).matches())) { 425 continue; 426 } 427 if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { 428 continue; // We regenerate it below. 429 } 430 names.add(entryName); 431 } 432 Collections.sort(names); 433 434 boolean firstEntry = true; 435 long offset = 0L; 436 437 // We do the copy in two passes -- first copying all the 438 // entries that are STORED, then copying all the entries that 439 // have any other compression flag (which in practice means 440 // DEFLATED). This groups all the stored entries together at 441 // the start of the file and makes it easier to do alignment 442 // on them (since only stored entries are aligned). 443 444 List<String> remainingNames = new ArrayList<>(names.size()); 445 for (String name : names) { 446 JarEntry inEntry = in.getJarEntry(name); 447 if (inEntry.getMethod() != JarEntry.STORED) { 448 // Defer outputting this entry until we're ready to output compressed entries. 449 remainingNames.add(name); 450 continue; 451 } 452 453 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 454 continue; 455 } 456 457 // Preserve the STORED method of the input entry. 458 JarEntry outEntry = new JarEntry(inEntry); 459 outEntry.setTime(timestamp); 460 // Discard comment and extra fields of this entry to 461 // simplify alignment logic below and for consistency with 462 // how compressed entries are handled later. 463 outEntry.setComment(null); 464 outEntry.setExtra(null); 465 466 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 467 // Alignment of the entry's data is achieved by adding a data block to the entry's Local 468 // File Header extra field. The data block contains information about the alignment 469 // value and the necessary padding bytes (0x00) to achieve the alignment. This works 470 // because the entry's data will be located immediately after the extra field. 471 // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format 472 // of the extra field. 473 474 // 'offset' is the offset into the file at which we expect the entry's data to begin. 475 // This is the value we need to make a multiple of 'alignment'. 476 offset += JarFile.LOCHDR + outEntry.getName().length(); 477 if (firstEntry) { 478 // The first entry in a jar file has an extra field of four bytes that you can't get 479 // rid of; any extra data you specify in the JarEntry is appended to these forced 480 // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. 481 // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 482 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. 483 offset += 4; 484 firstEntry = false; 485 } 486 int extraPaddingSizeBytes = 0; 487 if (alignment > 0) { 488 long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; 489 extraPaddingSizeBytes = 490 (alignment - (int) (paddingStartOffset % alignment)) % alignment; 491 } 492 byte[] extra = 493 new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; 494 ByteBuffer extraBuf = ByteBuffer.wrap(extra); 495 extraBuf.order(ByteOrder.LITTLE_ENDIAN); 496 extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID 497 extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size 498 extraBuf.putShort((short) alignment); 499 outEntry.setExtra(extra); 500 offset += extra.length; 501 502 long entryHeaderStart = outCounter.getWrittenBytes(); 503 out.putNextEntry(outEntry); 504 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 505 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 506 DataSink entryDataSink = 507 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 508 509 long entryDataStart = outCounter.getWrittenBytes(); 510 try (InputStream data = in.getInputStream(inEntry)) { 511 while ((num = data.read(buffer)) > 0) { 512 out.write(buffer, 0, num); 513 if (entryDataSink != null) { 514 entryDataSink.consume(buffer, 0, num); 515 } 516 offset += num; 517 } 518 } 519 out.closeEntry(); 520 out.flush(); 521 if (inspectEntryRequest != null) { 522 inspectEntryRequest.done(); 523 } 524 525 if (pinPatterns != null) { 526 boolean pinFileHeader = false; 527 for (Hints.PatternWithRange pinPattern : pinPatterns) { 528 if (!pinPattern.matcher(name).matches()) { 529 continue; 530 } 531 Hints.ByteRange dataRange = 532 new Hints.ByteRange( 533 entryDataStart, 534 outCounter.getWrittenBytes()); 535 Hints.ByteRange pinRange = 536 pinPattern.ClampToAbsoluteByteRange(dataRange); 537 if (pinRange != null) { 538 pinFileHeader = true; 539 pinByteRanges.add(pinRange); 540 } 541 } 542 if (pinFileHeader) { 543 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 544 entryDataStart)); 545 } 546 } 547 } 548 549 // Copy all the non-STORED entries. We don't attempt to 550 // maintain the 'offset' variable past this point; we don't do 551 // alignment on these entries. 552 553 for (String name : remainingNames) { 554 JarEntry inEntry = in.getJarEntry(name); 555 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 556 continue; 557 } 558 559 // Create a new entry so that the compressed len is recomputed. 560 JarEntry outEntry = new JarEntry(name); 561 outEntry.setTime(timestamp); 562 long entryHeaderStart = outCounter.getWrittenBytes(); 563 out.putNextEntry(outEntry); 564 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 565 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 566 DataSink entryDataSink = 567 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 568 569 long entryDataStart = outCounter.getWrittenBytes(); 570 InputStream data = in.getInputStream(inEntry); 571 while ((num = data.read(buffer)) > 0) { 572 out.write(buffer, 0, num); 573 if (entryDataSink != null) { 574 entryDataSink.consume(buffer, 0, num); 575 } 576 } 577 out.closeEntry(); 578 out.flush(); 579 if (inspectEntryRequest != null) { 580 inspectEntryRequest.done(); 581 } 582 583 if (pinPatterns != null) { 584 boolean pinFileHeader = false; 585 for (Hints.PatternWithRange pinPattern : pinPatterns) { 586 if (!pinPattern.matcher(name).matches()) { 587 continue; 588 } 589 Hints.ByteRange dataRange = 590 new Hints.ByteRange( 591 entryDataStart, 592 outCounter.getWrittenBytes()); 593 Hints.ByteRange pinRange = 594 pinPattern.ClampToAbsoluteByteRange(dataRange); 595 if (pinRange != null) { 596 pinFileHeader = true; 597 pinByteRanges.add(pinRange); 598 } 599 } 600 if (pinFileHeader) { 601 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 602 entryDataStart)); 603 } 604 } 605 } 606 607 if (pinByteRanges != null) { 608 // Cover central directory 609 pinByteRanges.add( 610 new Hints.ByteRange(outCounter.getWrittenBytes(), 611 Long.MAX_VALUE)); 612 addPinByteRanges(out, pinByteRanges, timestamp); 613 } 614 } 615 extractPinPatterns(JarFile in)616 private static List<Hints.PatternWithRange> extractPinPatterns(JarFile in) throws IOException { 617 ZipEntry pinMetaEntry = in.getEntry(Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME); 618 if (pinMetaEntry == null) { 619 return null; 620 } 621 InputStream pinMetaStream = in.getInputStream(pinMetaEntry); 622 byte[] patternBlob = new byte[(int) pinMetaEntry.getSize()]; 623 pinMetaStream.read(patternBlob); 624 return Hints.parsePinPatterns(patternBlob); 625 } 626 addPinByteRanges(JarOutputStream outputJar, ArrayList<Hints.ByteRange> pinByteRanges, long timestamp)627 private static void addPinByteRanges(JarOutputStream outputJar, 628 ArrayList<Hints.ByteRange> pinByteRanges, 629 long timestamp) throws IOException { 630 JarEntry je = new JarEntry(Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME); 631 je.setTime(timestamp); 632 outputJar.putNextEntry(je); 633 outputJar.write(Hints.encodeByteRangeList(pinByteRanges)); 634 } 635 shouldOutputApkEntry( ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)636 private static boolean shouldOutputApkEntry( 637 ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) 638 throws IOException { 639 if (apkSigner == null) { 640 return true; 641 } 642 643 ApkSignerEngine.InputJarEntryInstructions instructions = 644 apkSigner.inputJarEntry(inEntry.getName()); 645 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 646 instructions.getInspectJarEntryRequest(); 647 if (inspectEntryRequest != null) { 648 provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); 649 } 650 switch (instructions.getOutputPolicy()) { 651 case OUTPUT: 652 return true; 653 case SKIP: 654 case OUTPUT_BY_ENGINE: 655 return false; 656 default: 657 throw new RuntimeException( 658 "Unsupported output policy: " + instructions.getOutputPolicy()); 659 } 660 } 661 provideJarEntry( JarFile jarFile, JarEntry jarEntry, ApkSignerEngine.InspectJarEntryRequest request, byte[] tmpbuf)662 private static void provideJarEntry( 663 JarFile jarFile, 664 JarEntry jarEntry, 665 ApkSignerEngine.InspectJarEntryRequest request, 666 byte[] tmpbuf) throws IOException { 667 DataSink dataSink = request.getDataSink(); 668 try (InputStream in = jarFile.getInputStream(jarEntry)) { 669 int chunkSize; 670 while ((chunkSize = in.read(tmpbuf)) > 0) { 671 dataSink.consume(tmpbuf, 0, chunkSize); 672 } 673 request.done(); 674 } 675 } 676 677 /** 678 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 679 * relative to start of file or {@code 0} if alignment of this entry's data is not important. 680 */ getStoredEntryDataAlignment(String entryName, int defaultAlignment)681 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 682 if (defaultAlignment <= 0) { 683 return 0; 684 } 685 686 if (entryName.endsWith(".so")) { 687 // Align .so contents to memory page boundary to enable memory-mapped 688 // execution. 689 return 16384; 690 } else { 691 return defaultAlignment; 692 } 693 } 694 695 private static class WholeFileSignerOutputStream extends FilterOutputStream { 696 private boolean closing = false; 697 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 698 private OutputStream tee; 699 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)700 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 701 super(out); 702 this.tee = tee; 703 } 704 notifyClosing()705 public void notifyClosing() { 706 closing = true; 707 } 708 finish()709 public void finish() throws IOException { 710 closing = false; 711 712 byte[] data = footer.toByteArray(); 713 if (data.length < 2) 714 throw new IOException("Less than two bytes written to footer"); 715 write(data, 0, data.length - 2); 716 } 717 getTail()718 public byte[] getTail() { 719 return footer.toByteArray(); 720 } 721 722 @Override write(byte[] b)723 public void write(byte[] b) throws IOException { 724 write(b, 0, b.length); 725 } 726 727 @Override write(byte[] b, int off, int len)728 public void write(byte[] b, int off, int len) throws IOException { 729 if (closing) { 730 // if the jar is about to close, save the footer that will be written 731 footer.write(b, off, len); 732 } 733 else { 734 // write to both output streams. out is the CMSTypedData signer and tee is the file. 735 out.write(b, off, len); 736 tee.write(b, off, len); 737 } 738 } 739 740 @Override write(int b)741 public void write(int b) throws IOException { 742 if (closing) { 743 // if the jar is about to close, save the footer that will be written 744 footer.write(b); 745 } 746 else { 747 // write to both output streams. out is the CMSTypedData signer and tee is the file. 748 out.write(b); 749 tee.write(b); 750 } 751 } 752 } 753 754 private static class CMSSigner implements CMSTypedData { 755 private final JarFile inputJar; 756 private final File publicKeyFile; 757 private final X509Certificate publicKey; 758 private final PrivateKey privateKey; 759 private final int hash; 760 private final long timestamp; 761 private final OutputStream outputStream; 762 private final ASN1ObjectIdentifier type; 763 private WholeFileSignerOutputStream signer; 764 765 // Files matching this pattern are not copied to the output. 766 private static final Pattern STRIP_PATTERN = 767 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" 768 + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 769 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)770 public CMSSigner(JarFile inputJar, File publicKeyFile, 771 X509Certificate publicKey, PrivateKey privateKey, int hash, 772 long timestamp, OutputStream outputStream) { 773 this.inputJar = inputJar; 774 this.publicKeyFile = publicKeyFile; 775 this.publicKey = publicKey; 776 this.privateKey = privateKey; 777 this.hash = hash; 778 this.timestamp = timestamp; 779 this.outputStream = outputStream; 780 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 781 } 782 783 /** 784 * This should actually return byte[] or something similar, but nothing 785 * actually checks it currently. 786 */ 787 @Override getContent()788 public Object getContent() { 789 return this; 790 } 791 792 @Override getContentType()793 public ASN1ObjectIdentifier getContentType() { 794 return type; 795 } 796 797 @Override write(OutputStream out)798 public void write(OutputStream out) throws IOException { 799 try { 800 signer = new WholeFileSignerOutputStream(out, outputStream); 801 CountingOutputStream outputJarCounter = new CountingOutputStream(signer); 802 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 803 804 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, 805 outputJarCounter, timestamp, 0); 806 addOtacert(outputJar, publicKeyFile, timestamp); 807 808 signer.notifyClosing(); 809 outputJar.close(); 810 signer.finish(); 811 } 812 catch (Exception e) { 813 throw new IOException(e); 814 } 815 } 816 writeSignatureBlock(ByteArrayOutputStream temp)817 public void writeSignatureBlock(ByteArrayOutputStream temp) 818 throws IOException, 819 CertificateEncodingException, 820 OperatorCreationException, 821 CMSException { 822 SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); 823 } 824 getSigner()825 public WholeFileSignerOutputStream getSigner() { 826 return signer; 827 } 828 } 829 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)830 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 831 X509Certificate publicKey, PrivateKey privateKey, 832 int hash, long timestamp, 833 OutputStream outputStream) throws Exception { 834 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 835 publicKey, privateKey, hash, timestamp, outputStream); 836 837 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 838 839 // put a readable message and a null char at the start of the 840 // archive comment, so that tools that display the comment 841 // (hopefully) show something sensible. 842 // TODO: anything more useful we can put in this message? 843 byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8); 844 temp.write(message); 845 temp.write(0); 846 847 cmsOut.writeSignatureBlock(temp); 848 849 byte[] zipData = cmsOut.getSigner().getTail(); 850 851 // For a zip with no archive comment, the 852 // end-of-central-directory record will be 22 bytes long, so 853 // we expect to find the EOCD marker 22 bytes from the end. 854 if (zipData[zipData.length-22] != 0x50 || 855 zipData[zipData.length-21] != 0x4b || 856 zipData[zipData.length-20] != 0x05 || 857 zipData[zipData.length-19] != 0x06) { 858 throw new IllegalArgumentException("zip data already has an archive comment"); 859 } 860 861 int total_size = temp.size() + 6; 862 if (total_size > 0xffff) { 863 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 864 } 865 // signature starts this many bytes from the end of the file 866 int signature_start = total_size - message.length - 1; 867 temp.write(signature_start & 0xff); 868 temp.write((signature_start >> 8) & 0xff); 869 // Why the 0xff bytes? In a zip file with no archive comment, 870 // bytes [-6:-2] of the file are the little-endian offset from 871 // the start of the file to the central directory. So for the 872 // two high bytes to be 0xff 0xff, the archive would have to 873 // be nearly 4GB in size. So it's unlikely that a real 874 // commentless archive would have 0xffs here, and lets us tell 875 // an old signed archive from a new one. 876 temp.write(0xff); 877 temp.write(0xff); 878 temp.write(total_size & 0xff); 879 temp.write((total_size >> 8) & 0xff); 880 temp.flush(); 881 882 // Signature verification checks that the EOCD header is the 883 // last such sequence in the file (to avoid minzip finding a 884 // fake EOCD appended after the signature in its scan). The 885 // odds of producing this sequence by chance are very low, but 886 // let's catch it here if it does. 887 byte[] b = temp.toByteArray(); 888 for (int i = 0; i < b.length-3; ++i) { 889 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 890 throw new IllegalArgumentException("found spurious EOCD header at " + i); 891 } 892 } 893 894 outputStream.write(total_size & 0xff); 895 outputStream.write((total_size >> 8) & 0xff); 896 temp.writeTo(outputStream); 897 } 898 899 /** 900 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 901 * types that might be stored in PKCS#11-like storage. 902 */ loadProviderIfNecessary(String providerClassName, String providerArg)903 private static void loadProviderIfNecessary(String providerClassName, String providerArg) { 904 if (providerClassName == null) { 905 return; 906 } 907 908 final Class<?> klass; 909 try { 910 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 911 if (sysLoader != null) { 912 klass = sysLoader.loadClass(providerClassName); 913 } else { 914 klass = Class.forName(providerClassName); 915 } 916 } catch (ClassNotFoundException e) { 917 e.printStackTrace(); 918 System.exit(1); 919 return; 920 } 921 922 Constructor<?> constructor; 923 Object o = null; 924 if (providerArg == null) { 925 try { 926 constructor = klass.getConstructor(); 927 o = constructor.newInstance(); 928 } catch (ReflectiveOperationException e) { 929 e.printStackTrace(); 930 System.err.println("Unable to instantiate " + providerClassName 931 + " with a zero-arg constructor"); 932 System.exit(1); 933 } 934 } else { 935 try { 936 constructor = klass.getConstructor(String.class); 937 o = constructor.newInstance(providerArg); 938 } catch (ReflectiveOperationException e) { 939 // This is expected from JDK 9+; the single-arg constructor accepting the 940 // configuration has been replaced with a configure(String) method to be invoked 941 // after instantiating the Provider with the zero-arg constructor. 942 try { 943 constructor = klass.getConstructor(); 944 o = constructor.newInstance(); 945 // The configure method will return either the modified Provider or a new 946 // Provider if this one cannot be configured in-place. 947 o = klass.getMethod("configure", String.class).invoke(o, providerArg); 948 } catch (ReflectiveOperationException roe) { 949 roe.printStackTrace(); 950 System.err.println("Unable to instantiate " + providerClassName 951 + " with the provided argument " + providerArg); 952 System.exit(1); 953 } 954 } 955 } 956 957 if (!(o instanceof Provider)) { 958 System.err.println("Not a Provider class: " + providerClassName); 959 System.exit(1); 960 } 961 962 Security.insertProviderAt((Provider) o, 1); 963 } 964 createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)965 private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs( 966 PrivateKey[] privateKeys, X509Certificate[] certificates) { 967 if (privateKeys.length != certificates.length) { 968 throw new IllegalArgumentException( 969 "The number of private keys must match the number of certificates: " 970 + privateKeys.length + " vs" + certificates.length); 971 } 972 List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>(); 973 String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; 974 for (int i = 0; i < privateKeys.length; i++) { 975 String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); 976 DefaultApkSignerEngine.SignerConfig signerConfig = 977 new DefaultApkSignerEngine.SignerConfig.Builder( 978 signerName, 979 privateKeys[i], 980 Collections.singletonList(certificates[i])) 981 .build(); 982 signerConfigs.add(signerConfig); 983 } 984 return signerConfigs; 985 } 986 987 private static class ZipSections { 988 DataSource beforeCentralDir; 989 990 // The following fields are still valid after closing the backing DataSource. 991 long beforeCentralDirSize; 992 ByteBuffer centralDir; 993 ByteBuffer eocd; 994 } 995 findMainZipSections(DataSource apk)996 private static ZipSections findMainZipSections(DataSource apk) 997 throws IOException, ZipFormatException { 998 ApkUtils.ZipSections sections = ApkUtils.findZipSections(apk); 999 long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); 1000 long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); 1001 long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; 1002 long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); 1003 if (centralDirEndOffset != eocdStartOffset) { 1004 throw new ZipFormatException( 1005 "ZIP Central Directory is not immediately followed by End of Central Directory" 1006 + ". CD end: " + centralDirEndOffset 1007 + ", EoCD start: " + eocdStartOffset); 1008 } 1009 1010 ZipSections result = new ZipSections(); 1011 1012 result.beforeCentralDir = apk.slice(0, centralDirStartOffset); 1013 result.beforeCentralDirSize = result.beforeCentralDir.size(); 1014 1015 long centralDirSize = centralDirEndOffset - centralDirStartOffset; 1016 if (centralDirSize >= Integer.MAX_VALUE) throw new IndexOutOfBoundsException(); 1017 result.centralDir = apk.getByteBuffer(centralDirStartOffset, (int)centralDirSize); 1018 1019 long eocdSize = apk.size() - eocdStartOffset; 1020 if (eocdSize >= Integer.MAX_VALUE) throw new IndexOutOfBoundsException(); 1021 result.eocd = apk.getByteBuffer(eocdStartOffset, (int)eocdSize); 1022 1023 return result; 1024 } 1025 1026 /** 1027 * Returns the API Level corresponding to the APK's minSdkVersion. 1028 * 1029 * @throws MinSdkVersionException if the API Level cannot be determined from the APK. 1030 */ getMinSdkVersion(JarFile apk)1031 private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException { 1032 JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml"); 1033 if (manifestEntry == null) { 1034 throw new MinSdkVersionException("No AndroidManifest.xml in APK"); 1035 } 1036 byte[] manifestBytes; 1037 try { 1038 try (InputStream manifestIn = apk.getInputStream(manifestEntry)) { 1039 manifestBytes = toByteArray(manifestIn); 1040 } 1041 } catch (IOException e) { 1042 throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e); 1043 } 1044 return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes)); 1045 } 1046 toByteArray(InputStream in)1047 private static byte[] toByteArray(InputStream in) throws IOException { 1048 ByteArrayOutputStream result = new ByteArrayOutputStream(); 1049 byte[] buf = new byte[65536]; 1050 int chunkSize; 1051 while ((chunkSize = in.read(buf)) != -1) { 1052 result.write(buf, 0, chunkSize); 1053 } 1054 return result.toByteArray(); 1055 } 1056 usage()1057 private static void usage() { 1058 System.err.println("Usage: signapk [-w] " + 1059 "[-a <alignment>] " + 1060 "[--align-file-size] " + 1061 "[-providerClass <className>] " + 1062 "[-providerArg <configureArg>] " + 1063 "[-loadPrivateKeysFromKeyStore <keyStoreName>]" + 1064 "[-keyStorePin <pin>]" + 1065 "[--min-sdk-version <n>] " + 1066 "[--disable-v2] " + 1067 "[--enable-v4] " + 1068 "publickey.x509[.pem] privatekey.pk8 " + 1069 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 1070 "input.jar output.jar [output-v4-file]"); 1071 System.exit(2); 1072 } 1073 main(String[] args)1074 public static void main(String[] args) { 1075 if (args.length < 4) usage(); 1076 1077 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than 1078 // the standard or Bouncy Castle ones. 1079 Security.insertProviderAt(new OpenSSLProvider(), 1); 1080 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer 1081 // DSA which may still be needed. 1082 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. 1083 Security.addProvider(new BouncyCastleProvider()); 1084 1085 boolean signWholeFile = false; 1086 String providerClass = null; 1087 String providerArg = null; 1088 String keyStoreName = null; 1089 String keyStorePin = null; 1090 int alignment = 4; 1091 boolean alignFileSize = false; 1092 Integer minSdkVersionOverride = null; 1093 boolean signUsingApkSignatureSchemeV2 = true; 1094 boolean signUsingApkSignatureSchemeV4 = false; 1095 SigningCertificateLineage certLineage = null; 1096 Integer rotationMinSdkVersion = null; 1097 1098 int argstart = 0; 1099 while (argstart < args.length && args[argstart].startsWith("-")) { 1100 if ("-w".equals(args[argstart])) { 1101 signWholeFile = true; 1102 ++argstart; 1103 } else if ("-providerClass".equals(args[argstart])) { 1104 if (argstart + 1 >= args.length) { 1105 usage(); 1106 } 1107 providerClass = args[++argstart]; 1108 ++argstart; 1109 } else if("-providerArg".equals(args[argstart])) { 1110 if (argstart + 1 >= args.length) { 1111 usage(); 1112 } 1113 providerArg = args[++argstart]; 1114 ++argstart; 1115 } else if ("-loadPrivateKeysFromKeyStore".equals(args[argstart])) { 1116 if (argstart + 1 >= args.length) { 1117 usage(); 1118 } 1119 keyStoreName = args[++argstart]; 1120 ++argstart; 1121 } else if ("-keyStorePin".equals(args[argstart])) { 1122 if (argstart + 1 >= args.length) { 1123 usage(); 1124 } 1125 keyStorePin = args[++argstart]; 1126 ++argstart; 1127 } else if ("-a".equals(args[argstart])) { 1128 alignment = Integer.parseInt(args[++argstart]); 1129 ++argstart; 1130 } else if ("--align-file-size".equals(args[argstart])) { 1131 alignFileSize = true; 1132 ++argstart; 1133 } else if ("--min-sdk-version".equals(args[argstart])) { 1134 String minSdkVersionString = args[++argstart]; 1135 try { 1136 minSdkVersionOverride = Integer.parseInt(minSdkVersionString); 1137 } catch (NumberFormatException e) { 1138 throw new IllegalArgumentException( 1139 "--min-sdk-version must be a decimal number: " + minSdkVersionString); 1140 } 1141 ++argstart; 1142 } else if ("--disable-v2".equals(args[argstart])) { 1143 signUsingApkSignatureSchemeV2 = false; 1144 ++argstart; 1145 } else if ("--enable-v4".equals(args[argstart])) { 1146 signUsingApkSignatureSchemeV4 = true; 1147 ++argstart; 1148 } else if ("--lineage".equals(args[argstart])) { 1149 File lineageFile = new File(args[++argstart]); 1150 try { 1151 certLineage = SigningCertificateLineage.readFromFile(lineageFile); 1152 } catch (Exception e) { 1153 throw new IllegalArgumentException( 1154 "Error reading lineage file: " + e.getMessage()); 1155 } 1156 ++argstart; 1157 } else if ("--rotation-min-sdk-version".equals(args[argstart])) { 1158 String rotationMinSdkVersionString = args[++argstart]; 1159 try { 1160 rotationMinSdkVersion = Integer.parseInt(rotationMinSdkVersionString); 1161 } catch (NumberFormatException e) { 1162 throw new IllegalArgumentException( 1163 "--rotation-min-sdk-version must be a decimal number: " + rotationMinSdkVersionString); 1164 } 1165 ++argstart; 1166 } else { 1167 usage(); 1168 } 1169 } 1170 1171 int numArgsExcludeV4FilePath; 1172 if (signUsingApkSignatureSchemeV4) { 1173 numArgsExcludeV4FilePath = args.length - 1; 1174 } else { 1175 numArgsExcludeV4FilePath = args.length; 1176 } 1177 if ((numArgsExcludeV4FilePath - argstart) % 2 == 1) usage(); 1178 int numKeys = ((numArgsExcludeV4FilePath - argstart) / 2) - 1; 1179 if (signWholeFile && numKeys > 1) { 1180 System.err.println("Only one key may be used with -w."); 1181 System.exit(2); 1182 } 1183 1184 loadProviderIfNecessary(providerClass, providerArg); 1185 1186 String inputFilename = args[numArgsExcludeV4FilePath - 2]; 1187 String outputFilename = args[numArgsExcludeV4FilePath - 1]; 1188 String outputV4Filename = ""; 1189 if (signUsingApkSignatureSchemeV4) { 1190 outputV4Filename = args[args.length - 1]; 1191 } 1192 1193 JarFile inputJar = null; 1194 FileOutputStream outputFile = null; 1195 1196 try { 1197 File firstPublicKeyFile = new File(args[argstart+0]); 1198 1199 X509Certificate[] publicKey = new X509Certificate[numKeys]; 1200 try { 1201 for (int i = 0; i < numKeys; ++i) { 1202 int argNum = argstart + i*2; 1203 publicKey[i] = readPublicKey(new File(args[argNum])); 1204 } 1205 } catch (IllegalArgumentException e) { 1206 System.err.println(e); 1207 System.exit(1); 1208 } 1209 1210 // Set all ZIP file timestamps to Jan 1 2009 00:00:00. 1211 long timestamp = 1230768000000L; 1212 // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS 1213 // timestamp using the current timezone. We thus adjust the milliseconds since epoch 1214 // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. 1215 timestamp -= TimeZone.getDefault().getOffset(timestamp); 1216 KeyStore keyStore = null; 1217 if (keyStoreName != null) { 1218 keyStore = createKeyStore(keyStoreName, keyStorePin); 1219 } 1220 PrivateKey[] privateKey = new PrivateKey[numKeys]; 1221 for (int i = 0; i < numKeys; ++i) { 1222 int argNum = argstart + i*2 + 1; 1223 if (keyStore == null) { 1224 privateKey[i] = readPrivateKey(new File(args[argNum])); 1225 } else { 1226 final String keyAlias = args[argNum]; 1227 privateKey[i] = loadPrivateKeyFromKeyStore(keyStore, keyAlias); 1228 } 1229 } 1230 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 1231 1232 outputFile = new FileOutputStream(outputFilename); 1233 1234 // NOTE: Signing currently recompresses any compressed entries using Deflate (default 1235 // compression level for OTA update files and maximum compession level for APKs). 1236 if (signWholeFile) { 1237 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); 1238 signWholeFile(inputJar, firstPublicKeyFile, 1239 publicKey[0], privateKey[0], digestAlgorithm, 1240 timestamp, 1241 outputFile); 1242 } else { 1243 // Determine the value to use as minSdkVersion of the APK being signed 1244 int minSdkVersion; 1245 if (minSdkVersionOverride != null) { 1246 minSdkVersion = minSdkVersionOverride; 1247 } else { 1248 try { 1249 minSdkVersion = getMinSdkVersion(inputJar); 1250 } catch (MinSdkVersionException e) { 1251 throw new IllegalArgumentException( 1252 "Cannot detect minSdkVersion. Use --min-sdk-version to override", 1253 e); 1254 } 1255 } 1256 1257 DefaultApkSignerEngine.Builder builder = new DefaultApkSignerEngine.Builder( 1258 createSignerConfigs(privateKey, publicKey), minSdkVersion) 1259 .setV1SigningEnabled(true) 1260 .setV2SigningEnabled(signUsingApkSignatureSchemeV2) 1261 .setOtherSignersSignaturesPreserved(false) 1262 .setCreatedBy("1.0 (Android SignApk)"); 1263 1264 if (certLineage != null) { 1265 builder = builder.setSigningCertificateLineage(certLineage); 1266 } 1267 1268 if (rotationMinSdkVersion != null) { 1269 builder = builder.setMinSdkVersionForRotation(rotationMinSdkVersion); 1270 } 1271 1272 try (ApkSignerEngine apkSigner = builder.build()) { 1273 // We don't preserve the input APK's APK Signing Block (which contains v2 1274 // signatures) 1275 apkSigner.inputApkSigningBlock(null); 1276 1277 CountingOutputStream outputJarCounter = 1278 new CountingOutputStream(outputFile); 1279 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 1280 // Use maximum compression for compressed entries because the APK lives forever 1281 // on the system partition. 1282 outputJar.setLevel(9); 1283 copyFiles(inputJar, null, apkSigner, outputJar, 1284 outputJarCounter, timestamp, alignment); 1285 ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = 1286 apkSigner.outputJarEntries(); 1287 if (addV1SignatureRequest != null) { 1288 addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); 1289 addV1SignatureRequest.done(); 1290 } 1291 1292 // close output and switch to input mode 1293 outputJar.close(); 1294 outputJar = null; 1295 outputJarCounter = null; 1296 outputFile = null; 1297 RandomAccessFile v1SignedApk = new RandomAccessFile(outputFilename, "r"); 1298 1299 ZipSections zipSections = findMainZipSections(DataSources.asDataSource( 1300 v1SignedApk)); 1301 1302 ByteBuffer eocd = ByteBuffer.allocate(zipSections.eocd.remaining()); 1303 eocd.put(zipSections.eocd); 1304 eocd.flip(); 1305 eocd.order(ByteOrder.LITTLE_ENDIAN); 1306 1307 ByteBuffer[] outputChunks = new ByteBuffer[] {}; 1308 1309 // This loop is supposed to be iterated twice at most. 1310 // The second pass is to align the file size after amending EOCD comments 1311 // with assumption that re-generated signing block would be the same size. 1312 while (true) { 1313 ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest = 1314 apkSigner.outputZipSections2( 1315 zipSections.beforeCentralDir, 1316 DataSources.asDataSource(zipSections.centralDir), 1317 DataSources.asDataSource(eocd)); 1318 if (addV2SignatureRequest == null) break; 1319 1320 // Need to insert the returned APK Signing Block before ZIP Central 1321 // Directory. 1322 int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock(); 1323 byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); 1324 // Because the APK Signing Block is inserted before the Central Directory, 1325 // we need to adjust accordingly the offset of Central Directory inside the 1326 // ZIP End of Central Directory (EoCD) record. 1327 ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); 1328 modifiedEocd.put(eocd); 1329 modifiedEocd.flip(); 1330 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1331 ApkUtils.setZipEocdCentralDirectoryOffset( 1332 modifiedEocd, 1333 zipSections.beforeCentralDir.size() + padding + 1334 apkSigningBlock.length); 1335 outputChunks = 1336 new ByteBuffer[] { 1337 ByteBuffer.allocate(padding), 1338 ByteBuffer.wrap(apkSigningBlock), 1339 zipSections.centralDir, 1340 modifiedEocd}; 1341 addV2SignatureRequest.done(); 1342 1343 // Exit the loop if we don't need to align the file size 1344 if (!alignFileSize || alignment < 2) { 1345 break; 1346 } 1347 1348 // Calculate the file size 1349 eocd = modifiedEocd; 1350 long fileSize = zipSections.beforeCentralDirSize; 1351 for (ByteBuffer buf : outputChunks) { 1352 fileSize += buf.remaining(); 1353 } 1354 // Exit the loop because the file size is aligned. 1355 if (fileSize % alignment == 0) { 1356 break; 1357 } 1358 // Pad EOCD comment to align the file size. 1359 int commentLen = alignment - (int)(fileSize % alignment); 1360 modifiedEocd = ByteBuffer.allocate(eocd.remaining() + commentLen); 1361 modifiedEocd.put(eocd); 1362 modifiedEocd.rewind(); 1363 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1364 ApkUtils.updateZipEocdCommentLen(modifiedEocd); 1365 // Since V2 signing block should cover modified EOCD, 1366 // re-iterate the loop with modified EOCD. 1367 eocd = modifiedEocd; 1368 } 1369 1370 // close input and switch back to output mode 1371 v1SignedApk.close(); 1372 v1SignedApk = null; 1373 outputFile = new FileOutputStream(outputFilename, true); 1374 outputFile.getChannel().truncate(zipSections.beforeCentralDirSize); 1375 1376 // This assumes outputChunks are array-backed. To avoid this assumption, the 1377 // code could be rewritten to use FileChannel. 1378 for (ByteBuffer outputChunk : outputChunks) { 1379 outputFile.write( 1380 outputChunk.array(), 1381 outputChunk.arrayOffset() + outputChunk.position(), 1382 outputChunk.remaining()); 1383 outputChunk.position(outputChunk.limit()); 1384 } 1385 1386 outputFile.close(); 1387 outputFile = null; 1388 apkSigner.outputDone(); 1389 1390 if (signUsingApkSignatureSchemeV4) { 1391 final DataSource outputApkIn = DataSources.asDataSource( 1392 new RandomAccessFile(new File(outputFilename), "r")); 1393 final File outputV4File = new File(outputV4Filename); 1394 apkSigner.signV4(outputApkIn, outputV4File, false /* ignore failures */); 1395 } 1396 } 1397 1398 return; 1399 } 1400 } catch (Exception e) { 1401 e.printStackTrace(); 1402 System.exit(1); 1403 } finally { 1404 try { 1405 if (inputJar != null) inputJar.close(); 1406 if (outputFile != null) outputFile.close(); 1407 } catch (IOException e) { 1408 e.printStackTrace(); 1409 System.exit(1); 1410 } 1411 } 1412 } 1413 } 1414