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